Files
claw/tests/compat_runtime_test.rs
木炎 dd7805d341 feat: add deterministic tq lineloss submit path
Add the deterministic tq-lineloss routing and normalization flow so exact-suffix requests execute through the existing browser-script seam with canonical org and period arguments.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 13:10:58 +08:00

4043 lines
131 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::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_direct_submit_skill(
root: &PathBuf,
api_key: &str,
base_url: &str,
model: &str,
skills_dir: Option<&str>,
direct_submit_skill: 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);
}
if let Some(direct_submit_skill) = direct_submit_skill {
payload["directSubmitSkill"] = json!(direct_submit_skill);
}
fs::write(
&config_path,
serde_json::to_string_pretty(&payload).unwrap(),
)
.unwrap();
config_path
}
fn write_deepseek_config_with_skills_dir(
root: &PathBuf,
api_key: &str,
base_url: &str,
model: &str,
skills_dir: Option<&str>,
) -> PathBuf {
write_deepseek_config_with_direct_submit_skill(root, api_key, base_url, model, skills_dir, None)
}
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 {
let repo_parent = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.to_path_buf();
let hyphenated = repo_parent.join("skill-lib");
if hyphenated.exists() {
return hyphenated;
}
repo_parent.join("skill_lib")
}
fn success_browser_response(seq: u64, data: Value) -> BrowserMessage {
BrowserMessage::Response {
seq,
success: true,
data,
aom_snapshot: vec![],
timing: Timing {
queue_ms: 1,
exec_ms: 10,
},
}
}
fn request_tool_names(request: &Value) -> Vec<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}"),
}
};
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];
match stream.read(&mut chunk) {
Ok(bytes) => {
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");
}
Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
thread::sleep(Duration::from_millis(10));
}
Err(err) => panic!("failed while reading headers: {err}"),
}
}
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];
match stream.read(&mut chunk) {
Ok(bytes) => {
assert!(bytes > 0, "unexpected EOF while reading body");
buffer.extend_from_slice(&chunk[..bytes]);
}
Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
thread::sleep(Duration::from_millis(10));
}
Err(err) => panic!("failed while reading body: {err}"),
}
}
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,
})
.unwrap_or_else(|| panic!("expected successful task completion, sent messages were: {sent:?}"))
}
fn extract_generated_artifact_path(summary: &str, extension: &str) -> PathBuf {
summary
.split_whitespace()
.find(|token| token.ends_with(extension))
.map(PathBuf::from)
.expect("expected artifact path in task summary")
}
#[test]
fn compat_runtime_uses_zeroclaw_provider_path_and_executes_browser_actions() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let first_response = json!({
"choices": [{
"message": {
"content": "",
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {
"name": "browser_action",
"arguments": serde_json::to_string(&json!({
"action": "navigate",
"expected_domain": "www.baidu.com",
"url": "https://www.baidu.com"
})).unwrap()
}
},
{
"id": "call_2",
"type": "function",
"function": {
"name": "browser_action",
"arguments": serde_json::to_string(&json!({
"action": "type",
"expected_domain": "www.baidu.com",
"selector": "#kw",
"text": "天气",
"clear_first": true
})).unwrap()
}
}
]
}
}],
"usage": {
"prompt_tokens": 12,
"completion_tokens": 7
}
});
let second_response = json!({
"choices": [{
"message": {
"content": "已通过 ZeroClaw 执行任务: 打开百度搜索天气"
}
}],
"usage": {
"prompt_tokens": 15,
"completion_tokens": 8
}
});
let (base_url, requests, server_handle) =
start_fake_deepseek_server(vec![first_response, second_response]);
std::env::set_var("DEEPSEEK_API_KEY", "deepseek-test-key");
std::env::set_var("DEEPSEEK_BASE_URL", base_url);
std::env::set_var("DEEPSEEK_MODEL", "deepseek-chat");
let workspace_root = temp_workspace_root();
let settings = DeepSeekSettings::from_env().unwrap();
let transport = Arc::new(MockTransport::new(vec![
BrowserMessage::Response {
seq: 1,
success: true,
data: json!({ "navigated": true }),
aom_snapshot: vec![],
timing: Timing {
queue_ms: 1,
exec_ms: 10,
},
},
BrowserMessage::Response {
seq: 2,
success: true,
data: json!({ "typed": true }),
aom_snapshot: vec![],
timing: Timing {
queue_ms: 1,
exec_ms: 11,
},
},
]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
let summary = execute_task(
transport.as_ref(),
browser_tool,
"打开百度搜索天气",
&CompatTaskContext::default(),
&workspace_root,
&settings,
)
.unwrap();
server_handle.join().unwrap();
let request_bodies = requests.lock().unwrap().clone();
let sent = transport.sent_messages();
assert_eq!(summary, "已通过 ZeroClaw 执行任务: 打开百度搜索天气");
assert_eq!(request_bodies.len(), 2);
assert_eq!(request_bodies[0]["model"], json!("deepseek-chat"));
assert!(request_tool_names(&request_bodies[0]).contains(&"browser_action".to_string()));
assert!(request_tool_names(&request_bodies[0]).contains(&"superrpa_browser".to_string()));
assert!(request_bodies[1].to_string().contains("tool_call_id"));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "info" && message == "navigate https://www.baidu.com"
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "info" && message == "type 天气 into #kw"
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::Command { action, .. } if action == &Action::Navigate
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::Command { action, .. } if action == &Action::Type
)
}));
}
#[test]
fn compat_runtime_includes_default_workspace_skills_in_provider_request() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let response = json!({
"choices": [{
"message": {
"content": "已识别默认 workspace skill"
}
}]
});
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
let workspace_root = temp_workspace_root();
let default_skills_dir = workspace_root
.join(".sgclaw-zeroclaw-workspace")
.join("skills");
write_skill_package(
&default_skills_dir,
"workspace-zhihu-skill",
"# Workspace Zhihu Skill\nUse this workspace-local skill.\n",
);
let settings = DeepSeekSettings {
api_key: "deepseek-test-key".to_string(),
base_url,
model: "deepseek-chat".to_string(),
skills_dir: None,
};
let transport = Arc::new(MockTransport::new(vec![]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
let summary = execute_task(
transport.as_ref(),
browser_tool,
"列出当前可用 skill",
&CompatTaskContext::default(),
&workspace_root,
&settings,
)
.unwrap();
server_handle.join().unwrap();
let request_bodies = requests.lock().unwrap().clone();
assert_eq!(summary, "已识别默认 workspace skill");
assert_eq!(request_bodies.len(), 1);
assert!(request_bodies[0]
.to_string()
.contains("workspace-zhihu-skill"));
}
#[test]
fn handle_browser_message_loads_skills_from_configured_skills_dir() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let response = json!({
"choices": [{
"message": {
"content": "已识别自定义 skill 目录"
}
}]
});
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
let workspace_root = temp_workspace_root();
let default_skills_dir = workspace_root
.join(".sgclaw-zeroclaw-workspace")
.join("skills");
write_skill_package(
&default_skills_dir,
"workspace-only-skill",
"# Workspace Only Skill\nThis skill should be ignored when skillsDir is set.\n",
);
let custom_skill_repo = workspace_root.join("skill_lib");
let custom_skills_dir = custom_skill_repo.join("skills");
write_skill_package(
&custom_skills_dir,
"configured-zhihu-skill",
"# Configured Zhihu Skill\nUse the configured skills directory.\n",
);
std::env::remove_var("DEEPSEEK_API_KEY");
std::env::remove_var("DEEPSEEK_BASE_URL");
std::env::remove_var("DEEPSEEK_MODEL");
let config_path = write_deepseek_config_with_skills_dir(
&workspace_root,
"deepseek-test-key",
&base_url,
"deepseek-chat",
Some("skill_lib"),
);
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
let transport = Arc::new(MockTransport::new(vec![]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
handle_browser_message_with_context(
transport.as_ref(),
&browser_tool,
&runtime_context,
BrowserMessage::SubmitTask {
instruction: "告诉我当前有哪些 zhihu skill".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: String::new(),
page_title: String::new(),
},
)
.unwrap();
server_handle.join().unwrap();
let sent = transport.sent_messages();
let request_bodies = requests.lock().unwrap().clone();
let first_request = request_bodies[0].to_string();
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::TaskComplete { success, summary }
if *success && summary == "已识别自定义 skill 目录"
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "info" &&
message.contains("loaded skills: configured-zhihu-skill@0.1.0")
)
}));
assert_eq!(request_bodies.len(), 1);
assert!(first_request.contains("configured-zhihu-skill"));
assert!(!first_request.contains("workspace-only-skill"));
}
#[test]
fn handle_browser_message_routes_supported_instruction_to_compat_runtime_when_llm_is_configured() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let first_response = json!({
"choices": [{
"message": {
"content": "",
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {
"name": "browser_action",
"arguments": serde_json::to_string(&json!({
"action": "navigate",
"expected_domain": "www.baidu.com",
"url": "https://www.baidu.com"
})).unwrap()
}
},
{
"id": "call_2",
"type": "function",
"function": {
"name": "browser_action",
"arguments": serde_json::to_string(&json!({
"action": "type",
"expected_domain": "www.baidu.com",
"selector": "#kw",
"text": "天气",
"clear_first": true
})).unwrap()
}
},
{
"id": "call_3",
"type": "function",
"function": {
"name": "browser_action",
"arguments": serde_json::to_string(&json!({
"action": "click",
"expected_domain": "www.baidu.com",
"selector": "#su"
})).unwrap()
}
}
]
}
}]
});
let second_response = json!({
"choices": [{
"message": {
"content": "已在百度搜索天气"
}
}]
});
let (base_url, requests, server_handle) =
start_fake_deepseek_server(vec![first_response, second_response]);
std::env::remove_var("DEEPSEEK_API_KEY");
std::env::remove_var("DEEPSEEK_BASE_URL");
std::env::remove_var("DEEPSEEK_MODEL");
let workspace_root = temp_workspace_root();
let config_path = write_deepseek_config(
&workspace_root,
"deepseek-test-key",
&base_url,
"deepseek-chat",
);
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
let transport = Arc::new(MockTransport::new(vec![
BrowserMessage::Response {
seq: 1,
success: true,
data: json!({ "navigated": true }),
aom_snapshot: vec![],
timing: Timing {
queue_ms: 1,
exec_ms: 10,
},
},
BrowserMessage::Response {
seq: 2,
success: true,
data: json!({ "typed": true }),
aom_snapshot: vec![],
timing: Timing {
queue_ms: 1,
exec_ms: 10,
},
},
BrowserMessage::Response {
seq: 3,
success: true,
data: json!({ "clicked": true }),
aom_snapshot: vec![],
timing: Timing {
queue_ms: 1,
exec_ms: 10,
},
},
]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
handle_browser_message_with_context(
transport.as_ref(),
&browser_tool,
&runtime_context,
BrowserMessage::SubmitTask {
instruction: "打开百度搜索天气".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: String::new(),
page_title: String::new(),
},
)
.unwrap();
server_handle.join().unwrap();
let sent = transport.sent_messages();
let request_bodies = requests.lock().unwrap().clone();
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::TaskComplete { success, summary }
if *success && summary == "已在百度搜索天气"
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "info"
&& message.starts_with("sgclaw runtime version=")
&& message.ends_with(" protocol=1.0")
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "mode" && message == "compat_llm_primary"
)
}));
assert!(!sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "mode" && message == "deterministic_planner"
)
}));
assert_eq!(request_bodies.len(), 2);
}
#[test]
fn handle_browser_message_emits_plan_preview_before_runtime_execution() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let first_response = json!({
"choices": [{
"message": {
"content": "",
"tool_calls": [{
"id": "call_1",
"type": "function",
"function": {
"name": "browser_action",
"arguments": serde_json::to_string(&json!({
"action": "navigate",
"expected_domain": "www.baidu.com",
"url": "https://www.baidu.com"
})).unwrap()
}
}]
}
}]
});
let second_response = json!({
"choices": [{
"message": {
"content": "已打开百度首页"
}
}]
});
let (base_url, _requests, server_handle) =
start_fake_deepseek_server(vec![first_response, second_response]);
let workspace_root = temp_workspace_root();
let config_path = workspace_root.join("sgclaw_config.json");
fs::write(
&config_path,
serde_json::to_string_pretty(&json!({
"apiKey": "deepseek-test-key",
"baseUrl": base_url,
"model": "deepseek-chat",
"plannerMode": "zeroclawPlanFirst"
}))
.unwrap(),
)
.unwrap();
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
let transport = Arc::new(MockTransport::new(vec![success_browser_response(
1,
json!({ "navigated": true }),
)]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
handle_browser_message_with_context(
transport.as_ref(),
&browser_tool,
&runtime_context,
BrowserMessage::SubmitTask {
instruction: "打开百度".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: String::new(),
page_title: String::new(),
},
)
.unwrap();
server_handle.join().unwrap();
let sent = transport.sent_messages();
let preview_index = sent
.iter()
.position(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "plan" && message.contains("navigate https://www.baidu.com")
)
})
.expect("expected plan preview log entry");
let navigate_index = sent
.iter()
.position(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "info" && message == "navigate https://www.baidu.com"
)
})
.expect("expected runtime navigate log entry");
assert!(preview_index < navigate_index);
}
#[test]
fn handle_browser_message_falls_back_to_compat_runtime_for_unsupported_instruction() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let first_response = json!({
"choices": [{
"message": {
"content": "",
"tool_calls": [{
"id": "call_1",
"type": "function",
"function": {
"name": "browser_action",
"arguments": serde_json::to_string(&json!({
"action": "navigate",
"expected_domain": "www.baidu.com",
"url": "https://www.baidu.com"
})).unwrap()
}
}]
}
}]
});
let second_response = json!({
"choices": [{
"message": {
"content": "来自 ZeroClaw runtime"
}
}]
});
let (base_url, requests, server_handle) =
start_fake_deepseek_server(vec![first_response, second_response]);
std::env::set_var("DEEPSEEK_API_KEY", "deepseek-test-key");
std::env::set_var("DEEPSEEK_BASE_URL", base_url);
std::env::set_var("DEEPSEEK_MODEL", "deepseek-chat");
let workspace_root = temp_workspace_root();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(&workspace_root).unwrap();
let transport = Arc::new(MockTransport::new(vec![BrowserMessage::Response {
seq: 1,
success: true,
data: json!({ "navigated": true }),
aom_snapshot: vec![],
timing: Timing {
queue_ms: 1,
exec_ms: 10,
},
}]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
handle_browser_message(
transport.as_ref(),
&browser_tool,
BrowserMessage::SubmitTask {
instruction: "帮我打开百度首页".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: String::new(),
page_title: String::new(),
},
)
.unwrap();
server_handle.join().unwrap();
std::env::set_current_dir(original_dir).unwrap();
let sent = transport.sent_messages();
let request_bodies = requests.lock().unwrap().clone();
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::TaskComplete { success, summary }
if *success && summary == "来自 ZeroClaw runtime"
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "mode" && message == "compat_llm_primary"
)
}));
assert_eq!(request_bodies.len(), 2);
}
#[test]
fn handle_browser_message_requires_llm_configuration_when_no_model_is_available() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
std::env::remove_var("DEEPSEEK_API_KEY");
std::env::remove_var("DEEPSEEK_BASE_URL");
std::env::remove_var("DEEPSEEK_MODEL");
let transport = Arc::new(MockTransport::new(vec![]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
handle_browser_message(
transport.as_ref(),
&browser_tool,
BrowserMessage::SubmitTask {
instruction: "你好".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: String::new(),
page_title: String::new(),
},
)
.unwrap();
let sent = transport.sent_messages();
assert!(matches!(
sent.last(),
Some(AgentMessage::TaskComplete { success, summary })
if !success && summary.contains("未配置大语言模型")
));
}
#[test]
fn compat_runtime_includes_prior_turns_in_follow_up_provider_request() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let first_response = json!({
"choices": [{
"message": {
"content": "",
"tool_calls": [{
"id": "call_1",
"type": "function",
"function": {
"name": "browser_action",
"arguments": serde_json::to_string(&json!({
"action": "navigate",
"expected_domain": "www.zhihu.com",
"url": "https://www.zhihu.com/search?q=天气&type=content"
})).unwrap()
}
}]
}
}]
});
let second_response = json!({
"choices": [{
"message": {
"content": "已在知乎搜索天气"
}
}]
});
let (base_url, requests, server_handle) =
start_fake_deepseek_server(vec![first_response, second_response]);
let workspace_root = temp_workspace_root();
let settings = DeepSeekSettings {
api_key: "deepseek-test-key".to_string(),
base_url,
model: "deepseek-chat".to_string(),
skills_dir: None,
};
let transport = Arc::new(MockTransport::new(vec![BrowserMessage::Response {
seq: 1,
success: true,
data: json!({ "navigated": true }),
aom_snapshot: vec![],
timing: Timing {
queue_ms: 1,
exec_ms: 10,
},
}]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
let task_context = CompatTaskContext {
conversation_id: Some("conversation-1".to_string()),
messages: vec![
ConversationMessage {
role: "user".to_string(),
content: "打开百度搜索天气".to_string(),
},
ConversationMessage {
role: "assistant".to_string(),
content: "已在百度搜索天气".to_string(),
},
],
page_url: Some("https://www.zhihu.com/".to_string()),
page_title: Some("知乎".to_string()),
};
let summary = execute_task(
transport.as_ref(),
browser_tool,
"打开知乎搜索天气",
&task_context,
&workspace_root,
&settings,
)
.unwrap();
server_handle.join().unwrap();
let request_bodies = requests.lock().unwrap().clone();
let first_request_messages = request_bodies[0]["messages"]
.as_array()
.cloned()
.unwrap_or_default();
assert_eq!(summary, "已在知乎搜索天气");
assert!(first_request_messages.iter().any(|message| {
message["role"] == json!("user") && message["content"] == json!("打开百度搜索天气")
}));
assert!(first_request_messages.iter().any(|message| {
message["role"] == json!("assistant") && message["content"] == json!("已在百度搜索天气")
}));
}
#[test]
fn compat_runtime_does_not_forward_raw_aom_snapshot_back_to_provider() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let first_response = json!({
"choices": [{
"message": {
"content": "",
"tool_calls": [{
"id": "call_1",
"type": "function",
"function": {
"name": "browser_action",
"arguments": serde_json::to_string(&json!({
"action": "navigate",
"expected_domain": "www.baidu.com",
"url": "https://www.baidu.com"
})).unwrap()
}
}]
}
}]
});
let second_response = json!({
"choices": [{
"message": {
"content": "来自 ZeroClaw runtime"
}
}]
});
let (base_url, requests, server_handle) =
start_fake_deepseek_server(vec![first_response, second_response]);
let workspace_root = temp_workspace_root();
let settings = DeepSeekSettings {
api_key: "deepseek-test-key".to_string(),
base_url,
model: "deepseek-chat".to_string(),
skills_dir: None,
};
let large_snapshot_marker = "snapshot-marker ".repeat(2048);
let transport = Arc::new(MockTransport::new(vec![BrowserMessage::Response {
seq: 1,
success: true,
data: json!({ "navigated": true }),
aom_snapshot: vec![json!({
"role": "RootWebArea",
"name": "百度一下,你就知道",
"text": large_snapshot_marker
})],
timing: Timing {
queue_ms: 1,
exec_ms: 10,
},
}]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
let summary = execute_task(
transport.as_ref(),
browser_tool,
"打开百度首页",
&CompatTaskContext::default(),
&workspace_root,
&settings,
)
.unwrap();
server_handle.join().unwrap();
let request_bodies = requests.lock().unwrap().clone();
let follow_up_request = request_bodies[1].to_string();
assert_eq!(summary, "来自 ZeroClaw runtime");
assert_eq!(request_bodies.len(), 2);
assert!(!follow_up_request.contains("snapshot-marker"));
}
#[test]
fn compat_runtime_injects_browser_contract_and_page_context_into_provider_request() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let response = json!({
"choices": [{
"message": {
"content": "已收到页面上下文"
}
}]
});
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
let workspace_root = temp_workspace_root();
let settings = DeepSeekSettings {
api_key: "deepseek-test-key".to_string(),
base_url,
model: "deepseek-chat".to_string(),
skills_dir: None,
};
let transport = Arc::new(MockTransport::new(vec![]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
let task_context = CompatTaskContext {
conversation_id: Some("conversation-ctx".to_string()),
messages: vec![],
page_url: Some("https://www.zhihu.com/hot".to_string()),
page_title: Some("知乎热榜".to_string()),
};
let summary = execute_task(
transport.as_ref(),
browser_tool,
"统计一下知乎热帮信息",
&task_context,
&workspace_root,
&settings,
)
.unwrap();
server_handle.join().unwrap();
let request_bodies = requests.lock().unwrap().clone();
let first_request_messages = request_bodies[0]["messages"]
.as_array()
.cloned()
.unwrap_or_default();
let flattened = first_request_messages
.iter()
.filter_map(|message| message["content"].as_str())
.collect::<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(),
None,
)
.unwrap();
settings.runtime_profile = RuntimeProfile::GeneralAssistant;
let transport = Arc::new(MockTransport::new(vec![]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
let summary = execute_task_with_sgclaw_settings(
transport.as_ref(),
browser_tool,
"直接回答:你好",
&CompatTaskContext::default(),
&workspace_root,
&settings,
)
.unwrap();
server_handle.join().unwrap();
let request_bodies = requests.lock().unwrap().clone();
let flattened = request_bodies[0]["messages"]
.as_array()
.cloned()
.unwrap_or_default()
.iter()
.filter_map(|message| message["content"].as_str())
.collect::<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(),
None,
)
.unwrap();
let transport = Arc::new(MockTransport::new(vec![]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
let summary = execute_task_with_sgclaw_settings(
transport.as_ref(),
browser_tool,
"告诉我当前有哪些 skill",
&CompatTaskContext::default(),
&workspace_root,
&settings,
)
.unwrap();
server_handle.join().unwrap();
let request_bodies = requests.lock().unwrap().clone();
let tool_entries = request_bodies[0]["tools"]
.as_array()
.cloned()
.unwrap_or_default();
let tool_names = tool_entries
.into_iter()
.filter_map(|tool| tool["function"]["name"].as_str().map(str::to_string))
.collect::<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(),
None,
)
.unwrap();
settings.runtime_profile = RuntimeProfile::BrowserAttached;
let transport = Arc::new(MockTransport::new(vec![]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
zhihu_test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
let summary = execute_task_with_sgclaw_settings(
transport.as_ref(),
browser_tool,
"告诉我当前有哪些知乎热榜工具",
&CompatTaskContext::default(),
&workspace_root,
&settings,
)
.unwrap();
server_handle.join().unwrap();
let request_bodies = requests.lock().unwrap().clone();
let tool_names = request_tool_names(&request_bodies[0]);
assert_eq!(summary, "已看到 browser_script skill 工具");
assert!(tool_names.contains(&"browser_action".to_string()));
assert!(tool_names.contains(&"superrpa_browser".to_string()));
assert!(tool_names.contains(&"read_skill".to_string()));
assert!(tool_names.contains(&"workspace-zhihu-skill_extract_hotlist".to_string()));
}
#[test]
fn compat_runtime_executes_browser_script_skill_via_eval_without_gettext_probing() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let first_response = json!({
"choices": [{
"message": {
"content": "",
"tool_calls": [{
"id": "call_1",
"type": "function",
"function": {
"name": "workspace-zhihu-skill_extract_hotlist",
"arguments": serde_json::to_string(&json!({
"expected_domain": "www.zhihu.com",
"top_n": "10"
})).unwrap()
}
}]
}
}]
});
let second_response = json!({
"choices": [{
"message": {
"content": "已执行 browser_script skill"
}
}]
});
let (base_url, requests, server_handle) =
start_fake_deepseek_server(vec![first_response, second_response]);
let workspace_root = temp_workspace_root();
let default_skills_dir = workspace_root
.join(".sgclaw-zeroclaw-workspace")
.join("skills");
let skill_dir = write_skill_manifest_package(
&default_skills_dir,
"workspace-zhihu-skill",
r#"
[skill]
name = "workspace-zhihu-skill"
description = "Extract Zhihu hotlist rows with a packaged browser script."
version = "0.1.0"
[[tools]]
name = "extract_hotlist"
description = "Extract structured hotlist rows from the current Zhihu page."
kind = "browser_script"
command = "scripts/extract_hotlist.js"
[tools.args]
top_n = "How many hotlist rows to extract."
"#,
);
write_skill_script(
&skill_dir,
"scripts/extract_hotlist.js",
r#"
const topN = Number(args.top_n || 10);
return {
source: "https://www.zhihu.com/hot",
sheet_name: "知乎热榜",
columns: ["rank", "title", "heat"],
rows: [[1, "标题", `${topN}条`]]
};
"#,
);
let mut settings = SgClawSettings::from_legacy_deepseek_fields(
"deepseek-test-key".to_string(),
base_url,
"deepseek-chat".to_string(),
None,
)
.unwrap();
settings.runtime_profile = RuntimeProfile::BrowserAttached;
let transport = Arc::new(MockTransport::new(vec![success_browser_response(
1,
json!({
"text": {
"source": "https://www.zhihu.com/hot",
"sheet_name": "知乎热榜",
"columns": ["rank", "title", "heat"],
"rows": [[1, "标题", "10条"]]
}
}),
)]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
zhihu_test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
let summary = execute_task_with_sgclaw_settings(
transport.as_ref(),
browser_tool,
"用知乎热榜 skill 提取前十条结构化数据",
&CompatTaskContext::default(),
&workspace_root,
&settings,
)
.unwrap();
server_handle.join().unwrap();
let sent = transport.sent_messages();
let request_bodies = requests.lock().unwrap().clone();
let tool_names = request_tool_names(&request_bodies[0]);
assert_eq!(summary, "已执行 browser_script skill");
assert!(tool_names.contains(&"workspace-zhihu-skill_extract_hotlist".to_string()));
assert!(sent.iter().any(|message| {
matches!(message, AgentMessage::LogEntry { level, message }
if level == "info" && message == "call workspace-zhihu-skill.extract_hotlist")
}));
assert!(sent.iter().any(|message| {
matches!(message, AgentMessage::Command { action, .. } if action == &Action::Eval)
}));
assert!(!sent.iter().any(|message| {
matches!(message, AgentMessage::LogEntry { level, message }
if level == "info" && message.starts_with("getText "))
}));
}
#[test]
fn zhihu_hotlist_browser_skill_flow_does_not_expose_shell_or_glob_tools() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let response = json!({
"choices": [{
"message": {
"content": "已准备好知乎热榜技能"
}
}]
});
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
let workspace_root = temp_workspace_root();
let mut settings = SgClawSettings::from_legacy_deepseek_fields(
"deepseek-test-key".to_string(),
base_url,
"deepseek-chat".to_string(),
Some(real_skill_lib_root()),
)
.unwrap();
settings.runtime_profile = RuntimeProfile::BrowserAttached;
let transport = Arc::new(MockTransport::new(vec![]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
zhihu_test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
let summary = execute_task_with_sgclaw_settings(
transport.as_ref(),
browser_tool,
"统计一下知乎热榜前十,给出标题和热度",
&CompatTaskContext::default(),
&workspace_root,
&settings,
)
.unwrap();
server_handle.join().unwrap();
let request_bodies = requests.lock().unwrap().clone();
let tool_names = request_tool_names(&request_bodies[0]);
assert_eq!(summary, "已准备好知乎热榜技能");
assert!(tool_names.contains(&"superrpa_browser".to_string()));
assert!(tool_names.contains(&"browser_action".to_string()));
assert!(tool_names.contains(&"read_skill".to_string()));
assert!(tool_names.contains(&"zhihu-hotlist_extract_hotlist".to_string()));
assert!(!tool_names.contains(&"shell".to_string()));
assert!(!tool_names.contains(&"glob_search".to_string()));
}
#[test]
fn compat_runtime_browser_attached_profile_keeps_file_read_available_for_local_paths() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let response = json!({
"choices": [{
"message": {
"content": "已收到本地路径任务"
}
}]
});
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
let workspace_root = temp_workspace_root();
let mut settings = SgClawSettings::from_legacy_deepseek_fields(
"deepseek-test-key".to_string(),
base_url,
"deepseek-chat".to_string(),
None,
)
.unwrap();
settings.runtime_profile = RuntimeProfile::BrowserAttached;
let transport = Arc::new(MockTransport::new(vec![]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
let summary = execute_task_with_sgclaw_settings(
transport.as_ref(),
browser_tool,
"请读取本地文件 /home/zyl/data/report.md 的内容并总结",
&CompatTaskContext::default(),
&workspace_root,
&settings,
)
.unwrap();
server_handle.join().unwrap();
let request_bodies = requests.lock().unwrap().clone();
let tool_names = request_tool_names(&request_bodies[0]);
assert_eq!(summary, "已收到本地路径任务");
assert!(tool_names.contains(&"browser_action".to_string()));
assert!(tool_names.contains(&"superrpa_browser".to_string()));
assert!(tool_names.contains(&"file_read".to_string()));
}
#[test]
fn browser_attached_export_flow_exposes_browser_and_office_tools_only() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let response = json!({
"choices": [{
"message": {
"content": "已准备好知乎导出流程"
}
}]
});
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
let workspace_root = temp_workspace_root();
let mut settings = SgClawSettings::from_legacy_deepseek_fields(
"deepseek-test-key".to_string(),
base_url,
"deepseek-chat".to_string(),
Some(real_skill_lib_root()),
)
.unwrap();
settings.runtime_profile = RuntimeProfile::BrowserAttached;
let transport = Arc::new(MockTransport::new(vec![]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
zhihu_test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
let summary = execute_task_with_sgclaw_settings(
transport.as_ref(),
browser_tool,
"读取知乎热榜数据,并导出 excel 文件",
&CompatTaskContext::default(),
&workspace_root,
&settings,
)
.unwrap();
server_handle.join().unwrap();
let request_bodies = requests.lock().unwrap().clone();
let tool_names = request_tool_names(&request_bodies[0]);
assert_eq!(summary, "已准备好知乎导出流程");
assert!(tool_names.contains(&"superrpa_browser".to_string()));
assert!(tool_names.contains(&"browser_action".to_string()));
assert!(tool_names.contains(&"read_skill".to_string()));
assert!(tool_names.contains(&"zhihu-hotlist_extract_hotlist".to_string()));
assert!(tool_names.contains(&"openxml_office".to_string()));
assert!(!tool_names.contains(&"shell".to_string()));
assert!(!tool_names.contains(&"glob_search".to_string()));
}
#[test]
fn compat_runtime_allows_zhihu_hotlist_screen_export_tool_in_browser_profile() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let response = json!({
"choices": [{
"message": {
"content": "已准备好知乎热榜大屏流程"
}
}]
});
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
let workspace_root = temp_workspace_root();
let mut settings = SgClawSettings::from_legacy_deepseek_fields(
"deepseek-test-key".to_string(),
base_url,
"deepseek-chat".to_string(),
Some(real_skill_lib_root()),
)
.unwrap();
settings.runtime_profile = RuntimeProfile::BrowserAttached;
let transport = Arc::new(MockTransport::new(vec![]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
zhihu_test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
let summary = execute_task_with_sgclaw_settings(
transport.as_ref(),
browser_tool,
"读取知乎热榜数据并生成领导演示大屏,在新标签页展示",
&CompatTaskContext::default(),
&workspace_root,
&settings,
)
.unwrap();
server_handle.join().unwrap();
let request_bodies = requests.lock().unwrap().clone();
let tool_names = request_tool_names(&request_bodies[0]);
assert_eq!(summary, "已准备好知乎热榜大屏流程");
assert!(tool_names.contains(&"superrpa_browser".to_string()));
assert!(tool_names.contains(&"browser_action".to_string()));
assert!(tool_names.contains(&"read_skill".to_string()));
assert!(tool_names.contains(&"zhihu-hotlist_extract_hotlist".to_string()));
assert!(tool_names.contains(&"screen_html_export".to_string()));
assert!(!tool_names.contains(&"shell".to_string()));
assert!(!tool_names.contains(&"glob_search".to_string()));
}
#[test]
fn compat_runtime_logs_read_skill_usage_with_skill_name() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let first_response = json!({
"choices": [{
"message": {
"content": "",
"tool_calls": [{
"id": "call_1",
"type": "function",
"function": {
"name": "read_skill",
"arguments": serde_json::to_string(&json!({
"name": "workspace-zhihu-skill"
})).unwrap()
}
}]
}
}]
});
let second_response = json!({
"choices": [{
"message": {
"content": "已读取完整 skill"
}
}]
});
let (base_url, _requests, server_handle) =
start_fake_deepseek_server(vec![first_response, second_response]);
let workspace_root = temp_workspace_root();
let default_skills_dir = workspace_root
.join(".sgclaw-zeroclaw-workspace")
.join("skills");
write_skill_package(
&default_skills_dir,
"workspace-zhihu-skill",
"# Workspace Zhihu Skill\nUse this workspace-local skill.\n",
);
let settings = SgClawSettings::from_legacy_deepseek_fields(
"deepseek-test-key".to_string(),
base_url,
"deepseek-chat".to_string(),
None,
)
.unwrap();
let transport = Arc::new(MockTransport::new(vec![]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
let summary = execute_task_with_sgclaw_settings(
transport.as_ref(),
browser_tool,
"需要完整 zhihu skill 时先读取 skill",
&CompatTaskContext::default(),
&workspace_root,
&settings,
)
.unwrap();
server_handle.join().unwrap();
let sent = transport.sent_messages();
assert_eq!(summary, "已读取完整 skill");
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "info" &&
message == "read_skill workspace-zhihu-skill@0.1.0"
)
}));
}
#[test]
fn handle_browser_message_exposes_real_zhihu_skill_lib_to_provider_request() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let response = json!({
"choices": [{
"message": {
"content": "已看到真实知乎 skill"
}
}]
});
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
let workspace_root = temp_workspace_root();
let skills_dir = real_skill_lib_root();
let config_path = write_deepseek_config_with_skills_dir(
&workspace_root,
"deepseek-test-key",
&base_url,
"deepseek-chat",
Some(skills_dir.to_str().unwrap()),
);
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
let transport = Arc::new(MockTransport::new(vec![]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
zhihu_test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
handle_browser_message_with_context(
transport.as_ref(),
&browser_tool,
&runtime_context,
BrowserMessage::SubmitTask {
instruction: "告诉我当前有哪些知乎 skill".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "https://example.net/".to_string(),
page_title: "Example Domain".to_string(),
},
)
.unwrap();
server_handle.join().unwrap();
let sent = transport.sent_messages();
let request_bodies = requests.lock().unwrap().clone();
let first_request = request_bodies[0].to_string();
let tool_names = request_tool_names(&request_bodies[0]);
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::TaskComplete { success, summary }
if *success && summary == "已看到真实知乎 skill"
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "info"
&& message.contains("loaded skills:")
&& message.contains("office-export-xlsx@0.1.0")
&& message.contains("zhihu-hotlist@0.1.0")
&& message.contains("zhihu-hotlist-screen@0.1.0")
&& message.contains("zhihu-navigate@0.1.0")
&& 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(),
Some(real_skill_lib_root()),
)
.unwrap();
settings.runtime_profile = RuntimeProfile::BrowserAttached;
let transport = Arc::new(MockTransport::new(vec![]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
zhihu_test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
let summary = execute_task_with_sgclaw_settings(
transport.as_ref(),
browser_tool,
"读取知乎热榜数据,并导出 excel 文件",
&CompatTaskContext::default(),
&workspace_root,
&settings,
)
.unwrap();
server_handle.join().unwrap();
let request_bodies = requests.lock().unwrap().clone();
let first_request = request_bodies[0].to_string();
assert_eq!(summary, "已收到知乎导出任务");
assert!(first_request.contains("Zhihu hotlist execution contract"));
assert!(first_request.contains("Export completion contract"));
assert!(first_request.contains("openxml_office"));
assert!(!first_request.contains("Preloaded skill context:"));
}
#[test]
fn browser_attached_publish_request_injects_confirmation_contract() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let response = json!({
"choices": [{
"message": {
"content": "请先确认是否发布"
}
}]
});
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
let workspace_root = temp_workspace_root();
let mut settings = SgClawSettings::from_legacy_deepseek_fields(
"deepseek-test-key".to_string(),
base_url,
"deepseek-chat".to_string(),
Some(real_skill_lib_root()),
)
.unwrap();
settings.runtime_profile = RuntimeProfile::BrowserAttached;
let transport = Arc::new(MockTransport::new(vec![]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
zhihu_test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
execute_task_with_sgclaw_settings(
transport.as_ref(),
browser_tool,
"请直接发表这篇知乎文章,标题是测试标题,正文是第一段内容",
&CompatTaskContext {
conversation_id: None,
messages: vec![],
page_url: Some("https://www.zhihu.com/creator".to_string()),
page_title: Some("知乎创作中心".to_string()),
},
&workspace_root,
&settings,
)
.unwrap();
server_handle.join().unwrap();
let request_bodies = requests.lock().unwrap().clone();
let first_request = request_bodies[0].to_string();
assert!(first_request.contains("Zhihu article publish contract"));
assert!(first_request.contains("must not click publish without explicit human confirmation"));
assert!(first_request.contains("ask for confirmation concisely"));
assert!(first_request.contains("stop after the confirmation request"));
}
#[test]
fn handle_browser_message_executes_real_zhihu_hotlist_skill_flow() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let first_response = json!({
"choices": [{
"message": {
"content": "",
"tool_calls": [{
"id": "call_1",
"type": "function",
"function": {
"name": "zhihu-hotlist_extract_hotlist",
"arguments": serde_json::to_string(&json!({
"expected_domain": "www.zhihu.com",
"top_n": "10"
})).unwrap()
}
}]
}
}]
});
let second_response = json!({
"choices": [{
"message": {
"content": "已完成知乎热榜采集"
}
}]
});
let (base_url, requests, server_handle) =
start_fake_deepseek_server(vec![first_response, second_response]);
let workspace_root = temp_workspace_root();
let skills_dir = real_skill_lib_root();
let config_path = write_deepseek_config_with_skills_dir(
&workspace_root,
"deepseek-test-key",
&base_url,
"deepseek-chat",
Some(skills_dir.to_str().unwrap()),
);
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
let transport = Arc::new(MockTransport::new(vec![success_browser_response(
1,
json!({
"text": {
"source": "https://www.zhihu.com/hot",
"sheet_name": "知乎热榜",
"columns": ["rank", "title", "heat"],
"rows": [[1, "热榜项目 1", "1707万"], [2, "热榜项目 2", "1150万"]]
}
}),
)]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
zhihu_test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
handle_browser_message_with_context(
transport.as_ref(),
&browser_tool,
&runtime_context,
BrowserMessage::SubmitTask {
instruction: "统计一下知乎热榜前十".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "https://www.zhihu.com/".to_string(),
page_title: "知乎".to_string(),
},
)
.unwrap();
server_handle.join().unwrap();
let sent = transport.sent_messages();
let request_bodies = requests.lock().unwrap().clone();
let tool_content = tool_message_content(&request_bodies[1], "call_1").unwrap();
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::TaskComplete { success, summary }
if *success && summary == "已完成知乎热榜采集"
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "info" && message == "call zhihu-hotlist.extract_hotlist"
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::Command { action, params, .. }
if action == &Action::Eval &&
params["script"].as_str().unwrap_or_default().contains("columns: ['rank', 'title', 'heat']")
)
}));
assert_eq!(request_bodies.len(), 2);
assert!(tool_content.contains("知乎热榜"));
assert!(tool_content.contains("rank"));
assert!(tool_content.contains("heat"));
assert!(tool_content.contains("热榜项目 1"));
}
#[test]
fn handle_browser_message_chains_hotlist_skill_into_office_export_tool() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let workspace_root = temp_workspace_root();
let 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: "读取知乎热榜数据,并导出 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);
assert!(summary.contains("已导出知乎热榜 Excel"));
assert!(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::LogEntry { level, message }
if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary")
)
}));
}
#[test]
fn handle_browser_message_chains_hotlist_skill_into_screen_export_tool() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let workspace_root = temp_workspace_root();
let config_path = write_deepseek_config_with_skills_dir(
&workspace_root,
"deepseek-test-key",
"http://127.0.0.1:9",
"deepseek-chat",
Some(real_skill_lib_root().to_str().unwrap()),
);
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
let transport = Arc::new(MockTransport::new(vec![
success_browser_response(
1,
json!({ "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: "读取知乎热榜数据并生成领导演示大屏,在新标签页展示".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, ".html");
assert!(summary.contains("已生成知乎热榜大屏"));
assert!(summary.contains(".html"));
assert!(generated.exists());
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "mode" && message == "zeroclaw_process_message_primary"
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "info" && message == "call zhihu-hotlist.extract_hotlist"
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "info" && message == "call screen_html_export"
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::Command { action, .. } if action == &Action::Eval
)
}));
assert!(!sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary")
)
}));
}
#[test]
fn handle_browser_message_runs_zhihu_hotlist_export_via_zeroclaw_primary_orchestration() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let workspace_root = temp_workspace_root();
let config_path = write_deepseek_config_with_skills_dir(
&workspace_root,
"deepseek-test-key",
"http://127.0.0.1:9",
"deepseek-chat",
Some(real_skill_lib_root().to_str().unwrap()),
);
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
let transport = Arc::new(MockTransport::new(vec![
success_browser_response(
1,
json!({ "text": "知乎热榜\n1 问题一 344万热度\n2 问题二 266万热度\n3 问题三 181万热度" }),
),
success_browser_response(
2,
json!({
"text": {
"source": "https://www.zhihu.com/hot",
"sheet_name": "知乎热榜",
"columns": ["rank", "title", "heat"],
"rows": [
[1, "问题一", "344万"],
[2, "问题二", "266万"],
[3, "问题三", "181万"]
]
}
}),
),
]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
zhihu_test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
handle_browser_message_with_context(
transport.as_ref(),
&browser_tool,
&runtime_context,
BrowserMessage::SubmitTask {
instruction: "读取知乎热榜前10并导出 excel 文件".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "https://www.zhihu.com/hot".to_string(),
page_title: "知乎热榜".to_string(),
},
)
.unwrap();
let sent = transport.sent_messages();
let summary = task_complete_summary(&sent);
let generated = extract_generated_artifact_path(&summary, ".xlsx");
assert!(summary.contains(".xlsx"));
assert!(generated.exists());
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "mode" && message == "zeroclaw_process_message_primary"
)
}));
assert!(!sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary")
)
}));
}
#[test]
fn browser_submit_path_prefers_zeroclaw_process_message_orchestrator() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let workspace_root = temp_workspace_root();
let config_path = write_deepseek_config_with_skills_dir(
&workspace_root,
"deepseek-test-key",
"http://127.0.0.1:9",
"deepseek-chat",
Some(real_skill_lib_root().to_str().unwrap()),
);
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
let transport = Arc::new(MockTransport::new(vec![]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
zhihu_test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
handle_browser_message_with_context(
transport.as_ref(),
&browser_tool,
&runtime_context,
BrowserMessage::SubmitTask {
instruction: "读取知乎热榜前10并导出 excel 文件".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "https://www.zhihu.com/hot".to_string(),
page_title: "知乎热榜".to_string(),
},
)
.unwrap();
let sent = transport.sent_messages();
dbg!(&sent);
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "mode" && message == "zeroclaw_process_message_primary"
)
}));
assert!(!sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary")
)
}));
}
#[test]
fn browser_submit_path_prefers_zeroclaw_process_message_orchestrator_for_zhihu_publish() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let workspace_root = temp_workspace_root();
let config_path = write_deepseek_config_with_skills_dir(
&workspace_root,
"deepseek-test-key",
"http://127.0.0.1:9",
"deepseek-chat",
Some(real_skill_lib_root().to_str().unwrap()),
);
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
let transport = Arc::new(MockTransport::new(vec![]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
zhihu_test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
handle_browser_message_with_context(
transport.as_ref(),
&browser_tool,
&runtime_context,
BrowserMessage::SubmitTask {
instruction: "请直接发表这篇知乎文章,标题是测试标题,正文是第一段内容".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "https://www.zhihu.com/".to_string(),
page_title: "知乎".to_string(),
},
)
.unwrap();
let sent = transport.sent_messages();
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "mode" && message == "zeroclaw_process_message_primary"
)
}));
assert!(!sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary")
)
}));
}
#[test]
fn zhihu_publish_task_matches_primary_orchestration_gate() {
assert!(
sgclaw::compat::orchestration::should_use_primary_orchestration(
"请直接发表这篇知乎文章,标题是测试标题,正文是第一段内容",
Some("https://www.zhihu.com/"),
Some("知乎"),
)
);
}
#[test]
fn zhihu_article_entry_task_matches_primary_orchestration_gate() {
assert!(
sgclaw::compat::orchestration::should_use_primary_orchestration(
"打开知乎发文章页面",
Some("https://www.zhihu.com/"),
Some("知乎"),
)
);
}
#[test]
fn zhihu_hotlist_export_routes_prefer_direct_execution() {
use sgclaw::compat::workflow_executor::{prefers_direct_execution, WorkflowRoute};
assert!(prefers_direct_execution(
&WorkflowRoute::ZhihuHotlistExportXlsx
));
assert!(prefers_direct_execution(&WorkflowRoute::ZhihuHotlistScreen));
}
#[test]
fn zhihu_publish_without_article_inputs_returns_missing_fields_prompt() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let workspace_root = temp_workspace_root();
let config_path = write_deepseek_config_with_skills_dir(
&workspace_root,
"deepseek-test-key",
"http://127.0.0.1:9",
"deepseek-chat",
Some(real_skill_lib_root().to_str().unwrap()),
);
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
let transport = Arc::new(MockTransport::new(vec![]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
zhihu_test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
handle_browser_message_with_context(
transport.as_ref(),
&browser_tool,
&runtime_context,
BrowserMessage::SubmitTask {
instruction: "请发表知乎文章".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "https://www.zhihu.com/".to_string(),
page_title: "知乎".to_string(),
},
)
.unwrap();
let sent = transport.sent_messages();
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::TaskComplete { success, summary }
if *success &&
summary.contains("标题") &&
summary.contains("正文")
)
}));
assert!(!sent
.iter()
.any(|message| { matches!(message, AgentMessage::Command { .. }) }));
}
#[test]
fn zhihu_publish_accepts_literal_backslash_n_between_title_and_body() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let workspace_root = temp_workspace_root();
let config_path = write_deepseek_config_with_skills_dir(
&workspace_root,
"deepseek-test-key",
"http://127.0.0.1:9",
"deepseek-chat",
Some(real_skill_lib_root().to_str().unwrap()),
);
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
let transport = Arc::new(MockTransport::new(vec![
success_browser_response(1, json!({ "navigated": true })),
success_browser_response(
2,
json!({
"text": {
"status": "creator_entry_clicked",
"current_url": "https://www.zhihu.com/creator",
"next_url": "https://zhuanlan.zhihu.com/write"
}
}),
),
success_browser_response(3, json!({ "navigated": true })),
success_browser_response(
4,
json!({
"text": {
"status": "editor_ready",
"current_url": "https://zhuanlan.zhihu.com/write"
}
}),
),
success_browser_response(
5,
json!({
"text": {
"status": "draft_ready",
"current_url": "https://zhuanlan.zhihu.com/write",
"title": "ai时代普通人如何自救"
}
}),
),
]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
zhihu_test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
handle_browser_message_with_context(
transport.as_ref(),
&browser_tool,
&runtime_context,
BrowserMessage::SubmitTask {
instruction: "标题ai时代普通人如何自救 \\n正文第一段内容。 第二段内容。"
.to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "https://www.zhihu.com/creator".to_string(),
page_title: "知乎创作中心".to_string(),
},
)
.unwrap();
let sent = transport.sent_messages();
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::TaskComplete { success, summary }
if *success && summary == "已进入知乎文章编辑器并写入草稿《ai时代普通人如何自救》"
)
}));
}
#[test]
fn zhihu_article_entry_opens_editor_without_generic_selector_probing() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let workspace_root = temp_workspace_root();
let config_path = write_deepseek_config_with_skills_dir(
&workspace_root,
"deepseek-test-key",
"http://127.0.0.1:9",
"deepseek-chat",
Some(real_skill_lib_root().to_str().unwrap()),
);
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
let transport = Arc::new(MockTransport::new(vec![
success_browser_response(1, json!({ "navigated": true })),
success_browser_response(
2,
json!({
"text": {
"status": "creator_entry_clicked",
"current_url": "https://www.zhihu.com/creator",
"next_url": "https://zhuanlan.zhihu.com/write"
}
}),
),
success_browser_response(3, json!({ "navigated": true })),
success_browser_response(
4,
json!({
"text": {
"status": "editor_ready",
"current_url": "https://zhuanlan.zhihu.com/write"
}
}),
),
]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
zhihu_test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
handle_browser_message_with_context(
transport.as_ref(),
&browser_tool,
&runtime_context,
BrowserMessage::SubmitTask {
instruction: "打开知乎发文章页面".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "https://www.zhihu.com/".to_string(),
page_title: "知乎".to_string(),
},
)
.unwrap();
let sent = transport.sent_messages();
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::TaskComplete { success, summary }
if *success && summary.contains("编辑器")
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "mode" && message == "zeroclaw_process_message_primary"
)
}));
assert!(!sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "info" &&
(message.starts_with("getText ") || message.starts_with("click "))
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::Command { action, params, .. }
if action == &Action::Navigate &&
params["url"].as_str() == Some("https://zhuanlan.zhihu.com/write")
)
}));
}
#[test]
fn zhihu_article_entry_reports_editor_unavailable_without_protocol_error() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let workspace_root = temp_workspace_root();
let config_path = write_deepseek_config_with_skills_dir(
&workspace_root,
"deepseek-test-key",
"http://127.0.0.1:9",
"deepseek-chat",
Some(real_skill_lib_root().to_str().unwrap()),
);
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
let transport = Arc::new(MockTransport::new(vec![
success_browser_response(1, json!({ "navigated": true })),
success_browser_response(
2,
json!({
"text": {
"status": "creator_entry_clicked",
"current_url": "https://www.zhihu.com/creator",
"next_url": "https://zhuanlan.zhihu.com/write"
}
}),
),
success_browser_response(3, json!({ "navigated": true })),
success_browser_response(
4,
json!({
"text": {
"status": "editor_unavailable",
"current_url": "https://zhuanlan.zhihu.com/write"
}
}),
),
]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
zhihu_test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
handle_browser_message_with_context(
transport.as_ref(),
&browser_tool,
&runtime_context,
BrowserMessage::SubmitTask {
instruction: "打开知乎发文章页面".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "https://www.zhihu.com/".to_string(),
page_title: "知乎".to_string(),
},
)
.unwrap();
let sent = transport.sent_messages();
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::TaskComplete { success, summary }
if *success &&
summary.contains("未检测到文章编辑器")
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::Command { action, params, .. }
if action == &Action::Navigate &&
params["url"].as_str() == Some("https://zhuanlan.zhihu.com/write")
)
}));
}
#[test]
fn zhihu_article_entry_stops_when_creator_page_has_no_write_entry() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let workspace_root = temp_workspace_root();
let config_path = write_deepseek_config_with_skills_dir(
&workspace_root,
"deepseek-test-key",
"http://127.0.0.1:9",
"deepseek-chat",
Some(real_skill_lib_root().to_str().unwrap()),
);
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
let transport = Arc::new(MockTransport::new(vec![
success_browser_response(1, json!({ "navigated": true })),
success_browser_response(
2,
json!({
"text": {
"status": "creator_home",
"current_url": "https://www.zhihu.com/creator",
"desired_target": "article_editor"
}
}),
),
]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
zhihu_test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
handle_browser_message_with_context(
transport.as_ref(),
&browser_tool,
&runtime_context,
BrowserMessage::SubmitTask {
instruction: "打开知乎发文章页面".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "https://www.zhihu.com/".to_string(),
page_title: "知乎".to_string(),
},
)
.unwrap();
let sent = transport.sent_messages();
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::TaskComplete { success, summary }
if *success && summary.contains("未找到“写文章”入口")
)
}));
assert_eq!(
sent.iter()
.filter(|message| matches!(message, AgentMessage::Command { .. }))
.count(),
2
);
assert!(!sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "info" && message == "call zhihu-write.prepare_article_editor"
)
}));
}
#[test]
fn zhihu_publish_without_confirmation_returns_confirmation_before_any_browser_probing() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let workspace_root = temp_workspace_root();
let config_path = write_deepseek_config_with_skills_dir(
&workspace_root,
"deepseek-test-key",
"http://127.0.0.1:9",
"deepseek-chat",
Some(real_skill_lib_root().to_str().unwrap()),
);
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
let transport = Arc::new(MockTransport::new(vec![]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
zhihu_test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
handle_browser_message_with_context(
transport.as_ref(),
&browser_tool,
&runtime_context,
BrowserMessage::SubmitTask {
instruction: "请直接发表这篇知乎文章,标题是测试标题,正文是第一段内容".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "https://www.zhihu.com/".to_string(),
page_title: "知乎".to_string(),
},
)
.unwrap();
let sent = transport.sent_messages();
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::TaskComplete { success, summary }
if *success && summary.contains("确认发布")
)
}));
assert!(!sent
.iter()
.any(|message| { matches!(message, AgentMessage::Command { .. }) }));
assert!(!sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "info" &&
(message.starts_with("navigate ") ||
message.starts_with("getText ") ||
message.starts_with("click ") ||
message.starts_with("type "))
)
}));
}
#[test]
fn zhihu_publish_after_confirmation_reports_login_block_without_selector_probing() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let workspace_root = temp_workspace_root();
let config_path = write_deepseek_config_with_skills_dir(
&workspace_root,
"deepseek-test-key",
"http://127.0.0.1:9",
"deepseek-chat",
Some(real_skill_lib_root().to_str().unwrap()),
);
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
let transport = Arc::new(MockTransport::new(vec![
success_browser_response(
1,
json!({ "navigated": true, "url": "https://www.zhihu.com/signin?next=%2Fcreator" }),
),
success_browser_response(
2,
json!({
"text": {
"status": "login_required",
"current_url": "https://www.zhihu.com/signin?next=%2Fcreator"
}
}),
),
]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
zhihu_test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
handle_browser_message_with_context(
transport.as_ref(),
&browser_tool,
&runtime_context,
BrowserMessage::SubmitTask {
instruction: "确认发布".to_string(),
conversation_id: "conversation-1".to_string(),
messages: vec![ConversationMessage {
role: "user".to_string(),
content: "请直接发表这篇知乎文章,标题是测试标题,正文是第一段内容".to_string(),
}],
page_url: "https://www.zhihu.com/".to_string(),
page_title: "知乎".to_string(),
},
)
.unwrap();
let sent = transport.sent_messages();
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::TaskComplete { success, summary }
if *success && (summary.contains("未登录") || summary.contains("登录"))
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::Command { action, params, .. }
if action == &Action::Navigate &&
params["url"].as_str() == Some("https://www.zhihu.com/creator")
)
}));
assert!(sent.iter().any(|message| {
matches!(message, AgentMessage::Command { action, .. } if action == &Action::Eval)
}));
assert!(!sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "info" &&
(message.starts_with("getText ") || message.starts_with("click "))
)
}));
}
#[test]
fn browser_orchestration_registers_superrpa_tools_natively() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let 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万热度" }),
),
success_browser_response(
2,
json!({
"text": {
"source": "https://www.zhihu.com/hot",
"sheet_name": "知乎热榜",
"columns": ["rank", "title", "heat"],
"rows": [[1, "问题一", "344万"]]
}
}),
),
]));
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 == "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::LogEntry { level, message }
if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary")
)
}));
}
#[test]
fn zhihu_export_does_not_use_frontend_owned_mainline() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let workspace_root = temp_workspace_root();
let config_path = write_deepseek_config_with_skills_dir(
&workspace_root,
"deepseek-test-key",
"http://127.0.0.1:9",
"deepseek-chat",
Some(real_skill_lib_root().to_str().unwrap()),
);
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
let transport = Arc::new(MockTransport::new(vec![]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
zhihu_test_policy(),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
handle_browser_message_with_context(
transport.as_ref(),
&browser_tool,
&runtime_context,
BrowserMessage::SubmitTask {
instruction: "读取热榜前10并导出 excel 文件".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "https://www.zhihu.com/hot".to_string(),
page_title: "知乎热榜".to_string(),
},
)
.unwrap();
let sent = transport.sent_messages();
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "mode" && message == "zeroclaw_process_message_primary"
)
}));
assert!(!sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "mode" &&
(message == "compat_llm_primary" || message == "compat_skill_runner_primary")
)
}));
}
#[test]
fn browser_skill_usage_is_execution_not_prompt_only() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let workspace_root = temp_workspace_root();
let 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);
assert!(summary.contains(".xlsx"));
assert!(!sent.iter().any(|message| {
matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "info" && message.starts_with("read_skill ")
)
}));
assert!(sent.iter().any(|message| {
matches!(message, AgentMessage::Command { action, .. } if action == &Action::GetText)
}));
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_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")
)
}));
}
fn staged_lineloss_skills_root() -> PathBuf {
PathBuf::from("D:/data/ideaSpace/rust/sgClaw/claw/claw/skills/skill_staging/skills")
}
fn build_fault_details_direct_skill_root() -> PathBuf {
let root = temp_workspace_root();
let skill_dir = write_skill_manifest_package(
&root,
"fault-details-report",
r#"
[skill]
name = "fault-details-report"
description = "Collect 95598 fault detail data via browser eval."
version = "0.1.0"
[[tools]]
name = "collect_fault_details"
description = "Collect structured fault detail rows for a specific period."
kind = "browser_script"
command = "scripts/collect_fault_details.js"
[tools.args]
period = "YYYY-MM period to collect."
"#,
);
write_skill_script(
&skill_dir,
"scripts/collect_fault_details.js",
r#"
return {
fault_type: "outage",
observed_at: `${args.period}-15 09:00`,
affected_scope: "line-7"
};
"#,
);
root
}
#[test]
fn deterministic_lineloss_runtime_passes_canonical_args_to_browser_script_tool() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let workspace_root = temp_workspace_root();
let config_path = write_deepseek_config_with_skills_dir(
&workspace_root,
"deepseek-test-key",
"http://127.0.0.1:9",
"deepseek-chat",
Some(staged_lineloss_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": {
"type": "report-artifact",
"report_name": "tq-lineloss-report",
"status": "ok",
"org": {
"label": "国网兰州供电公司",
"code": "62401"
},
"period": {
"mode": "month",
"mode_code": "1",
"value": "2026-03",
"payload": {
"fdate": "2026-03"
}
},
"rows": [
{ "ORG_NAME": "国网兰州供电公司", "LINE_LOSS_RATE": "3.21" }
],
"counts": {
"rows": 1
},
"reasons": []
}
}),
)]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
policy_for_domains(&["20.76.57.61"]),
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-03。。。".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "http://20.76.57.61:8080/#/lineloss".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 == "direct_skill_primary"
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::TaskComplete { success, summary }
if *success
&& summary.contains("tq-lineloss-report")
&& summary.contains("国网兰州供电公司")
&& summary.contains("2026-03")
&& summary.contains("rows=1")
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::Command {
action,
params,
security,
..
}
if action == &Action::Eval
&& security.expected_domain == "20.76.57.61"
&& params["script"].as_str().is_some_and(|script|
script.contains("\"org_label\":\"国网兰州供电公司\"")
&& script.contains("\"org_code\":\"62401\"")
&& script.contains("\"period_mode\":\"month\"")
&& script.contains("\"period_mode_code\":\"1\"")
&& script.contains("\"period_value\":\"2026-03\"")
&& script.contains("fdate")
)
)
}));
}
#[test]
fn deterministic_lineloss_missing_company_prompt_skips_browser_execution() {
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_lineloss_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(),
policy_for_domains(&["20.76.57.61"]),
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-03。。。".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "http://20.76.57.61:8080/#/lineloss".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 deterministic_lineloss_runtime_maps_partial_artifact_to_success_summary() {
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_lineloss_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": {
"type": "report-artifact",
"report_name": "tq-lineloss-report",
"status": "partial",
"org": {
"label": "国网兰州供电公司",
"code": "62401"
},
"period": {
"mode": "month",
"mode_code": "1",
"value": "2026-03",
"payload": {
"fdate": "2026-03"
}
},
"rows": [
{ "ORG_NAME": "国网兰州供电公司", "LINE_LOSS_RATE": "3.21" }
],
"counts": {
"rows": 1
},
"reasons": ["report_log_failed"]
}
}),
)]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
policy_for_domains(&["20.76.57.61"]),
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-03。。。".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "http://20.76.57.61:8080/#/lineloss".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("status=partial")
&& summary.contains("rows=1")
&& summary.contains("reasons=report_log_failed")
)
}));
}
#[test]
fn deterministic_lineloss_runtime_maps_blocked_artifact_to_failed_completion() {
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_lineloss_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": {
"type": "report-artifact",
"report_name": "tq-lineloss-report",
"status": "blocked",
"org": {
"label": "国网兰州供电公司",
"code": "62401"
},
"period": {
"mode": "month",
"mode_code": "1",
"value": "2026-03",
"payload": {
"fdate": "2026-03"
}
},
"rows": [],
"counts": {
"rows": 0
},
"reasons": ["selected_range_unavailable"]
}
}),
)]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
policy_for_domains(&["20.76.57.61"]),
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-03。。。".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "http://20.76.57.61:8080/#/lineloss".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("status=blocked")
&& summary.contains("reasons=selected_range_unavailable")
)
}));
}
#[test]
fn deterministic_suffix_non_lineloss_request_does_not_fall_into_zhihu_logic() {
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/hot".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 == "mode"
&& (message == "zeroclaw_process_message_primary"
|| message == "compat_llm_primary")
)
}));
}
#[test]
fn existing_fault_details_direct_browser_script_path_remains_unchanged() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let skill_root = build_fault_details_direct_skill_root();
let workspace_root = temp_workspace_root();
let config_path = write_deepseek_config_with_direct_submit_skill(
&workspace_root,
"deepseek-test-key",
"http://127.0.0.1:9",
"deepseek-chat",
Some(skill_root.to_str().unwrap()),
Some("fault-details-report.collect_fault_details"),
);
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
let transport = Arc::new(MockTransport::new(vec![success_browser_response(
1,
json!({
"text": {
"type": "report-artifact",
"report_name": "fault-details-report",
"period": "2026-03",
"counts": {
"detail_rows": 1,
"summary_rows": 1
},
"rows": [{ "qxdbh": "QX-1" }],
"sections": [{
"name": "summary-sheet",
"rows": [{ "index": 1 }]
}],
"status": "partial",
"partial_reasons": ["report_log_failed"]
}
}),
)]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
policy_for_domains(&["95598.sgcc.com.cn"]),
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-03 的故障明细并返回结果".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "https://95598.sgcc.com.cn/".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 == "direct_skill_primary"
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::TaskComplete { success, summary }
if *success
&& summary.contains("fault-details-report")
&& summary.contains("status=partial")
&& summary.contains("detail_rows=1")
&& summary.contains("summary_rows=1")
&& summary.contains("report_log_failed")
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::Command {
action,
params,
security,
..
}
if action == &Action::Eval
&& security.expected_domain == "95598.sgcc.com.cn"
&& params["script"].as_str().is_some_and(|script|
script.contains("\"period\":\"2026-03\"")
)
)
}));
}