mod common; use std::sync::Arc; use std::time::Duration; use common::MockTransport; use sgclaw::pipe::{Action, AgentMessage, BrowserMessage, BrowserPipeTool, Timing}; use sgclaw::security::MacPolicy; use sgclaw::skill::zhihu_navigation::{execute, load_catalog, ZhihuNavigateRequest}; fn test_policy() -> MacPolicy { MacPolicy::from_json_str( r#"{ "version": "1.0", "domains": { "allowed": ["www.zhihu.com", "zhuanlan.zhihu.com"] }, "pipe_actions": { "allowed": ["click", "navigate", "getText", "waitForSelector"], "blocked": [] } }"#, ) .unwrap() } fn response(seq: u64, data: serde_json::Value) -> BrowserMessage { BrowserMessage::Response { seq, success: true, data, aom_snapshot: vec![], timing: Timing { queue_ms: 1, exec_ms: 10, }, } } #[test] fn load_catalog_preserves_confirmed_content_analysis_route() { let catalog = load_catalog().unwrap(); assert_eq!(catalog.domains["creator"], "www.zhihu.com"); assert_eq!( catalog.routes["content_analysis"].url, "https://www.zhihu.com/creator/analytics/work/all" ); assert_eq!( catalog.targets["content_analysis"].route_ref.as_deref(), Some("content_analysis") ); assert!(catalog.routes["content_analysis"] .aliases .iter() .any(|alias| alias == "知乎内容分析页面")); } #[test] fn load_catalog_includes_top_level_navigation_targets() { let catalog = load_catalog().unwrap(); assert_eq!(catalog.routes["home"].url, "https://www.zhihu.com/"); assert_eq!(catalog.routes["hot_list"].url, "https://www.zhihu.com/hot"); assert_eq!( catalog.routes["column_home"].url, "https://zhuanlan.zhihu.com/" ); assert_eq!( catalog.routes["messages_page"].url, "https://www.zhihu.com/messages" ); assert_eq!( catalog.routes["notifications_page"].url, "https://www.zhihu.com/notifications" ); assert_eq!( catalog.targets["messages_unread_tab"] .component_ref .as_deref(), Some("messages_tab_unread") ); assert_eq!( catalog.targets["notifications_replies_tab"] .component_ref .as_deref(), Some("notifications_tab_replies") ); assert_eq!( catalog.targets["notifications_settings_menu"] .component_ref .as_deref(), Some("notifications_settings_menu") ); assert_eq!( catalog.targets["profile_page"].flow_ref.as_deref(), Some("open_profile_from_avatar_menu") ); assert_eq!( catalog.targets["notifications_menu"].flow_ref.as_deref(), Some("open_notifications_menu") ); assert_eq!( catalog.targets["search_box"].component_ref.as_deref(), Some("top_nav_search") ); assert_eq!( catalog.components["creator_write_button"] .result_domain_ref .as_deref(), Some("editor") ); } #[test] fn load_catalog_includes_expanded_profile_and_settings_flows() { let catalog = load_catalog().unwrap(); assert_eq!( catalog.targets["profile_answers_tab"].flow_ref.as_deref(), Some("open_profile_answers_tab") ); assert_eq!( catalog.targets["profile_followers_tab"].flow_ref.as_deref(), Some("open_profile_followers_tab") ); assert_eq!( catalog.targets["settings_account_menu"].flow_ref.as_deref(), Some("open_account_settings_from_avatar_menu") ); assert_eq!( catalog.targets["settings_privacy_menu"].flow_ref.as_deref(), Some("open_privacy_settings_from_avatar_menu") ); assert_eq!( catalog.targets["settings_security_menu"] .flow_ref .as_deref(), Some("open_security_settings_from_avatar_menu") ); } #[test] fn zhihu_navigation_skill_opens_content_analysis_page() { let transport = Arc::new(MockTransport::new(vec![response( 1, serde_json::json!({ "url": "https://www.zhihu.com/creator/analytics/work/all" }), )])); 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 result = execute( transport.as_ref(), &browser_tool, ZhihuNavigateRequest { page: "content_analysis".to_string(), ensure_loaded: true, }, ) .unwrap(); let sent = transport.sent_messages(); assert_eq!( result.summary, "知乎页面已打开:内容分析 (https://www.zhihu.com/creator/analytics/work/all)" ); assert_eq!(result.page, "content_analysis"); assert_eq!( result.final_url, "https://www.zhihu.com/creator/analytics/work/all" ); assert_eq!(sent.len(), 2); assert!(matches!( &sent[0], AgentMessage::LogEntry { level, message } if level == "info" && message == "navigate https://www.zhihu.com/creator/analytics/work/all" )); assert!(matches!( &sent[1], AgentMessage::Command { seq, action, .. } if *seq == 1 && action == &Action::Navigate )); } #[test] fn zhihu_navigation_skill_clicks_creator_write_button() { let transport = Arc::new(MockTransport::new(vec![ response( 1, serde_json::json!({ "url": "https://www.zhihu.com/creator" }), ), response( 2, serde_json::json!({ "clicked": true, "url": "https://zhuanlan.zhihu.com/write" }), ), response( 3, serde_json::json!({ "ready": true, "url": "https://zhuanlan.zhihu.com/write" }), ), ])); 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 result = execute( transport.as_ref(), &browser_tool, ZhihuNavigateRequest { page: "creator_write_button".to_string(), ensure_loaded: true, }, ) .unwrap(); let sent = transport.sent_messages(); assert_eq!( result.summary, "知乎入口已打开:写文章入口按钮 (https://zhuanlan.zhihu.com/write)" ); assert_eq!(result.page, "creator_write_button"); assert_eq!(result.final_url, "https://zhuanlan.zhihu.com/write"); assert_eq!(sent.len(), 6); assert!(matches!( &sent[0], AgentMessage::LogEntry { level, message } if level == "info" && message == "navigate https://www.zhihu.com/creator" )); assert!(matches!( &sent[1], AgentMessage::Command { seq, action, .. } if *seq == 1 && action == &Action::Navigate )); assert!(matches!( &sent[2], AgentMessage::LogEntry { level, message } if level == "info" && message == "click 写文章入口按钮" )); assert!(matches!( &sent[3], AgentMessage::Command { seq, action, security, .. } if *seq == 2 && action == &Action::Click && security.expected_domain == "www.zhihu.com" )); assert!(matches!( &sent[4], AgentMessage::LogEntry { level, message } if level == "info" && message.contains("wait for textarea") )); assert!(matches!( &sent[5], AgentMessage::Command { seq, action, security, .. } if *seq == 3 && action == &Action::WaitForSelector && security.expected_domain == "zhuanlan.zhihu.com" )); } #[test] fn zhihu_navigation_skill_opens_notifications_menu_flow() { let transport = Arc::new(MockTransport::new(vec![ response(1, serde_json::json!({ "url": "https://www.zhihu.com/" })), response(2, serde_json::json!({ "clicked": true })), ])); 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 result = execute( transport.as_ref(), &browser_tool, ZhihuNavigateRequest { page: "notifications_menu".to_string(), ensure_loaded: true, }, ) .unwrap(); let sent = transport.sent_messages(); assert_eq!(result.summary, "知乎菜单已打开:通知菜单"); assert_eq!(result.page, "notifications_menu"); assert_eq!(result.final_url, "https://www.zhihu.com/"); assert_eq!(sent.len(), 4); assert!(matches!( &sent[0], AgentMessage::LogEntry { level, message } if level == "info" && message == "navigate https://www.zhihu.com/" )); assert!(matches!( &sent[1], AgentMessage::Command { seq, action, .. } if *seq == 1 && action == &Action::Navigate )); assert!(matches!( &sent[2], AgentMessage::LogEntry { level, message } if level == "info" && message == "click 通知菜单" )); assert!(matches!( &sent[3], AgentMessage::Command { seq, action, security, .. } if *seq == 2 && action == &Action::Click && security.expected_domain == "www.zhihu.com" )); } #[test] fn zhihu_navigation_skill_opens_profile_page_from_avatar_menu() { let transport = Arc::new(MockTransport::new(vec![ response(1, serde_json::json!({ "url": "https://www.zhihu.com/" })), response(2, serde_json::json!({ "clicked": true })), response( 3, serde_json::json!({ "clicked": true, "url": "https://www.zhihu.com/people/test-user" }), ), ])); 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 result = execute( transport.as_ref(), &browser_tool, ZhihuNavigateRequest { page: "profile_page".to_string(), ensure_loaded: true, }, ) .unwrap(); let sent = transport.sent_messages(); assert_eq!( result.summary, "知乎导航已完成:个人主页 (https://www.zhihu.com/people/test-user)" ); assert_eq!(result.page, "profile_page"); assert_eq!(result.final_url, "https://www.zhihu.com/people/test-user"); assert_eq!(sent.len(), 6); assert!(matches!( &sent[0], AgentMessage::LogEntry { level, message } if level == "info" && message == "navigate https://www.zhihu.com/" )); assert!(matches!( &sent[1], AgentMessage::Command { seq, action, .. } if *seq == 1 && action == &Action::Navigate )); assert!(matches!( &sent[2], AgentMessage::LogEntry { level, message } if level == "info" && message == "click 头像菜单" )); assert!(matches!( &sent[3], AgentMessage::Command { seq, action, .. } if *seq == 2 && action == &Action::Click )); assert!(matches!( &sent[4], AgentMessage::LogEntry { level, message } if level == "info" && message == "click 个人主页入口" )); assert!(matches!( &sent[5], AgentMessage::Command { seq, action, security, .. } if *seq == 3 && action == &Action::Click && security.expected_domain == "www.zhihu.com" )); } #[test] fn zhihu_navigation_skill_opens_profile_answers_tab_from_avatar_menu() { let transport = Arc::new(MockTransport::new(vec![ response(1, serde_json::json!({ "url": "https://www.zhihu.com/" })), response(2, serde_json::json!({ "clicked": true })), response( 3, serde_json::json!({ "clicked": true, "url": "https://www.zhihu.com/people/test-user" }), ), response( 4, serde_json::json!({ "clicked": true, "url": "https://www.zhihu.com/people/test-user/answers" }), ), ])); 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 result = execute( transport.as_ref(), &browser_tool, ZhihuNavigateRequest { page: "profile_answers_tab".to_string(), ensure_loaded: true, }, ) .unwrap(); let sent = transport.sent_messages(); assert_eq!( result.summary, "知乎导航已完成:回答分栏 (https://www.zhihu.com/people/test-user/answers)" ); assert_eq!(result.page, "profile_answers_tab"); assert_eq!( result.final_url, "https://www.zhihu.com/people/test-user/answers" ); assert_eq!(sent.len(), 8); assert!(matches!( &sent[0], AgentMessage::LogEntry { level, message } if level == "info" && message == "navigate https://www.zhihu.com/" )); assert!(matches!( &sent[1], AgentMessage::Command { seq, action, .. } if *seq == 1 && action == &Action::Navigate )); assert!(matches!( &sent[2], AgentMessage::LogEntry { level, message } if level == "info" && message == "click 头像菜单" )); assert!(matches!( &sent[3], AgentMessage::Command { seq, action, .. } if *seq == 2 && action == &Action::Click )); assert!(matches!( &sent[4], AgentMessage::LogEntry { level, message } if level == "info" && message == "click 个人主页入口" )); assert!(matches!( &sent[5], AgentMessage::Command { seq, action, .. } if *seq == 3 && action == &Action::Click )); assert!(matches!( &sent[6], AgentMessage::LogEntry { level, message } if level == "info" && message == "click 回答分栏" )); assert!(matches!( &sent[7], AgentMessage::Command { seq, action, security, .. } if *seq == 4 && action == &Action::Click && security.expected_domain == "www.zhihu.com" )); } #[test] fn zhihu_navigation_skill_opens_account_settings_from_avatar_menu() { let transport = Arc::new(MockTransport::new(vec![ response(1, serde_json::json!({ "url": "https://www.zhihu.com/" })), response(2, serde_json::json!({ "clicked": true })), response( 3, serde_json::json!({ "clicked": true, "url": "https://www.zhihu.com/settings/account" }), ), ])); 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 result = execute( transport.as_ref(), &browser_tool, ZhihuNavigateRequest { page: "settings_account_menu".to_string(), ensure_loaded: true, }, ) .unwrap(); let sent = transport.sent_messages(); assert_eq!( result.summary, "知乎导航已完成:账号设置菜单 (https://www.zhihu.com/settings/account)" ); assert_eq!(result.page, "settings_account_menu"); assert_eq!(result.final_url, "https://www.zhihu.com/settings/account"); assert_eq!(sent.len(), 6); assert!(matches!( &sent[0], AgentMessage::LogEntry { level, message } if level == "info" && message == "navigate https://www.zhihu.com/" )); assert!(matches!( &sent[1], AgentMessage::Command { seq, action, .. } if *seq == 1 && action == &Action::Navigate )); assert!(matches!( &sent[2], AgentMessage::LogEntry { level, message } if level == "info" && message == "click 头像菜单" )); assert!(matches!( &sent[3], AgentMessage::Command { seq, action, .. } if *seq == 2 && action == &Action::Click )); assert!(matches!( &sent[4], AgentMessage::LogEntry { level, message } if level == "info" && message == "click 账号设置菜单" )); assert!(matches!( &sent[5], AgentMessage::Command { seq, action, security, .. } if *seq == 3 && action == &Action::Click && security.expected_domain == "www.zhihu.com" )); } #[test] fn zhihu_navigation_skill_opens_notifications_replies_tab() { let transport = Arc::new(MockTransport::new(vec![ response( 1, serde_json::json!({ "url": "https://www.zhihu.com/notifications" }), ), response( 2, serde_json::json!({ "clicked": true, "url": "https://www.zhihu.com/notifications/replies" }), ), ])); 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 result = execute( transport.as_ref(), &browser_tool, ZhihuNavigateRequest { page: "notifications_replies_tab".to_string(), ensure_loaded: true, }, ) .unwrap(); let sent = transport.sent_messages(); assert_eq!( result.summary, "知乎入口已打开:回复我的 (https://www.zhihu.com/notifications/replies)" ); assert_eq!(sent.len(), 4); assert!(matches!( &sent[0], AgentMessage::LogEntry { level, message } if level == "info" && message == "navigate https://www.zhihu.com/notifications" )); assert!(matches!( &sent[1], AgentMessage::Command { seq, action, .. } if *seq == 1 && action == &Action::Navigate )); assert!(matches!( &sent[2], AgentMessage::LogEntry { level, message } if level == "info" && message == "click 回复我的" )); assert!(matches!( &sent[3], AgentMessage::Command { seq, action, security, .. } if *seq == 2 && action == &Action::Click && security.expected_domain == "www.zhihu.com" )); } #[test] fn zhihu_navigation_skill_opens_messages_settings_menu() { let transport = Arc::new(MockTransport::new(vec![ response( 1, serde_json::json!({ "url": "https://www.zhihu.com/messages" }), ), response(2, serde_json::json!({ "clicked": true })), ])); 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 result = execute( transport.as_ref(), &browser_tool, ZhihuNavigateRequest { page: "messages_settings_menu".to_string(), ensure_loaded: true, }, ) .unwrap(); let sent = transport.sent_messages(); assert_eq!(result.summary, "知乎菜单已打开:消息设置菜单"); assert_eq!(result.final_url, "https://www.zhihu.com/messages"); assert_eq!(sent.len(), 4); assert!(matches!( &sent[0], AgentMessage::LogEntry { level, message } if level == "info" && message == "navigate https://www.zhihu.com/messages" )); assert!(matches!( &sent[1], AgentMessage::Command { seq, action, .. } if *seq == 1 && action == &Action::Navigate )); assert!(matches!( &sent[2], AgentMessage::LogEntry { level, message } if level == "info" && message == "click 消息设置菜单" )); assert!(matches!( &sent[3], AgentMessage::Command { seq, action, security, .. } if *seq == 2 && action == &Action::Click && security.expected_domain == "www.zhihu.com" )); } #[test] fn zhihu_navigation_skill_rejects_unknown_target() { let transport = Arc::new(MockTransport::new(vec![])); 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 err = execute( transport.as_ref(), &browser_tool, ZhihuNavigateRequest { page: "unknown_target".to_string(), ensure_loaded: true, }, ) .unwrap_err(); assert!(err .to_string() .contains("unknown zhihu target: unknown_target")); }