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>
This commit is contained in:
357
tests/zhihu_skill_test.rs
Normal file
357
tests/zhihu_skill_test.rs
Normal file
@@ -0,0 +1,357 @@
|
||||
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::{execute, load_flow, ZhihuWriteRequest};
|
||||
|
||||
fn test_policy() -> MacPolicy {
|
||||
MacPolicy::from_json_str(
|
||||
r#"{
|
||||
"version": "1.0",
|
||||
"domains": { "allowed": ["www.zhihu.com", "zhuanlan.zhihu.com"] },
|
||||
"pipe_actions": {
|
||||
"allowed": ["click", "type", "navigate", "getText", "getHtml", "waitForSelector", "scrollTo"],
|
||||
"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_flow_preserves_validated_zhihu_literals() {
|
||||
let flow = load_flow().unwrap();
|
||||
|
||||
assert_eq!(flow.entry_url, "https://www.zhihu.com/creator");
|
||||
assert_eq!(flow.editor_url, "https://zhuanlan.zhihu.com/write");
|
||||
assert_eq!(flow.literals["write_entry_text"], "写文章");
|
||||
assert_eq!(flow.literals["publish_confirm_text"], "确认发布");
|
||||
assert_eq!(
|
||||
flow.literals["title_placeholder"],
|
||||
"请输入标题(最多 100 个字)"
|
||||
);
|
||||
assert_eq!(
|
||||
flow.selectors["creator_write_entry"],
|
||||
"div.css-1q62b6s > div.css-byu4by"
|
||||
);
|
||||
assert_eq!(
|
||||
flow.selectors["publish_confirm_button"],
|
||||
"div[role='dialog'] button.Button--primary.Button--blue"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zhihu_skill_stops_before_publish_when_publish_is_false() {
|
||||
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" }),
|
||||
),
|
||||
response(4, serde_json::json!({ "typed": true })),
|
||||
response(5, serde_json::json!({ "typed": 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,
|
||||
ZhihuWriteRequest {
|
||||
title: "自动发文能力测试".to_string(),
|
||||
body: "第一段\n\n第二段".to_string(),
|
||||
publish: false,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let sent = transport.sent_messages();
|
||||
|
||||
assert_eq!(result.summary, "知乎文章草稿已填充:自动发文能力测试");
|
||||
assert_eq!(sent.len(), 10);
|
||||
assert!(matches!(
|
||||
&sent[5],
|
||||
AgentMessage::Command { seq, action, .. }
|
||||
if *seq == 3 && action == &Action::WaitForSelector
|
||||
));
|
||||
assert!(matches!(
|
||||
&sent[9],
|
||||
AgentMessage::Command { seq, action, .. }
|
||||
if *seq == 5 && action == &Action::Type
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zhihu_skill_publishes_only_after_confirming_dialog_title_and_final_url() {
|
||||
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" }),
|
||||
),
|
||||
response(4, serde_json::json!({ "typed": true })),
|
||||
response(5, serde_json::json!({ "typed": true })),
|
||||
response(6, serde_json::json!({ "scrolled": true })),
|
||||
response(
|
||||
7,
|
||||
serde_json::json!({ "clicked": true, "url": "https://zhuanlan.zhihu.com/write" }),
|
||||
),
|
||||
response(8, serde_json::json!({ "ready": true })),
|
||||
response(
|
||||
9,
|
||||
serde_json::json!({ "clicked": true, "url": "https://zhuanlan.zhihu.com/p/123456" }),
|
||||
),
|
||||
response(
|
||||
10,
|
||||
serde_json::json!({ "ready": true, "url": "https://zhuanlan.zhihu.com/p/123456" }),
|
||||
),
|
||||
response(
|
||||
11,
|
||||
serde_json::json!({ "text": "自动发文能力测试", "url": "https://zhuanlan.zhihu.com/p/123456" }),
|
||||
),
|
||||
]));
|
||||
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,
|
||||
ZhihuWriteRequest {
|
||||
title: "自动发文能力测试".to_string(),
|
||||
body: "第一段\n\n第二段".to_string(),
|
||||
publish: true,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let sent = transport.sent_messages();
|
||||
|
||||
assert_eq!(
|
||||
result.summary,
|
||||
"知乎文章已发布:自动发文能力测试 (https://zhuanlan.zhihu.com/p/123456)"
|
||||
);
|
||||
assert_eq!(
|
||||
result.final_url.as_deref(),
|
||||
Some("https://zhuanlan.zhihu.com/p/123456")
|
||||
);
|
||||
assert!(result.published);
|
||||
assert_eq!(sent.len(), 22);
|
||||
assert!(matches!(
|
||||
&sent[11],
|
||||
AgentMessage::Command { seq, action, .. }
|
||||
if *seq == 6 && action == &Action::ScrollTo
|
||||
));
|
||||
assert!(matches!(
|
||||
&sent[21],
|
||||
AgentMessage::Command { seq, action, .. }
|
||||
if *seq == 11 && action == &Action::GetText
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zhihu_skill_accepts_edit_url_as_published_article_url() {
|
||||
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" }),
|
||||
),
|
||||
response(4, serde_json::json!({ "typed": true })),
|
||||
response(5, serde_json::json!({ "typed": true })),
|
||||
response(6, serde_json::json!({ "scrolled": true })),
|
||||
response(
|
||||
7,
|
||||
serde_json::json!({ "clicked": true, "url": "https://zhuanlan.zhihu.com/write" }),
|
||||
),
|
||||
response(8, serde_json::json!({ "ready": true })),
|
||||
response(
|
||||
9,
|
||||
serde_json::json!({ "clicked": true, "url": "https://zhuanlan.zhihu.com/p/123456/edit" }),
|
||||
),
|
||||
response(
|
||||
10,
|
||||
serde_json::json!({ "ready": true, "url": "https://zhuanlan.zhihu.com/p/123456/edit" }),
|
||||
),
|
||||
response(
|
||||
11,
|
||||
serde_json::json!({ "text": "自动发文能力测试", "url": "https://zhuanlan.zhihu.com/p/123456/edit" }),
|
||||
),
|
||||
]));
|
||||
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,
|
||||
ZhihuWriteRequest {
|
||||
title: "自动发文能力测试".to_string(),
|
||||
body: "第一段\n\n第二段".to_string(),
|
||||
publish: true,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result.final_url.as_deref(),
|
||||
Some("https://zhuanlan.zhihu.com/p/123456")
|
||||
);
|
||||
assert_eq!(
|
||||
result.summary,
|
||||
"知乎文章已发布:自动发文能力测试 (https://zhuanlan.zhihu.com/p/123456)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zhihu_skill_fails_when_publish_confirmation_never_returns_article_url() {
|
||||
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" }),
|
||||
),
|
||||
response(4, serde_json::json!({ "typed": true })),
|
||||
response(5, serde_json::json!({ "typed": true })),
|
||||
response(6, serde_json::json!({ "scrolled": true })),
|
||||
response(
|
||||
7,
|
||||
serde_json::json!({ "clicked": true, "url": "https://zhuanlan.zhihu.com/write" }),
|
||||
),
|
||||
response(8, serde_json::json!({ "ready": true })),
|
||||
response(9, serde_json::json!({ "clicked": true })),
|
||||
response(10, serde_json::json!({ "ready": true })),
|
||||
response(11, serde_json::json!({ "text": "自动发文能力测试" })),
|
||||
]));
|
||||
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,
|
||||
ZhihuWriteRequest {
|
||||
title: "自动发文能力测试".to_string(),
|
||||
body: "第一段\n\n第二段".to_string(),
|
||||
publish: true,
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(err.to_string().contains("did not return article url"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zhihu_skill_fails_when_published_title_does_not_match_request_title() {
|
||||
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" }),
|
||||
),
|
||||
response(4, serde_json::json!({ "typed": true })),
|
||||
response(5, serde_json::json!({ "typed": true })),
|
||||
response(6, serde_json::json!({ "scrolled": true })),
|
||||
response(
|
||||
7,
|
||||
serde_json::json!({ "clicked": true, "url": "https://zhuanlan.zhihu.com/write" }),
|
||||
),
|
||||
response(8, serde_json::json!({ "ready": true })),
|
||||
response(
|
||||
9,
|
||||
serde_json::json!({ "clicked": true, "url": "https://zhuanlan.zhihu.com/p/123456" }),
|
||||
),
|
||||
response(
|
||||
10,
|
||||
serde_json::json!({ "ready": true, "url": "https://zhuanlan.zhihu.com/p/123456" }),
|
||||
),
|
||||
response(
|
||||
11,
|
||||
serde_json::json!({ "text": "别的标题", "url": "https://zhuanlan.zhihu.com/p/123456" }),
|
||||
),
|
||||
]));
|
||||
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,
|
||||
ZhihuWriteRequest {
|
||||
title: "自动发文能力测试".to_string(),
|
||||
body: "第一段\n\n第二段".to_string(),
|
||||
publish: true,
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(err
|
||||
.to_string()
|
||||
.contains("expected text `自动发文能力测试`, got `别的标题`"));
|
||||
}
|
||||
Reference in New Issue
Block a user