mod common; use std::sync::Arc; use std::time::Duration; use common::MockTransport; use sgclaw::agent::handle_browser_message; use sgclaw::agent::runtime::{browser_action_tool_definition, execute_task_with_provider}; use sgclaw::llm::{ChatMessage, LlmError, LlmProvider, ToolDefinition, ToolFunctionCall}; use sgclaw::pipe::{Action, AgentMessage, BrowserMessage, BrowserPipeTool, Timing}; use sgclaw::security::MacPolicy; struct FakeProvider { calls: Vec, } impl LlmProvider for FakeProvider { fn chat( &self, _messages: &[ChatMessage], _tools: &[ToolDefinition], ) -> Result, LlmError> { Ok(self.calls.clone()) } } fn test_policy() -> MacPolicy { MacPolicy::from_json_str( r#"{ "version": "1.0", "domains": { "allowed": ["www.baidu.com"] }, "pipe_actions": { "allowed": ["click", "type", "navigate", "getText"], "blocked": [] } }"#, ) .unwrap() } #[test] fn browser_action_tool_definition_uses_expected_name() { let tool = browser_action_tool_definition(); assert_eq!(tool.name, "browser_action"); assert_eq!(tool.parameters["required"][0], "action"); assert_eq!(tool.parameters["required"][1], "expected_domain"); } #[test] fn runtime_executes_provider_tool_calls_and_returns_summary() { let transport = Arc::new(MockTransport::new(vec![ BrowserMessage::Response { seq: 1, success: true, data: serde_json::json!({ "navigated": true }), aom_snapshot: vec![], timing: Timing { queue_ms: 1, exec_ms: 10, }, }, BrowserMessage::Response { seq: 2, success: true, data: serde_json::json!({ "typed": true }), aom_snapshot: vec![], timing: Timing { queue_ms: 1, exec_ms: 10, }, }, ])); let browser_tool = BrowserPipeTool::new( transport.clone(), test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); let provider = FakeProvider { calls: vec![ ToolFunctionCall { id: "call-1".to_string(), name: "browser_action".to_string(), arguments: serde_json::json!({ "action": "navigate", "expected_domain": "www.baidu.com", "url": "https://www.baidu.com" }), }, ToolFunctionCall { id: "call-2".to_string(), name: "browser_action".to_string(), arguments: serde_json::json!({ "action": "type", "expected_domain": "www.baidu.com", "selector": "#kw", "text": "天气", "clear_first": true }), }, ], }; let summary = execute_task_with_provider( transport.as_ref(), &browser_tool, &provider, "打开百度搜索天气", ) .unwrap(); let sent = transport.sent_messages(); assert_eq!(summary, "已通过 Agent 执行任务: 打开百度搜索天气"); assert!(matches!( &sent[0], AgentMessage::LogEntry { level, message } if level == "info" && message == "navigate www.baidu.com" )); assert!(matches!( &sent[1], AgentMessage::Command { seq, action, .. } if *seq == 1 && action == &Action::Navigate )); assert!(matches!( &sent[2], AgentMessage::LogEntry { level, message } if level == "info" && message == "type www.baidu.com" )); assert!(matches!( &sent[3], AgentMessage::Command { seq, action, .. } if *seq == 2 && action == &Action::Type )); } #[test] fn legacy_agent_runtime_is_explicitly_dev_only() { assert!(sgclaw::agent::runtime::LEGACY_DEV_ONLY); } #[test] fn production_submit_task_does_not_route_into_legacy_runtime_without_llm_config() { std::env::remove_var("DEEPSEEK_API_KEY"); std::env::remove_var("DEEPSEEK_BASE_URL"); std::env::remove_var("DEEPSEEK_MODEL"); let transport = Arc::new(MockTransport::new(vec![])); let browser_tool = BrowserPipeTool::new( transport.clone(), test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); handle_browser_message( transport.as_ref(), &browser_tool, BrowserMessage::SubmitTask { instruction: "打开百度".to_string(), conversation_id: String::new(), messages: vec![], page_url: String::new(), page_title: String::new(), }, ) .unwrap(); let sent = transport.sent_messages(); assert!(matches!( sent.last(), Some(AgentMessage::TaskComplete { success, summary }) if !success && summary.contains("未配置大语言模型") )); assert!(!sent .iter() .any(|message| { matches!(message, AgentMessage::Command { .. }) })); }