Files
claw/tests/compat_runtime_test.rs
木炎 96c3bf1dee feat: route staged scene skills through runtime
Add registry-driven scene routing and multi-root skill loading so fault-details and 95598 scene skills can be triggered from natural language while still running through the browser-backed runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 16:17:17 +08:00

4525 lines
149 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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::workflow_executor::finalize_screen_export;
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<Mutex<()>> = 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 staged_skill_root() -> PathBuf {
PathBuf::from("D:/data/ideaSpace/rust/sgClaw/claw/claw/skills/skill_staging")
}
fn project_skills_root() -> PathBuf {
staged_skill_root()
.parent()
.expect("staged skill root should have parent")
.to_path_buf()
}
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<String> {
request["tools"]
.as_array()
.cloned()
.unwrap_or_default()
.into_iter()
.filter_map(|tool| tool["function"]["name"].as_str().map(str::to_string))
.collect::<Vec<_>>()
}
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<Value>,
) -> (String, Arc<Mutex<Vec<Value>>>, 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}"),
}
};
stream.set_nonblocking(false).unwrap();
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::<usize>().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: Vec::new(),
};
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_executes_without_legacy_plan_preview() {
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();
assert!(!sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "plan" && message.contains("navigate https://www.baidu.com")
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "info" && message == "navigate https://www.baidu.com"
)
}));
}
#[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: Vec::new(),
};
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: Vec::new(),
};
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: Vec::new(),
};
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::<Vec<_>>()
.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(),
Vec::new(),
)
.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::<Vec<_>>()
.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::<Vec<_>>();
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(),
Vec::new(),
)
.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::<Vec<_>>();
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(),
Vec::new(),
)
.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(),
Vec::new(),
)
.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(),
vec![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(),
Vec::new(),
)
.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(),
vec![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(),
vec![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(),
Vec::new(),
)
.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]);
let loaded_skills_message = sent
.iter()
.find_map(|message| match message {
AgentMessage::LogEntry { level, message } if level == "info" && message.starts_with("loaded skills: ") => {
Some(message.clone())
}
_ => None,
})
.expect("expected loaded skills log entry");
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::TaskComplete { success, summary }
if *success && summary == "已看到真实知乎 skill"
)
}));
assert!(loaded_skills_message.contains("office-export-xlsx@0.1.0"));
assert!(loaded_skills_message.contains("zhihu-hotlist@0.1.0"));
assert!(loaded_skills_message.contains("zhihu-hotlist-screen@0.1.0"));
assert!(loaded_skills_message.contains("zhihu-navigate@0.1.0"));
assert!(loaded_skills_message.contains("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(),
vec![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(),
vec![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_xlsx_export_and_auto_open() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
std::env::set_var("SGCLAW_DISABLE_POST_EXPORT_OPEN", "1");
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: "读取知乎热榜数据,并导出 excel 文件".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, ".xlsx");
assert!(summary.contains("已导出并打开知乎热榜 Excel"));
assert!(summary.contains(".xlsx"));
assert!(generated.exists());
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::LogEntry { level, message }
if level == "info" && message == "call openxml_office"
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::Command { action, .. } if action == &Action::Eval
)
}));
assert!(!sent.iter().any(|message| {
matches!(
message,
AgentMessage::Command { action, params, .. }
if action == &Action::Navigate && params.get("sgclaw_local_dashboard_open").is_some()
)
}));
assert!(!sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary")
)
}));
std::env::remove_var("SGCLAW_DISABLE_POST_EXPORT_OPEN");
}
#[test]
fn handle_browser_message_chains_hotlist_skill_into_screen_export_and_auto_open() {
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万"]]
}
}),
),
success_browser_response(4, 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();
let sent = transport.sent_messages();
let summary = task_complete_summary(&sent);
let generated = extract_generated_artifact_path(&summary, ".html");
let navigate = sent
.iter()
.find_map(|message| match message {
AgentMessage::Command {
action,
params,
security,
..
} if action == &Action::Navigate
&& security.expected_domain == "__sgclaw_local_dashboard__" => Some((params, security)),
_ => None,
})
.expect("dashboard route should emit local-dashboard navigate request");
assert!(summary.contains("已在浏览器中打开知乎热榜大屏"));
assert!(summary.contains(".html"));
assert!(generated.exists());
assert_eq!(
navigate.0["sgclaw_local_dashboard_open"]["output_path"].as_str(),
generated.to_str()
);
assert!(navigate.0["url"]
.as_str()
.expect("dashboard open url should be present")
.starts_with("file://"));
assert_eq!(
navigate.0["sgclaw_local_dashboard_open"]["source"],
json!("compat.workflow_executor")
);
assert_eq!(
navigate.0["sgclaw_local_dashboard_open"]["kind"],
json!("zhihu_hotlist_screen")
);
assert_eq!(
navigate.0["sgclaw_local_dashboard_open"]["presentation_url"],
navigate.0["url"]
);
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 == "info" && message == "call openxml_office"
)
}));
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_reports_dashboard_auto_open_protocol_error_when_presentation_url_is_missing() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
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 browser_backend = sgclaw::browser::PipeBrowserBackend::from_inner(browser_tool);
let workspace_root = temp_workspace_root();
let output_path = workspace_root.join("zhihu-hotlist-screen.html");
fs::write(&output_path, "<html><body>fixture</body></html>").unwrap();
let payload = json!({
"title": "知乎热榜大屏",
"output_path": output_path,
"renderer": "screen_html_export",
"row_count": 2,
"snapshot_id": "snapshot-test",
"presentation": {
"mode": "new_tab",
"title": "知乎热榜大屏",
"open_in_new_tab": true
}
});
let summary = finalize_screen_export(&browser_backend, &payload.to_string()).unwrap();
assert!(summary.contains("已生成知乎热榜大屏"));
assert!(summary.contains(output_path.to_string_lossy().as_ref()));
assert!(summary.contains("但浏览器自动打开失败screen_html_export did not return presentation.url"));
let sent = transport.sent_messages();
assert!(!sent.iter().any(|message| {
matches!(
message,
AgentMessage::Command { action, params, .. }
if action == &Action::Navigate && params.get("sgclaw_local_dashboard_open").is_some()
)
}));
}
#[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());
std::env::set_var("SGCLAW_DISABLE_POST_EXPORT_OPEN", "1");
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")
)
}));
std::env::remove_var("SGCLAW_DISABLE_POST_EXPORT_OPEN");
}
#[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 handle_browser_message_exposes_project_skills_and_staged_scene_skills_together() {
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 config_path = write_deepseek_config_with_skills_dir(
&workspace_root,
"deepseek-test-key",
&base_url,
"deepseek-chat",
Some(project_skills_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: "告诉我当前有哪些知乎和95598相关 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 loaded_skills_message = sent
.iter()
.find_map(|message| match message {
AgentMessage::LogEntry { level, message }
if level == "info" && message.starts_with("loaded skills: ") =>
{
Some(message.clone())
}
_ => None,
})
.expect("expected loaded skills log entry");
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::TaskComplete { success, summary }
if *success && summary == "已同时看到顶层与场景技能"
)
}));
assert!(loaded_skills_message.contains("zhihu-hotlist@0.1.0"));
assert!(loaded_skills_message.contains("zhihu-write@0.1.0"));
assert!(loaded_skills_message.contains("fault-details-report@0.1.0"));
assert!(loaded_skills_message.contains("95598-repair-city-dispatch@0.1.0"));
assert!(first_request.contains("zhihu-hotlist"));
assert!(first_request.contains("zhihu-write"));
assert!(first_request.contains("fault-details-report"));
assert!(first_request.contains("95598-repair-city-dispatch"));
}
#[test]
fn fault_details_route_finds_staged_scene_skill_under_project_skills_root() {
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(project_skills_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": {
"sheet_name": "故障明细",
"rows": [["2026-04", "已完成"]]
}
}),
)]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
policy_for_domains(&["example.invalid"]),
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: "导出 2026-04 故障明细".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "https://example.invalid/workbench".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("sheet_name") && summary.contains("故障明细")
)
}));
assert!(sent.iter().any(|message| {
matches!(message, AgentMessage::Command { action, .. } if action == &Action::Eval)
}));
}
#[test]
fn handle_browser_message_exposes_staged_95598_scene_skills_and_contract_on_agent_path() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let response = json!({
"choices": [{
"message": {
"content": "已按95598场景进入通用代理路径"
}
}]
});
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![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(staged_skill_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(),
policy_for_domains(&["95598.example.invalid"]),
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: "请处理95598-repair-city-dispatch场景查看抢修市指派单并汇总当前队列"
.to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "https://95598.example.invalid/dispatch".to_string(),
page_title: "95598抢修市指监测".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 loaded_skills_message = sent
.iter()
.find_map(|message| match message {
AgentMessage::LogEntry { level, message }
if level == "info" && message.starts_with("loaded skills: ") =>
{
Some(message.clone())
}
_ => None,
})
.expect("expected loaded skills log entry");
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::TaskComplete { success, summary }
if *success && summary == "已按95598场景进入通用代理路径"
)
}));
assert!(loaded_skills_message.contains("fault-details-report@0.1.0"));
assert!(loaded_skills_message.contains("95598-repair-city-dispatch@0.1.0"));
assert_eq!(request_bodies.len(), 1);
assert!(first_request.contains("95598-repair-city-dispatch.collect_repair_orders"));
assert!(first_request.contains("Current page URL: https://95598.example.invalid/dispatch"));
assert!(first_request.contains("Current page title: 95598抢修市指监测"));
}
#[test]
fn browser_surface_disabled_fault_details_turn_uses_general_assistant_provider_path() {
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 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",
"runtimeProfile": "generalAssistant",
"skillsDir": staged_skill_root(),
}))
.unwrap(),
)
.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(),
policy_for_domains(&["example.invalid"]),
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: "导出 2026-04 故障明细".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "https://example.invalid/workbench".to_string(),
page_title: "业务台账".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 loaded_skills_message = sent.iter().find_map(|message| match message {
AgentMessage::LogEntry { level, message }
if level == "info" && message.starts_with("loaded skills: ") =>
{
Some(message.clone())
}
_ => None,
});
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::TaskComplete { success, summary }
if *success && summary == "已走通用助手路径"
)
}));
assert_eq!(request_bodies.len(), 1);
assert!(!first_request.contains("95598-repair-city-dispatch.collect_repair_orders"));
assert!(!first_request.contains("browser workflow, not a text-only task"));
assert!(!first_request.contains("generic browser probing only after"));
assert!(loaded_skills_message.is_none());
assert!(!sent.iter().any(|message| {
matches!(message, AgentMessage::Command { action, .. } if action == &Action::Eval)
}));
}
#[test]
fn browser_attached_zhihu_hotlist_request_keeps_zhihu_contract_without_scene_injection() {
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(),
vec![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_eq!(request_bodies.len(), 1);
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("95598 repair city dispatch execution contract"));
assert!(!first_request.contains("browser workflow, not a text-only task"));
assert!(!first_request.contains("generic browser probing only after"));
}
#[test]
fn fault_details_direct_browser_scene_matches_primary_orchestration_gate() {
assert!(sgclaw::compat::orchestration::should_use_primary_orchestration(
"导出故障明细",
Some("https://example.invalid/workbench"),
Some("业务台账"),
));
}
#[test]
fn fault_details_direct_browser_scene_detects_direct_route() {
use sgclaw::compat::workflow_executor::{detect_route, WorkflowRoute};
assert_eq!(
detect_route(
"导出故障明细",
Some("https://example.invalid/workbench"),
Some("业务台账")
),
Some(WorkflowRoute::FaultDetailsReport)
);
}
#[test]
fn missing_scene_metadata_keeps_unrelated_primary_routing_unchanged() {
let registry = [sgclaw::runtime::SceneRegistryEntry {
id: "unrelated-scene".to_string(),
name: "无关场景".to_string(),
summary: "与故障明细无关。".to_string(),
tags: vec!["other".to_string()],
inputs: vec!["period".to_string()],
outputs: vec!["artifact".to_string()],
skill_package: "unrelated-skill".to_string(),
skill_tool: "run_other".to_string(),
skill_artifact_type: "artifact".to_string(),
dispatch_mode: sgclaw::runtime::DispatchMode::DirectBrowser,
expected_domain: "other.example.invalid".to_string(),
aliases: vec!["别的事情".to_string()],
default_args: serde_json::Map::new(),
}];
assert!(sgclaw::runtime::match_scene_instruction_in_registry(&registry, "别的事情").is_some());
assert!(sgclaw::runtime::match_scene_instruction_in_registry(&registry, "导出故障明细").is_none());
assert!(!sgclaw::compat::orchestration::should_use_primary_orchestration(
"帮我汇总今天待办",
Some("https://example.invalid/workbench"),
Some("业务台账"),
));
}
#[test]
fn fault_details_route_returns_clear_failure_when_period_cannot_be_derived() {
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(staged_skill_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://example.invalid/workbench".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::TaskComplete { success, summary }
if !*success && summary.contains("period") && summary.contains("无法")
)
}));
assert!(!sent.iter().any(|message| {
matches!(message, AgentMessage::Command { action, .. } if action == &Action::Eval)
}));
}
#[test]
fn fault_details_route_uses_current_page_host_as_expected_domain() {
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(staged_skill_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": {
"sheet_name": "故障明细",
"rows": [["2026-04", "已完成"]]
}
}),
)]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
policy_for_domains(&["example.invalid"]),
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: "导出 2026-04 故障明细".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "https://example.invalid/workbench".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("sheet_name") && summary.contains("故障明细")
)
}));
let eval_command = sent.iter().find_map(|message| match message {
AgentMessage::Command {
action,
params,
security,
..
} if action == &Action::Eval => Some((params.clone(), security.expected_domain.clone())),
_ => None,
});
let (params, expected_domain) = eval_command.expect("direct route should call browser eval");
assert_eq!(expected_domain, "example.invalid");
let script = params["script"].as_str().unwrap_or_default();
assert!(script.contains("const args = {\"period\":\"2026-04\"};"));
}
#[test]
fn fault_details_route_uses_packaged_browser_script_from_configured_skills_root() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let workspace_root = temp_workspace_root();
let staged_root = workspace_root.join("custom_fault_details_staging");
let custom_skills_dir = staged_root.join("skills");
let skill_dir = write_skill_manifest_package(
&custom_skills_dir,
"fault-details-report",
r#"
[skill]
name = "fault-details-report"
description = "Collect fault detail rows via a packaged browser script."
version = "0.1.0"
[[tools]]
name = "collect_fault_details"
description = "Collect fault detail rows for the target period."
kind = "browser_script"
command = "scripts/custom_fault_details.js"
[tools.args]
period = "Target report period."
"#,
);
write_skill_script(
&skill_dir,
"scripts/custom_fault_details.js",
r#"
return {
sheet_name: "故障明细",
rows: [[args.period || "unknown", "CUSTOM_FAULT_DETAILS_MARKER"]],
marker: "CUSTOM_FAULT_DETAILS_MARKER"
};
"#,
);
let config_path = write_deepseek_config_with_skills_dir(
&workspace_root,
"deepseek-test-key",
"http://127.0.0.1:9",
"deepseek-chat",
Some(staged_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": {
"sheet_name": "故障明细",
"rows": [["2026-04", "已完成"]]
}
}),
)]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
policy_for_domains(&["example.invalid"]),
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: "导出 2026-04 故障明细".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "https://example.invalid/workbench".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("sheet_name") && summary.contains("故障明细")
)
}));
let eval_command = sent.iter().find_map(|message| match message {
AgentMessage::Command {
action,
params,
security,
..
} if action == &Action::Eval => Some((params.clone(), security.expected_domain.clone())),
_ => None,
});
let (params, expected_domain) = eval_command.expect("direct route should call browser eval");
assert_eq!(expected_domain, "example.invalid");
let script = params["script"].as_str().unwrap_or_default();
assert!(script.contains("const args = {\"period\":\"2026-04\"};"));
assert!(script.contains("CUSTOM_FAULT_DETAILS_MARKER"));
assert!(!script.contains("collect_fault_details.js"));
}
#[test]
fn fault_details_route_executes_browser_script_eval_when_period_is_derived() {
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(staged_skill_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": {
"sheet_name": "故障明细",
"rows": [["2026-04", "已完成"]]
}
}),
)]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
policy_for_domains(&["example.invalid"]),
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: "导出 2026-04 故障明细".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "https://example.invalid/workbench".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("sheet_name") && summary.contains("故障明细")
)
}));
let eval_command = sent.iter().find_map(|message| match message {
AgentMessage::Command {
action,
params,
security,
..
} if action == &Action::Eval => Some((params.clone(), security.expected_domain.clone())),
_ => None,
});
let (params, expected_domain) = eval_command.expect("direct route should call browser eval");
assert_eq!(expected_domain, "example.invalid");
let script = params["script"].as_str().unwrap_or_default();
assert!(script.contains("const args = {\"period\":\"2026-04\"};"));
assert!(script.contains("sheet_name") || script.contains("return JSON.stringify") || script.contains("rows"));
}
#[test]
fn zhihu_generated_auto_publish_matches_primary_orchestration_gate() {
assert!(
sgclaw::compat::orchestration::should_use_primary_orchestration(
"在知乎自动发表一篇名称为人工智能技能大全",
Some("https://www.zhihu.com/"),
Some("知乎"),
)
);
}
#[test]
fn zhihu_hotlist_export_route_stays_ahead_of_generated_article_publish() {
use sgclaw::compat::workflow_executor::{detect_route, WorkflowRoute};
assert_eq!(
detect_route(
"打开知乎热榜获取前10条数据并导出 Excel",
Some("https://www.zhihu.com/"),
Some("知乎")
),
Some(WorkflowRoute::ZhihuHotlistExportXlsx)
);
}
#[test]
fn zhihu_generated_auto_publish_uses_provider_and_submits_publish_without_confirmation() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let response = json!({
"choices": [{
"message": {
"content": "标题:人工智能技能大全\n正文:第一段内容。\n\n第二段内容。"
}
}]
});
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![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!({ "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": "publish_submitted",
"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/".to_string(),
page_title: "知乎".to_string(),
},
)
.unwrap();
server_handle.join().unwrap();
let sent = transport.sent_messages();
let request_bodies = requests.lock().unwrap().clone();
assert_eq!(request_bodies.len(), 1);
assert!(request_bodies[0].to_string().contains("人工智能技能大全"));
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-write.fill_article_draft"
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::Command { action, .. } if action == &Action::Navigate
)
}));
assert!(!sent.iter().any(|message| {
matches!(
message,
AgentMessage::TaskComplete { success, summary }
if *success && summary.contains("确认发布")
)
}));
}
#[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_executes_hotlist_export_natively_from_hotlist_page() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
std::env::set_var("SGCLAW_DISABLE_POST_EXPORT_OPEN", "1");
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万热度" }),
),
success_browser_response(
2,
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: "读取知乎热榜前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 == "info" && message == "call zhihu-hotlist.extract_hotlist"
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "info" && message == "call openxml_office"
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::Command { action, .. } if action == &Action::GetText
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::Command { action, .. } if action == &Action::Eval
)
}));
assert!(!sent.iter().any(|message| {
matches!(
message,
AgentMessage::Command { action, .. } if action == &Action::Navigate
)
}));
assert!(!sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "mode" &&
(message == "compat_llm_primary" || message == "compat_skill_runner_primary")
)
}));
std::env::remove_var("SGCLAW_DISABLE_POST_EXPORT_OPEN");
}
#[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());
std::env::set_var("SGCLAW_DISABLE_POST_EXPORT_OPEN", "1");
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: "读取知乎热榜前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::TaskComplete { success, summary }
if *success && 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 openxml_office"
)
}));
assert!(!sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "mode" &&
(message == "compat_llm_primary" || message == "compat_skill_runner_primary")
)
}));
std::env::remove_var("SGCLAW_DISABLE_POST_EXPORT_OPEN");
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")
)
}));
}
#[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")
)
}));
}