Add registry-driven scene routing and multi-root skill loading so fault-details and 95598 scene skills can be triggered from natural language while still running through the browser-backed runtime. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4525 lines
149 KiB
Rust
4525 lines
149 KiB
Rust
mod common;
|
||
|
||
use std::fs;
|
||
use std::io::{Read, Write};
|
||
use std::net::TcpListener;
|
||
use std::path::PathBuf;
|
||
use std::sync::{Arc, Mutex, OnceLock};
|
||
use std::thread;
|
||
use std::time::Duration;
|
||
|
||
use common::MockTransport;
|
||
use serde_json::{json, Value};
|
||
use sgclaw::agent::{
|
||
handle_browser_message, handle_browser_message_with_context, AgentRuntimeContext,
|
||
};
|
||
use sgclaw::compat::workflow_executor::finalize_screen_export;
|
||
use sgclaw::compat::runtime::{execute_task, execute_task_with_sgclaw_settings, CompatTaskContext};
|
||
use sgclaw::config::{DeepSeekSettings, SgClawSettings};
|
||
use sgclaw::pipe::{
|
||
Action, AgentMessage, BrowserMessage, BrowserPipeTool, ConversationMessage, Timing,
|
||
};
|
||
use sgclaw::runtime::RuntimeProfile;
|
||
use sgclaw::security::MacPolicy;
|
||
use uuid::Uuid;
|
||
|
||
fn env_lock() -> &'static Mutex<()> {
|
||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||
LOCK.get_or_init(|| Mutex::new(()))
|
||
}
|
||
|
||
fn test_policy() -> MacPolicy {
|
||
policy_for_domains(&["www.baidu.com"])
|
||
}
|
||
|
||
fn zhihu_test_policy() -> MacPolicy {
|
||
policy_for_domains(&["www.zhihu.com", "zhuanlan.zhihu.com"])
|
||
}
|
||
|
||
fn policy_for_domains(domains: &[&str]) -> MacPolicy {
|
||
MacPolicy::from_json_str(
|
||
&json!({
|
||
"version": "1.0",
|
||
"domains": { "allowed": domains },
|
||
"pipe_actions": {
|
||
"allowed": ["click", "type", "navigate", "getText", "waitForSelector", "eval"],
|
||
"blocked": []
|
||
}
|
||
})
|
||
.to_string(),
|
||
)
|
||
.unwrap()
|
||
}
|
||
|
||
fn temp_workspace_root() -> PathBuf {
|
||
let root = std::env::temp_dir().join(format!("sgclaw-compat-runtime-{}", Uuid::new_v4()));
|
||
std::fs::create_dir_all(&root).unwrap();
|
||
root
|
||
}
|
||
|
||
fn write_deepseek_config(root: &PathBuf, api_key: &str, base_url: &str, model: &str) -> PathBuf {
|
||
write_deepseek_config_with_skills_dir(root, api_key, base_url, model, None)
|
||
}
|
||
|
||
fn write_deepseek_config_with_skills_dir(
|
||
root: &PathBuf,
|
||
api_key: &str,
|
||
base_url: &str,
|
||
model: &str,
|
||
skills_dir: Option<&str>,
|
||
) -> PathBuf {
|
||
let config_path = root.join("sgclaw_config.json");
|
||
let mut payload = json!({
|
||
"apiKey": api_key,
|
||
"baseUrl": base_url,
|
||
"model": model,
|
||
});
|
||
if let Some(skills_dir) = skills_dir {
|
||
payload["skillsDir"] = json!(skills_dir);
|
||
}
|
||
|
||
fs::write(
|
||
&config_path,
|
||
serde_json::to_string_pretty(&payload).unwrap(),
|
||
)
|
||
.unwrap();
|
||
config_path
|
||
}
|
||
|
||
fn write_skill_package(skills_dir: &std::path::Path, skill_name: &str, body: &str) {
|
||
let skill_dir = skills_dir.join(skill_name);
|
||
fs::create_dir_all(&skill_dir).unwrap();
|
||
fs::write(skill_dir.join("SKILL.md"), body).unwrap();
|
||
}
|
||
|
||
fn write_skill_manifest_package(
|
||
skills_dir: &std::path::Path,
|
||
skill_name: &str,
|
||
manifest: &str,
|
||
) -> PathBuf {
|
||
let skill_dir = skills_dir.join(skill_name);
|
||
fs::create_dir_all(&skill_dir).unwrap();
|
||
fs::write(skill_dir.join("SKILL.toml"), manifest).unwrap();
|
||
skill_dir
|
||
}
|
||
|
||
fn write_skill_script(skill_dir: &std::path::Path, relative_path: &str, body: &str) {
|
||
let script_path = skill_dir.join(relative_path);
|
||
if let Some(parent) = script_path.parent() {
|
||
fs::create_dir_all(parent).unwrap();
|
||
}
|
||
fs::write(script_path, body).unwrap();
|
||
}
|
||
|
||
fn real_skill_lib_root() -> PathBuf {
|
||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||
.parent()
|
||
.unwrap()
|
||
.join("skill_lib")
|
||
}
|
||
|
||
fn staged_skill_root() -> PathBuf {
|
||
PathBuf::from("D:/data/ideaSpace/rust/sgClaw/claw/claw/skills/skill_staging")
|
||
}
|
||
|
||
fn project_skills_root() -> PathBuf {
|
||
staged_skill_root()
|
||
.parent()
|
||
.expect("staged skill root should have parent")
|
||
.to_path_buf()
|
||
}
|
||
|
||
fn success_browser_response(seq: u64, data: Value) -> BrowserMessage {
|
||
BrowserMessage::Response {
|
||
seq,
|
||
success: true,
|
||
data,
|
||
aom_snapshot: vec![],
|
||
timing: Timing {
|
||
queue_ms: 1,
|
||
exec_ms: 10,
|
||
},
|
||
}
|
||
}
|
||
|
||
fn request_tool_names(request: &Value) -> Vec<String> {
|
||
request["tools"]
|
||
.as_array()
|
||
.cloned()
|
||
.unwrap_or_default()
|
||
.into_iter()
|
||
.filter_map(|tool| tool["function"]["name"].as_str().map(str::to_string))
|
||
.collect::<Vec<_>>()
|
||
}
|
||
|
||
fn tool_message_content<'a>(request: &'a Value, tool_call_id: &str) -> Option<&'a str> {
|
||
request["messages"].as_array().and_then(|messages| {
|
||
messages.iter().find_map(|message| {
|
||
(message["role"].as_str() == Some("tool")
|
||
&& message["tool_call_id"].as_str() == Some(tool_call_id))
|
||
.then(|| message["content"].as_str())
|
||
.flatten()
|
||
})
|
||
})
|
||
}
|
||
|
||
fn start_fake_deepseek_server(
|
||
responses: Vec<Value>,
|
||
) -> (String, Arc<Mutex<Vec<Value>>>, thread::JoinHandle<()>) {
|
||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||
listener.set_nonblocking(true).unwrap();
|
||
let address = format!("http://{}", listener.local_addr().unwrap());
|
||
let requests = Arc::new(Mutex::new(Vec::new()));
|
||
let request_log = requests.clone();
|
||
|
||
let handle = thread::spawn(move || {
|
||
for response in responses {
|
||
let deadline = std::time::Instant::now() + Duration::from_secs(5);
|
||
let (mut stream, _) = loop {
|
||
match listener.accept() {
|
||
Ok(pair) => break pair,
|
||
Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
|
||
assert!(
|
||
std::time::Instant::now() < deadline,
|
||
"timed out waiting for provider request"
|
||
);
|
||
thread::sleep(Duration::from_millis(10));
|
||
}
|
||
Err(err) => panic!("failed to accept provider request: {err}"),
|
||
}
|
||
};
|
||
stream.set_nonblocking(false).unwrap();
|
||
let body = read_http_json_body(&mut stream);
|
||
request_log.lock().unwrap().push(body);
|
||
|
||
let payload = response.to_string();
|
||
let reply = format!(
|
||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||
payload.as_bytes().len(),
|
||
payload
|
||
);
|
||
stream.write_all(reply.as_bytes()).unwrap();
|
||
stream.flush().unwrap();
|
||
}
|
||
});
|
||
|
||
(address, requests, handle)
|
||
}
|
||
|
||
fn read_http_json_body(stream: &mut impl Read) -> Value {
|
||
let mut buffer = Vec::new();
|
||
let mut headers_end = None;
|
||
|
||
while headers_end.is_none() {
|
||
let mut chunk = [0_u8; 1024];
|
||
let bytes = stream.read(&mut chunk).unwrap();
|
||
assert!(bytes > 0, "unexpected EOF while reading headers");
|
||
buffer.extend_from_slice(&chunk[..bytes]);
|
||
headers_end = buffer.windows(4).position(|window| window == b"\r\n\r\n");
|
||
}
|
||
|
||
let headers_end = headers_end.unwrap() + 4;
|
||
let headers = String::from_utf8(buffer[..headers_end].to_vec()).unwrap();
|
||
let content_length = headers
|
||
.lines()
|
||
.find_map(|line| {
|
||
let (name, value) = line.split_once(':')?;
|
||
name.eq_ignore_ascii_case("content-length")
|
||
.then(|| value.trim().parse::<usize>().unwrap())
|
||
})
|
||
.unwrap();
|
||
|
||
while buffer.len() < headers_end + content_length {
|
||
let mut chunk = vec![0_u8; content_length];
|
||
let bytes = stream.read(&mut chunk).unwrap();
|
||
assert!(bytes > 0, "unexpected EOF while reading body");
|
||
buffer.extend_from_slice(&chunk[..bytes]);
|
||
}
|
||
|
||
serde_json::from_slice(&buffer[headers_end..headers_end + content_length]).unwrap()
|
||
}
|
||
|
||
fn task_complete_summary(sent: &[AgentMessage]) -> String {
|
||
sent.iter()
|
||
.find_map(|message| match message {
|
||
AgentMessage::TaskComplete { success, summary } if *success => Some(summary.clone()),
|
||
_ => None,
|
||
})
|
||
.expect("expected successful task completion")
|
||
}
|
||
|
||
fn extract_generated_artifact_path(summary: &str, extension: &str) -> PathBuf {
|
||
summary
|
||
.split_whitespace()
|
||
.find(|token| token.ends_with(extension))
|
||
.map(PathBuf::from)
|
||
.expect("expected artifact path in task summary")
|
||
}
|
||
|
||
#[test]
|
||
fn compat_runtime_uses_zeroclaw_provider_path_and_executes_browser_actions() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let first_response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "",
|
||
"tool_calls": [
|
||
{
|
||
"id": "call_1",
|
||
"type": "function",
|
||
"function": {
|
||
"name": "browser_action",
|
||
"arguments": serde_json::to_string(&json!({
|
||
"action": "navigate",
|
||
"expected_domain": "www.baidu.com",
|
||
"url": "https://www.baidu.com"
|
||
})).unwrap()
|
||
}
|
||
},
|
||
{
|
||
"id": "call_2",
|
||
"type": "function",
|
||
"function": {
|
||
"name": "browser_action",
|
||
"arguments": serde_json::to_string(&json!({
|
||
"action": "type",
|
||
"expected_domain": "www.baidu.com",
|
||
"selector": "#kw",
|
||
"text": "天气",
|
||
"clear_first": true
|
||
})).unwrap()
|
||
}
|
||
}
|
||
]
|
||
}
|
||
}],
|
||
"usage": {
|
||
"prompt_tokens": 12,
|
||
"completion_tokens": 7
|
||
}
|
||
});
|
||
let second_response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已通过 ZeroClaw 执行任务: 打开百度搜索天气"
|
||
}
|
||
}],
|
||
"usage": {
|
||
"prompt_tokens": 15,
|
||
"completion_tokens": 8
|
||
}
|
||
});
|
||
let (base_url, requests, server_handle) =
|
||
start_fake_deepseek_server(vec![first_response, second_response]);
|
||
|
||
std::env::set_var("DEEPSEEK_API_KEY", "deepseek-test-key");
|
||
std::env::set_var("DEEPSEEK_BASE_URL", base_url);
|
||
std::env::set_var("DEEPSEEK_MODEL", "deepseek-chat");
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let settings = DeepSeekSettings::from_env().unwrap();
|
||
let transport = Arc::new(MockTransport::new(vec![
|
||
BrowserMessage::Response {
|
||
seq: 1,
|
||
success: true,
|
||
data: json!({ "navigated": true }),
|
||
aom_snapshot: vec![],
|
||
timing: Timing {
|
||
queue_ms: 1,
|
||
exec_ms: 10,
|
||
},
|
||
},
|
||
BrowserMessage::Response {
|
||
seq: 2,
|
||
success: true,
|
||
data: json!({ "typed": true }),
|
||
aom_snapshot: vec![],
|
||
timing: Timing {
|
||
queue_ms: 1,
|
||
exec_ms: 11,
|
||
},
|
||
},
|
||
]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
let summary = execute_task(
|
||
transport.as_ref(),
|
||
browser_tool,
|
||
"打开百度搜索天气",
|
||
&CompatTaskContext::default(),
|
||
&workspace_root,
|
||
&settings,
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
let sent = transport.sent_messages();
|
||
|
||
assert_eq!(summary, "已通过 ZeroClaw 执行任务: 打开百度搜索天气");
|
||
assert_eq!(request_bodies.len(), 2);
|
||
assert_eq!(request_bodies[0]["model"], json!("deepseek-chat"));
|
||
assert!(request_tool_names(&request_bodies[0]).contains(&"browser_action".to_string()));
|
||
assert!(request_tool_names(&request_bodies[0]).contains(&"superrpa_browser".to_string()));
|
||
assert!(request_bodies[1].to_string().contains("tool_call_id"));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message == "navigate https://www.baidu.com"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message == "type 天气 into #kw"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::Command { action, .. } if action == &Action::Navigate
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::Command { action, .. } if action == &Action::Type
|
||
)
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn compat_runtime_includes_default_workspace_skills_in_provider_request() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已识别默认 workspace skill"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let default_skills_dir = workspace_root
|
||
.join(".sgclaw-zeroclaw-workspace")
|
||
.join("skills");
|
||
write_skill_package(
|
||
&default_skills_dir,
|
||
"workspace-zhihu-skill",
|
||
"# Workspace Zhihu Skill\nUse this workspace-local skill.\n",
|
||
);
|
||
|
||
let settings = DeepSeekSettings {
|
||
api_key: "deepseek-test-key".to_string(),
|
||
base_url,
|
||
model: "deepseek-chat".to_string(),
|
||
skills_dir: Vec::new(),
|
||
};
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
let summary = execute_task(
|
||
transport.as_ref(),
|
||
browser_tool,
|
||
"列出当前可用 skill",
|
||
&CompatTaskContext::default(),
|
||
&workspace_root,
|
||
&settings,
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
|
||
assert_eq!(summary, "已识别默认 workspace skill");
|
||
assert_eq!(request_bodies.len(), 1);
|
||
assert!(request_bodies[0]
|
||
.to_string()
|
||
.contains("workspace-zhihu-skill"));
|
||
}
|
||
|
||
#[test]
|
||
fn handle_browser_message_loads_skills_from_configured_skills_dir() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已识别自定义 skill 目录"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let default_skills_dir = workspace_root
|
||
.join(".sgclaw-zeroclaw-workspace")
|
||
.join("skills");
|
||
write_skill_package(
|
||
&default_skills_dir,
|
||
"workspace-only-skill",
|
||
"# Workspace Only Skill\nThis skill should be ignored when skillsDir is set.\n",
|
||
);
|
||
|
||
let custom_skill_repo = workspace_root.join("skill_lib");
|
||
let custom_skills_dir = custom_skill_repo.join("skills");
|
||
write_skill_package(
|
||
&custom_skills_dir,
|
||
"configured-zhihu-skill",
|
||
"# Configured Zhihu Skill\nUse the configured skills directory.\n",
|
||
);
|
||
|
||
std::env::remove_var("DEEPSEEK_API_KEY");
|
||
std::env::remove_var("DEEPSEEK_BASE_URL");
|
||
std::env::remove_var("DEEPSEEK_MODEL");
|
||
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
&base_url,
|
||
"deepseek-chat",
|
||
Some("skill_lib"),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "告诉我当前有哪些 zhihu skill".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: String::new(),
|
||
page_title: String::new(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
let first_request = request_bodies[0].to_string();
|
||
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && summary == "已识别自定义 skill 目录"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" &&
|
||
message.contains("loaded skills: configured-zhihu-skill@0.1.0")
|
||
)
|
||
}));
|
||
assert_eq!(request_bodies.len(), 1);
|
||
assert!(first_request.contains("configured-zhihu-skill"));
|
||
assert!(!first_request.contains("workspace-only-skill"));
|
||
}
|
||
|
||
#[test]
|
||
fn handle_browser_message_routes_supported_instruction_to_compat_runtime_when_llm_is_configured() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let first_response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "",
|
||
"tool_calls": [
|
||
{
|
||
"id": "call_1",
|
||
"type": "function",
|
||
"function": {
|
||
"name": "browser_action",
|
||
"arguments": serde_json::to_string(&json!({
|
||
"action": "navigate",
|
||
"expected_domain": "www.baidu.com",
|
||
"url": "https://www.baidu.com"
|
||
})).unwrap()
|
||
}
|
||
},
|
||
{
|
||
"id": "call_2",
|
||
"type": "function",
|
||
"function": {
|
||
"name": "browser_action",
|
||
"arguments": serde_json::to_string(&json!({
|
||
"action": "type",
|
||
"expected_domain": "www.baidu.com",
|
||
"selector": "#kw",
|
||
"text": "天气",
|
||
"clear_first": true
|
||
})).unwrap()
|
||
}
|
||
},
|
||
{
|
||
"id": "call_3",
|
||
"type": "function",
|
||
"function": {
|
||
"name": "browser_action",
|
||
"arguments": serde_json::to_string(&json!({
|
||
"action": "click",
|
||
"expected_domain": "www.baidu.com",
|
||
"selector": "#su"
|
||
})).unwrap()
|
||
}
|
||
}
|
||
]
|
||
}
|
||
}]
|
||
});
|
||
let second_response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已在百度搜索天气"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) =
|
||
start_fake_deepseek_server(vec![first_response, second_response]);
|
||
|
||
std::env::remove_var("DEEPSEEK_API_KEY");
|
||
std::env::remove_var("DEEPSEEK_BASE_URL");
|
||
std::env::remove_var("DEEPSEEK_MODEL");
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
&base_url,
|
||
"deepseek-chat",
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![
|
||
BrowserMessage::Response {
|
||
seq: 1,
|
||
success: true,
|
||
data: json!({ "navigated": true }),
|
||
aom_snapshot: vec![],
|
||
timing: Timing {
|
||
queue_ms: 1,
|
||
exec_ms: 10,
|
||
},
|
||
},
|
||
BrowserMessage::Response {
|
||
seq: 2,
|
||
success: true,
|
||
data: json!({ "typed": true }),
|
||
aom_snapshot: vec![],
|
||
timing: Timing {
|
||
queue_ms: 1,
|
||
exec_ms: 10,
|
||
},
|
||
},
|
||
BrowserMessage::Response {
|
||
seq: 3,
|
||
success: true,
|
||
data: json!({ "clicked": true }),
|
||
aom_snapshot: vec![],
|
||
timing: Timing {
|
||
queue_ms: 1,
|
||
exec_ms: 10,
|
||
},
|
||
},
|
||
]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "打开百度搜索天气".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: String::new(),
|
||
page_title: String::new(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && summary == "已在百度搜索天气"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" &&
|
||
message == "sgclaw runtime version=0.1.0 protocol=1.0"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" && message == "compat_llm_primary"
|
||
)
|
||
}));
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" && message == "deterministic_planner"
|
||
)
|
||
}));
|
||
assert_eq!(request_bodies.len(), 2);
|
||
}
|
||
|
||
#[test]
|
||
fn handle_browser_message_executes_without_legacy_plan_preview() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let first_response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "",
|
||
"tool_calls": [{
|
||
"id": "call_1",
|
||
"type": "function",
|
||
"function": {
|
||
"name": "browser_action",
|
||
"arguments": serde_json::to_string(&json!({
|
||
"action": "navigate",
|
||
"expected_domain": "www.baidu.com",
|
||
"url": "https://www.baidu.com"
|
||
})).unwrap()
|
||
}
|
||
}]
|
||
}
|
||
}]
|
||
});
|
||
let second_response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已打开百度首页"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, _requests, server_handle) =
|
||
start_fake_deepseek_server(vec![first_response, second_response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = workspace_root.join("sgclaw_config.json");
|
||
fs::write(
|
||
&config_path,
|
||
serde_json::to_string_pretty(&json!({
|
||
"apiKey": "deepseek-test-key",
|
||
"baseUrl": base_url,
|
||
"model": "deepseek-chat",
|
||
"plannerMode": "zeroclawPlanFirst"
|
||
}))
|
||
.unwrap(),
|
||
)
|
||
.unwrap();
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![success_browser_response(
|
||
1,
|
||
json!({ "navigated": true }),
|
||
)]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "打开百度".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: String::new(),
|
||
page_title: String::new(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "plan" && message.contains("navigate https://www.baidu.com")
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message == "navigate https://www.baidu.com"
|
||
)
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn handle_browser_message_falls_back_to_compat_runtime_for_unsupported_instruction() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let first_response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "",
|
||
"tool_calls": [{
|
||
"id": "call_1",
|
||
"type": "function",
|
||
"function": {
|
||
"name": "browser_action",
|
||
"arguments": serde_json::to_string(&json!({
|
||
"action": "navigate",
|
||
"expected_domain": "www.baidu.com",
|
||
"url": "https://www.baidu.com"
|
||
})).unwrap()
|
||
}
|
||
}]
|
||
}
|
||
}]
|
||
});
|
||
let second_response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "来自 ZeroClaw runtime"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) =
|
||
start_fake_deepseek_server(vec![first_response, second_response]);
|
||
|
||
std::env::set_var("DEEPSEEK_API_KEY", "deepseek-test-key");
|
||
std::env::set_var("DEEPSEEK_BASE_URL", base_url);
|
||
std::env::set_var("DEEPSEEK_MODEL", "deepseek-chat");
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let original_dir = std::env::current_dir().unwrap();
|
||
std::env::set_current_dir(&workspace_root).unwrap();
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![BrowserMessage::Response {
|
||
seq: 1,
|
||
success: true,
|
||
data: json!({ "navigated": true }),
|
||
aom_snapshot: vec![],
|
||
timing: Timing {
|
||
queue_ms: 1,
|
||
exec_ms: 10,
|
||
},
|
||
}]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "帮我打开百度首页".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: String::new(),
|
||
page_title: String::new(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
std::env::set_current_dir(original_dir).unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && summary == "来自 ZeroClaw runtime"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" && message == "compat_llm_primary"
|
||
)
|
||
}));
|
||
assert_eq!(request_bodies.len(), 2);
|
||
}
|
||
|
||
#[test]
|
||
fn handle_browser_message_requires_llm_configuration_when_no_model_is_available() {
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "你好".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: String::new(),
|
||
page_title: String::new(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
assert!(matches!(
|
||
sent.last(),
|
||
Some(AgentMessage::TaskComplete { success, summary })
|
||
if !success && summary.contains("未配置大语言模型")
|
||
));
|
||
}
|
||
|
||
#[test]
|
||
fn compat_runtime_includes_prior_turns_in_follow_up_provider_request() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let first_response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "",
|
||
"tool_calls": [{
|
||
"id": "call_1",
|
||
"type": "function",
|
||
"function": {
|
||
"name": "browser_action",
|
||
"arguments": serde_json::to_string(&json!({
|
||
"action": "navigate",
|
||
"expected_domain": "www.zhihu.com",
|
||
"url": "https://www.zhihu.com/search?q=天气&type=content"
|
||
})).unwrap()
|
||
}
|
||
}]
|
||
}
|
||
}]
|
||
});
|
||
let second_response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已在知乎搜索天气"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) =
|
||
start_fake_deepseek_server(vec![first_response, second_response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let settings = DeepSeekSettings {
|
||
api_key: "deepseek-test-key".to_string(),
|
||
base_url,
|
||
model: "deepseek-chat".to_string(),
|
||
skills_dir: Vec::new(),
|
||
};
|
||
let transport = Arc::new(MockTransport::new(vec![BrowserMessage::Response {
|
||
seq: 1,
|
||
success: true,
|
||
data: json!({ "navigated": true }),
|
||
aom_snapshot: vec![],
|
||
timing: Timing {
|
||
queue_ms: 1,
|
||
exec_ms: 10,
|
||
},
|
||
}]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
let task_context = CompatTaskContext {
|
||
conversation_id: Some("conversation-1".to_string()),
|
||
messages: vec![
|
||
ConversationMessage {
|
||
role: "user".to_string(),
|
||
content: "打开百度搜索天气".to_string(),
|
||
},
|
||
ConversationMessage {
|
||
role: "assistant".to_string(),
|
||
content: "已在百度搜索天气".to_string(),
|
||
},
|
||
],
|
||
page_url: Some("https://www.zhihu.com/".to_string()),
|
||
page_title: Some("知乎".to_string()),
|
||
};
|
||
|
||
let summary = execute_task(
|
||
transport.as_ref(),
|
||
browser_tool,
|
||
"打开知乎搜索天气",
|
||
&task_context,
|
||
&workspace_root,
|
||
&settings,
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
let first_request_messages = request_bodies[0]["messages"]
|
||
.as_array()
|
||
.cloned()
|
||
.unwrap_or_default();
|
||
|
||
assert_eq!(summary, "已在知乎搜索天气");
|
||
assert!(first_request_messages.iter().any(|message| {
|
||
message["role"] == json!("user") && message["content"] == json!("打开百度搜索天气")
|
||
}));
|
||
assert!(first_request_messages.iter().any(|message| {
|
||
message["role"] == json!("assistant") && message["content"] == json!("已在百度搜索天气")
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn compat_runtime_does_not_forward_raw_aom_snapshot_back_to_provider() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let first_response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "",
|
||
"tool_calls": [{
|
||
"id": "call_1",
|
||
"type": "function",
|
||
"function": {
|
||
"name": "browser_action",
|
||
"arguments": serde_json::to_string(&json!({
|
||
"action": "navigate",
|
||
"expected_domain": "www.baidu.com",
|
||
"url": "https://www.baidu.com"
|
||
})).unwrap()
|
||
}
|
||
}]
|
||
}
|
||
}]
|
||
});
|
||
let second_response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "来自 ZeroClaw runtime"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) =
|
||
start_fake_deepseek_server(vec![first_response, second_response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let settings = DeepSeekSettings {
|
||
api_key: "deepseek-test-key".to_string(),
|
||
base_url,
|
||
model: "deepseek-chat".to_string(),
|
||
skills_dir: Vec::new(),
|
||
};
|
||
|
||
let large_snapshot_marker = "snapshot-marker ".repeat(2048);
|
||
let transport = Arc::new(MockTransport::new(vec![BrowserMessage::Response {
|
||
seq: 1,
|
||
success: true,
|
||
data: json!({ "navigated": true }),
|
||
aom_snapshot: vec![json!({
|
||
"role": "RootWebArea",
|
||
"name": "百度一下,你就知道",
|
||
"text": large_snapshot_marker
|
||
})],
|
||
timing: Timing {
|
||
queue_ms: 1,
|
||
exec_ms: 10,
|
||
},
|
||
}]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
let summary = execute_task(
|
||
transport.as_ref(),
|
||
browser_tool,
|
||
"打开百度首页",
|
||
&CompatTaskContext::default(),
|
||
&workspace_root,
|
||
&settings,
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
let follow_up_request = request_bodies[1].to_string();
|
||
|
||
assert_eq!(summary, "来自 ZeroClaw runtime");
|
||
assert_eq!(request_bodies.len(), 2);
|
||
assert!(!follow_up_request.contains("snapshot-marker"));
|
||
}
|
||
|
||
#[test]
|
||
fn compat_runtime_injects_browser_contract_and_page_context_into_provider_request() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已收到页面上下文"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let settings = DeepSeekSettings {
|
||
api_key: "deepseek-test-key".to_string(),
|
||
base_url,
|
||
model: "deepseek-chat".to_string(),
|
||
skills_dir: Vec::new(),
|
||
};
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
let task_context = CompatTaskContext {
|
||
conversation_id: Some("conversation-ctx".to_string()),
|
||
messages: vec![],
|
||
page_url: Some("https://www.zhihu.com/hot".to_string()),
|
||
page_title: Some("知乎热榜".to_string()),
|
||
};
|
||
|
||
let summary = execute_task(
|
||
transport.as_ref(),
|
||
browser_tool,
|
||
"统计一下知乎热帮信息",
|
||
&task_context,
|
||
&workspace_root,
|
||
&settings,
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
let first_request_messages = request_bodies[0]["messages"]
|
||
.as_array()
|
||
.cloned()
|
||
.unwrap_or_default();
|
||
let flattened = first_request_messages
|
||
.iter()
|
||
.filter_map(|message| message["content"].as_str())
|
||
.collect::<Vec<_>>()
|
||
.join("\n");
|
||
|
||
assert_eq!(summary, "已收到页面上下文");
|
||
assert!(
|
||
flattened.contains("expected_domain must be the bare hostname"),
|
||
"missing browser tool contract guidance: {flattened}"
|
||
);
|
||
assert!(
|
||
flattened.contains("document.querySelector"),
|
||
"missing CSS selector guidance: {flattened}"
|
||
);
|
||
assert!(
|
||
flattened.contains("https://www.zhihu.com/hot"),
|
||
"missing page url context: {flattened}"
|
||
);
|
||
assert!(
|
||
flattened.contains("知乎热榜"),
|
||
"missing page title context: {flattened}"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn compat_runtime_can_complete_a_text_only_turn_without_browser_tool_calls() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "这是纯文本回答"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let mut settings = SgClawSettings::from_legacy_deepseek_fields(
|
||
"deepseek-test-key".to_string(),
|
||
base_url,
|
||
"deepseek-chat".to_string(),
|
||
Vec::new(),
|
||
)
|
||
.unwrap();
|
||
settings.runtime_profile = RuntimeProfile::GeneralAssistant;
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
let summary = execute_task_with_sgclaw_settings(
|
||
transport.as_ref(),
|
||
browser_tool,
|
||
"直接回答:你好",
|
||
&CompatTaskContext::default(),
|
||
&workspace_root,
|
||
&settings,
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
let flattened = request_bodies[0]["messages"]
|
||
.as_array()
|
||
.cloned()
|
||
.unwrap_or_default()
|
||
.iter()
|
||
.filter_map(|message| message["content"].as_str())
|
||
.collect::<Vec<_>>()
|
||
.join("\n");
|
||
let tool_entries = request_bodies[0]["tools"]
|
||
.as_array()
|
||
.cloned()
|
||
.unwrap_or_default();
|
||
let tool_names = tool_entries
|
||
.into_iter()
|
||
.filter_map(|tool| tool["function"]["name"].as_str().map(str::to_string))
|
||
.collect::<Vec<_>>();
|
||
let sent = transport.sent_messages();
|
||
|
||
assert_eq!(summary, "这是纯文本回答");
|
||
assert!(!flattened.contains("Browser tool contract"));
|
||
assert!(!tool_names.contains(&"browser_action".to_string()));
|
||
assert!(!sent
|
||
.iter()
|
||
.any(|message| { matches!(message, AgentMessage::Command { .. }) }));
|
||
}
|
||
|
||
#[test]
|
||
fn compat_runtime_allows_read_skill_under_compact_mode_policy() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已看到 compact skill 工具"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let default_skills_dir = workspace_root
|
||
.join(".sgclaw-zeroclaw-workspace")
|
||
.join("skills");
|
||
write_skill_package(
|
||
&default_skills_dir,
|
||
"workspace-zhihu-skill",
|
||
"# Workspace Zhihu Skill\nUse this workspace-local skill.\n",
|
||
);
|
||
|
||
let settings = SgClawSettings::from_legacy_deepseek_fields(
|
||
"deepseek-test-key".to_string(),
|
||
base_url,
|
||
"deepseek-chat".to_string(),
|
||
Vec::new(),
|
||
)
|
||
.unwrap();
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
let summary = execute_task_with_sgclaw_settings(
|
||
transport.as_ref(),
|
||
browser_tool,
|
||
"告诉我当前有哪些 skill",
|
||
&CompatTaskContext::default(),
|
||
&workspace_root,
|
||
&settings,
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
let tool_entries = request_bodies[0]["tools"]
|
||
.as_array()
|
||
.cloned()
|
||
.unwrap_or_default();
|
||
let tool_names = tool_entries
|
||
.into_iter()
|
||
.filter_map(|tool| tool["function"]["name"].as_str().map(str::to_string))
|
||
.collect::<Vec<_>>();
|
||
|
||
assert_eq!(summary, "已看到 compact skill 工具");
|
||
assert!(tool_names.contains(&"browser_action".to_string()));
|
||
assert!(tool_names.contains(&"superrpa_browser".to_string()));
|
||
assert!(tool_names.contains(&"read_skill".to_string()));
|
||
assert!(!tool_names.contains(&"zhihu-hotlist_extract_hotlist".to_string()));
|
||
}
|
||
|
||
#[test]
|
||
fn compat_runtime_exposes_browser_script_skill_tools_in_browser_attached_mode() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已看到 browser_script skill 工具"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let default_skills_dir = workspace_root
|
||
.join(".sgclaw-zeroclaw-workspace")
|
||
.join("skills");
|
||
let skill_dir = write_skill_manifest_package(
|
||
&default_skills_dir,
|
||
"workspace-zhihu-skill",
|
||
r#"
|
||
[skill]
|
||
name = "workspace-zhihu-skill"
|
||
description = "Extract Zhihu hotlist rows with a packaged browser script."
|
||
version = "0.1.0"
|
||
|
||
[[tools]]
|
||
name = "extract_hotlist"
|
||
description = "Extract structured hotlist rows from the current Zhihu page."
|
||
kind = "browser_script"
|
||
command = "scripts/extract_hotlist.js"
|
||
|
||
[tools.args]
|
||
top_n = "How many hotlist rows to extract."
|
||
"#,
|
||
);
|
||
write_skill_script(
|
||
&skill_dir,
|
||
"scripts/extract_hotlist.js",
|
||
"return { rows: [] };",
|
||
);
|
||
|
||
let mut settings = SgClawSettings::from_legacy_deepseek_fields(
|
||
"deepseek-test-key".to_string(),
|
||
base_url,
|
||
"deepseek-chat".to_string(),
|
||
Vec::new(),
|
||
)
|
||
.unwrap();
|
||
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
let summary = execute_task_with_sgclaw_settings(
|
||
transport.as_ref(),
|
||
browser_tool,
|
||
"告诉我当前有哪些知乎热榜工具",
|
||
&CompatTaskContext::default(),
|
||
&workspace_root,
|
||
&settings,
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
let tool_names = request_tool_names(&request_bodies[0]);
|
||
|
||
assert_eq!(summary, "已看到 browser_script skill 工具");
|
||
assert!(tool_names.contains(&"browser_action".to_string()));
|
||
assert!(tool_names.contains(&"superrpa_browser".to_string()));
|
||
assert!(tool_names.contains(&"read_skill".to_string()));
|
||
assert!(tool_names.contains(&"workspace-zhihu-skill_extract_hotlist".to_string()));
|
||
}
|
||
|
||
#[test]
|
||
fn compat_runtime_executes_browser_script_skill_via_eval_without_gettext_probing() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let first_response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "",
|
||
"tool_calls": [{
|
||
"id": "call_1",
|
||
"type": "function",
|
||
"function": {
|
||
"name": "workspace-zhihu-skill_extract_hotlist",
|
||
"arguments": serde_json::to_string(&json!({
|
||
"expected_domain": "www.zhihu.com",
|
||
"top_n": "10"
|
||
})).unwrap()
|
||
}
|
||
}]
|
||
}
|
||
}]
|
||
});
|
||
let second_response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已执行 browser_script skill"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) =
|
||
start_fake_deepseek_server(vec![first_response, second_response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let default_skills_dir = workspace_root
|
||
.join(".sgclaw-zeroclaw-workspace")
|
||
.join("skills");
|
||
let skill_dir = write_skill_manifest_package(
|
||
&default_skills_dir,
|
||
"workspace-zhihu-skill",
|
||
r#"
|
||
[skill]
|
||
name = "workspace-zhihu-skill"
|
||
description = "Extract Zhihu hotlist rows with a packaged browser script."
|
||
version = "0.1.0"
|
||
|
||
[[tools]]
|
||
name = "extract_hotlist"
|
||
description = "Extract structured hotlist rows from the current Zhihu page."
|
||
kind = "browser_script"
|
||
command = "scripts/extract_hotlist.js"
|
||
|
||
[tools.args]
|
||
top_n = "How many hotlist rows to extract."
|
||
"#,
|
||
);
|
||
write_skill_script(
|
||
&skill_dir,
|
||
"scripts/extract_hotlist.js",
|
||
r#"
|
||
const topN = Number(args.top_n || 10);
|
||
return {
|
||
source: "https://www.zhihu.com/hot",
|
||
sheet_name: "知乎热榜",
|
||
columns: ["rank", "title", "heat"],
|
||
rows: [[1, "标题", `${topN}条`]]
|
||
};
|
||
"#,
|
||
);
|
||
|
||
let mut settings = SgClawSettings::from_legacy_deepseek_fields(
|
||
"deepseek-test-key".to_string(),
|
||
base_url,
|
||
"deepseek-chat".to_string(),
|
||
Vec::new(),
|
||
)
|
||
.unwrap();
|
||
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![success_browser_response(
|
||
1,
|
||
json!({
|
||
"text": {
|
||
"source": "https://www.zhihu.com/hot",
|
||
"sheet_name": "知乎热榜",
|
||
"columns": ["rank", "title", "heat"],
|
||
"rows": [[1, "标题", "10条"]]
|
||
}
|
||
}),
|
||
)]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
let summary = execute_task_with_sgclaw_settings(
|
||
transport.as_ref(),
|
||
browser_tool,
|
||
"用知乎热榜 skill 提取前十条结构化数据",
|
||
&CompatTaskContext::default(),
|
||
&workspace_root,
|
||
&settings,
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
let tool_names = request_tool_names(&request_bodies[0]);
|
||
|
||
assert_eq!(summary, "已执行 browser_script skill");
|
||
assert!(tool_names.contains(&"workspace-zhihu-skill_extract_hotlist".to_string()));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(message, AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message == "call workspace-zhihu-skill.extract_hotlist")
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(message, AgentMessage::Command { action, .. } if action == &Action::Eval)
|
||
}));
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(message, AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message.starts_with("getText "))
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn zhihu_hotlist_browser_skill_flow_does_not_expose_shell_or_glob_tools() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已准备好知乎热榜技能"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let mut settings = SgClawSettings::from_legacy_deepseek_fields(
|
||
"deepseek-test-key".to_string(),
|
||
base_url,
|
||
"deepseek-chat".to_string(),
|
||
vec![real_skill_lib_root()],
|
||
)
|
||
.unwrap();
|
||
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
let summary = execute_task_with_sgclaw_settings(
|
||
transport.as_ref(),
|
||
browser_tool,
|
||
"统计一下知乎热榜前十,给出标题和热度",
|
||
&CompatTaskContext::default(),
|
||
&workspace_root,
|
||
&settings,
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
let tool_names = request_tool_names(&request_bodies[0]);
|
||
|
||
assert_eq!(summary, "已准备好知乎热榜技能");
|
||
assert!(tool_names.contains(&"superrpa_browser".to_string()));
|
||
assert!(tool_names.contains(&"browser_action".to_string()));
|
||
assert!(tool_names.contains(&"read_skill".to_string()));
|
||
assert!(tool_names.contains(&"zhihu-hotlist_extract_hotlist".to_string()));
|
||
assert!(!tool_names.contains(&"shell".to_string()));
|
||
assert!(!tool_names.contains(&"glob_search".to_string()));
|
||
}
|
||
|
||
#[test]
|
||
fn compat_runtime_browser_attached_profile_keeps_file_read_available_for_local_paths() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已收到本地路径任务"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let mut settings = SgClawSettings::from_legacy_deepseek_fields(
|
||
"deepseek-test-key".to_string(),
|
||
base_url,
|
||
"deepseek-chat".to_string(),
|
||
Vec::new(),
|
||
)
|
||
.unwrap();
|
||
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
let summary = execute_task_with_sgclaw_settings(
|
||
transport.as_ref(),
|
||
browser_tool,
|
||
"请读取本地文件 /home/zyl/data/report.md 的内容并总结",
|
||
&CompatTaskContext::default(),
|
||
&workspace_root,
|
||
&settings,
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
let tool_names = request_tool_names(&request_bodies[0]);
|
||
|
||
assert_eq!(summary, "已收到本地路径任务");
|
||
assert!(tool_names.contains(&"browser_action".to_string()));
|
||
assert!(tool_names.contains(&"superrpa_browser".to_string()));
|
||
assert!(tool_names.contains(&"file_read".to_string()));
|
||
}
|
||
|
||
#[test]
|
||
fn browser_attached_export_flow_exposes_browser_and_office_tools_only() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已准备好知乎导出流程"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let mut settings = SgClawSettings::from_legacy_deepseek_fields(
|
||
"deepseek-test-key".to_string(),
|
||
base_url,
|
||
"deepseek-chat".to_string(),
|
||
vec![real_skill_lib_root()],
|
||
)
|
||
.unwrap();
|
||
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
let summary = execute_task_with_sgclaw_settings(
|
||
transport.as_ref(),
|
||
browser_tool,
|
||
"读取知乎热榜数据,并导出 excel 文件",
|
||
&CompatTaskContext::default(),
|
||
&workspace_root,
|
||
&settings,
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
let tool_names = request_tool_names(&request_bodies[0]);
|
||
|
||
assert_eq!(summary, "已准备好知乎导出流程");
|
||
assert!(tool_names.contains(&"superrpa_browser".to_string()));
|
||
assert!(tool_names.contains(&"browser_action".to_string()));
|
||
assert!(tool_names.contains(&"read_skill".to_string()));
|
||
assert!(tool_names.contains(&"zhihu-hotlist_extract_hotlist".to_string()));
|
||
assert!(tool_names.contains(&"openxml_office".to_string()));
|
||
assert!(!tool_names.contains(&"shell".to_string()));
|
||
assert!(!tool_names.contains(&"glob_search".to_string()));
|
||
}
|
||
|
||
#[test]
|
||
fn compat_runtime_allows_zhihu_hotlist_screen_export_tool_in_browser_profile() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已准备好知乎热榜大屏流程"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let mut settings = SgClawSettings::from_legacy_deepseek_fields(
|
||
"deepseek-test-key".to_string(),
|
||
base_url,
|
||
"deepseek-chat".to_string(),
|
||
vec![real_skill_lib_root()],
|
||
)
|
||
.unwrap();
|
||
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
let summary = execute_task_with_sgclaw_settings(
|
||
transport.as_ref(),
|
||
browser_tool,
|
||
"读取知乎热榜数据并生成领导演示大屏,在新标签页展示",
|
||
&CompatTaskContext::default(),
|
||
&workspace_root,
|
||
&settings,
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
let tool_names = request_tool_names(&request_bodies[0]);
|
||
|
||
assert_eq!(summary, "已准备好知乎热榜大屏流程");
|
||
assert!(tool_names.contains(&"superrpa_browser".to_string()));
|
||
assert!(tool_names.contains(&"browser_action".to_string()));
|
||
assert!(tool_names.contains(&"read_skill".to_string()));
|
||
assert!(tool_names.contains(&"zhihu-hotlist_extract_hotlist".to_string()));
|
||
assert!(tool_names.contains(&"screen_html_export".to_string()));
|
||
assert!(!tool_names.contains(&"shell".to_string()));
|
||
assert!(!tool_names.contains(&"glob_search".to_string()));
|
||
}
|
||
|
||
#[test]
|
||
fn compat_runtime_logs_read_skill_usage_with_skill_name() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let first_response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "",
|
||
"tool_calls": [{
|
||
"id": "call_1",
|
||
"type": "function",
|
||
"function": {
|
||
"name": "read_skill",
|
||
"arguments": serde_json::to_string(&json!({
|
||
"name": "workspace-zhihu-skill"
|
||
})).unwrap()
|
||
}
|
||
}]
|
||
}
|
||
}]
|
||
});
|
||
let second_response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已读取完整 skill"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, _requests, server_handle) =
|
||
start_fake_deepseek_server(vec![first_response, second_response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let default_skills_dir = workspace_root
|
||
.join(".sgclaw-zeroclaw-workspace")
|
||
.join("skills");
|
||
write_skill_package(
|
||
&default_skills_dir,
|
||
"workspace-zhihu-skill",
|
||
"# Workspace Zhihu Skill\nUse this workspace-local skill.\n",
|
||
);
|
||
|
||
let settings = SgClawSettings::from_legacy_deepseek_fields(
|
||
"deepseek-test-key".to_string(),
|
||
base_url,
|
||
"deepseek-chat".to_string(),
|
||
Vec::new(),
|
||
)
|
||
.unwrap();
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
let summary = execute_task_with_sgclaw_settings(
|
||
transport.as_ref(),
|
||
browser_tool,
|
||
"需要完整 zhihu skill 时先读取 skill",
|
||
&CompatTaskContext::default(),
|
||
&workspace_root,
|
||
&settings,
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
|
||
assert_eq!(summary, "已读取完整 skill");
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" &&
|
||
message == "read_skill workspace-zhihu-skill@0.1.0"
|
||
)
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn handle_browser_message_exposes_real_zhihu_skill_lib_to_provider_request() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已看到真实知乎 skill"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let skills_dir = real_skill_lib_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
&base_url,
|
||
"deepseek-chat",
|
||
Some(skills_dir.to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "告诉我当前有哪些知乎 skill".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://example.net/".to_string(),
|
||
page_title: "Example Domain".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
let first_request = request_bodies[0].to_string();
|
||
let tool_names = request_tool_names(&request_bodies[0]);
|
||
let loaded_skills_message = sent
|
||
.iter()
|
||
.find_map(|message| match message {
|
||
AgentMessage::LogEntry { level, message } if level == "info" && message.starts_with("loaded skills: ") => {
|
||
Some(message.clone())
|
||
}
|
||
_ => None,
|
||
})
|
||
.expect("expected loaded skills log entry");
|
||
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && summary == "已看到真实知乎 skill"
|
||
)
|
||
}));
|
||
assert!(loaded_skills_message.contains("office-export-xlsx@0.1.0"));
|
||
assert!(loaded_skills_message.contains("zhihu-hotlist@0.1.0"));
|
||
assert!(loaded_skills_message.contains("zhihu-hotlist-screen@0.1.0"));
|
||
assert!(loaded_skills_message.contains("zhihu-navigate@0.1.0"));
|
||
assert!(loaded_skills_message.contains("zhihu-write@0.1.0"));
|
||
assert_eq!(request_bodies.len(), 1);
|
||
assert!(first_request.contains("office-export-xlsx"));
|
||
assert!(first_request.contains("zhihu-hotlist"));
|
||
assert!(first_request.contains("zhihu-hotlist-screen"));
|
||
assert!(first_request.contains("zhihu-navigate"));
|
||
assert!(first_request.contains("zhihu-write"));
|
||
assert!(tool_names.contains(&"browser_action".to_string()));
|
||
assert!(tool_names.contains(&"superrpa_browser".to_string()));
|
||
assert!(tool_names.contains(&"read_skill".to_string()));
|
||
}
|
||
|
||
#[test]
|
||
fn browser_attached_excel_request_uses_execution_contract_not_skill_source_stuffing() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已收到知乎导出任务"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let mut settings = SgClawSettings::from_legacy_deepseek_fields(
|
||
"deepseek-test-key".to_string(),
|
||
base_url,
|
||
"deepseek-chat".to_string(),
|
||
vec![real_skill_lib_root()],
|
||
)
|
||
.unwrap();
|
||
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
let summary = execute_task_with_sgclaw_settings(
|
||
transport.as_ref(),
|
||
browser_tool,
|
||
"读取知乎热榜数据,并导出 excel 文件",
|
||
&CompatTaskContext::default(),
|
||
&workspace_root,
|
||
&settings,
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
let first_request = request_bodies[0].to_string();
|
||
|
||
assert_eq!(summary, "已收到知乎导出任务");
|
||
assert!(first_request.contains("Zhihu hotlist execution contract"));
|
||
assert!(first_request.contains("Export completion contract"));
|
||
assert!(first_request.contains("openxml_office"));
|
||
assert!(!first_request.contains("Preloaded skill context:"));
|
||
}
|
||
|
||
#[test]
|
||
fn browser_attached_publish_request_injects_confirmation_contract() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "请先确认是否发布"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let mut settings = SgClawSettings::from_legacy_deepseek_fields(
|
||
"deepseek-test-key".to_string(),
|
||
base_url,
|
||
"deepseek-chat".to_string(),
|
||
vec![real_skill_lib_root()],
|
||
)
|
||
.unwrap();
|
||
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
execute_task_with_sgclaw_settings(
|
||
transport.as_ref(),
|
||
browser_tool,
|
||
"请直接发表这篇知乎文章,标题是测试标题,正文是第一段内容",
|
||
&CompatTaskContext {
|
||
conversation_id: None,
|
||
messages: vec![],
|
||
page_url: Some("https://www.zhihu.com/creator".to_string()),
|
||
page_title: Some("知乎创作中心".to_string()),
|
||
},
|
||
&workspace_root,
|
||
&settings,
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
let first_request = request_bodies[0].to_string();
|
||
|
||
assert!(first_request.contains("Zhihu article publish contract"));
|
||
assert!(first_request.contains("must not click publish without explicit human confirmation"));
|
||
assert!(first_request.contains("ask for confirmation concisely"));
|
||
assert!(first_request.contains("stop after the confirmation request"));
|
||
}
|
||
|
||
#[test]
|
||
fn handle_browser_message_executes_real_zhihu_hotlist_skill_flow() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let first_response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "",
|
||
"tool_calls": [{
|
||
"id": "call_1",
|
||
"type": "function",
|
||
"function": {
|
||
"name": "zhihu-hotlist_extract_hotlist",
|
||
"arguments": serde_json::to_string(&json!({
|
||
"expected_domain": "www.zhihu.com",
|
||
"top_n": "10"
|
||
})).unwrap()
|
||
}
|
||
}]
|
||
}
|
||
}]
|
||
});
|
||
let second_response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已完成知乎热榜采集"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) =
|
||
start_fake_deepseek_server(vec![first_response, second_response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let skills_dir = real_skill_lib_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
&base_url,
|
||
"deepseek-chat",
|
||
Some(skills_dir.to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![success_browser_response(
|
||
1,
|
||
json!({
|
||
"text": {
|
||
"source": "https://www.zhihu.com/hot",
|
||
"sheet_name": "知乎热榜",
|
||
"columns": ["rank", "title", "heat"],
|
||
"rows": [[1, "热榜项目 1", "1707万"], [2, "热榜项目 2", "1150万"]]
|
||
}
|
||
}),
|
||
)]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "统计一下知乎热榜前十".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://www.zhihu.com/".to_string(),
|
||
page_title: "知乎".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
let tool_content = tool_message_content(&request_bodies[1], "call_1").unwrap();
|
||
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && summary == "已完成知乎热榜采集"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message == "call zhihu-hotlist.extract_hotlist"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::Command { action, params, .. }
|
||
if action == &Action::Eval &&
|
||
params["script"].as_str().unwrap_or_default().contains("columns: ['rank', 'title', 'heat']")
|
||
)
|
||
}));
|
||
assert_eq!(request_bodies.len(), 2);
|
||
assert!(tool_content.contains("知乎热榜"));
|
||
assert!(tool_content.contains("rank"));
|
||
assert!(tool_content.contains("heat"));
|
||
assert!(tool_content.contains("热榜项目 1"));
|
||
}
|
||
|
||
#[test]
|
||
fn handle_browser_message_chains_hotlist_skill_into_xlsx_export_and_auto_open() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
std::env::set_var("SGCLAW_DISABLE_POST_EXPORT_OPEN", "1");
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
"http://127.0.0.1:9",
|
||
"deepseek-chat",
|
||
Some(real_skill_lib_root().to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![
|
||
success_browser_response(1, json!({ "navigated": true })),
|
||
success_browser_response(
|
||
2,
|
||
json!({ "text": "知乎热榜\n1 问题一 344万热度\n2 问题二 266万热度" }),
|
||
),
|
||
success_browser_response(
|
||
3,
|
||
json!({
|
||
"text": {
|
||
"source": "https://www.zhihu.com/hot",
|
||
"sheet_name": "知乎热榜",
|
||
"columns": ["rank", "title", "heat"],
|
||
"rows": [[1, "问题一", "344万"], [2, "问题二", "266万"]]
|
||
}
|
||
}),
|
||
),
|
||
]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "读取知乎热榜数据,并导出 excel 文件".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://www.zhihu.com/".to_string(),
|
||
page_title: "知乎".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
let summary = task_complete_summary(&sent);
|
||
let generated = extract_generated_artifact_path(&summary, ".xlsx");
|
||
|
||
assert!(summary.contains("已导出并打开知乎热榜 Excel"));
|
||
assert!(summary.contains(".xlsx"));
|
||
assert!(generated.exists());
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && summary.contains("已导出并打开知乎热榜 Excel") && summary.contains(".xlsx")
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" && message == "zeroclaw_process_message_primary"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message == "call zhihu-hotlist.extract_hotlist"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message == "call openxml_office"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::Command { action, .. } if action == &Action::Eval
|
||
)
|
||
}));
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::Command { action, params, .. }
|
||
if action == &Action::Navigate && params.get("sgclaw_local_dashboard_open").is_some()
|
||
)
|
||
}));
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary")
|
||
)
|
||
}));
|
||
std::env::remove_var("SGCLAW_DISABLE_POST_EXPORT_OPEN");
|
||
}
|
||
|
||
#[test]
|
||
fn handle_browser_message_chains_hotlist_skill_into_screen_export_and_auto_open() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
"http://127.0.0.1:9",
|
||
"deepseek-chat",
|
||
Some(real_skill_lib_root().to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![
|
||
success_browser_response(1, json!({ "navigated": true })),
|
||
success_browser_response(
|
||
2,
|
||
json!({ "text": "知乎热榜\n1 问题一 344万热度\n2 问题二 266万热度" }),
|
||
),
|
||
success_browser_response(
|
||
3,
|
||
json!({
|
||
"text": {
|
||
"source": "https://www.zhihu.com/hot",
|
||
"sheet_name": "知乎热榜",
|
||
"columns": ["rank", "title", "heat"],
|
||
"rows": [[1, "问题一", "344万"], [2, "问题二", "266万"]]
|
||
}
|
||
}),
|
||
),
|
||
success_browser_response(4, json!({ "navigated": true })),
|
||
]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "读取知乎热榜数据并生成领导演示大屏,在新标签页展示".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://www.zhihu.com/".to_string(),
|
||
page_title: "知乎".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
let summary = task_complete_summary(&sent);
|
||
let generated = extract_generated_artifact_path(&summary, ".html");
|
||
let navigate = sent
|
||
.iter()
|
||
.find_map(|message| match message {
|
||
AgentMessage::Command {
|
||
action,
|
||
params,
|
||
security,
|
||
..
|
||
} if action == &Action::Navigate
|
||
&& security.expected_domain == "__sgclaw_local_dashboard__" => Some((params, security)),
|
||
_ => None,
|
||
})
|
||
.expect("dashboard route should emit local-dashboard navigate request");
|
||
|
||
assert!(summary.contains("已在浏览器中打开知乎热榜大屏"));
|
||
assert!(summary.contains(".html"));
|
||
assert!(generated.exists());
|
||
assert_eq!(
|
||
navigate.0["sgclaw_local_dashboard_open"]["output_path"].as_str(),
|
||
generated.to_str()
|
||
);
|
||
assert!(navigate.0["url"]
|
||
.as_str()
|
||
.expect("dashboard open url should be present")
|
||
.starts_with("file://"));
|
||
assert_eq!(
|
||
navigate.0["sgclaw_local_dashboard_open"]["source"],
|
||
json!("compat.workflow_executor")
|
||
);
|
||
assert_eq!(
|
||
navigate.0["sgclaw_local_dashboard_open"]["kind"],
|
||
json!("zhihu_hotlist_screen")
|
||
);
|
||
assert_eq!(
|
||
navigate.0["sgclaw_local_dashboard_open"]["presentation_url"],
|
||
navigate.0["url"]
|
||
);
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" && message == "zeroclaw_process_message_primary"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message == "call zhihu-hotlist.extract_hotlist"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message == "call screen_html_export"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::Command { action, .. } if action == &Action::Eval
|
||
)
|
||
}));
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message == "call openxml_office"
|
||
)
|
||
}));
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary")
|
||
)
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn handle_browser_message_reports_dashboard_auto_open_protocol_error_when_presentation_url_is_missing() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
let browser_backend = sgclaw::browser::PipeBrowserBackend::from_inner(browser_tool);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let output_path = workspace_root.join("zhihu-hotlist-screen.html");
|
||
fs::write(&output_path, "<html><body>fixture</body></html>").unwrap();
|
||
let payload = json!({
|
||
"title": "知乎热榜大屏",
|
||
"output_path": output_path,
|
||
"renderer": "screen_html_export",
|
||
"row_count": 2,
|
||
"snapshot_id": "snapshot-test",
|
||
"presentation": {
|
||
"mode": "new_tab",
|
||
"title": "知乎热榜大屏",
|
||
"open_in_new_tab": true
|
||
}
|
||
});
|
||
|
||
let summary = finalize_screen_export(&browser_backend, &payload.to_string()).unwrap();
|
||
|
||
assert!(summary.contains("已生成知乎热榜大屏"));
|
||
assert!(summary.contains(output_path.to_string_lossy().as_ref()));
|
||
assert!(summary.contains("但浏览器自动打开失败:screen_html_export did not return presentation.url"));
|
||
|
||
let sent = transport.sent_messages();
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::Command { action, params, .. }
|
||
if action == &Action::Navigate && params.get("sgclaw_local_dashboard_open").is_some()
|
||
)
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn handle_browser_message_runs_zhihu_hotlist_export_via_zeroclaw_primary_orchestration() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
std::env::set_var("SGCLAW_DISABLE_POST_EXPORT_OPEN", "1");
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
"http://127.0.0.1:9",
|
||
"deepseek-chat",
|
||
Some(real_skill_lib_root().to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![
|
||
success_browser_response(
|
||
1,
|
||
json!({ "text": "知乎热榜\n1 问题一 344万热度\n2 问题二 266万热度\n3 问题三 181万热度" }),
|
||
),
|
||
success_browser_response(
|
||
2,
|
||
json!({
|
||
"text": {
|
||
"source": "https://www.zhihu.com/hot",
|
||
"sheet_name": "知乎热榜",
|
||
"columns": ["rank", "title", "heat"],
|
||
"rows": [
|
||
[1, "问题一", "344万"],
|
||
[2, "问题二", "266万"],
|
||
[3, "问题三", "181万"]
|
||
]
|
||
}
|
||
}),
|
||
),
|
||
]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "读取知乎热榜前10,并导出 excel 文件".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://www.zhihu.com/hot".to_string(),
|
||
page_title: "知乎热榜".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
let summary = task_complete_summary(&sent);
|
||
let generated = extract_generated_artifact_path(&summary, ".xlsx");
|
||
|
||
assert!(summary.contains(".xlsx"));
|
||
assert!(generated.exists());
|
||
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" && message == "zeroclaw_process_message_primary"
|
||
)
|
||
}));
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary")
|
||
)
|
||
}));
|
||
std::env::remove_var("SGCLAW_DISABLE_POST_EXPORT_OPEN");
|
||
}
|
||
|
||
#[test]
|
||
fn browser_submit_path_prefers_zeroclaw_process_message_orchestrator() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
"http://127.0.0.1:9",
|
||
"deepseek-chat",
|
||
Some(real_skill_lib_root().to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "读取知乎热榜前10,并导出 excel 文件".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://www.zhihu.com/hot".to_string(),
|
||
page_title: "知乎热榜".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
dbg!(&sent);
|
||
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" && message == "zeroclaw_process_message_primary"
|
||
)
|
||
}));
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary")
|
||
)
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn browser_submit_path_prefers_zeroclaw_process_message_orchestrator_for_zhihu_publish() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
"http://127.0.0.1:9",
|
||
"deepseek-chat",
|
||
Some(real_skill_lib_root().to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "请直接发表这篇知乎文章,标题是测试标题,正文是第一段内容".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://www.zhihu.com/".to_string(),
|
||
page_title: "知乎".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" && message == "zeroclaw_process_message_primary"
|
||
)
|
||
}));
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary")
|
||
)
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn handle_browser_message_exposes_project_skills_and_staged_scene_skills_together() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已同时看到顶层与场景技能"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
&base_url,
|
||
"deepseek-chat",
|
||
Some(project_skills_root().to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "告诉我当前有哪些知乎和95598相关 skill".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://example.net/".to_string(),
|
||
page_title: "Example Domain".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
let first_request = request_bodies[0].to_string();
|
||
let loaded_skills_message = sent
|
||
.iter()
|
||
.find_map(|message| match message {
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message.starts_with("loaded skills: ") =>
|
||
{
|
||
Some(message.clone())
|
||
}
|
||
_ => None,
|
||
})
|
||
.expect("expected loaded skills log entry");
|
||
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && summary == "已同时看到顶层与场景技能"
|
||
)
|
||
}));
|
||
assert!(loaded_skills_message.contains("zhihu-hotlist@0.1.0"));
|
||
assert!(loaded_skills_message.contains("zhihu-write@0.1.0"));
|
||
assert!(loaded_skills_message.contains("fault-details-report@0.1.0"));
|
||
assert!(loaded_skills_message.contains("95598-repair-city-dispatch@0.1.0"));
|
||
assert!(first_request.contains("zhihu-hotlist"));
|
||
assert!(first_request.contains("zhihu-write"));
|
||
assert!(first_request.contains("fault-details-report"));
|
||
assert!(first_request.contains("95598-repair-city-dispatch"));
|
||
}
|
||
|
||
#[test]
|
||
fn fault_details_route_finds_staged_scene_skill_under_project_skills_root() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
"http://127.0.0.1:9",
|
||
"deepseek-chat",
|
||
Some(project_skills_root().to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![success_browser_response(
|
||
1,
|
||
json!({
|
||
"text": {
|
||
"sheet_name": "故障明细",
|
||
"rows": [["2026-04", "已完成"]]
|
||
}
|
||
}),
|
||
)]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
policy_for_domains(&["example.invalid"]),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "导出 2026-04 故障明细".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://example.invalid/workbench".to_string(),
|
||
page_title: "业务台账".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && summary.contains("sheet_name") && summary.contains("故障明细")
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(message, AgentMessage::Command { action, .. } if action == &Action::Eval)
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn handle_browser_message_exposes_staged_95598_scene_skills_and_contract_on_agent_path() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已按95598场景进入通用代理路径"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
&base_url,
|
||
"deepseek-chat",
|
||
Some(staged_skill_root().to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
policy_for_domains(&["95598.example.invalid"]),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "请处理95598-repair-city-dispatch场景,查看抢修市指派单并汇总当前队列"
|
||
.to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://95598.example.invalid/dispatch".to_string(),
|
||
page_title: "95598抢修市指监测".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
let first_request = request_bodies[0].to_string();
|
||
let loaded_skills_message = sent
|
||
.iter()
|
||
.find_map(|message| match message {
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message.starts_with("loaded skills: ") =>
|
||
{
|
||
Some(message.clone())
|
||
}
|
||
_ => None,
|
||
})
|
||
.expect("expected loaded skills log entry");
|
||
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" && message == "compat_llm_primary"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && summary == "已按95598场景进入通用代理路径"
|
||
)
|
||
}));
|
||
assert!(loaded_skills_message.contains("fault-details-report@0.1.0"));
|
||
assert!(loaded_skills_message.contains("95598-repair-city-dispatch@0.1.0"));
|
||
assert_eq!(request_bodies.len(), 1);
|
||
assert!(first_request.contains("95598-repair-city-dispatch.collect_repair_orders"));
|
||
assert!(first_request.contains("Current page URL: https://95598.example.invalid/dispatch"));
|
||
assert!(first_request.contains("Current page title: 95598抢修市指监测"));
|
||
}
|
||
|
||
#[test]
|
||
fn browser_surface_disabled_fault_details_turn_uses_general_assistant_provider_path() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已走通用助手路径"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = workspace_root.join("sgclaw_config.json");
|
||
fs::write(
|
||
&config_path,
|
||
serde_json::to_string_pretty(&json!({
|
||
"apiKey": "deepseek-test-key",
|
||
"baseUrl": base_url,
|
||
"model": "deepseek-chat",
|
||
"runtimeProfile": "generalAssistant",
|
||
"skillsDir": staged_skill_root(),
|
||
}))
|
||
.unwrap(),
|
||
)
|
||
.unwrap();
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
policy_for_domains(&["example.invalid"]),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "导出 2026-04 故障明细".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://example.invalid/workbench".to_string(),
|
||
page_title: "业务台账".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
let first_request = request_bodies[0].to_string();
|
||
let loaded_skills_message = sent.iter().find_map(|message| match message {
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message.starts_with("loaded skills: ") =>
|
||
{
|
||
Some(message.clone())
|
||
}
|
||
_ => None,
|
||
});
|
||
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" && message == "compat_llm_primary"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && summary == "已走通用助手路径"
|
||
)
|
||
}));
|
||
assert_eq!(request_bodies.len(), 1);
|
||
assert!(!first_request.contains("95598-repair-city-dispatch.collect_repair_orders"));
|
||
assert!(!first_request.contains("browser workflow, not a text-only task"));
|
||
assert!(!first_request.contains("generic browser probing only after"));
|
||
assert!(loaded_skills_message.is_none());
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(message, AgentMessage::Command { action, .. } if action == &Action::Eval)
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn browser_attached_zhihu_hotlist_request_keeps_zhihu_contract_without_scene_injection() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已收到知乎导出任务"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let mut settings = SgClawSettings::from_legacy_deepseek_fields(
|
||
"deepseek-test-key".to_string(),
|
||
base_url,
|
||
"deepseek-chat".to_string(),
|
||
vec![real_skill_lib_root()],
|
||
)
|
||
.unwrap();
|
||
settings.runtime_profile = RuntimeProfile::BrowserAttached;
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
let summary = execute_task_with_sgclaw_settings(
|
||
transport.as_ref(),
|
||
browser_tool,
|
||
"读取知乎热榜数据,并导出 excel 文件",
|
||
&CompatTaskContext::default(),
|
||
&workspace_root,
|
||
&settings,
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
let first_request = request_bodies[0].to_string();
|
||
|
||
assert_eq!(summary, "已收到知乎导出任务");
|
||
assert_eq!(request_bodies.len(), 1);
|
||
assert!(first_request.contains("Zhihu hotlist execution contract"));
|
||
assert!(first_request.contains("Export completion contract"));
|
||
assert!(first_request.contains("openxml_office"));
|
||
assert!(!first_request.contains("95598 repair city dispatch execution contract"));
|
||
assert!(!first_request.contains("browser workflow, not a text-only task"));
|
||
assert!(!first_request.contains("generic browser probing only after"));
|
||
}
|
||
|
||
#[test]
|
||
fn fault_details_direct_browser_scene_matches_primary_orchestration_gate() {
|
||
assert!(sgclaw::compat::orchestration::should_use_primary_orchestration(
|
||
"导出故障明细",
|
||
Some("https://example.invalid/workbench"),
|
||
Some("业务台账"),
|
||
));
|
||
}
|
||
|
||
#[test]
|
||
fn fault_details_direct_browser_scene_detects_direct_route() {
|
||
use sgclaw::compat::workflow_executor::{detect_route, WorkflowRoute};
|
||
|
||
assert_eq!(
|
||
detect_route(
|
||
"导出故障明细",
|
||
Some("https://example.invalid/workbench"),
|
||
Some("业务台账")
|
||
),
|
||
Some(WorkflowRoute::FaultDetailsReport)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn missing_scene_metadata_keeps_unrelated_primary_routing_unchanged() {
|
||
let registry = [sgclaw::runtime::SceneRegistryEntry {
|
||
id: "unrelated-scene".to_string(),
|
||
name: "无关场景".to_string(),
|
||
summary: "与故障明细无关。".to_string(),
|
||
tags: vec!["other".to_string()],
|
||
inputs: vec!["period".to_string()],
|
||
outputs: vec!["artifact".to_string()],
|
||
skill_package: "unrelated-skill".to_string(),
|
||
skill_tool: "run_other".to_string(),
|
||
skill_artifact_type: "artifact".to_string(),
|
||
dispatch_mode: sgclaw::runtime::DispatchMode::DirectBrowser,
|
||
expected_domain: "other.example.invalid".to_string(),
|
||
aliases: vec!["别的事情".to_string()],
|
||
default_args: serde_json::Map::new(),
|
||
}];
|
||
|
||
assert!(sgclaw::runtime::match_scene_instruction_in_registry(®istry, "别的事情").is_some());
|
||
assert!(sgclaw::runtime::match_scene_instruction_in_registry(®istry, "导出故障明细").is_none());
|
||
assert!(!sgclaw::compat::orchestration::should_use_primary_orchestration(
|
||
"帮我汇总今天待办",
|
||
Some("https://example.invalid/workbench"),
|
||
Some("业务台账"),
|
||
));
|
||
}
|
||
|
||
#[test]
|
||
fn fault_details_route_returns_clear_failure_when_period_cannot_be_derived() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
"http://127.0.0.1:9",
|
||
"deepseek-chat",
|
||
Some(staged_skill_root().to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "导出故障明细".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://example.invalid/workbench".to_string(),
|
||
page_title: "业务台账".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" && message == "zeroclaw_process_message_primary"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if !*success && summary.contains("period") && summary.contains("无法")
|
||
)
|
||
}));
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(message, AgentMessage::Command { action, .. } if action == &Action::Eval)
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn fault_details_route_uses_current_page_host_as_expected_domain() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
"http://127.0.0.1:9",
|
||
"deepseek-chat",
|
||
Some(staged_skill_root().to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![success_browser_response(
|
||
1,
|
||
json!({
|
||
"text": {
|
||
"sheet_name": "故障明细",
|
||
"rows": [["2026-04", "已完成"]]
|
||
}
|
||
}),
|
||
)]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
policy_for_domains(&["example.invalid"]),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "导出 2026-04 故障明细".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://example.invalid/workbench".to_string(),
|
||
page_title: "业务台账".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && summary.contains("sheet_name") && summary.contains("故障明细")
|
||
)
|
||
}));
|
||
let eval_command = sent.iter().find_map(|message| match message {
|
||
AgentMessage::Command {
|
||
action,
|
||
params,
|
||
security,
|
||
..
|
||
} if action == &Action::Eval => Some((params.clone(), security.expected_domain.clone())),
|
||
_ => None,
|
||
});
|
||
let (params, expected_domain) = eval_command.expect("direct route should call browser eval");
|
||
assert_eq!(expected_domain, "example.invalid");
|
||
let script = params["script"].as_str().unwrap_or_default();
|
||
assert!(script.contains("const args = {\"period\":\"2026-04\"};"));
|
||
}
|
||
|
||
#[test]
|
||
fn fault_details_route_uses_packaged_browser_script_from_configured_skills_root() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let staged_root = workspace_root.join("custom_fault_details_staging");
|
||
let custom_skills_dir = staged_root.join("skills");
|
||
let skill_dir = write_skill_manifest_package(
|
||
&custom_skills_dir,
|
||
"fault-details-report",
|
||
r#"
|
||
[skill]
|
||
name = "fault-details-report"
|
||
description = "Collect fault detail rows via a packaged browser script."
|
||
version = "0.1.0"
|
||
|
||
[[tools]]
|
||
name = "collect_fault_details"
|
||
description = "Collect fault detail rows for the target period."
|
||
kind = "browser_script"
|
||
command = "scripts/custom_fault_details.js"
|
||
|
||
[tools.args]
|
||
period = "Target report period."
|
||
"#,
|
||
);
|
||
write_skill_script(
|
||
&skill_dir,
|
||
"scripts/custom_fault_details.js",
|
||
r#"
|
||
return {
|
||
sheet_name: "故障明细",
|
||
rows: [[args.period || "unknown", "CUSTOM_FAULT_DETAILS_MARKER"]],
|
||
marker: "CUSTOM_FAULT_DETAILS_MARKER"
|
||
};
|
||
"#,
|
||
);
|
||
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
"http://127.0.0.1:9",
|
||
"deepseek-chat",
|
||
Some(staged_root.to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![success_browser_response(
|
||
1,
|
||
json!({
|
||
"text": {
|
||
"sheet_name": "故障明细",
|
||
"rows": [["2026-04", "已完成"]]
|
||
}
|
||
}),
|
||
)]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
policy_for_domains(&["example.invalid"]),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "导出 2026-04 故障明细".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://example.invalid/workbench".to_string(),
|
||
page_title: "业务台账".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && summary.contains("sheet_name") && summary.contains("故障明细")
|
||
)
|
||
}));
|
||
let eval_command = sent.iter().find_map(|message| match message {
|
||
AgentMessage::Command {
|
||
action,
|
||
params,
|
||
security,
|
||
..
|
||
} if action == &Action::Eval => Some((params.clone(), security.expected_domain.clone())),
|
||
_ => None,
|
||
});
|
||
let (params, expected_domain) = eval_command.expect("direct route should call browser eval");
|
||
assert_eq!(expected_domain, "example.invalid");
|
||
let script = params["script"].as_str().unwrap_or_default();
|
||
assert!(script.contains("const args = {\"period\":\"2026-04\"};"));
|
||
assert!(script.contains("CUSTOM_FAULT_DETAILS_MARKER"));
|
||
assert!(!script.contains("collect_fault_details.js"));
|
||
}
|
||
|
||
#[test]
|
||
fn fault_details_route_executes_browser_script_eval_when_period_is_derived() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
"http://127.0.0.1:9",
|
||
"deepseek-chat",
|
||
Some(staged_skill_root().to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![success_browser_response(
|
||
1,
|
||
json!({
|
||
"text": {
|
||
"sheet_name": "故障明细",
|
||
"rows": [["2026-04", "已完成"]]
|
||
}
|
||
}),
|
||
)]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
policy_for_domains(&["example.invalid"]),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "导出 2026-04 故障明细".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://example.invalid/workbench".to_string(),
|
||
page_title: "业务台账".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && summary.contains("sheet_name") && summary.contains("故障明细")
|
||
)
|
||
}));
|
||
let eval_command = sent.iter().find_map(|message| match message {
|
||
AgentMessage::Command {
|
||
action,
|
||
params,
|
||
security,
|
||
..
|
||
} if action == &Action::Eval => Some((params.clone(), security.expected_domain.clone())),
|
||
_ => None,
|
||
});
|
||
let (params, expected_domain) = eval_command.expect("direct route should call browser eval");
|
||
assert_eq!(expected_domain, "example.invalid");
|
||
let script = params["script"].as_str().unwrap_or_default();
|
||
assert!(script.contains("const args = {\"period\":\"2026-04\"};"));
|
||
assert!(script.contains("sheet_name") || script.contains("return JSON.stringify") || script.contains("rows"));
|
||
}
|
||
|
||
#[test]
|
||
fn zhihu_generated_auto_publish_matches_primary_orchestration_gate() {
|
||
assert!(
|
||
sgclaw::compat::orchestration::should_use_primary_orchestration(
|
||
"在知乎自动发表一篇名称为人工智能技能大全",
|
||
Some("https://www.zhihu.com/"),
|
||
Some("知乎"),
|
||
)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn zhihu_hotlist_export_route_stays_ahead_of_generated_article_publish() {
|
||
use sgclaw::compat::workflow_executor::{detect_route, WorkflowRoute};
|
||
|
||
assert_eq!(
|
||
detect_route(
|
||
"打开知乎热榜,获取前10条数据,并导出 Excel",
|
||
Some("https://www.zhihu.com/"),
|
||
Some("知乎")
|
||
),
|
||
Some(WorkflowRoute::ZhihuHotlistExportXlsx)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn zhihu_generated_auto_publish_uses_provider_and_submits_publish_without_confirmation() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "标题:人工智能技能大全\n正文:第一段内容。\n\n第二段内容。"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
&base_url,
|
||
"deepseek-chat",
|
||
Some(real_skill_lib_root().to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![
|
||
success_browser_response(1, json!({ "navigated": true })),
|
||
success_browser_response(
|
||
2,
|
||
json!({
|
||
"text": {
|
||
"status": "creator_entry_clicked",
|
||
"current_url": "https://www.zhihu.com/creator",
|
||
"next_url": "https://zhuanlan.zhihu.com/write"
|
||
}
|
||
}),
|
||
),
|
||
success_browser_response(3, json!({ "navigated": true })),
|
||
success_browser_response(
|
||
4,
|
||
json!({
|
||
"text": {
|
||
"status": "editor_ready",
|
||
"current_url": "https://zhuanlan.zhihu.com/write"
|
||
}
|
||
}),
|
||
),
|
||
success_browser_response(
|
||
5,
|
||
json!({
|
||
"text": {
|
||
"status": "publish_submitted",
|
||
"current_url": "https://zhuanlan.zhihu.com/write",
|
||
"title": "人工智能技能大全"
|
||
}
|
||
}),
|
||
),
|
||
]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "在知乎自动发表一篇名称为人工智能技能大全".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://www.zhihu.com/".to_string(),
|
||
page_title: "知乎".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
|
||
assert_eq!(request_bodies.len(), 1);
|
||
assert!(request_bodies[0].to_string().contains("人工智能技能大全"));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && summary == "已提交知乎文章发布流程《人工智能技能大全》"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message == "call zhihu-write.fill_article_draft"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::Command { action, .. } if action == &Action::Navigate
|
||
)
|
||
}));
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && summary.contains("确认发布")
|
||
)
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn zhihu_publish_task_matches_primary_orchestration_gate() {
|
||
assert!(
|
||
sgclaw::compat::orchestration::should_use_primary_orchestration(
|
||
"请直接发表这篇知乎文章,标题是测试标题,正文是第一段内容",
|
||
Some("https://www.zhihu.com/"),
|
||
Some("知乎"),
|
||
)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn zhihu_article_entry_task_matches_primary_orchestration_gate() {
|
||
assert!(
|
||
sgclaw::compat::orchestration::should_use_primary_orchestration(
|
||
"打开知乎发文章页面",
|
||
Some("https://www.zhihu.com/"),
|
||
Some("知乎"),
|
||
)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn zhihu_hotlist_export_routes_prefer_direct_execution() {
|
||
use sgclaw::compat::workflow_executor::{prefers_direct_execution, WorkflowRoute};
|
||
|
||
assert!(prefers_direct_execution(
|
||
&WorkflowRoute::ZhihuHotlistExportXlsx
|
||
));
|
||
assert!(prefers_direct_execution(&WorkflowRoute::ZhihuHotlistScreen));
|
||
}
|
||
|
||
#[test]
|
||
fn zhihu_publish_without_article_inputs_returns_missing_fields_prompt() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
"http://127.0.0.1:9",
|
||
"deepseek-chat",
|
||
Some(real_skill_lib_root().to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "请发表知乎文章".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://www.zhihu.com/".to_string(),
|
||
page_title: "知乎".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success &&
|
||
summary.contains("标题") &&
|
||
summary.contains("正文")
|
||
)
|
||
}));
|
||
assert!(!sent
|
||
.iter()
|
||
.any(|message| { matches!(message, AgentMessage::Command { .. }) }));
|
||
}
|
||
|
||
#[test]
|
||
fn zhihu_publish_accepts_literal_backslash_n_between_title_and_body() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
"http://127.0.0.1:9",
|
||
"deepseek-chat",
|
||
Some(real_skill_lib_root().to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![
|
||
success_browser_response(1, json!({ "navigated": true })),
|
||
success_browser_response(
|
||
2,
|
||
json!({
|
||
"text": {
|
||
"status": "creator_entry_clicked",
|
||
"current_url": "https://www.zhihu.com/creator",
|
||
"next_url": "https://zhuanlan.zhihu.com/write"
|
||
}
|
||
}),
|
||
),
|
||
success_browser_response(3, json!({ "navigated": true })),
|
||
success_browser_response(
|
||
4,
|
||
json!({
|
||
"text": {
|
||
"status": "editor_ready",
|
||
"current_url": "https://zhuanlan.zhihu.com/write"
|
||
}
|
||
}),
|
||
),
|
||
success_browser_response(
|
||
5,
|
||
json!({
|
||
"text": {
|
||
"status": "draft_ready",
|
||
"current_url": "https://zhuanlan.zhihu.com/write",
|
||
"title": "ai时代,普通人如何自救"
|
||
}
|
||
}),
|
||
),
|
||
]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "标题:ai时代,普通人如何自救 \\n正文:第一段内容。 第二段内容。"
|
||
.to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://www.zhihu.com/creator".to_string(),
|
||
page_title: "知乎创作中心".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && summary == "已进入知乎文章编辑器并写入草稿《ai时代,普通人如何自救》"
|
||
)
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn zhihu_article_entry_opens_editor_without_generic_selector_probing() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
"http://127.0.0.1:9",
|
||
"deepseek-chat",
|
||
Some(real_skill_lib_root().to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![
|
||
success_browser_response(1, json!({ "navigated": true })),
|
||
success_browser_response(
|
||
2,
|
||
json!({
|
||
"text": {
|
||
"status": "creator_entry_clicked",
|
||
"current_url": "https://www.zhihu.com/creator",
|
||
"next_url": "https://zhuanlan.zhihu.com/write"
|
||
}
|
||
}),
|
||
),
|
||
success_browser_response(3, json!({ "navigated": true })),
|
||
success_browser_response(
|
||
4,
|
||
json!({
|
||
"text": {
|
||
"status": "editor_ready",
|
||
"current_url": "https://zhuanlan.zhihu.com/write"
|
||
}
|
||
}),
|
||
),
|
||
]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "打开知乎发文章页面".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://www.zhihu.com/".to_string(),
|
||
page_title: "知乎".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && summary.contains("编辑器")
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" && message == "zeroclaw_process_message_primary"
|
||
)
|
||
}));
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" &&
|
||
(message.starts_with("getText ") || message.starts_with("click "))
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::Command { action, params, .. }
|
||
if action == &Action::Navigate &&
|
||
params["url"].as_str() == Some("https://zhuanlan.zhihu.com/write")
|
||
)
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn zhihu_article_entry_reports_editor_unavailable_without_protocol_error() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
"http://127.0.0.1:9",
|
||
"deepseek-chat",
|
||
Some(real_skill_lib_root().to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![
|
||
success_browser_response(1, json!({ "navigated": true })),
|
||
success_browser_response(
|
||
2,
|
||
json!({
|
||
"text": {
|
||
"status": "creator_entry_clicked",
|
||
"current_url": "https://www.zhihu.com/creator",
|
||
"next_url": "https://zhuanlan.zhihu.com/write"
|
||
}
|
||
}),
|
||
),
|
||
success_browser_response(3, json!({ "navigated": true })),
|
||
success_browser_response(
|
||
4,
|
||
json!({
|
||
"text": {
|
||
"status": "editor_unavailable",
|
||
"current_url": "https://zhuanlan.zhihu.com/write"
|
||
}
|
||
}),
|
||
),
|
||
]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "打开知乎发文章页面".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://www.zhihu.com/".to_string(),
|
||
page_title: "知乎".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success &&
|
||
summary.contains("未检测到文章编辑器")
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::Command { action, params, .. }
|
||
if action == &Action::Navigate &&
|
||
params["url"].as_str() == Some("https://zhuanlan.zhihu.com/write")
|
||
)
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn zhihu_article_entry_stops_when_creator_page_has_no_write_entry() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
"http://127.0.0.1:9",
|
||
"deepseek-chat",
|
||
Some(real_skill_lib_root().to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![
|
||
success_browser_response(1, json!({ "navigated": true })),
|
||
success_browser_response(
|
||
2,
|
||
json!({
|
||
"text": {
|
||
"status": "creator_home",
|
||
"current_url": "https://www.zhihu.com/creator",
|
||
"desired_target": "article_editor"
|
||
}
|
||
}),
|
||
),
|
||
]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "打开知乎发文章页面".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://www.zhihu.com/".to_string(),
|
||
page_title: "知乎".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && summary.contains("未找到“写文章”入口")
|
||
)
|
||
}));
|
||
assert_eq!(
|
||
sent.iter()
|
||
.filter(|message| matches!(message, AgentMessage::Command { .. }))
|
||
.count(),
|
||
2
|
||
);
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message == "call zhihu-write.prepare_article_editor"
|
||
)
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn zhihu_publish_without_confirmation_returns_confirmation_before_any_browser_probing() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
"http://127.0.0.1:9",
|
||
"deepseek-chat",
|
||
Some(real_skill_lib_root().to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "请直接发表这篇知乎文章,标题是测试标题,正文是第一段内容".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://www.zhihu.com/".to_string(),
|
||
page_title: "知乎".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && summary.contains("确认发布")
|
||
)
|
||
}));
|
||
assert!(!sent
|
||
.iter()
|
||
.any(|message| { matches!(message, AgentMessage::Command { .. }) }));
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" &&
|
||
(message.starts_with("navigate ") ||
|
||
message.starts_with("getText ") ||
|
||
message.starts_with("click ") ||
|
||
message.starts_with("type "))
|
||
)
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn zhihu_publish_after_confirmation_reports_login_block_without_selector_probing() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
"http://127.0.0.1:9",
|
||
"deepseek-chat",
|
||
Some(real_skill_lib_root().to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![
|
||
success_browser_response(
|
||
1,
|
||
json!({ "navigated": true, "url": "https://www.zhihu.com/signin?next=%2Fcreator" }),
|
||
),
|
||
success_browser_response(
|
||
2,
|
||
json!({
|
||
"text": {
|
||
"status": "login_required",
|
||
"current_url": "https://www.zhihu.com/signin?next=%2Fcreator"
|
||
}
|
||
}),
|
||
),
|
||
]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "确认发布".to_string(),
|
||
conversation_id: "conversation-1".to_string(),
|
||
messages: vec![ConversationMessage {
|
||
role: "user".to_string(),
|
||
content: "请直接发表这篇知乎文章,标题是测试标题,正文是第一段内容".to_string(),
|
||
}],
|
||
page_url: "https://www.zhihu.com/".to_string(),
|
||
page_title: "知乎".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && (summary.contains("未登录") || summary.contains("登录"))
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::Command { action, params, .. }
|
||
if action == &Action::Navigate &&
|
||
params["url"].as_str() == Some("https://www.zhihu.com/creator")
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(message, AgentMessage::Command { action, .. } if action == &Action::Eval)
|
||
}));
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" &&
|
||
(message.starts_with("getText ") || message.starts_with("click "))
|
||
)
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn browser_orchestration_executes_hotlist_export_natively_from_hotlist_page() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
std::env::set_var("SGCLAW_DISABLE_POST_EXPORT_OPEN", "1");
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
"http://127.0.0.1:9",
|
||
"deepseek-chat",
|
||
Some(real_skill_lib_root().to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![
|
||
success_browser_response(
|
||
1,
|
||
json!({ "text": "知乎热榜\n1 问题一 344万热度\n2 问题二 266万热度" }),
|
||
),
|
||
success_browser_response(
|
||
2,
|
||
json!({
|
||
"text": {
|
||
"source": "https://www.zhihu.com/hot",
|
||
"sheet_name": "知乎热榜",
|
||
"columns": ["rank", "title", "heat"],
|
||
"rows": [[1, "问题一", "344万"], [2, "问题二", "266万"]]
|
||
}
|
||
}),
|
||
),
|
||
]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "读取知乎热榜前10,并导出 excel 文件".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://www.zhihu.com/hot".to_string(),
|
||
page_title: "知乎热榜".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
let summary = task_complete_summary(&sent);
|
||
let generated = extract_generated_artifact_path(&summary, ".xlsx");
|
||
|
||
assert!(summary.contains(".xlsx"));
|
||
assert!(generated.exists());
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" && message == "zeroclaw_process_message_primary"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message == "call zhihu-hotlist.extract_hotlist"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message == "call openxml_office"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::Command { action, .. } if action == &Action::GetText
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::Command { action, .. } if action == &Action::Eval
|
||
)
|
||
}));
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::Command { action, .. } if action == &Action::Navigate
|
||
)
|
||
}));
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" &&
|
||
(message == "compat_llm_primary" || message == "compat_skill_runner_primary")
|
||
)
|
||
}));
|
||
std::env::remove_var("SGCLAW_DISABLE_POST_EXPORT_OPEN");
|
||
}
|
||
|
||
#[test]
|
||
fn zhihu_export_does_not_use_frontend_owned_mainline() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
"http://127.0.0.1:9",
|
||
"deepseek-chat",
|
||
Some(real_skill_lib_root().to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "读取热榜前10,并导出 excel 文件".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://www.zhihu.com/hot".to_string(),
|
||
page_title: "知乎热榜".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" && message == "zeroclaw_process_message_primary"
|
||
)
|
||
}));
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" &&
|
||
(message == "compat_llm_primary" || message == "compat_skill_runner_primary")
|
||
)
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn browser_skill_usage_is_execution_not_prompt_only() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
std::env::set_var("SGCLAW_DISABLE_POST_EXPORT_OPEN", "1");
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
"http://127.0.0.1:9",
|
||
"deepseek-chat",
|
||
Some(real_skill_lib_root().to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![
|
||
success_browser_response(1, json!({ "navigated": true })),
|
||
success_browser_response(
|
||
2,
|
||
json!({ "text": "知乎热榜\n1 问题一 344万热度\n2 问题二 266万热度" }),
|
||
),
|
||
success_browser_response(
|
||
3,
|
||
json!({
|
||
"text": {
|
||
"source": "https://www.zhihu.com/hot",
|
||
"sheet_name": "知乎热榜",
|
||
"columns": ["rank", "title", "heat"],
|
||
"rows": [[1, "问题一", "344万"], [2, "问题二", "266万"]]
|
||
}
|
||
}),
|
||
),
|
||
]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "读取知乎热榜前10,并导出 excel 文件".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://www.zhihu.com/hot".to_string(),
|
||
page_title: "知乎热榜".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
let summary = task_complete_summary(&sent);
|
||
let generated = extract_generated_artifact_path(&summary, ".xlsx");
|
||
|
||
assert!(summary.contains(".xlsx"));
|
||
assert!(generated.exists());
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && summary.contains(".xlsx")
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" && message == "zeroclaw_process_message_primary"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message == "call openxml_office"
|
||
)
|
||
}));
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" &&
|
||
(message == "compat_llm_primary" || message == "compat_skill_runner_primary")
|
||
)
|
||
}));
|
||
std::env::remove_var("SGCLAW_DISABLE_POST_EXPORT_OPEN");
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message.starts_with("read_skill ")
|
||
)
|
||
}));
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" &&
|
||
(message == "getText .HotList-item" ||
|
||
message == "getText [data-hot-item]" ||
|
||
message == "getText ol li")
|
||
)
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn handle_browser_message_executes_real_zhihu_navigate_skill_flow() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let first_response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "",
|
||
"tool_calls": [{
|
||
"id": "call_1",
|
||
"type": "function",
|
||
"function": {
|
||
"name": "read_skill",
|
||
"arguments": serde_json::to_string(&json!({
|
||
"name": "zhihu-navigate"
|
||
})).unwrap()
|
||
}
|
||
}]
|
||
}
|
||
}]
|
||
});
|
||
let second_response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "",
|
||
"tool_calls": [{
|
||
"id": "call_2",
|
||
"type": "function",
|
||
"function": {
|
||
"name": "browser_action",
|
||
"arguments": serde_json::to_string(&json!({
|
||
"action": "navigate",
|
||
"expected_domain": "www.zhihu.com",
|
||
"url": "https://www.zhihu.com/hot"
|
||
})).unwrap()
|
||
}
|
||
}]
|
||
}
|
||
}]
|
||
});
|
||
let third_response = json!({
|
||
"choices": [{
|
||
"message": {
|
||
"content": "已打开知乎热榜"
|
||
}
|
||
}]
|
||
});
|
||
let (base_url, requests, server_handle) =
|
||
start_fake_deepseek_server(vec![first_response, second_response, third_response]);
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let skills_dir = real_skill_lib_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
&base_url,
|
||
"deepseek-chat",
|
||
Some(skills_dir.to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![success_browser_response(
|
||
1,
|
||
json!({ "navigated": true }),
|
||
)]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "打开知乎热榜".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://www.zhihu.com/".to_string(),
|
||
page_title: "知乎".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
server_handle.join().unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
let request_bodies = requests.lock().unwrap().clone();
|
||
let tool_content = tool_message_content(&request_bodies[1], "call_1").unwrap();
|
||
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && summary == "已打开知乎热榜"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message == "read_skill zhihu-navigate@0.1.0"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::Command { action, params, .. }
|
||
if action == &Action::Navigate &&
|
||
params["url"].as_str() == Some("https://www.zhihu.com/hot")
|
||
)
|
||
}));
|
||
assert_eq!(request_bodies.len(), 3);
|
||
assert!(tool_content.len() > 100);
|
||
assert!(tool_content.contains("Zhihu page"));
|
||
}
|
||
|
||
#[test]
|
||
fn handle_browser_message_executes_real_zhihu_write_skill_flow() {
|
||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||
|
||
let workspace_root = temp_workspace_root();
|
||
let skills_dir = real_skill_lib_root();
|
||
let config_path = write_deepseek_config_with_skills_dir(
|
||
&workspace_root,
|
||
"deepseek-test-key",
|
||
"http://127.0.0.1:9",
|
||
"deepseek-chat",
|
||
Some(skills_dir.to_str().unwrap()),
|
||
);
|
||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||
|
||
let transport = Arc::new(MockTransport::new(vec![
|
||
success_browser_response(1, json!({ "navigated": true })),
|
||
success_browser_response(
|
||
2,
|
||
json!({
|
||
"text": {
|
||
"status": "creator_entry_clicked",
|
||
"current_url": "https://www.zhihu.com/creator",
|
||
"next_url": "https://zhuanlan.zhihu.com/write"
|
||
}
|
||
}),
|
||
),
|
||
success_browser_response(3, json!({ "navigated": true })),
|
||
success_browser_response(
|
||
4,
|
||
json!({
|
||
"text": {
|
||
"status": "editor_ready",
|
||
"current_url": "https://zhuanlan.zhihu.com/write"
|
||
}
|
||
}),
|
||
),
|
||
success_browser_response(
|
||
5,
|
||
json!({
|
||
"text": {
|
||
"status": "draft_ready",
|
||
"current_url": "https://zhuanlan.zhihu.com/write",
|
||
"title": "测试标题"
|
||
}
|
||
}),
|
||
),
|
||
]));
|
||
let browser_tool = BrowserPipeTool::new(
|
||
transport.clone(),
|
||
zhihu_test_policy(),
|
||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||
)
|
||
.with_response_timeout(Duration::from_secs(1));
|
||
|
||
handle_browser_message_with_context(
|
||
transport.as_ref(),
|
||
&browser_tool,
|
||
&runtime_context,
|
||
BrowserMessage::SubmitTask {
|
||
instruction: "帮我写一篇知乎文章,标题是测试标题,正文是第一段内容".to_string(),
|
||
conversation_id: String::new(),
|
||
messages: vec![],
|
||
page_url: "https://www.zhihu.com/creator".to_string(),
|
||
page_title: "知乎创作中心".to_string(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
|
||
let sent = transport.sent_messages();
|
||
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::TaskComplete { success, summary }
|
||
if *success && summary == "已进入知乎文章编辑器并写入草稿《测试标题》"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" && message == "zeroclaw_process_message_primary"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message == "call zhihu-navigate.open_creator_entry"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message == "call zhihu-write.prepare_article_editor"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "info" && message == "call zhihu-write.fill_article_draft"
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::Command { action, params, .. }
|
||
if action == &Action::Navigate &&
|
||
params["url"].as_str() == Some("https://www.zhihu.com/creator")
|
||
)
|
||
}));
|
||
assert!(sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::Command { action, params, .. }
|
||
if action == &Action::Navigate &&
|
||
params["url"].as_str() == Some("https://zhuanlan.zhihu.com/write")
|
||
)
|
||
}));
|
||
assert!(
|
||
sent.iter()
|
||
.filter(|message| {
|
||
matches!(message, AgentMessage::Command { action, .. } if action == &Action::Eval)
|
||
})
|
||
.count()
|
||
>= 2
|
||
);
|
||
assert!(!sent.iter().any(|message| {
|
||
matches!(
|
||
message,
|
||
AgentMessage::LogEntry { level, message }
|
||
if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary")
|
||
)
|
||
}));
|
||
}
|