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, ) -> (Arc, ZeroClawBrowserTool) { 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")); }