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>
358 lines
11 KiB
Rust
358 lines
11 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::{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 `别的标题`"));
|
|
}
|