Files
skill-lib/tests/zhihu_navigation_skill_test.rs
木炎 6aad2ce48e feat: restore zhihu browser skills
Reconnect the recovered Zhihu skill flows to the live browser runtime and resolve their resources relative to the executable so they work outside the repo root.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 14:29:38 +08:00

662 lines
20 KiB
Rust

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"));
}