mod common; use std::fs; use std::io::{Read, Write}; use std::net::TcpListener; use std::path::PathBuf; use std::sync::{Arc, Mutex, OnceLock}; use std::thread; use std::time::Duration; use common::MockTransport; use serde_json::{json, Value}; use sgclaw::agent::{ handle_browser_message, handle_browser_message_with_context, AgentRuntimeContext, }; use sgclaw::compat::runtime::{execute_task, execute_task_with_sgclaw_settings, CompatTaskContext}; use sgclaw::config::{DeepSeekSettings, SgClawSettings}; use sgclaw::pipe::{ Action, AgentMessage, BrowserMessage, BrowserPipeTool, ConversationMessage, Timing, }; use sgclaw::runtime::RuntimeProfile; use sgclaw::security::MacPolicy; use uuid::Uuid; fn env_lock() -> &'static Mutex<()> { static LOCK: OnceLock> = OnceLock::new(); LOCK.get_or_init(|| Mutex::new(())) } fn test_policy() -> MacPolicy { policy_for_domains(&["www.baidu.com"]) } fn zhihu_test_policy() -> MacPolicy { policy_for_domains(&["www.zhihu.com", "zhuanlan.zhihu.com"]) } fn policy_for_domains(domains: &[&str]) -> MacPolicy { MacPolicy::from_json_str( &json!({ "version": "1.0", "domains": { "allowed": domains }, "pipe_actions": { "allowed": ["click", "type", "navigate", "getText", "waitForSelector", "eval"], "blocked": [] } }) .to_string(), ) .unwrap() } fn temp_workspace_root() -> PathBuf { let root = std::env::temp_dir().join(format!("sgclaw-compat-runtime-{}", Uuid::new_v4())); std::fs::create_dir_all(&root).unwrap(); root } fn write_deepseek_config(root: &PathBuf, api_key: &str, base_url: &str, model: &str) -> PathBuf { write_deepseek_config_with_skills_dir(root, api_key, base_url, model, None) } fn write_deepseek_config_with_skills_dir( root: &PathBuf, api_key: &str, base_url: &str, model: &str, skills_dir: Option<&str>, ) -> PathBuf { let config_path = root.join("sgclaw_config.json"); let mut payload = json!({ "apiKey": api_key, "baseUrl": base_url, "model": model, }); if let Some(skills_dir) = skills_dir { payload["skillsDir"] = json!(skills_dir); } fs::write( &config_path, serde_json::to_string_pretty(&payload).unwrap(), ) .unwrap(); config_path } fn write_skill_package(skills_dir: &std::path::Path, skill_name: &str, body: &str) { let skill_dir = skills_dir.join(skill_name); fs::create_dir_all(&skill_dir).unwrap(); fs::write(skill_dir.join("SKILL.md"), body).unwrap(); } fn write_skill_manifest_package( skills_dir: &std::path::Path, skill_name: &str, manifest: &str, ) -> PathBuf { let skill_dir = skills_dir.join(skill_name); fs::create_dir_all(&skill_dir).unwrap(); fs::write(skill_dir.join("SKILL.toml"), manifest).unwrap(); skill_dir } fn write_skill_script(skill_dir: &std::path::Path, relative_path: &str, body: &str) { let script_path = skill_dir.join(relative_path); if let Some(parent) = script_path.parent() { fs::create_dir_all(parent).unwrap(); } fs::write(script_path, body).unwrap(); } fn real_skill_lib_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() .unwrap() .join("skill_lib") } fn success_browser_response(seq: u64, data: Value) -> BrowserMessage { BrowserMessage::Response { seq, success: true, data, aom_snapshot: vec![], timing: Timing { queue_ms: 1, exec_ms: 10, }, } } fn request_tool_names(request: &Value) -> Vec { request["tools"] .as_array() .cloned() .unwrap_or_default() .into_iter() .filter_map(|tool| tool["function"]["name"].as_str().map(str::to_string)) .collect::>() } fn tool_message_content<'a>(request: &'a Value, tool_call_id: &str) -> Option<&'a str> { request["messages"].as_array().and_then(|messages| { messages.iter().find_map(|message| { (message["role"].as_str() == Some("tool") && message["tool_call_id"].as_str() == Some(tool_call_id)) .then(|| message["content"].as_str()) .flatten() }) }) } fn start_fake_deepseek_server( responses: Vec, ) -> (String, Arc>>, thread::JoinHandle<()>) { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); listener.set_nonblocking(true).unwrap(); let address = format!("http://{}", listener.local_addr().unwrap()); let requests = Arc::new(Mutex::new(Vec::new())); let request_log = requests.clone(); let handle = thread::spawn(move || { for response in responses { let deadline = std::time::Instant::now() + Duration::from_secs(5); let (mut stream, _) = loop { match listener.accept() { Ok(pair) => break pair, Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => { assert!( std::time::Instant::now() < deadline, "timed out waiting for provider request" ); thread::sleep(Duration::from_millis(10)); } Err(err) => panic!("failed to accept provider request: {err}"), } }; let body = read_http_json_body(&mut stream); request_log.lock().unwrap().push(body); let payload = response.to_string(); let reply = format!( "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", payload.as_bytes().len(), payload ); stream.write_all(reply.as_bytes()).unwrap(); stream.flush().unwrap(); } }); (address, requests, handle) } fn read_http_json_body(stream: &mut impl Read) -> Value { let mut buffer = Vec::new(); let mut headers_end = None; while headers_end.is_none() { let mut chunk = [0_u8; 1024]; let bytes = stream.read(&mut chunk).unwrap(); assert!(bytes > 0, "unexpected EOF while reading headers"); buffer.extend_from_slice(&chunk[..bytes]); headers_end = buffer.windows(4).position(|window| window == b"\r\n\r\n"); } let headers_end = headers_end.unwrap() + 4; let headers = String::from_utf8(buffer[..headers_end].to_vec()).unwrap(); let content_length = headers .lines() .find_map(|line| { let (name, value) = line.split_once(':')?; name.eq_ignore_ascii_case("content-length") .then(|| value.trim().parse::().unwrap()) }) .unwrap(); while buffer.len() < headers_end + content_length { let mut chunk = vec![0_u8; content_length]; let bytes = stream.read(&mut chunk).unwrap(); assert!(bytes > 0, "unexpected EOF while reading body"); buffer.extend_from_slice(&chunk[..bytes]); } serde_json::from_slice(&buffer[headers_end..headers_end + content_length]).unwrap() } fn task_complete_summary(sent: &[AgentMessage]) -> String { sent.iter() .find_map(|message| match message { AgentMessage::TaskComplete { success, summary } if *success => Some(summary.clone()), _ => None, }) .expect("expected successful task completion") } fn extract_generated_artifact_path(summary: &str, extension: &str) -> PathBuf { summary .split_whitespace() .find(|token| token.ends_with(extension)) .map(PathBuf::from) .expect("expected artifact path in task summary") } #[test] fn compat_runtime_uses_zeroclaw_provider_path_and_executes_browser_actions() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let first_response = json!({ "choices": [{ "message": { "content": "", "tool_calls": [ { "id": "call_1", "type": "function", "function": { "name": "browser_action", "arguments": serde_json::to_string(&json!({ "action": "navigate", "expected_domain": "www.baidu.com", "url": "https://www.baidu.com" })).unwrap() } }, { "id": "call_2", "type": "function", "function": { "name": "browser_action", "arguments": serde_json::to_string(&json!({ "action": "type", "expected_domain": "www.baidu.com", "selector": "#kw", "text": "天气", "clear_first": true })).unwrap() } } ] } }], "usage": { "prompt_tokens": 12, "completion_tokens": 7 } }); let second_response = json!({ "choices": [{ "message": { "content": "已通过 ZeroClaw 执行任务: 打开百度搜索天气" } }], "usage": { "prompt_tokens": 15, "completion_tokens": 8 } }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![first_response, second_response]); std::env::set_var("DEEPSEEK_API_KEY", "deepseek-test-key"); std::env::set_var("DEEPSEEK_BASE_URL", base_url); std::env::set_var("DEEPSEEK_MODEL", "deepseek-chat"); let workspace_root = temp_workspace_root(); let settings = DeepSeekSettings::from_env().unwrap(); let transport = Arc::new(MockTransport::new(vec![ BrowserMessage::Response { seq: 1, success: true, data: json!({ "navigated": true }), aom_snapshot: vec![], timing: Timing { queue_ms: 1, exec_ms: 10, }, }, BrowserMessage::Response { seq: 2, success: true, data: json!({ "typed": true }), aom_snapshot: vec![], timing: Timing { queue_ms: 1, exec_ms: 11, }, }, ])); 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 summary = execute_task( transport.as_ref(), browser_tool, "打开百度搜索天气", &CompatTaskContext::default(), &workspace_root, &settings, ) .unwrap(); server_handle.join().unwrap(); let request_bodies = requests.lock().unwrap().clone(); let sent = transport.sent_messages(); assert_eq!(summary, "已通过 ZeroClaw 执行任务: 打开百度搜索天气"); assert_eq!(request_bodies.len(), 2); assert_eq!(request_bodies[0]["model"], json!("deepseek-chat")); assert!(request_tool_names(&request_bodies[0]).contains(&"browser_action".to_string())); assert!(request_tool_names(&request_bodies[0]).contains(&"superrpa_browser".to_string())); assert!(request_bodies[1].to_string().contains("tool_call_id")); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "info" && message == "navigate https://www.baidu.com" ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "info" && message == "type 天气 into #kw" ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::Command { action, .. } if action == &Action::Navigate ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::Command { action, .. } if action == &Action::Type ) })); } #[test] fn compat_runtime_includes_default_workspace_skills_in_provider_request() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let response = json!({ "choices": [{ "message": { "content": "已识别默认 workspace skill" } }] }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]); let workspace_root = temp_workspace_root(); let default_skills_dir = workspace_root .join(".sgclaw-zeroclaw-workspace") .join("skills"); write_skill_package( &default_skills_dir, "workspace-zhihu-skill", "# Workspace Zhihu Skill\nUse this workspace-local skill.\n", ); let settings = DeepSeekSettings { api_key: "deepseek-test-key".to_string(), base_url, model: "deepseek-chat".to_string(), skills_dir: None, }; 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)); let summary = execute_task( transport.as_ref(), browser_tool, "列出当前可用 skill", &CompatTaskContext::default(), &workspace_root, &settings, ) .unwrap(); server_handle.join().unwrap(); let request_bodies = requests.lock().unwrap().clone(); assert_eq!(summary, "已识别默认 workspace skill"); assert_eq!(request_bodies.len(), 1); assert!(request_bodies[0] .to_string() .contains("workspace-zhihu-skill")); } #[test] fn handle_browser_message_loads_skills_from_configured_skills_dir() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let response = json!({ "choices": [{ "message": { "content": "已识别自定义 skill 目录" } }] }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]); let workspace_root = temp_workspace_root(); let default_skills_dir = workspace_root .join(".sgclaw-zeroclaw-workspace") .join("skills"); write_skill_package( &default_skills_dir, "workspace-only-skill", "# Workspace Only Skill\nThis skill should be ignored when skillsDir is set.\n", ); let custom_skill_repo = workspace_root.join("skill_lib"); let custom_skills_dir = custom_skill_repo.join("skills"); write_skill_package( &custom_skills_dir, "configured-zhihu-skill", "# Configured Zhihu Skill\nUse the configured skills directory.\n", ); std::env::remove_var("DEEPSEEK_API_KEY"); std::env::remove_var("DEEPSEEK_BASE_URL"); std::env::remove_var("DEEPSEEK_MODEL"); let config_path = write_deepseek_config_with_skills_dir( &workspace_root, "deepseek-test-key", &base_url, "deepseek-chat", Some("skill_lib"), ); let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); 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_with_context( transport.as_ref(), &browser_tool, &runtime_context, BrowserMessage::SubmitTask { instruction: "告诉我当前有哪些 zhihu skill".to_string(), conversation_id: String::new(), messages: vec![], page_url: String::new(), page_title: String::new(), }, ) .unwrap(); server_handle.join().unwrap(); let sent = transport.sent_messages(); let request_bodies = requests.lock().unwrap().clone(); let first_request = request_bodies[0].to_string(); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::TaskComplete { success, summary } if *success && summary == "已识别自定义 skill 目录" ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "info" && message.contains("loaded skills: configured-zhihu-skill@0.1.0") ) })); assert_eq!(request_bodies.len(), 1); assert!(first_request.contains("configured-zhihu-skill")); assert!(!first_request.contains("workspace-only-skill")); } #[test] fn handle_browser_message_routes_supported_instruction_to_compat_runtime_when_llm_is_configured() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let first_response = json!({ "choices": [{ "message": { "content": "", "tool_calls": [ { "id": "call_1", "type": "function", "function": { "name": "browser_action", "arguments": serde_json::to_string(&json!({ "action": "navigate", "expected_domain": "www.baidu.com", "url": "https://www.baidu.com" })).unwrap() } }, { "id": "call_2", "type": "function", "function": { "name": "browser_action", "arguments": serde_json::to_string(&json!({ "action": "type", "expected_domain": "www.baidu.com", "selector": "#kw", "text": "天气", "clear_first": true })).unwrap() } }, { "id": "call_3", "type": "function", "function": { "name": "browser_action", "arguments": serde_json::to_string(&json!({ "action": "click", "expected_domain": "www.baidu.com", "selector": "#su" })).unwrap() } } ] } }] }); let second_response = json!({ "choices": [{ "message": { "content": "已在百度搜索天气" } }] }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![first_response, second_response]); std::env::remove_var("DEEPSEEK_API_KEY"); std::env::remove_var("DEEPSEEK_BASE_URL"); std::env::remove_var("DEEPSEEK_MODEL"); let workspace_root = temp_workspace_root(); let config_path = write_deepseek_config( &workspace_root, "deepseek-test-key", &base_url, "deepseek-chat", ); let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); let transport = Arc::new(MockTransport::new(vec![ BrowserMessage::Response { seq: 1, success: true, data: json!({ "navigated": true }), aom_snapshot: vec![], timing: Timing { queue_ms: 1, exec_ms: 10, }, }, BrowserMessage::Response { seq: 2, success: true, data: json!({ "typed": true }), aom_snapshot: vec![], timing: Timing { queue_ms: 1, exec_ms: 10, }, }, BrowserMessage::Response { seq: 3, success: true, data: json!({ "clicked": 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)); handle_browser_message_with_context( transport.as_ref(), &browser_tool, &runtime_context, BrowserMessage::SubmitTask { instruction: "打开百度搜索天气".to_string(), conversation_id: String::new(), messages: vec![], page_url: String::new(), page_title: String::new(), }, ) .unwrap(); server_handle.join().unwrap(); let sent = transport.sent_messages(); let request_bodies = requests.lock().unwrap().clone(); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::TaskComplete { success, summary } if *success && summary == "已在百度搜索天气" ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "info" && message == "sgclaw runtime version=0.1.0 protocol=1.0" ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "mode" && message == "compat_llm_primary" ) })); assert!(!sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "mode" && message == "deterministic_planner" ) })); assert_eq!(request_bodies.len(), 2); } #[test] fn handle_browser_message_emits_plan_preview_before_runtime_execution() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let first_response = json!({ "choices": [{ "message": { "content": "", "tool_calls": [{ "id": "call_1", "type": "function", "function": { "name": "browser_action", "arguments": serde_json::to_string(&json!({ "action": "navigate", "expected_domain": "www.baidu.com", "url": "https://www.baidu.com" })).unwrap() } }] } }] }); let second_response = json!({ "choices": [{ "message": { "content": "已打开百度首页" } }] }); let (base_url, _requests, server_handle) = start_fake_deepseek_server(vec![first_response, second_response]); let workspace_root = temp_workspace_root(); let config_path = workspace_root.join("sgclaw_config.json"); fs::write( &config_path, serde_json::to_string_pretty(&json!({ "apiKey": "deepseek-test-key", "baseUrl": base_url, "model": "deepseek-chat", "plannerMode": "zeroclawPlanFirst" })) .unwrap(), ) .unwrap(); let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); let transport = Arc::new(MockTransport::new(vec![success_browser_response( 1, json!({ "navigated": true }), )])); 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_with_context( transport.as_ref(), &browser_tool, &runtime_context, BrowserMessage::SubmitTask { instruction: "打开百度".to_string(), conversation_id: String::new(), messages: vec![], page_url: String::new(), page_title: String::new(), }, ) .unwrap(); server_handle.join().unwrap(); let sent = transport.sent_messages(); let preview_index = sent .iter() .position(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "plan" && message.contains("navigate https://www.baidu.com") ) }) .expect("expected plan preview log entry"); let navigate_index = sent .iter() .position(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "info" && message == "navigate https://www.baidu.com" ) }) .expect("expected runtime navigate log entry"); assert!(preview_index < navigate_index); } #[test] fn handle_browser_message_falls_back_to_compat_runtime_for_unsupported_instruction() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let first_response = json!({ "choices": [{ "message": { "content": "", "tool_calls": [{ "id": "call_1", "type": "function", "function": { "name": "browser_action", "arguments": serde_json::to_string(&json!({ "action": "navigate", "expected_domain": "www.baidu.com", "url": "https://www.baidu.com" })).unwrap() } }] } }] }); let second_response = json!({ "choices": [{ "message": { "content": "来自 ZeroClaw runtime" } }] }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![first_response, second_response]); std::env::set_var("DEEPSEEK_API_KEY", "deepseek-test-key"); std::env::set_var("DEEPSEEK_BASE_URL", base_url); std::env::set_var("DEEPSEEK_MODEL", "deepseek-chat"); let workspace_root = temp_workspace_root(); let original_dir = std::env::current_dir().unwrap(); std::env::set_current_dir(&workspace_root).unwrap(); let transport = Arc::new(MockTransport::new(vec![BrowserMessage::Response { seq: 1, success: true, data: json!({ "navigated": 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)); 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(); server_handle.join().unwrap(); std::env::set_current_dir(original_dir).unwrap(); let sent = transport.sent_messages(); let request_bodies = requests.lock().unwrap().clone(); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::TaskComplete { success, summary } if *success && summary == "来自 ZeroClaw runtime" ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "mode" && message == "compat_llm_primary" ) })); assert_eq!(request_bodies.len(), 2); } #[test] fn handle_browser_message_requires_llm_configuration_when_no_model_is_available() { 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("未配置大语言模型") )); } #[test] fn compat_runtime_includes_prior_turns_in_follow_up_provider_request() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let first_response = json!({ "choices": [{ "message": { "content": "", "tool_calls": [{ "id": "call_1", "type": "function", "function": { "name": "browser_action", "arguments": serde_json::to_string(&json!({ "action": "navigate", "expected_domain": "www.zhihu.com", "url": "https://www.zhihu.com/search?q=天气&type=content" })).unwrap() } }] } }] }); let second_response = json!({ "choices": [{ "message": { "content": "已在知乎搜索天气" } }] }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![first_response, second_response]); let workspace_root = temp_workspace_root(); let settings = DeepSeekSettings { api_key: "deepseek-test-key".to_string(), base_url, model: "deepseek-chat".to_string(), skills_dir: None, }; let transport = Arc::new(MockTransport::new(vec![BrowserMessage::Response { seq: 1, success: true, data: json!({ "navigated": 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 task_context = CompatTaskContext { conversation_id: Some("conversation-1".to_string()), messages: vec![ ConversationMessage { role: "user".to_string(), content: "打开百度搜索天气".to_string(), }, ConversationMessage { role: "assistant".to_string(), content: "已在百度搜索天气".to_string(), }, ], page_url: Some("https://www.zhihu.com/".to_string()), page_title: Some("知乎".to_string()), }; let summary = execute_task( transport.as_ref(), browser_tool, "打开知乎搜索天气", &task_context, &workspace_root, &settings, ) .unwrap(); server_handle.join().unwrap(); let request_bodies = requests.lock().unwrap().clone(); let first_request_messages = request_bodies[0]["messages"] .as_array() .cloned() .unwrap_or_default(); assert_eq!(summary, "已在知乎搜索天气"); assert!(first_request_messages.iter().any(|message| { message["role"] == json!("user") && message["content"] == json!("打开百度搜索天气") })); assert!(first_request_messages.iter().any(|message| { message["role"] == json!("assistant") && message["content"] == json!("已在百度搜索天气") })); } #[test] fn compat_runtime_does_not_forward_raw_aom_snapshot_back_to_provider() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let first_response = json!({ "choices": [{ "message": { "content": "", "tool_calls": [{ "id": "call_1", "type": "function", "function": { "name": "browser_action", "arguments": serde_json::to_string(&json!({ "action": "navigate", "expected_domain": "www.baidu.com", "url": "https://www.baidu.com" })).unwrap() } }] } }] }); let second_response = json!({ "choices": [{ "message": { "content": "来自 ZeroClaw runtime" } }] }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![first_response, second_response]); let workspace_root = temp_workspace_root(); let settings = DeepSeekSettings { api_key: "deepseek-test-key".to_string(), base_url, model: "deepseek-chat".to_string(), skills_dir: None, }; let large_snapshot_marker = "snapshot-marker ".repeat(2048); let transport = Arc::new(MockTransport::new(vec![BrowserMessage::Response { seq: 1, success: true, data: json!({ "navigated": true }), aom_snapshot: vec![json!({ "role": "RootWebArea", "name": "百度一下,你就知道", "text": large_snapshot_marker })], 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 summary = execute_task( transport.as_ref(), browser_tool, "打开百度首页", &CompatTaskContext::default(), &workspace_root, &settings, ) .unwrap(); server_handle.join().unwrap(); let request_bodies = requests.lock().unwrap().clone(); let follow_up_request = request_bodies[1].to_string(); assert_eq!(summary, "来自 ZeroClaw runtime"); assert_eq!(request_bodies.len(), 2); assert!(!follow_up_request.contains("snapshot-marker")); } #[test] fn compat_runtime_injects_browser_contract_and_page_context_into_provider_request() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let response = json!({ "choices": [{ "message": { "content": "已收到页面上下文" } }] }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]); let workspace_root = temp_workspace_root(); let settings = DeepSeekSettings { api_key: "deepseek-test-key".to_string(), base_url, model: "deepseek-chat".to_string(), skills_dir: None, }; 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)); let task_context = CompatTaskContext { conversation_id: Some("conversation-ctx".to_string()), messages: vec![], page_url: Some("https://www.zhihu.com/hot".to_string()), page_title: Some("知乎热榜".to_string()), }; let summary = execute_task( transport.as_ref(), browser_tool, "统计一下知乎热帮信息", &task_context, &workspace_root, &settings, ) .unwrap(); server_handle.join().unwrap(); let request_bodies = requests.lock().unwrap().clone(); let first_request_messages = request_bodies[0]["messages"] .as_array() .cloned() .unwrap_or_default(); let flattened = first_request_messages .iter() .filter_map(|message| message["content"].as_str()) .collect::>() .join("\n"); assert_eq!(summary, "已收到页面上下文"); assert!( flattened.contains("expected_domain must be the bare hostname"), "missing browser tool contract guidance: {flattened}" ); assert!( flattened.contains("document.querySelector"), "missing CSS selector guidance: {flattened}" ); assert!( flattened.contains("https://www.zhihu.com/hot"), "missing page url context: {flattened}" ); assert!( flattened.contains("知乎热榜"), "missing page title context: {flattened}" ); } #[test] fn compat_runtime_can_complete_a_text_only_turn_without_browser_tool_calls() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let response = json!({ "choices": [{ "message": { "content": "这是纯文本回答" } }] }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]); let workspace_root = temp_workspace_root(); let mut settings = SgClawSettings::from_legacy_deepseek_fields( "deepseek-test-key".to_string(), base_url, "deepseek-chat".to_string(), None, ) .unwrap(); settings.runtime_profile = RuntimeProfile::GeneralAssistant; 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)); let summary = execute_task_with_sgclaw_settings( transport.as_ref(), browser_tool, "直接回答:你好", &CompatTaskContext::default(), &workspace_root, &settings, ) .unwrap(); server_handle.join().unwrap(); let request_bodies = requests.lock().unwrap().clone(); let flattened = request_bodies[0]["messages"] .as_array() .cloned() .unwrap_or_default() .iter() .filter_map(|message| message["content"].as_str()) .collect::>() .join("\n"); let tool_entries = request_bodies[0]["tools"] .as_array() .cloned() .unwrap_or_default(); let tool_names = tool_entries .into_iter() .filter_map(|tool| tool["function"]["name"].as_str().map(str::to_string)) .collect::>(); let sent = transport.sent_messages(); assert_eq!(summary, "这是纯文本回答"); assert!(!flattened.contains("Browser tool contract")); assert!(!tool_names.contains(&"browser_action".to_string())); assert!(!sent .iter() .any(|message| { matches!(message, AgentMessage::Command { .. }) })); } #[test] fn compat_runtime_allows_read_skill_under_compact_mode_policy() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let response = json!({ "choices": [{ "message": { "content": "已看到 compact skill 工具" } }] }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]); let workspace_root = temp_workspace_root(); let default_skills_dir = workspace_root .join(".sgclaw-zeroclaw-workspace") .join("skills"); write_skill_package( &default_skills_dir, "workspace-zhihu-skill", "# Workspace Zhihu Skill\nUse this workspace-local skill.\n", ); let settings = SgClawSettings::from_legacy_deepseek_fields( "deepseek-test-key".to_string(), base_url, "deepseek-chat".to_string(), None, ) .unwrap(); 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)); let summary = execute_task_with_sgclaw_settings( transport.as_ref(), browser_tool, "告诉我当前有哪些 skill", &CompatTaskContext::default(), &workspace_root, &settings, ) .unwrap(); server_handle.join().unwrap(); let request_bodies = requests.lock().unwrap().clone(); let tool_entries = request_bodies[0]["tools"] .as_array() .cloned() .unwrap_or_default(); let tool_names = tool_entries .into_iter() .filter_map(|tool| tool["function"]["name"].as_str().map(str::to_string)) .collect::>(); assert_eq!(summary, "已看到 compact skill 工具"); assert!(tool_names.contains(&"browser_action".to_string())); assert!(tool_names.contains(&"superrpa_browser".to_string())); assert!(tool_names.contains(&"read_skill".to_string())); assert!(!tool_names.contains(&"zhihu-hotlist_extract_hotlist".to_string())); } #[test] fn compat_runtime_exposes_browser_script_skill_tools_in_browser_attached_mode() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let response = json!({ "choices": [{ "message": { "content": "已看到 browser_script skill 工具" } }] }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]); let workspace_root = temp_workspace_root(); let default_skills_dir = workspace_root .join(".sgclaw-zeroclaw-workspace") .join("skills"); let skill_dir = write_skill_manifest_package( &default_skills_dir, "workspace-zhihu-skill", r#" [skill] name = "workspace-zhihu-skill" description = "Extract Zhihu hotlist rows with a packaged browser script." version = "0.1.0" [[tools]] name = "extract_hotlist" description = "Extract structured hotlist rows from the current Zhihu page." kind = "browser_script" command = "scripts/extract_hotlist.js" [tools.args] top_n = "How many hotlist rows to extract." "#, ); write_skill_script( &skill_dir, "scripts/extract_hotlist.js", "return { rows: [] };", ); let mut settings = SgClawSettings::from_legacy_deepseek_fields( "deepseek-test-key".to_string(), base_url, "deepseek-chat".to_string(), None, ) .unwrap(); settings.runtime_profile = RuntimeProfile::BrowserAttached; let transport = Arc::new(MockTransport::new(vec![])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); let summary = execute_task_with_sgclaw_settings( transport.as_ref(), browser_tool, "告诉我当前有哪些知乎热榜工具", &CompatTaskContext::default(), &workspace_root, &settings, ) .unwrap(); server_handle.join().unwrap(); let request_bodies = requests.lock().unwrap().clone(); let tool_names = request_tool_names(&request_bodies[0]); assert_eq!(summary, "已看到 browser_script skill 工具"); assert!(tool_names.contains(&"browser_action".to_string())); assert!(tool_names.contains(&"superrpa_browser".to_string())); assert!(tool_names.contains(&"read_skill".to_string())); assert!(tool_names.contains(&"workspace-zhihu-skill_extract_hotlist".to_string())); } #[test] fn compat_runtime_executes_browser_script_skill_via_eval_without_gettext_probing() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let first_response = json!({ "choices": [{ "message": { "content": "", "tool_calls": [{ "id": "call_1", "type": "function", "function": { "name": "workspace-zhihu-skill_extract_hotlist", "arguments": serde_json::to_string(&json!({ "expected_domain": "www.zhihu.com", "top_n": "10" })).unwrap() } }] } }] }); let second_response = json!({ "choices": [{ "message": { "content": "已执行 browser_script skill" } }] }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![first_response, second_response]); let workspace_root = temp_workspace_root(); let default_skills_dir = workspace_root .join(".sgclaw-zeroclaw-workspace") .join("skills"); let skill_dir = write_skill_manifest_package( &default_skills_dir, "workspace-zhihu-skill", r#" [skill] name = "workspace-zhihu-skill" description = "Extract Zhihu hotlist rows with a packaged browser script." version = "0.1.0" [[tools]] name = "extract_hotlist" description = "Extract structured hotlist rows from the current Zhihu page." kind = "browser_script" command = "scripts/extract_hotlist.js" [tools.args] top_n = "How many hotlist rows to extract." "#, ); write_skill_script( &skill_dir, "scripts/extract_hotlist.js", r#" const topN = Number(args.top_n || 10); return { source: "https://www.zhihu.com/hot", sheet_name: "知乎热榜", columns: ["rank", "title", "heat"], rows: [[1, "标题", `${topN}条`]] }; "#, ); let mut settings = SgClawSettings::from_legacy_deepseek_fields( "deepseek-test-key".to_string(), base_url, "deepseek-chat".to_string(), None, ) .unwrap(); settings.runtime_profile = RuntimeProfile::BrowserAttached; let transport = Arc::new(MockTransport::new(vec![success_browser_response( 1, json!({ "text": { "source": "https://www.zhihu.com/hot", "sheet_name": "知乎热榜", "columns": ["rank", "title", "heat"], "rows": [[1, "标题", "10条"]] } }), )])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); let summary = execute_task_with_sgclaw_settings( transport.as_ref(), browser_tool, "用知乎热榜 skill 提取前十条结构化数据", &CompatTaskContext::default(), &workspace_root, &settings, ) .unwrap(); server_handle.join().unwrap(); let sent = transport.sent_messages(); let request_bodies = requests.lock().unwrap().clone(); let tool_names = request_tool_names(&request_bodies[0]); assert_eq!(summary, "已执行 browser_script skill"); assert!(tool_names.contains(&"workspace-zhihu-skill_extract_hotlist".to_string())); assert!(sent.iter().any(|message| { matches!(message, AgentMessage::LogEntry { level, message } if level == "info" && message == "call workspace-zhihu-skill.extract_hotlist") })); assert!(sent.iter().any(|message| { matches!(message, AgentMessage::Command { action, .. } if action == &Action::Eval) })); assert!(!sent.iter().any(|message| { matches!(message, AgentMessage::LogEntry { level, message } if level == "info" && message.starts_with("getText ")) })); } #[test] fn zhihu_hotlist_browser_skill_flow_does_not_expose_shell_or_glob_tools() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let response = json!({ "choices": [{ "message": { "content": "已准备好知乎热榜技能" } }] }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]); let workspace_root = temp_workspace_root(); let mut settings = SgClawSettings::from_legacy_deepseek_fields( "deepseek-test-key".to_string(), base_url, "deepseek-chat".to_string(), Some(real_skill_lib_root()), ) .unwrap(); settings.runtime_profile = RuntimeProfile::BrowserAttached; let transport = Arc::new(MockTransport::new(vec![])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); let summary = execute_task_with_sgclaw_settings( transport.as_ref(), browser_tool, "统计一下知乎热榜前十,给出标题和热度", &CompatTaskContext::default(), &workspace_root, &settings, ) .unwrap(); server_handle.join().unwrap(); let request_bodies = requests.lock().unwrap().clone(); let tool_names = request_tool_names(&request_bodies[0]); assert_eq!(summary, "已准备好知乎热榜技能"); assert!(tool_names.contains(&"superrpa_browser".to_string())); assert!(tool_names.contains(&"browser_action".to_string())); assert!(tool_names.contains(&"read_skill".to_string())); assert!(tool_names.contains(&"zhihu-hotlist_extract_hotlist".to_string())); assert!(!tool_names.contains(&"shell".to_string())); assert!(!tool_names.contains(&"glob_search".to_string())); } #[test] fn compat_runtime_browser_attached_profile_keeps_file_read_available_for_local_paths() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let response = json!({ "choices": [{ "message": { "content": "已收到本地路径任务" } }] }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]); let workspace_root = temp_workspace_root(); let mut settings = SgClawSettings::from_legacy_deepseek_fields( "deepseek-test-key".to_string(), base_url, "deepseek-chat".to_string(), None, ) .unwrap(); settings.runtime_profile = RuntimeProfile::BrowserAttached; 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)); let summary = execute_task_with_sgclaw_settings( transport.as_ref(), browser_tool, "请读取本地文件 /home/zyl/data/report.md 的内容并总结", &CompatTaskContext::default(), &workspace_root, &settings, ) .unwrap(); server_handle.join().unwrap(); let request_bodies = requests.lock().unwrap().clone(); let tool_names = request_tool_names(&request_bodies[0]); assert_eq!(summary, "已收到本地路径任务"); assert!(tool_names.contains(&"browser_action".to_string())); assert!(tool_names.contains(&"superrpa_browser".to_string())); assert!(tool_names.contains(&"file_read".to_string())); } #[test] fn browser_attached_export_flow_exposes_browser_and_office_tools_only() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let response = json!({ "choices": [{ "message": { "content": "已准备好知乎导出流程" } }] }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]); let workspace_root = temp_workspace_root(); let mut settings = SgClawSettings::from_legacy_deepseek_fields( "deepseek-test-key".to_string(), base_url, "deepseek-chat".to_string(), Some(real_skill_lib_root()), ) .unwrap(); settings.runtime_profile = RuntimeProfile::BrowserAttached; let transport = Arc::new(MockTransport::new(vec![])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); let summary = execute_task_with_sgclaw_settings( transport.as_ref(), browser_tool, "读取知乎热榜数据,并导出 excel 文件", &CompatTaskContext::default(), &workspace_root, &settings, ) .unwrap(); server_handle.join().unwrap(); let request_bodies = requests.lock().unwrap().clone(); let tool_names = request_tool_names(&request_bodies[0]); assert_eq!(summary, "已准备好知乎导出流程"); assert!(tool_names.contains(&"superrpa_browser".to_string())); assert!(tool_names.contains(&"browser_action".to_string())); assert!(tool_names.contains(&"read_skill".to_string())); assert!(tool_names.contains(&"zhihu-hotlist_extract_hotlist".to_string())); assert!(tool_names.contains(&"openxml_office".to_string())); assert!(!tool_names.contains(&"shell".to_string())); assert!(!tool_names.contains(&"glob_search".to_string())); } #[test] fn compat_runtime_allows_zhihu_hotlist_screen_export_tool_in_browser_profile() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let response = json!({ "choices": [{ "message": { "content": "已准备好知乎热榜大屏流程" } }] }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]); let workspace_root = temp_workspace_root(); let mut settings = SgClawSettings::from_legacy_deepseek_fields( "deepseek-test-key".to_string(), base_url, "deepseek-chat".to_string(), Some(real_skill_lib_root()), ) .unwrap(); settings.runtime_profile = RuntimeProfile::BrowserAttached; let transport = Arc::new(MockTransport::new(vec![])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); let summary = execute_task_with_sgclaw_settings( transport.as_ref(), browser_tool, "读取知乎热榜数据并生成领导演示大屏,在新标签页展示", &CompatTaskContext::default(), &workspace_root, &settings, ) .unwrap(); server_handle.join().unwrap(); let request_bodies = requests.lock().unwrap().clone(); let tool_names = request_tool_names(&request_bodies[0]); assert_eq!(summary, "已准备好知乎热榜大屏流程"); assert!(tool_names.contains(&"superrpa_browser".to_string())); assert!(tool_names.contains(&"browser_action".to_string())); assert!(tool_names.contains(&"read_skill".to_string())); assert!(tool_names.contains(&"zhihu-hotlist_extract_hotlist".to_string())); assert!(tool_names.contains(&"screen_html_export".to_string())); assert!(!tool_names.contains(&"shell".to_string())); assert!(!tool_names.contains(&"glob_search".to_string())); } #[test] fn compat_runtime_logs_read_skill_usage_with_skill_name() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let first_response = json!({ "choices": [{ "message": { "content": "", "tool_calls": [{ "id": "call_1", "type": "function", "function": { "name": "read_skill", "arguments": serde_json::to_string(&json!({ "name": "workspace-zhihu-skill" })).unwrap() } }] } }] }); let second_response = json!({ "choices": [{ "message": { "content": "已读取完整 skill" } }] }); let (base_url, _requests, server_handle) = start_fake_deepseek_server(vec![first_response, second_response]); let workspace_root = temp_workspace_root(); let default_skills_dir = workspace_root .join(".sgclaw-zeroclaw-workspace") .join("skills"); write_skill_package( &default_skills_dir, "workspace-zhihu-skill", "# Workspace Zhihu Skill\nUse this workspace-local skill.\n", ); let settings = SgClawSettings::from_legacy_deepseek_fields( "deepseek-test-key".to_string(), base_url, "deepseek-chat".to_string(), None, ) .unwrap(); 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)); let summary = execute_task_with_sgclaw_settings( transport.as_ref(), browser_tool, "需要完整 zhihu skill 时先读取 skill", &CompatTaskContext::default(), &workspace_root, &settings, ) .unwrap(); server_handle.join().unwrap(); let sent = transport.sent_messages(); assert_eq!(summary, "已读取完整 skill"); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "info" && message == "read_skill workspace-zhihu-skill@0.1.0" ) })); } #[test] fn handle_browser_message_exposes_real_zhihu_skill_lib_to_provider_request() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let response = json!({ "choices": [{ "message": { "content": "已看到真实知乎 skill" } }] }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]); let workspace_root = temp_workspace_root(); let skills_dir = real_skill_lib_root(); let config_path = write_deepseek_config_with_skills_dir( &workspace_root, "deepseek-test-key", &base_url, "deepseek-chat", Some(skills_dir.to_str().unwrap()), ); let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); let transport = Arc::new(MockTransport::new(vec![])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); handle_browser_message_with_context( transport.as_ref(), &browser_tool, &runtime_context, BrowserMessage::SubmitTask { instruction: "告诉我当前有哪些知乎 skill".to_string(), conversation_id: String::new(), messages: vec![], page_url: "https://example.net/".to_string(), page_title: "Example Domain".to_string(), }, ) .unwrap(); server_handle.join().unwrap(); let sent = transport.sent_messages(); let request_bodies = requests.lock().unwrap().clone(); let first_request = request_bodies[0].to_string(); let tool_names = request_tool_names(&request_bodies[0]); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::TaskComplete { success, summary } if *success && summary == "已看到真实知乎 skill" ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "info" && message == "loaded skills: office-export-xlsx@0.1.0, zhihu-hotlist@0.1.0, zhihu-hotlist-screen@0.1.0, zhihu-navigate@0.1.0, zhihu-write@0.1.0" ) })); assert_eq!(request_bodies.len(), 1); assert!(first_request.contains("office-export-xlsx")); assert!(first_request.contains("zhihu-hotlist")); assert!(first_request.contains("zhihu-hotlist-screen")); assert!(first_request.contains("zhihu-navigate")); assert!(first_request.contains("zhihu-write")); assert!(tool_names.contains(&"browser_action".to_string())); assert!(tool_names.contains(&"superrpa_browser".to_string())); assert!(tool_names.contains(&"read_skill".to_string())); } #[test] fn browser_attached_excel_request_uses_execution_contract_not_skill_source_stuffing() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let response = json!({ "choices": [{ "message": { "content": "已收到知乎导出任务" } }] }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]); let workspace_root = temp_workspace_root(); let mut settings = SgClawSettings::from_legacy_deepseek_fields( "deepseek-test-key".to_string(), base_url, "deepseek-chat".to_string(), Some(real_skill_lib_root()), ) .unwrap(); settings.runtime_profile = RuntimeProfile::BrowserAttached; let transport = Arc::new(MockTransport::new(vec![])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); let summary = execute_task_with_sgclaw_settings( transport.as_ref(), browser_tool, "读取知乎热榜数据,并导出 excel 文件", &CompatTaskContext::default(), &workspace_root, &settings, ) .unwrap(); server_handle.join().unwrap(); let request_bodies = requests.lock().unwrap().clone(); let first_request = request_bodies[0].to_string(); assert_eq!(summary, "已收到知乎导出任务"); assert!(first_request.contains("Zhihu hotlist execution contract")); assert!(first_request.contains("Export completion contract")); assert!(first_request.contains("openxml_office")); assert!(!first_request.contains("Preloaded skill context:")); } #[test] fn browser_attached_publish_request_injects_confirmation_contract() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let response = json!({ "choices": [{ "message": { "content": "请先确认是否发布" } }] }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]); let workspace_root = temp_workspace_root(); let mut settings = SgClawSettings::from_legacy_deepseek_fields( "deepseek-test-key".to_string(), base_url, "deepseek-chat".to_string(), Some(real_skill_lib_root()), ) .unwrap(); settings.runtime_profile = RuntimeProfile::BrowserAttached; let transport = Arc::new(MockTransport::new(vec![])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); execute_task_with_sgclaw_settings( transport.as_ref(), browser_tool, "请直接发表这篇知乎文章,标题是测试标题,正文是第一段内容", &CompatTaskContext { conversation_id: None, messages: vec![], page_url: Some("https://www.zhihu.com/creator".to_string()), page_title: Some("知乎创作中心".to_string()), }, &workspace_root, &settings, ) .unwrap(); server_handle.join().unwrap(); let request_bodies = requests.lock().unwrap().clone(); let first_request = request_bodies[0].to_string(); assert!(first_request.contains("Zhihu article publish contract")); assert!(first_request.contains("must not click publish without explicit human confirmation")); assert!(first_request.contains("ask for confirmation concisely")); assert!(first_request.contains("stop after the confirmation request")); } #[test] fn handle_browser_message_executes_real_zhihu_hotlist_skill_flow() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let first_response = json!({ "choices": [{ "message": { "content": "", "tool_calls": [{ "id": "call_1", "type": "function", "function": { "name": "zhihu-hotlist_extract_hotlist", "arguments": serde_json::to_string(&json!({ "expected_domain": "www.zhihu.com", "top_n": "10" })).unwrap() } }] } }] }); let second_response = json!({ "choices": [{ "message": { "content": "已完成知乎热榜采集" } }] }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![first_response, second_response]); let workspace_root = temp_workspace_root(); let skills_dir = real_skill_lib_root(); let config_path = write_deepseek_config_with_skills_dir( &workspace_root, "deepseek-test-key", &base_url, "deepseek-chat", Some(skills_dir.to_str().unwrap()), ); let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); let transport = Arc::new(MockTransport::new(vec![success_browser_response( 1, json!({ "text": { "source": "https://www.zhihu.com/hot", "sheet_name": "知乎热榜", "columns": ["rank", "title", "heat"], "rows": [[1, "热榜项目 1", "1707万"], [2, "热榜项目 2", "1150万"]] } }), )])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); handle_browser_message_with_context( transport.as_ref(), &browser_tool, &runtime_context, BrowserMessage::SubmitTask { instruction: "统计一下知乎热榜前十".to_string(), conversation_id: String::new(), messages: vec![], page_url: "https://www.zhihu.com/".to_string(), page_title: "知乎".to_string(), }, ) .unwrap(); server_handle.join().unwrap(); let sent = transport.sent_messages(); let request_bodies = requests.lock().unwrap().clone(); let tool_content = tool_message_content(&request_bodies[1], "call_1").unwrap(); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::TaskComplete { success, summary } if *success && summary == "已完成知乎热榜采集" ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "info" && message == "call zhihu-hotlist.extract_hotlist" ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::Command { action, params, .. } if action == &Action::Eval && params["script"].as_str().unwrap_or_default().contains("columns: ['rank', 'title', 'heat']") ) })); assert_eq!(request_bodies.len(), 2); assert!(tool_content.contains("知乎热榜")); assert!(tool_content.contains("rank")); assert!(tool_content.contains("heat")); assert!(tool_content.contains("热榜项目 1")); } #[test] fn handle_browser_message_chains_hotlist_skill_into_office_export_tool() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let workspace_root = temp_workspace_root(); let output_path = workspace_root.join("out/zhihu-hotlist.xlsx"); let output_path_str = output_path.to_string_lossy().to_string(); let first_response = json!({ "choices": [{ "message": { "content": "", "tool_calls": [{ "id": "call_1", "type": "function", "function": { "name": "zhihu-hotlist_extract_hotlist", "arguments": serde_json::to_string(&json!({ "expected_domain": "www.zhihu.com", "top_n": "10" })).unwrap() } }] } }] }); let third_response = json!({ "choices": [{ "message": { "content": "", "tool_calls": [{ "id": "call_3", "type": "function", "function": { "name": "openxml_office", "arguments": serde_json::to_string(&json!({ "sheet_name": "知乎热榜", "columns": ["rank", "title", "heat"], "rows": [ [1, "问题一", "344万"], [2, "问题二", "266万"] ], "output_path": output_path_str })).unwrap() } }] } }] }); let fourth_response = json!({ "choices": [{ "message": { "content": format!("已导出知乎热榜 Excel {output_path_str}") } }] }); let (base_url, _requests, server_handle) = start_fake_deepseek_server(vec![first_response, third_response, fourth_response]); let config_path = write_deepseek_config_with_skills_dir( &workspace_root, "deepseek-test-key", &base_url, "deepseek-chat", Some(real_skill_lib_root().to_str().unwrap()), ); let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); let transport = Arc::new(MockTransport::new(vec![success_browser_response( 1, json!({ "text": { "source": "https://www.zhihu.com/hot", "sheet_name": "知乎热榜", "columns": ["rank", "title", "heat"], "rows": [[1, "问题一", "344万"], [2, "问题二", "266万"]] } }), )])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); handle_browser_message_with_context( transport.as_ref(), &browser_tool, &runtime_context, BrowserMessage::SubmitTask { instruction: "读取知乎热榜数据,并导出 excel 文件".to_string(), conversation_id: String::new(), messages: vec![], page_url: "https://www.zhihu.com/".to_string(), page_title: "知乎".to_string(), }, ) .unwrap(); server_handle.join().unwrap(); let sent = transport.sent_messages(); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::TaskComplete { success, summary } if *success && summary.contains("已导出知乎热榜 Excel") && summary.contains(".xlsx") ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "mode" && message == "zeroclaw_process_message_primary" ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "info" && message == "call zhihu-hotlist.extract_hotlist" ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::Command { action, .. } if action == &Action::Eval ) })); assert!(!sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary") ) })); } #[test] fn handle_browser_message_chains_hotlist_skill_into_screen_export_tool() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let workspace_root = temp_workspace_root(); let config_path = write_deepseek_config_with_skills_dir( &workspace_root, "deepseek-test-key", "http://127.0.0.1:9", "deepseek-chat", Some(real_skill_lib_root().to_str().unwrap()), ); let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); let transport = Arc::new(MockTransport::new(vec![ success_browser_response(1, json!({ "navigated": true })), success_browser_response( 2, json!({ "text": "知乎热榜\n1 问题一 344万热度\n2 问题二 266万热度" }), ), success_browser_response( 3, json!({ "text": { "source": "https://www.zhihu.com/hot", "sheet_name": "知乎热榜", "columns": ["rank", "title", "heat"], "rows": [[1, "问题一", "344万"], [2, "问题二", "266万"]] } }), ), ])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); handle_browser_message_with_context( transport.as_ref(), &browser_tool, &runtime_context, BrowserMessage::SubmitTask { instruction: "读取知乎热榜数据并生成领导演示大屏,在新标签页展示".to_string(), conversation_id: String::new(), messages: vec![], page_url: "https://www.zhihu.com/".to_string(), page_title: "知乎".to_string(), }, ) .unwrap(); let sent = transport.sent_messages(); let summary = task_complete_summary(&sent); let generated = extract_generated_artifact_path(&summary, ".html"); assert!(summary.contains("已生成知乎热榜大屏")); assert!(summary.contains(".html")); assert!(generated.exists()); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "mode" && message == "zeroclaw_process_message_primary" ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "info" && message == "call zhihu-hotlist.extract_hotlist" ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "info" && message == "call screen_html_export" ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::Command { action, .. } if action == &Action::Eval ) })); assert!(!sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary") ) })); } #[test] fn handle_browser_message_runs_zhihu_hotlist_export_via_zeroclaw_primary_orchestration() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let workspace_root = temp_workspace_root(); let config_path = write_deepseek_config_with_skills_dir( &workspace_root, "deepseek-test-key", "http://127.0.0.1:9", "deepseek-chat", Some(real_skill_lib_root().to_str().unwrap()), ); let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); let transport = Arc::new(MockTransport::new(vec![ success_browser_response( 1, json!({ "text": "知乎热榜\n1 问题一 344万热度\n2 问题二 266万热度\n3 问题三 181万热度" }), ), success_browser_response( 2, json!({ "text": { "source": "https://www.zhihu.com/hot", "sheet_name": "知乎热榜", "columns": ["rank", "title", "heat"], "rows": [ [1, "问题一", "344万"], [2, "问题二", "266万"], [3, "问题三", "181万"] ] } }), ), ])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); handle_browser_message_with_context( transport.as_ref(), &browser_tool, &runtime_context, BrowserMessage::SubmitTask { instruction: "读取知乎热榜前10,并导出 excel 文件".to_string(), conversation_id: String::new(), messages: vec![], page_url: "https://www.zhihu.com/hot".to_string(), page_title: "知乎热榜".to_string(), }, ) .unwrap(); let sent = transport.sent_messages(); let summary = task_complete_summary(&sent); let generated = extract_generated_artifact_path(&summary, ".xlsx"); assert!(summary.contains(".xlsx")); assert!(generated.exists()); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "mode" && message == "zeroclaw_process_message_primary" ) })); assert!(!sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary") ) })); } #[test] fn browser_submit_path_prefers_zeroclaw_process_message_orchestrator() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let workspace_root = temp_workspace_root(); let config_path = write_deepseek_config_with_skills_dir( &workspace_root, "deepseek-test-key", "http://127.0.0.1:9", "deepseek-chat", Some(real_skill_lib_root().to_str().unwrap()), ); let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); let transport = Arc::new(MockTransport::new(vec![])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); handle_browser_message_with_context( transport.as_ref(), &browser_tool, &runtime_context, BrowserMessage::SubmitTask { instruction: "读取知乎热榜前10,并导出 excel 文件".to_string(), conversation_id: String::new(), messages: vec![], page_url: "https://www.zhihu.com/hot".to_string(), page_title: "知乎热榜".to_string(), }, ) .unwrap(); let sent = transport.sent_messages(); dbg!(&sent); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "mode" && message == "zeroclaw_process_message_primary" ) })); assert!(!sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary") ) })); } #[test] fn browser_submit_path_prefers_zeroclaw_process_message_orchestrator_for_zhihu_publish() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let workspace_root = temp_workspace_root(); let config_path = write_deepseek_config_with_skills_dir( &workspace_root, "deepseek-test-key", "http://127.0.0.1:9", "deepseek-chat", Some(real_skill_lib_root().to_str().unwrap()), ); let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); let transport = Arc::new(MockTransport::new(vec![])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); handle_browser_message_with_context( transport.as_ref(), &browser_tool, &runtime_context, BrowserMessage::SubmitTask { instruction: "请直接发表这篇知乎文章,标题是测试标题,正文是第一段内容".to_string(), conversation_id: String::new(), messages: vec![], page_url: "https://www.zhihu.com/".to_string(), page_title: "知乎".to_string(), }, ) .unwrap(); let sent = transport.sent_messages(); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "mode" && message == "zeroclaw_process_message_primary" ) })); assert!(!sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary") ) })); } #[test] fn zhihu_publish_task_matches_primary_orchestration_gate() { assert!( sgclaw::compat::orchestration::should_use_primary_orchestration( "请直接发表这篇知乎文章,标题是测试标题,正文是第一段内容", Some("https://www.zhihu.com/"), Some("知乎"), ) ); } #[test] fn zhihu_article_entry_task_matches_primary_orchestration_gate() { assert!( sgclaw::compat::orchestration::should_use_primary_orchestration( "打开知乎发文章页面", Some("https://www.zhihu.com/"), Some("知乎"), ) ); } #[test] fn zhihu_hotlist_export_routes_prefer_direct_execution() { use sgclaw::compat::workflow_executor::{prefers_direct_execution, WorkflowRoute}; assert!(prefers_direct_execution( &WorkflowRoute::ZhihuHotlistExportXlsx )); assert!(prefers_direct_execution(&WorkflowRoute::ZhihuHotlistScreen)); } #[test] fn zhihu_publish_without_article_inputs_returns_missing_fields_prompt() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let workspace_root = temp_workspace_root(); let config_path = write_deepseek_config_with_skills_dir( &workspace_root, "deepseek-test-key", "http://127.0.0.1:9", "deepseek-chat", Some(real_skill_lib_root().to_str().unwrap()), ); let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); let transport = Arc::new(MockTransport::new(vec![])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); handle_browser_message_with_context( transport.as_ref(), &browser_tool, &runtime_context, BrowserMessage::SubmitTask { instruction: "请发表知乎文章".to_string(), conversation_id: String::new(), messages: vec![], page_url: "https://www.zhihu.com/".to_string(), page_title: "知乎".to_string(), }, ) .unwrap(); let sent = transport.sent_messages(); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::TaskComplete { success, summary } if *success && summary.contains("标题") && summary.contains("正文") ) })); assert!(!sent .iter() .any(|message| { matches!(message, AgentMessage::Command { .. }) })); } #[test] fn zhihu_publish_accepts_literal_backslash_n_between_title_and_body() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let workspace_root = temp_workspace_root(); let config_path = write_deepseek_config_with_skills_dir( &workspace_root, "deepseek-test-key", "http://127.0.0.1:9", "deepseek-chat", Some(real_skill_lib_root().to_str().unwrap()), ); let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); let transport = Arc::new(MockTransport::new(vec![ success_browser_response(1, json!({ "navigated": true })), success_browser_response( 2, json!({ "text": { "status": "creator_entry_clicked", "current_url": "https://www.zhihu.com/creator", "next_url": "https://zhuanlan.zhihu.com/write" } }), ), success_browser_response(3, json!({ "navigated": true })), success_browser_response( 4, json!({ "text": { "status": "editor_ready", "current_url": "https://zhuanlan.zhihu.com/write" } }), ), success_browser_response( 5, json!({ "text": { "status": "draft_ready", "current_url": "https://zhuanlan.zhihu.com/write", "title": "ai时代,普通人如何自救" } }), ), ])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); handle_browser_message_with_context( transport.as_ref(), &browser_tool, &runtime_context, BrowserMessage::SubmitTask { instruction: "标题:ai时代,普通人如何自救 \\n正文:第一段内容。 第二段内容。" .to_string(), conversation_id: String::new(), messages: vec![], page_url: "https://www.zhihu.com/creator".to_string(), page_title: "知乎创作中心".to_string(), }, ) .unwrap(); let sent = transport.sent_messages(); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::TaskComplete { success, summary } if *success && summary == "已进入知乎文章编辑器并写入草稿《ai时代,普通人如何自救》" ) })); } #[test] fn zhihu_article_entry_opens_editor_without_generic_selector_probing() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let workspace_root = temp_workspace_root(); let config_path = write_deepseek_config_with_skills_dir( &workspace_root, "deepseek-test-key", "http://127.0.0.1:9", "deepseek-chat", Some(real_skill_lib_root().to_str().unwrap()), ); let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); let transport = Arc::new(MockTransport::new(vec![ success_browser_response(1, json!({ "navigated": true })), success_browser_response( 2, json!({ "text": { "status": "creator_entry_clicked", "current_url": "https://www.zhihu.com/creator", "next_url": "https://zhuanlan.zhihu.com/write" } }), ), success_browser_response(3, json!({ "navigated": true })), success_browser_response( 4, json!({ "text": { "status": "editor_ready", "current_url": "https://zhuanlan.zhihu.com/write" } }), ), ])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); handle_browser_message_with_context( transport.as_ref(), &browser_tool, &runtime_context, BrowserMessage::SubmitTask { instruction: "打开知乎发文章页面".to_string(), conversation_id: String::new(), messages: vec![], page_url: "https://www.zhihu.com/".to_string(), page_title: "知乎".to_string(), }, ) .unwrap(); let sent = transport.sent_messages(); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::TaskComplete { success, summary } if *success && summary.contains("编辑器") ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "mode" && message == "zeroclaw_process_message_primary" ) })); assert!(!sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "info" && (message.starts_with("getText ") || message.starts_with("click ")) ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::Command { action, params, .. } if action == &Action::Navigate && params["url"].as_str() == Some("https://zhuanlan.zhihu.com/write") ) })); } #[test] fn zhihu_article_entry_reports_editor_unavailable_without_protocol_error() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let workspace_root = temp_workspace_root(); let config_path = write_deepseek_config_with_skills_dir( &workspace_root, "deepseek-test-key", "http://127.0.0.1:9", "deepseek-chat", Some(real_skill_lib_root().to_str().unwrap()), ); let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); let transport = Arc::new(MockTransport::new(vec![ success_browser_response(1, json!({ "navigated": true })), success_browser_response( 2, json!({ "text": { "status": "creator_entry_clicked", "current_url": "https://www.zhihu.com/creator", "next_url": "https://zhuanlan.zhihu.com/write" } }), ), success_browser_response(3, json!({ "navigated": true })), success_browser_response( 4, json!({ "text": { "status": "editor_unavailable", "current_url": "https://zhuanlan.zhihu.com/write" } }), ), ])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); handle_browser_message_with_context( transport.as_ref(), &browser_tool, &runtime_context, BrowserMessage::SubmitTask { instruction: "打开知乎发文章页面".to_string(), conversation_id: String::new(), messages: vec![], page_url: "https://www.zhihu.com/".to_string(), page_title: "知乎".to_string(), }, ) .unwrap(); let sent = transport.sent_messages(); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::TaskComplete { success, summary } if *success && summary.contains("未检测到文章编辑器") ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::Command { action, params, .. } if action == &Action::Navigate && params["url"].as_str() == Some("https://zhuanlan.zhihu.com/write") ) })); } #[test] fn zhihu_article_entry_stops_when_creator_page_has_no_write_entry() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let workspace_root = temp_workspace_root(); let config_path = write_deepseek_config_with_skills_dir( &workspace_root, "deepseek-test-key", "http://127.0.0.1:9", "deepseek-chat", Some(real_skill_lib_root().to_str().unwrap()), ); let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); let transport = Arc::new(MockTransport::new(vec![ success_browser_response(1, json!({ "navigated": true })), success_browser_response( 2, json!({ "text": { "status": "creator_home", "current_url": "https://www.zhihu.com/creator", "desired_target": "article_editor" } }), ), ])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); handle_browser_message_with_context( transport.as_ref(), &browser_tool, &runtime_context, BrowserMessage::SubmitTask { instruction: "打开知乎发文章页面".to_string(), conversation_id: String::new(), messages: vec![], page_url: "https://www.zhihu.com/".to_string(), page_title: "知乎".to_string(), }, ) .unwrap(); let sent = transport.sent_messages(); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::TaskComplete { success, summary } if *success && summary.contains("未找到“写文章”入口") ) })); assert_eq!( sent.iter() .filter(|message| matches!(message, AgentMessage::Command { .. })) .count(), 2 ); assert!(!sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "info" && message == "call zhihu-write.prepare_article_editor" ) })); } #[test] fn zhihu_publish_without_confirmation_returns_confirmation_before_any_browser_probing() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let workspace_root = temp_workspace_root(); let config_path = write_deepseek_config_with_skills_dir( &workspace_root, "deepseek-test-key", "http://127.0.0.1:9", "deepseek-chat", Some(real_skill_lib_root().to_str().unwrap()), ); let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); let transport = Arc::new(MockTransport::new(vec![])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); handle_browser_message_with_context( transport.as_ref(), &browser_tool, &runtime_context, BrowserMessage::SubmitTask { instruction: "请直接发表这篇知乎文章,标题是测试标题,正文是第一段内容".to_string(), conversation_id: String::new(), messages: vec![], page_url: "https://www.zhihu.com/".to_string(), page_title: "知乎".to_string(), }, ) .unwrap(); let sent = transport.sent_messages(); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::TaskComplete { success, summary } if *success && summary.contains("确认发布") ) })); assert!(!sent .iter() .any(|message| { matches!(message, AgentMessage::Command { .. }) })); assert!(!sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "info" && (message.starts_with("navigate ") || message.starts_with("getText ") || message.starts_with("click ") || message.starts_with("type ")) ) })); } #[test] fn zhihu_publish_after_confirmation_reports_login_block_without_selector_probing() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let workspace_root = temp_workspace_root(); let config_path = write_deepseek_config_with_skills_dir( &workspace_root, "deepseek-test-key", "http://127.0.0.1:9", "deepseek-chat", Some(real_skill_lib_root().to_str().unwrap()), ); let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); let transport = Arc::new(MockTransport::new(vec![ success_browser_response( 1, json!({ "navigated": true, "url": "https://www.zhihu.com/signin?next=%2Fcreator" }), ), success_browser_response( 2, json!({ "text": { "status": "login_required", "current_url": "https://www.zhihu.com/signin?next=%2Fcreator" } }), ), ])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); handle_browser_message_with_context( transport.as_ref(), &browser_tool, &runtime_context, BrowserMessage::SubmitTask { instruction: "确认发布".to_string(), conversation_id: "conversation-1".to_string(), messages: vec![ConversationMessage { role: "user".to_string(), content: "请直接发表这篇知乎文章,标题是测试标题,正文是第一段内容".to_string(), }], page_url: "https://www.zhihu.com/".to_string(), page_title: "知乎".to_string(), }, ) .unwrap(); let sent = transport.sent_messages(); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::TaskComplete { success, summary } if *success && (summary.contains("未登录") || summary.contains("登录")) ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::Command { action, params, .. } if action == &Action::Navigate && params["url"].as_str() == Some("https://www.zhihu.com/creator") ) })); assert!(sent.iter().any(|message| { matches!(message, AgentMessage::Command { action, .. } if action == &Action::Eval) })); assert!(!sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "info" && (message.starts_with("getText ") || message.starts_with("click ")) ) })); } #[test] fn browser_orchestration_registers_superrpa_tools_natively() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let first_response = json!({ "choices": [{ "message": { "content": "", "tool_calls": [{ "id": "call_1", "type": "function", "function": { "name": "superrpa_browser", "arguments": serde_json::to_string(&json!({ "action": "getText", "expected_domain": "www.zhihu.com", "selector": "main" })).unwrap() } }] } }] }); let second_response = json!({ "choices": [{ "message": { "content": "", "tool_calls": [{ "id": "call_2", "type": "function", "function": { "name": "openxml_office", "arguments": serde_json::to_string(&json!({ "sheet_name": "知乎热榜", "columns": ["rank", "title", "heat"], "rows": [[1, "问题一", "344万"]] })).unwrap() } }] } }] }); let third_response = json!({ "choices": [{ "message": { "content": "已导出知乎热榜 Excel" } }] }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![first_response, second_response, third_response]); let workspace_root = temp_workspace_root(); let config_path = write_deepseek_config_with_skills_dir( &workspace_root, "deepseek-test-key", &base_url, "deepseek-chat", Some(real_skill_lib_root().to_str().unwrap()), ); let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); let transport = Arc::new(MockTransport::new(vec![success_browser_response( 1, json!({ "text": "知乎热榜\n1\n问题一\n344万热度" }), )])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); handle_browser_message_with_context( transport.as_ref(), &browser_tool, &runtime_context, BrowserMessage::SubmitTask { instruction: "读取知乎热榜前10,并导出 excel 文件".to_string(), conversation_id: String::new(), messages: vec![], page_url: "https://www.zhihu.com/hot".to_string(), page_title: "知乎热榜".to_string(), }, ) .unwrap(); let request_bodies = requests.lock().unwrap().clone(); let sent = transport.sent_messages(); assert!( !request_bodies.is_empty(), "expected provider request, sent messages were: {sent:?}" ); server_handle.join().unwrap(); let first_request = request_bodies .first() .expect("expected first provider request") .to_string(); let tool_names = request_tool_names(&request_bodies[0]); assert!(first_request.contains("superrpa_browser")); assert!(tool_names.contains(&"superrpa_browser".to_string())); assert!(tool_names.contains(&"openxml_office".to_string())); } #[test] fn zhihu_export_does_not_use_frontend_owned_mainline() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let workspace_root = temp_workspace_root(); let config_path = write_deepseek_config_with_skills_dir( &workspace_root, "deepseek-test-key", "http://127.0.0.1:9", "deepseek-chat", Some(real_skill_lib_root().to_str().unwrap()), ); let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); let transport = Arc::new(MockTransport::new(vec![])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); handle_browser_message_with_context( transport.as_ref(), &browser_tool, &runtime_context, BrowserMessage::SubmitTask { instruction: "读取热榜前10,并导出 excel 文件".to_string(), conversation_id: String::new(), messages: vec![], page_url: "https://www.zhihu.com/hot".to_string(), page_title: "知乎热榜".to_string(), }, ) .unwrap(); let sent = transport.sent_messages(); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "mode" && message == "zeroclaw_process_message_primary" ) })); assert!(!sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary") ) })); } #[test] fn browser_skill_usage_is_execution_not_prompt_only() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let workspace_root = temp_workspace_root(); let output_path = workspace_root.join("out/zhihu-hotlist-execution.xlsx"); let output_path_str = output_path.to_string_lossy().to_string(); let first_response = json!({ "choices": [{ "message": { "content": "", "tool_calls": [{ "id": "call_1", "type": "function", "function": { "name": "superrpa_browser", "arguments": serde_json::to_string(&json!({ "action": "navigate", "expected_domain": "www.zhihu.com", "url": "https://www.zhihu.com/hot" })).unwrap() } }] } }] }); let second_response = json!({ "choices": [{ "message": { "content": "", "tool_calls": [{ "id": "call_2", "type": "function", "function": { "name": "superrpa_browser", "arguments": serde_json::to_string(&json!({ "action": "getText", "expected_domain": "www.zhihu.com", "selector": "main" })).unwrap() } }] } }] }); let third_response = json!({ "choices": [{ "message": { "content": "", "tool_calls": [{ "id": "call_3", "type": "function", "function": { "name": "openxml_office", "arguments": serde_json::to_string(&json!({ "sheet_name": "知乎热榜", "columns": ["rank", "title", "heat"], "rows": [ [1, "问题一", "344万"], [2, "问题二", "266万"] ], "output_path": output_path_str })).unwrap() } }] } }] }); let fourth_response = json!({ "choices": [{ "message": { "content": format!("已导出知乎热榜 Excel {output_path_str}") } }] }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![ first_response, second_response, third_response, fourth_response, ]); let config_path = write_deepseek_config_with_skills_dir( &workspace_root, "deepseek-test-key", &base_url, "deepseek-chat", Some(real_skill_lib_root().to_str().unwrap()), ); let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); let transport = Arc::new(MockTransport::new(vec![ success_browser_response(1, json!({ "navigated": true })), success_browser_response( 2, json!({ "text": "知乎热榜\n1\n问题一\n344万热度\n2\n问题二\n266万热度" }), ), ])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); handle_browser_message_with_context( transport.as_ref(), &browser_tool, &runtime_context, BrowserMessage::SubmitTask { instruction: "读取知乎热榜前10,并导出 excel 文件".to_string(), conversation_id: String::new(), messages: vec![], page_url: "https://www.zhihu.com/hot".to_string(), page_title: "知乎热榜".to_string(), }, ) .unwrap(); server_handle.join().unwrap(); let request_bodies = requests.lock().unwrap().clone(); let sent = transport.sent_messages(); let first_request = request_bodies .first() .expect("expected first provider request") .to_string(); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::TaskComplete { success, summary } if *success && summary.contains(".xlsx") ) })); assert!(!sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "info" && message.starts_with("read_skill ") ) })); assert!(!sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "info" && (message == "getText .HotList-item" || message == "getText [data-hot-item]" || message == "getText ol li") ) })); assert!(!first_request.contains("Preloaded skill context:")); } #[test] fn handle_browser_message_executes_real_zhihu_navigate_skill_flow() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let first_response = json!({ "choices": [{ "message": { "content": "", "tool_calls": [{ "id": "call_1", "type": "function", "function": { "name": "read_skill", "arguments": serde_json::to_string(&json!({ "name": "zhihu-navigate" })).unwrap() } }] } }] }); let second_response = json!({ "choices": [{ "message": { "content": "", "tool_calls": [{ "id": "call_2", "type": "function", "function": { "name": "browser_action", "arguments": serde_json::to_string(&json!({ "action": "navigate", "expected_domain": "www.zhihu.com", "url": "https://www.zhihu.com/hot" })).unwrap() } }] } }] }); let third_response = json!({ "choices": [{ "message": { "content": "已打开知乎热榜" } }] }); let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![first_response, second_response, third_response]); let workspace_root = temp_workspace_root(); let skills_dir = real_skill_lib_root(); let config_path = write_deepseek_config_with_skills_dir( &workspace_root, "deepseek-test-key", &base_url, "deepseek-chat", Some(skills_dir.to_str().unwrap()), ); let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); let transport = Arc::new(MockTransport::new(vec![success_browser_response( 1, json!({ "navigated": true }), )])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); handle_browser_message_with_context( transport.as_ref(), &browser_tool, &runtime_context, BrowserMessage::SubmitTask { instruction: "打开知乎热榜".to_string(), conversation_id: String::new(), messages: vec![], page_url: "https://www.zhihu.com/".to_string(), page_title: "知乎".to_string(), }, ) .unwrap(); server_handle.join().unwrap(); let sent = transport.sent_messages(); let request_bodies = requests.lock().unwrap().clone(); let tool_content = tool_message_content(&request_bodies[1], "call_1").unwrap(); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::TaskComplete { success, summary } if *success && summary == "已打开知乎热榜" ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "info" && message == "read_skill zhihu-navigate@0.1.0" ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::Command { action, params, .. } if action == &Action::Navigate && params["url"].as_str() == Some("https://www.zhihu.com/hot") ) })); assert_eq!(request_bodies.len(), 3); assert!(tool_content.len() > 100); assert!(tool_content.contains("Zhihu page")); } #[test] fn handle_browser_message_executes_real_zhihu_write_skill_flow() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let workspace_root = temp_workspace_root(); let skills_dir = real_skill_lib_root(); let config_path = write_deepseek_config_with_skills_dir( &workspace_root, "deepseek-test-key", "http://127.0.0.1:9", "deepseek-chat", Some(skills_dir.to_str().unwrap()), ); let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); let transport = Arc::new(MockTransport::new(vec![ success_browser_response(1, json!({ "navigated": true })), success_browser_response( 2, json!({ "text": { "status": "creator_entry_clicked", "current_url": "https://www.zhihu.com/creator", "next_url": "https://zhuanlan.zhihu.com/write" } }), ), success_browser_response(3, json!({ "navigated": true })), success_browser_response( 4, json!({ "text": { "status": "editor_ready", "current_url": "https://zhuanlan.zhihu.com/write" } }), ), success_browser_response( 5, json!({ "text": { "status": "draft_ready", "current_url": "https://zhuanlan.zhihu.com/write", "title": "测试标题" } }), ), ])); let browser_tool = BrowserPipeTool::new( transport.clone(), zhihu_test_policy(), vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); handle_browser_message_with_context( transport.as_ref(), &browser_tool, &runtime_context, BrowserMessage::SubmitTask { instruction: "帮我写一篇知乎文章,标题是测试标题,正文是第一段内容".to_string(), conversation_id: String::new(), messages: vec![], page_url: "https://www.zhihu.com/creator".to_string(), page_title: "知乎创作中心".to_string(), }, ) .unwrap(); let sent = transport.sent_messages(); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::TaskComplete { success, summary } if *success && summary == "已进入知乎文章编辑器并写入草稿《测试标题》" ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "mode" && message == "zeroclaw_process_message_primary" ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "info" && message == "call zhihu-navigate.open_creator_entry" ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "info" && message == "call zhihu-write.prepare_article_editor" ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "info" && message == "call zhihu-write.fill_article_draft" ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::Command { action, params, .. } if action == &Action::Navigate && params["url"].as_str() == Some("https://www.zhihu.com/creator") ) })); assert!(sent.iter().any(|message| { matches!( message, AgentMessage::Command { action, params, .. } if action == &Action::Navigate && params["url"].as_str() == Some("https://zhuanlan.zhihu.com/write") ) })); assert!( sent.iter() .filter(|message| { matches!(message, AgentMessage::Command { action, .. } if action == &Action::Eval) }) .count() >= 2 ); assert!(!sent.iter().any(|message| { matches!( message, AgentMessage::LogEntry { level, message } if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary") ) })); }