mod common; use std::collections::HashMap; use std::fs; use std::path::PathBuf; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use common::MockTransport; use sgclaw::browser::{BrowserBackend, PipeBrowserBackend}; use sgclaw::compat::browser_script_skill_tool::build_browser_script_skill_tools; use sgclaw::pipe::{Action, CommandOutput, ExecutionSurfaceKind, ExecutionSurfaceMetadata}; use sgclaw::security::MacPolicy; use zeroclaw::skills::{Skill, SkillTool}; fn backend_policy() -> MacPolicy { MacPolicy::from_json_str( r#"{ "version": "1.0", "domains": { "allowed": ["oa.example.com", "erp.example.com"] }, "pipe_actions": { "allowed": ["click", "type", "navigate", "getText"], "blocked": ["eval", "executeJsInPage"] } }"#, ) .unwrap() } fn eval_policy() -> MacPolicy { MacPolicy::from_json_str( r#"{ "version": "1.0", "domains": { "allowed": ["www.zhihu.com"] }, "pipe_actions": { "allowed": ["click", "type", "navigate", "getText", "eval"], "blocked": [] } }"#, ) .unwrap() } #[test] fn pipe_browser_backend_keeps_privileged_pipe_surface_metadata() { let transport = Arc::new(MockTransport::new(vec![])); let backend = PipeBrowserBackend::new(transport, backend_policy(), vec![1, 2, 3, 4]); let metadata = backend.surface_metadata(); assert_eq!(metadata.kind, ExecutionSurfaceKind::PrivilegedBrowserPipe); assert!(metadata.privileged); assert!(!metadata.defines_runtime_identity); assert_eq!(metadata.guard, "mac_policy"); assert_eq!( metadata.allowed_domains, vec!["oa.example.com", "erp.example.com"] ); assert_eq!( metadata.allowed_actions, vec!["click", "type", "navigate", "getText"] ); } #[test] fn pipe_browser_backend_reports_eval_capability_from_mac_policy() { let transport = Arc::new(MockTransport::new(vec![])); let backend = PipeBrowserBackend::new(transport, eval_policy(), vec![1, 2, 3, 4]); assert!(backend.supports_eval()); } #[test] fn browser_script_tools_are_hidden_when_backend_cannot_eval() { let skill_root = unique_temp_dir("sgclaw-browser-backend-capability"); let scripts_dir = skill_root.join("scripts"); fs::create_dir_all(&scripts_dir).unwrap(); fs::write( scripts_dir.join("extract_hotlist.js"), "return { rows: [[1, '标题', '10万热度']] };", ) .unwrap(); let skills = vec![Skill { name: "zhihu-hotlist".to_string(), description: "Zhihu hotlist helpers".to_string(), version: "1.0.0".to_string(), author: None, tags: vec![], tools: vec![SkillTool { name: "extract_hotlist".to_string(), description: "Extract structured hotlist rows".to_string(), kind: "browser_script".to_string(), command: "scripts/extract_hotlist.js".to_string(), args: HashMap::new(), }], prompts: vec![], location: Some(skill_root.join("skill.json")), }]; let backend: Arc = Arc::new(FakeBrowserBackend::new(false)); let tools = build_browser_script_skill_tools(&skills, backend).unwrap(); assert!(tools.is_empty()); } #[derive(Default)] struct FakeBrowserBackend { supports_eval: bool, } impl FakeBrowserBackend { fn new(supports_eval: bool) -> Self { Self { supports_eval } } } impl BrowserBackend for FakeBrowserBackend { fn invoke( &self, _action: Action, _params: serde_json::Value, _expected_domain: &str, ) -> Result { panic!("invoke should not be called in this capability-gating test") } fn surface_metadata(&self) -> ExecutionSurfaceMetadata { ExecutionSurfaceMetadata::privileged_browser_pipe("fake_backend") } fn supports_eval(&self) -> bool { self.supports_eval } } fn unique_temp_dir(prefix: &str) -> PathBuf { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_nanos(); let path = std::env::temp_dir().join(format!("{prefix}-{nanos}")); fs::create_dir_all(&path).unwrap(); path }