mod common; use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; use std::time::{SystemTime, UNIX_EPOCH}; use std::fs; use common::MockTransport; use serde_json::json; use sgclaw::compat::browser_script_skill_tool::BrowserScriptSkillTool; use sgclaw::pipe::{Action, AgentMessage, BrowserMessage, BrowserPipeTool, Timing}; use sgclaw::security::MacPolicy; use zeroclaw::skills::SkillTool; use zeroclaw::tools::Tool; fn test_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() } #[tokio::test] async fn browser_script_skill_tool_executes_packaged_script_via_eval() { let skill_dir = unique_temp_dir("sgclaw-browser-script-skill"); let scripts_dir = skill_dir.join("scripts"); fs::create_dir_all(&scripts_dir).unwrap(); fs::write( scripts_dir.join("extract_hotlist.js"), r#" const topN = Number(args.top_n || 10); return { sheet_name: "知乎热榜", rows: [[1, "标题", `${topN}条`]] }; "#, ) .unwrap(); let transport = Arc::new(MockTransport::new(vec![BrowserMessage::Response { seq: 1, success: true, data: json!({ "text": { "sheet_name": "知乎热榜", "rows": [[1, "标题", "10条"]] } }), aom_snapshot: vec![], timing: Timing { queue_ms: 1, exec_ms: 5, }, }])); 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 mut args = HashMap::new(); args.insert("top_n".to_string(), "How many rows to extract".to_string()); let skill_tool = 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, }; let tool = BrowserScriptSkillTool::new( "zhihu-hotlist", &skill_tool, &skill_dir, browser_tool, ) .unwrap(); let result = tool .execute(json!({ "expected_domain": "https://www.zhihu.com/hot", "top_n": "10" })) .await .unwrap(); let sent = transport.sent_messages(); assert!(result.success); assert_eq!( serde_json::from_str::(&result.output).unwrap(), json!({ "sheet_name": "知乎热榜", "rows": [[1, "标题", "10条"]] }) ); assert!(matches!( &sent[0], AgentMessage::Command { action, params, security, .. } if action == &Action::Eval && security.expected_domain == "www.zhihu.com" && params["script"].as_str().unwrap().contains("const args = {\"top_n\":\"10\"};") && params["script"].as_str().unwrap().contains("return {") )); } 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 }