325 lines
8.9 KiB
Rust
325 lines
8.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, ExecutionSurfaceKind, 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"]));
|
|
}
|
|
|
|
#[test]
|
|
fn zeroclaw_browser_tool_marks_browser_action_as_privileged_surface() {
|
|
let (_, tool) = build_adapter(vec![]);
|
|
let metadata = tool.surface_metadata();
|
|
|
|
assert_eq!(metadata.kind, ExecutionSurfaceKind::PrivilegedBrowserPipe);
|
|
assert!(metadata.privileged);
|
|
assert!(!metadata.defines_runtime_identity);
|
|
assert_eq!(metadata.guard, "mac_policy");
|
|
}
|
|
|
|
#[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_normalizes_expected_domain_before_sending_command() {
|
|
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!({ "clicked": true }),
|
|
aom_snapshot: vec![],
|
|
timing: Timing {
|
|
queue_ms: 2,
|
|
exec_ms: 12,
|
|
},
|
|
},
|
|
]);
|
|
|
|
let navigate = tool
|
|
.execute(json!({
|
|
"action": "navigate",
|
|
"expected_domain": "https://www.baidu.com/s?wd=天气",
|
|
"url": "https://www.baidu.com/s?wd=天气"
|
|
}))
|
|
.await
|
|
.unwrap();
|
|
let click = tool
|
|
.execute(json!({
|
|
"action": "click",
|
|
"expected_domain": "https://www.baidu.com/s?wd=天气",
|
|
"selector": "#su"
|
|
}))
|
|
.await
|
|
.unwrap();
|
|
|
|
let sent = transport.sent_messages();
|
|
assert!(navigate.success);
|
|
assert!(click.success);
|
|
assert!(matches!(
|
|
&sent[0],
|
|
AgentMessage::Command { security, .. }
|
|
if security.expected_domain == "www.baidu.com"
|
|
));
|
|
assert!(matches!(
|
|
&sent[1],
|
|
AgentMessage::Command { security, .. }
|
|
if security.expected_domain == "www.baidu.com"
|
|
));
|
|
}
|
|
|
|
#[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")
|
|
);
|
|
}
|