#![cfg(not(windows))] use anyhow::Result; use async_trait::async_trait; use openhuman_core::openhuman::agent::dispatcher::NativeToolDispatcher; use openhuman_core::openhuman::agent::harness::run_queue::RunQueue; use openhuman_core::openhuman::agent::harness::session::Agent; use openhuman_core::openhuman::agent::host_runtime::NativeRuntime; use openhuman_core::openhuman::config::AgentConfig; use openhuman_core::openhuman::inference::provider::thread_context::with_thread_id; use openhuman_core::openhuman::inference::provider::traits::ProviderCapabilities; use openhuman_core::openhuman::inference::provider::{ ChatMessage, ChatRequest, ChatResponse, Provider, ToolCall, }; use openhuman_core::openhuman::memory::{ Memory, MemoryCategory, MemoryEntry, NamespaceSummary, RecallOpts, }; use openhuman_core::openhuman::monitor::tools::{ MonitorListTool, MonitorReadTool, MonitorStopTool, MonitorTool, }; use openhuman_core::openhuman::security::{AuditLogger, AutonomyLevel, SecurityPolicy}; use openhuman_core::openhuman::tools::Tool; use parking_lot::Mutex; use serde_json::json; use std::collections::VecDeque; use std::path::Path; use std::sync::{Arc, OnceLock}; use tempfile::TempDir; use tokio::time::{sleep, Duration}; type ResponseFactory = Box ChatResponse + Send + Sync>; enum ProviderStep { Static(ChatResponse), Delayed(Duration, ChatResponse), FromHistory(ResponseFactory), } #[derive(Clone, Debug)] struct CapturedRequest { messages: Vec, tool_names: Vec, } struct ScriptedProvider { steps: Mutex>, requests: Mutex>, } fn monitor_e2e_lock() -> &'static tokio::sync::Mutex<()> { static LOCK: OnceLock> = OnceLock::new(); LOCK.get_or_init(|| tokio::sync::Mutex::new(())) } impl ScriptedProvider { fn new(steps: Vec) -> Arc { Arc::new(Self { steps: Mutex::new(steps.into()), requests: Mutex::new(Vec::new()), }) } fn requests(&self) -> Vec { self.requests.lock().clone() } } #[async_trait] impl Provider for ScriptedProvider { fn capabilities(&self) -> ProviderCapabilities { ProviderCapabilities { native_tool_calling: true, vision: false, } } async fn chat_with_system( &self, _system_prompt: Option<&str>, message: &str, _model: &str, _temperature: f64, ) -> Result { Ok(format!("summary:{message}")) } async fn chat( &self, request: ChatRequest<'_>, _model: &str, _temperature: f64, ) -> Result { let messages = request.messages.to_vec(); self.requests.lock().push(CapturedRequest { messages: messages.clone(), tool_names: request .tools .map(|tools| tools.iter().map(|tool| tool.name.clone()).collect()) .unwrap_or_default(), }); let step = self.steps.lock().pop_front(); match step { Some(ProviderStep::Static(response)) => Ok(response), Some(ProviderStep::Delayed(delay, response)) => { sleep(delay).await; Ok(response) } Some(ProviderStep::FromHistory(factory)) => Ok(factory(&messages)), None => Ok(text_response("default monitor final")), } } } #[derive(Default)] struct StubMemory { entries: Mutex>, } #[async_trait] impl Memory for StubMemory { fn name(&self) -> &str { "monitor-agent-e2e-memory" } async fn store( &self, namespace: &str, key: &str, content: &str, category: MemoryCategory, session_id: Option<&str>, ) -> Result<()> { let mut entries = self.entries.lock(); let id = format!("{namespace}:{key}:{}", entries.len()); entries.push(MemoryEntry { id, key: key.to_string(), content: content.to_string(), namespace: Some(namespace.to_string()), category, timestamp: "2026-06-04T00:00:00Z".to_string(), session_id: session_id.map(str::to_string), score: Some(0.9), taint: Default::default(), }); Ok(()) } async fn recall( &self, _query: &str, limit: usize, _opts: RecallOpts<'_>, ) -> Result> { Ok(self.entries.lock().iter().take(limit).cloned().collect()) } async fn get(&self, namespace: &str, key: &str) -> Result> { Ok(self .entries .lock() .iter() .find(|entry| entry.namespace.as_deref() == Some(namespace) && entry.key == key) .cloned()) } async fn list( &self, namespace: Option<&str>, category: Option<&MemoryCategory>, session_id: Option<&str>, ) -> Result> { Ok(self .entries .lock() .iter() .filter(|entry| namespace.is_none_or(|ns| entry.namespace.as_deref() == Some(ns))) .filter(|entry| category.is_none_or(|cat| &entry.category == cat)) .filter(|entry| session_id.is_none_or(|sid| entry.session_id.as_deref() == Some(sid))) .cloned() .collect()) } async fn forget(&self, namespace: &str, key: &str) -> Result { let mut entries = self.entries.lock(); let before = entries.len(); entries.retain(|entry| entry.namespace.as_deref() != Some(namespace) || entry.key != key); Ok(entries.len() != before) } async fn namespace_summaries(&self) -> Result> { Ok(Vec::new()) } async fn count(&self) -> Result { Ok(self.entries.lock().len()) } async fn health_check(&self) -> bool { true } } fn text_response(text: &str) -> ChatResponse { ChatResponse { text: Some(text.to_string()), tool_calls: Vec::new(), usage: None, reasoning_content: None, } } fn tool_response(id: &str, name: &str, arguments: serde_json::Value) -> ChatResponse { ChatResponse { text: Some(format!("calling {name}")), tool_calls: vec![ToolCall { id: id.to_string(), name: name.to_string(), arguments: arguments.to_string(), extra_content: None, }], usage: None, reasoning_content: Some(format!("need {name}")), } } fn all_messages_text(messages: &[ChatMessage]) -> String { messages .iter() .map(|message| format!("{}:{}", message.role, message.content)) .collect::>() .join("\n") } fn first_monitor_id(messages: &[ChatMessage]) -> String { let text = all_messages_text(messages); let start = text .find("mon_") .unwrap_or_else(|| panic!("expected monitor id in messages:\n{text}")); text[start..] .chars() .take_while(|ch| ch.is_ascii_alphanumeric() || *ch == '_') .collect() } fn contains_tool_json_pair(text: &str, key: &str, value: &str) -> bool { text.contains(&format!("\"{key}\":\"{value}\"")) || text.contains(&format!("\\\"{key}\\\":\\\"{value}\\\"")) } fn monitor_tools(workspace: &Path, autonomy: AutonomyLevel) -> Vec> { let security = Arc::new(SecurityPolicy { autonomy, workspace_dir: workspace.to_path_buf(), action_dir: workspace.to_path_buf(), ..SecurityPolicy::default() }); vec![ Box::new(MonitorTool::new( security, Arc::new(NativeRuntime::new()), AuditLogger::disabled(), )), Box::new(MonitorListTool), Box::new(MonitorStopTool), Box::new(MonitorReadTool), ] } fn build_agent( workspace: &Path, provider: Arc, tools: Vec>, max_tool_iterations: usize, ) -> Agent { let mut agent = Agent::builder() .provider_arc(provider) .tools(tools) .memory(Arc::new(StubMemory::default())) .tool_dispatcher(Box::new(NativeToolDispatcher)) .config(AgentConfig { max_tool_iterations, max_history_messages: 16, ..AgentConfig::default() }) .model_name("monitor-e2e-model".to_string()) .temperature(0.0) .workspace_dir(workspace.to_path_buf()) .workflows(Vec::new()) .auto_save(false) .event_context("monitor-e2e-session", "monitor-e2e-channel") .agent_definition_name("orchestrator") .omit_profile(true) .omit_memory_md(true) .explicit_preferences_enabled(false) .build() .unwrap(); agent.set_connected_integrations(Vec::new()); agent } async fn run_monitor_turn( tmp: &TempDir, provider: Arc, autonomy: AutonomyLevel, max_tool_iterations: usize, ) -> String { let mut agent = build_agent( tmp.path(), provider, monitor_tools(tmp.path(), autonomy), max_tool_iterations, ); agent.set_run_queue(Some(RunQueue::new())); with_thread_id("monitor-agent-e2e-thread", async move { agent .turn("Start a background monitor and react when it reports.") .await .unwrap() }) .await } #[tokio::test] async fn orchestrator_monitor_line_reaches_next_llm_call_as_collect_context() { let _guard = monitor_e2e_lock().lock().await; let tmp = tempfile::tempdir().unwrap(); let provider = ScriptedProvider::new(vec![ ProviderStep::Static(tool_response( "call-monitor", "monitor", json!({ "command": "printf 'MONITOR_READY\\n'; sleep 0.1", "description": "e2e line injection monitor", "timeout_ms": 2_000, "persistent": false }), )), ProviderStep::Delayed( Duration::from_millis(150), tool_response("call-list", "monitor_list", json!({})), ), ProviderStep::FromHistory(Box::new(|messages| { let text = all_messages_text(messages); assert!( text.contains("[Additional context from user]: [Monitor mon_"), "expected monitor collect injection in messages:\n{text}" ); assert!( text.contains("MONITOR_READY"), "expected monitor line in messages:\n{text}" ); text_response("orchestrator observed monitor event") })), ]); let answer = run_monitor_turn(&tmp, provider.clone(), AutonomyLevel::Supervised, 5).await; assert_eq!(answer, "orchestrator observed monitor event"); let requests = provider.requests(); assert_eq!(requests.len(), 3); assert!(requests[0].tool_names.contains(&"monitor".to_string())); assert!(requests[0].tool_names.contains(&"monitor_list".to_string())); assert!(requests[2] .messages .iter() .any(|message| message.content.contains("MONITOR_READY"))); } #[tokio::test] async fn orchestrator_reads_monitor_output_after_registration() { let _guard = monitor_e2e_lock().lock().await; let tmp = tempfile::tempdir().unwrap(); let provider = ScriptedProvider::new(vec![ ProviderStep::Static(tool_response( "call-monitor", "monitor", json!({ "command": "printf 'READBACK_LINE\\n'", "description": "e2e monitor read", "timeout_ms": 2_000, "persistent": false }), )), ProviderStep::Delayed( Duration::from_millis(120), tool_response("call-list", "monitor_list", json!({})), ), ProviderStep::FromHistory(Box::new(|messages| { let monitor_id = first_monitor_id(messages); tool_response( "call-read", "monitor_read", json!({ "monitor_id": monitor_id, "max_bytes": 4096 }), ) })), ProviderStep::FromHistory(Box::new(|messages| { let text = all_messages_text(messages); assert!(text.contains("READBACK_LINE")); text_response("orchestrator read monitor output") })), ]); let answer = run_monitor_turn(&tmp, provider, AutonomyLevel::Supervised, 6).await; assert_eq!(answer, "orchestrator read monitor output"); } #[tokio::test] async fn orchestrator_stops_a_running_monitor_by_id_from_tool_result() { let _guard = monitor_e2e_lock().lock().await; let tmp = tempfile::tempdir().unwrap(); let provider = ScriptedProvider::new(vec![ ProviderStep::Static(tool_response( "call-monitor", "monitor", json!({ "command": "sleep 5", "description": "e2e stoppable monitor", "timeout_ms": 5_000, "persistent": false }), )), ProviderStep::FromHistory(Box::new(|messages| { let monitor_id = first_monitor_id(messages); tool_response( "call-stop", "monitor_stop", json!({ "monitor_id": monitor_id }), ) })), ProviderStep::FromHistory(Box::new(|messages| { let text = all_messages_text(messages); assert!( contains_tool_json_pair(&text, "status", "stopped"), "expected stopped status in messages:\n{text}" ); text_response("orchestrator stopped monitor") })), ]); let answer = run_monitor_turn(&tmp, provider, AutonomyLevel::Supervised, 5).await; assert_eq!(answer, "orchestrator stopped monitor"); } #[tokio::test] async fn orchestrator_sees_monitor_timeout_status_through_list() { let _guard = monitor_e2e_lock().lock().await; let tmp = tempfile::tempdir().unwrap(); let provider = ScriptedProvider::new(vec![ ProviderStep::Static(tool_response( "call-monitor", "monitor", json!({ "command": "sleep 1", "description": "e2e timed monitor", "timeout_ms": 40, "persistent": false }), )), ProviderStep::Delayed( Duration::from_millis(120), tool_response("call-list", "monitor_list", json!({})), ), ProviderStep::FromHistory(Box::new(|messages| { let text = all_messages_text(messages); assert!( text.contains("e2e timed monitor") && contains_tool_json_pair(&text, "status", "timed_out"), "expected timed_out status in messages:\n{text}" ); text_response("orchestrator saw monitor timeout") })), ]); let answer = run_monitor_turn(&tmp, provider, AutonomyLevel::Supervised, 5).await; assert_eq!(answer, "orchestrator saw monitor timeout"); } #[tokio::test] async fn orchestrator_gets_denial_when_monitor_command_violates_policy() { let _guard = monitor_e2e_lock().lock().await; let tmp = tempfile::tempdir().unwrap(); let provider = ScriptedProvider::new(vec![ ProviderStep::Static(tool_response( "call-monitor", "monitor", json!({ "command": "touch denied-monitor-file", "description": "e2e denied monitor", "timeout_ms": 2_000, "persistent": false }), )), ProviderStep::FromHistory(Box::new(|messages| { let text = all_messages_text(messages); assert!( text.contains("[policy-blocked] Tool 'monitor' was blocked by the security policy"), "expected policy denial in messages:\n{text}" ); assert!( !text.contains("\"monitorId\":\"mon_"), "denied monitor must not return a monitor id:\n{text}" ); text_response("orchestrator received monitor denial") })), ]); let answer = run_monitor_turn(&tmp, provider, AutonomyLevel::ReadOnly, 4).await; assert_eq!(answer, "orchestrator received monitor denial"); assert!(!tmp.path().join("denied-monitor-file").exists()); }