Files
skill-lib/tests/compat_browser_tool_test.rs
木炎 6aad2ce48e feat: restore zhihu browser skills
Reconnect the recovered Zhihu skill flows to the live browser runtime and resolve their resources relative to the executable so they work outside the repo root.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 14:29:38 +08:00

251 lines
6.9 KiB
Rust

mod common;
use std::sync::Arc;
use std::time::Duration;
use common::MockTransport;
use serde_json::{json, Value};
use sgclaw::security::MacPolicy;
use sgclaw::{
compat::browser_tool_adapter::ZeroClawBrowserTool,
pipe::{Action, AgentMessage, BrowserMessage, BrowserPipeTool, Timing},
};
use zeroclaw::tools::Tool;
fn test_policy() -> MacPolicy {
MacPolicy::from_json_str(
r#"{
"version": "1.0",
"domains": { "allowed": ["www.baidu.com"] },
"pipe_actions": {
"allowed": ["click", "type", "navigate", "getText"],
"blocked": ["eval", "executeJsInPage"]
}
}"#,
)
.unwrap()
}
fn build_adapter(
messages: Vec<BrowserMessage>,
) -> (Arc<MockTransport>, ZeroClawBrowserTool<MockTransport>) {
let transport = Arc::new(MockTransport::new(messages));
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));
(transport, ZeroClawBrowserTool::new(browser_tool))
}
#[test]
fn zeroclaw_browser_tool_schema_exposes_only_supported_safe_actions() {
let (_, tool) = build_adapter(vec![]);
let schema = tool.parameters_schema();
assert_eq!(tool.name(), "browser_action");
assert_eq!(
schema["properties"]["action"]["enum"],
json!(["click", "type", "navigate", "getText"])
);
assert_eq!(schema["required"], json!(["action", "expected_domain"]));
}
#[tokio::test]
async fn zeroclaw_browser_tool_executes_supported_actions_and_returns_observation_payload() {
let (transport, tool) = build_adapter(vec![
BrowserMessage::Response {
seq: 1,
success: true,
data: json!({ "navigated": true }),
aom_snapshot: vec![],
timing: Timing {
queue_ms: 1,
exec_ms: 11,
},
},
BrowserMessage::Response {
seq: 2,
success: true,
data: json!({ "typed": true }),
aom_snapshot: vec![],
timing: Timing {
queue_ms: 2,
exec_ms: 12,
},
},
BrowserMessage::Response {
seq: 3,
success: true,
data: json!({ "clicked": true }),
aom_snapshot: vec![],
timing: Timing {
queue_ms: 3,
exec_ms: 13,
},
},
BrowserMessage::Response {
seq: 4,
success: true,
data: json!({ "text": "天气" }),
aom_snapshot: vec![json!({
"role": "textbox",
"name": "百度一下"
})],
timing: Timing {
queue_ms: 4,
exec_ms: 14,
},
},
]);
let navigate = tool
.execute(json!({
"action": "navigate",
"expected_domain": "www.baidu.com",
"url": "https://www.baidu.com"
}))
.await
.unwrap();
let type_text = tool
.execute(json!({
"action": "type",
"expected_domain": "www.baidu.com",
"selector": "#kw",
"text": "天气",
"clear_first": true
}))
.await
.unwrap();
let click = tool
.execute(json!({
"action": "click",
"expected_domain": "www.baidu.com",
"selector": "#su"
}))
.await
.unwrap();
let get_text = tool
.execute(json!({
"action": "getText",
"expected_domain": "www.baidu.com",
"selector": "#content_left"
}))
.await
.unwrap();
let navigate_output: Value = serde_json::from_str(&navigate.output).unwrap();
let get_text_output: Value = serde_json::from_str(&get_text.output).unwrap();
let sent = transport.sent_messages();
assert!(navigate.success);
assert!(type_text.success);
assert!(click.success);
assert!(get_text.success);
assert_eq!(navigate_output["data"], json!({ "navigated": true }));
assert_eq!(get_text_output["data"], json!({ "text": "天气" }));
assert_eq!(
get_text_output["aom_snapshot"],
json!([{ "role": "textbox", "name": "百度一下" }])
);
assert_eq!(
get_text_output["timing"],
json!({
"queue_ms": 4,
"exec_ms": 14
})
);
assert!(matches!(
&sent[0],
AgentMessage::Command { seq, action, .. }
if *seq == 1 && action == &Action::Navigate
));
assert!(matches!(
&sent[1],
AgentMessage::Command { seq, action, .. }
if *seq == 2 && action == &Action::Type
));
assert!(matches!(
&sent[2],
AgentMessage::Command { seq, action, .. }
if *seq == 3 && action == &Action::Click
));
assert!(matches!(
&sent[3],
AgentMessage::Command { seq, action, .. }
if *seq == 4 && action == &Action::GetText
));
}
#[tokio::test]
async fn zeroclaw_browser_tool_keeps_domain_validation_in_mac_policy() {
let (transport, tool) = build_adapter(vec![]);
let result = tool
.execute(json!({
"action": "navigate",
"expected_domain": "www.zhihu.com",
"url": "https://www.zhihu.com"
}))
.await
.unwrap();
assert!(!result.success);
assert!(result.output.is_empty());
assert_eq!(transport.sent_messages().len(), 0);
assert!(result
.error
.as_deref()
.unwrap()
.contains("domain is not allowed"));
}
#[tokio::test]
async fn zeroclaw_browser_tool_rejects_missing_required_action_parameters() {
let (transport, tool) = build_adapter(vec![]);
let missing_click_selector = tool
.execute(json!({
"action": "click",
"expected_domain": "www.baidu.com"
}))
.await
.unwrap();
let missing_text_selector = tool
.execute(json!({
"action": "getText",
"expected_domain": "www.baidu.com"
}))
.await
.unwrap();
let missing_navigate_url = tool
.execute(json!({
"action": "navigate",
"expected_domain": "www.baidu.com"
}))
.await
.unwrap();
assert!(!missing_click_selector.success);
assert!(!missing_text_selector.success);
assert!(!missing_navigate_url.success);
assert_eq!(transport.sent_messages().len(), 0);
assert!(missing_click_selector
.error
.as_deref()
.unwrap()
.contains("click requires selector"));
assert!(missing_text_selector
.error
.as_deref()
.unwrap()
.contains("getText requires selector"));
assert!(missing_navigate_url
.error
.as_deref()
.unwrap()
.contains("navigate requires url"));
}