feat: add generated scene skill platform hardening

This commit is contained in:
木炎
2026-04-21 23:19:06 +08:00
parent 118fc77935
commit 956f0c2b68
439 changed files with 61974 additions and 3645 deletions

View File

@@ -42,7 +42,10 @@ fn start_fake_deepseek_server(
match listener.accept() {
Ok(pair) => break pair,
Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
assert!(Instant::now() < deadline, "timed out waiting for provider request");
assert!(
Instant::now() < deadline,
"timed out waiting for provider request"
);
thread::sleep(Duration::from_millis(10));
}
Err(err) => panic!("failed to accept provider request: {err}"),
@@ -167,8 +170,12 @@ fn start_callback_host_hotlist_browser_server(
let handle = thread::spawn(move || {
let (stream, _) = listener.accept().unwrap();
stream.set_read_timeout(Some(Duration::from_secs(2))).unwrap();
stream.set_write_timeout(Some(Duration::from_secs(2))).unwrap();
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.unwrap();
stream
.set_write_timeout(Some(Duration::from_secs(2)))
.unwrap();
let mut websocket = accept(stream).unwrap();
let register = match websocket.read().unwrap() {
@@ -188,7 +195,7 @@ fn start_callback_host_hotlist_browser_server(
let first_action = match websocket.read().unwrap() {
Message::Text(text) => serde_json::from_str::<Value>(&text).unwrap(),
other => panic!("expected browser action frame, got {other:?}"),
other => panic!("expected first browser action frame, got {other:?}"),
};
event_tx
.send(CallbackHostBrowserEvent::BrowserFrame(first_action.clone()))
@@ -199,7 +206,9 @@ fn start_callback_host_hotlist_browser_server(
other => panic!("expected second browser action frame, got {other:?}"),
};
event_tx
.send(CallbackHostBrowserEvent::BrowserFrame(second_action.clone()))
.send(CallbackHostBrowserEvent::BrowserFrame(
second_action.clone(),
))
.unwrap();
let Some(close_values) = first_action.as_array() else {
@@ -378,7 +387,315 @@ fn start_callback_host_hotlist_browser_server(
(format!("ws://{address}"), handle)
}
fn start_direct_zhihu_browser_ws_server() -> (String, Arc<Mutex<Vec<String>>>, thread::JoinHandle<()>) {
fn start_callback_host_manifest_scene_browser_server(
event_tx: mpsc::Sender<CallbackHostBrowserEvent>,
) -> (String, thread::JoinHandle<()>) {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let address = listener.local_addr().unwrap();
let handle = thread::spawn(move || {
let (stream, _) = listener.accept().unwrap();
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.unwrap();
stream
.set_write_timeout(Some(Duration::from_secs(2)))
.unwrap();
let mut websocket = accept(stream).unwrap();
let register = match websocket.read().unwrap() {
Message::Text(text) => serde_json::from_str::<Value>(&text).unwrap(),
other => panic!("expected register frame, got {other:?}"),
};
event_tx
.send(CallbackHostBrowserEvent::BrowserFrame(register))
.unwrap();
websocket
.send(Message::Text(
r#"{"type":"welcome","client_id":1,"server_time":"2026-04-04T00:00:00"}"#
.to_string()
.into(),
))
.unwrap();
let first_action = match websocket.read().unwrap() {
Message::Text(text) => serde_json::from_str::<Value>(&text).unwrap(),
other => panic!("expected first browser action frame, got {other:?}"),
};
event_tx
.send(CallbackHostBrowserEvent::BrowserFrame(first_action.clone()))
.unwrap();
let second_action = match websocket.read().unwrap() {
Message::Text(text) => serde_json::from_str::<Value>(&text).unwrap(),
other => panic!("expected second browser action frame, got {other:?}"),
};
event_tx
.send(CallbackHostBrowserEvent::BrowserFrame(
second_action.clone(),
))
.unwrap();
let Some(close_values) = first_action.as_array() else {
websocket.close(None).ok();
return;
};
let is_helper_close = close_values.len() >= 3
&& close_values[1] == json!("sgHideBrowerserClosePage")
&& close_values[2]
.as_str()
.is_some_and(|url| url.ends_with("/sgclaw/browser-helper.html"));
if !is_helper_close {
websocket.close(None).ok();
return;
}
let Some(values) = second_action.as_array() else {
websocket.close(None).ok();
return;
};
let is_helper_open = values.len() >= 3
&& values[1] == json!("sgHideBrowerserOpenPage")
&& values[2]
.as_str()
.is_some_and(|url| url.ends_with("/sgclaw/browser-helper.html"));
if !is_helper_open {
websocket.close(None).ok();
return;
}
let helper_url = values[2].as_str().unwrap().to_string();
let helper_origin = helper_url
.trim_end_matches("/sgclaw/browser-helper.html")
.to_string();
let helper_client = Client::builder()
.timeout(Duration::from_secs(2))
.pool_max_idle_per_host(0)
.build()
.unwrap();
let helper_html = helper_client
.get(&helper_url)
.send()
.unwrap()
.error_for_status()
.unwrap()
.text()
.unwrap();
assert!(helper_html.contains("sgclawReady"));
assert!(helper_html.contains("sgclawOnLoaded"));
assert!(helper_html.contains("sgclawOnGetText"));
assert!(helper_html.contains("sgclawOnEval"));
let pre_ready_command: Value = helper_client
.get(format!("{helper_origin}/sgclaw/callback/commands/next"))
.send()
.unwrap()
.error_for_status()
.unwrap()
.json()
.unwrap();
event_tx
.send(CallbackHostBrowserEvent::CommandEnvelope(pre_ready_command))
.unwrap();
helper_client
.post(format!("{helper_origin}/sgclaw/callback/ready"))
.json(&json!({
"type": "ready",
"helper_url": helper_url,
}))
.send()
.unwrap()
.error_for_status()
.unwrap();
let manifest_payload = json!({
"type": "report-artifact",
"report_name": "manifest-scene-report",
"status": "ok",
"columns": ["ORG_NAME"],
"rows": [{"ORG_NAME": "国网兰州供电公司"}],
"counts": {"rows": 1}
})
.to_string();
let deadline = Instant::now() + Duration::from_secs(10);
let mut saw_eval = false;
while Instant::now() < deadline {
let envelope: Value = match helper_client
.get(format!("{helper_origin}/sgclaw/callback/commands/next"))
.send()
.and_then(|response| response.error_for_status())
.and_then(|response| response.json())
{
Ok(envelope) => envelope,
Err(_) => {
thread::sleep(Duration::from_millis(20));
continue;
}
};
let Some(command) = envelope.get("command").and_then(Value::as_object) else {
thread::sleep(Duration::from_millis(20));
continue;
};
event_tx
.send(CallbackHostBrowserEvent::CommandEnvelope(envelope.clone()))
.unwrap();
let action_name = command
.get("action")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
helper_client
.post(format!("{helper_origin}/sgclaw/callback/commands/ack"))
.json(&json!({ "type": "command_ack" }))
.send()
.unwrap()
.error_for_status()
.unwrap();
let args = command
.get("args")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
match action_name.as_str() {
"sgBrowerserOpenPage" => {
helper_client
.post(format!("{helper_origin}/sgclaw/callback/events"))
.json(&json!({
"callback": "sgclawOnLoaded",
"request_url": helper_url,
"target_url": "https://manifest.example.test/report",
"action": "navigate",
"payload": { "loaded": true }
}))
.send()
.unwrap()
.error_for_status()
.unwrap();
}
"sgBrowserExcuteJsCodeByDomain" => {
let script = args.get(1).and_then(Value::as_str).unwrap_or_default();
assert!(
script.contains("manifest-scene-report"),
"expected manifest-scene eval script, got {script}"
);
saw_eval = true;
helper_client
.post(format!("{helper_origin}/sgclaw/callback/events"))
.json(&json!({
"callback": "sgclawOnEval",
"request_url": helper_url,
"target_url": "https://manifest.example.test/report",
"action": action_name,
"payload": { "value": manifest_payload }
}))
.send()
.unwrap()
.error_for_status()
.unwrap();
break;
}
other => panic!("unexpected callback-host command action {other}"),
}
}
assert!(saw_eval, "expected callback-host eval command");
websocket.close(None).ok();
});
(format!("ws://{address}"), handle)
}
fn temp_manifest_scene_skill_root() -> std::path::PathBuf {
let root = std::env::temp_dir().join(format!(
"sgclaw-service-manifest-scene-skill-root-{}",
uuid::Uuid::new_v4()
));
let skill_dir = root.join("manifest-scene-report");
let script_dir = skill_dir.join("scripts");
std::fs::create_dir_all(&script_dir).unwrap();
std::fs::write(
skill_dir.join("SKILL.toml"),
r#"
[skill]
name = "manifest-scene-report"
description = "Collect manifest scene report data."
version = "0.1.0"
[[tools]]
name = "collect_manifest_scene"
description = "Collect manifest scene report rows."
kind = "browser_script"
command = "scripts/collect_manifest_scene.js"
"#,
)
.unwrap();
std::fs::write(
skill_dir.join("scene.toml"),
r#"
[scene]
id = "manifest-scene-report"
skill = "manifest-scene-report"
tool = "collect_manifest_scene"
kind = "browser_script"
version = "0.1.0"
category = "report_collection"
[manifest]
schema_version = "1"
[bootstrap]
expected_domain = "manifest.example.test"
target_url = "https://manifest.example.test/report"
page_title_keywords = []
requires_target_page = true
[deterministic]
suffix = "。。。"
include_keywords = ["自定义场景报表"]
exclude_keywords = []
[[params]]
name = "period"
resolver = "literal_passthrough"
required = false
prompt_missing = "missing"
prompt_ambiguous = "ambiguous"
[params.resolver_config]
output_field = "period_value"
value = "2026-03"
[artifact]
type = "report-artifact"
success_status = ["ok", "partial", "empty"]
failure_status = ["blocked", "error"]
"#,
)
.unwrap();
std::fs::write(
script_dir.join("collect_manifest_scene.js"),
r#"
return {
type: "report-artifact",
report_name: "manifest-scene-report",
status: "ok",
columns: ["ORG_NAME"],
rows: [{ ORG_NAME: "国网兰州供电公司" }],
counts: { rows: 1 }
};
"#,
)
.unwrap();
root
}
fn start_direct_zhihu_browser_ws_server(
) -> (String, Arc<Mutex<Vec<String>>>, thread::JoinHandle<()>) {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let address = listener.local_addr().unwrap();
let frames = Arc::new(Mutex::new(Vec::new()));
@@ -386,8 +703,12 @@ fn start_direct_zhihu_browser_ws_server() -> (String, Arc<Mutex<Vec<String>>>, t
let handle = thread::spawn(move || {
let (stream, _) = listener.accept().unwrap();
stream.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
stream.set_write_timeout(Some(Duration::from_secs(5))).unwrap();
stream
.set_read_timeout(Some(Duration::from_secs(5)))
.unwrap();
stream
.set_write_timeout(Some(Duration::from_secs(5)))
.unwrap();
let mut socket = accept(stream).unwrap();
let mut action_count = 0_u64;
@@ -414,7 +735,9 @@ fn start_direct_zhihu_browser_ws_server() -> (String, Arc<Mutex<Vec<String>>>, t
continue;
}
let values = parsed.as_array().expect("browser action frame should be an array");
let values = parsed
.as_array()
.expect("browser action frame should be an array");
let request_url = values[0].as_str().expect("request_url should be a string");
let action = values[1].as_str().expect("action should be a string");
action_count += 1;
@@ -430,7 +753,9 @@ fn start_direct_zhihu_browser_ws_server() -> (String, Arc<Mutex<Vec<String>>>, t
let callback_frame = match action {
"sgHideBrowserCallAfterLoaded" => {
let target_url = values[2].as_str().expect("navigate target_url should be a string");
let target_url = values[2]
.as_str()
.expect("navigate target_url should be a string");
json!([
request_url,
"callBackJsToCpp",
@@ -440,7 +765,9 @@ fn start_direct_zhihu_browser_ws_server() -> (String, Arc<Mutex<Vec<String>>>, t
])
}
"sgBrowserExcuteJsCodeByArea" => {
let target_url = values[2].as_str().expect("script target_url should be a string");
let target_url = values[2]
.as_str()
.expect("script target_url should be a string");
let response_text = if action_count == 2 {
"知乎热榜\n1 问题一 344万热度\n2 问题二 266万热度".to_string()
} else {
@@ -496,7 +823,8 @@ fn service_run_requires_llm_config_for_startup() {
#[test]
fn service_startup_config_loads_ws_endpoints_from_browser_config() {
let root = std::env::temp_dir().join(format!("sgclaw-service-startup-{}", uuid::Uuid::new_v4()));
let root =
std::env::temp_dir().join(format!("sgclaw-service-startup-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&root).unwrap();
let config_path = root.join("sgclaw_config.json");
std::fs::write(
@@ -511,13 +839,14 @@ fn service_startup_config_loads_ws_endpoints_from_browser_config() {
)
.unwrap();
let startup = sgclaw::service::load_startup_config(&AgentRuntimeContext::new(
Some(config_path),
root,
))
.unwrap();
let startup =
sgclaw::service::load_startup_config(&AgentRuntimeContext::new(Some(config_path), root))
.unwrap();
assert_eq!(startup.browser_ws_url.as_deref(), Some("ws://127.0.0.1:12345"));
assert_eq!(
startup.browser_ws_url.as_deref(),
Some("ws://127.0.0.1:12345")
);
assert_eq!(
startup.service_ws_listen_addr.as_deref(),
Some("127.0.0.1:42321")
@@ -526,7 +855,8 @@ fn service_startup_config_loads_ws_endpoints_from_browser_config() {
#[test]
fn service_startup_config_uses_default_ws_endpoints_when_not_configured() {
let root = std::env::temp_dir().join(format!("sgclaw-service-defaults-{}", uuid::Uuid::new_v4()));
let root =
std::env::temp_dir().join(format!("sgclaw-service-defaults-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&root).unwrap();
let config_path = root.join("sgclaw_config.json");
std::fs::write(
@@ -539,11 +869,9 @@ fn service_startup_config_uses_default_ws_endpoints_when_not_configured() {
)
.unwrap();
let startup = sgclaw::service::load_startup_config(&AgentRuntimeContext::new(
Some(config_path),
root,
))
.unwrap();
let startup =
sgclaw::service::load_startup_config(&AgentRuntimeContext::new(Some(config_path), root))
.unwrap();
assert_eq!(
startup.browser_ws_url.as_deref(),
@@ -695,8 +1023,14 @@ fn service_binary_keeps_service_ws_listener_available_for_client_connections() {
})
.unwrap_or_default();
assert!(connected, "service ws listener never became available; stderr={stderr}");
assert!(status.is_none(), "sg_claw exited before client could connect; stderr={stderr}");
assert!(
connected,
"service ws listener never became available; stderr={stderr}"
);
assert!(
status.is_none(),
"sg_claw exited before client could connect; stderr={stderr}"
);
}
#[test]
@@ -704,7 +1038,10 @@ fn service_binary_survives_real_client_disconnect_after_task_complete() {
let service_listener = TcpListener::bind("127.0.0.1:0").unwrap();
let service_addr = service_listener.local_addr().unwrap();
drop(service_listener);
let root = std::env::temp_dir().join(format!("sgclaw-service-disconnect-{}", uuid::Uuid::new_v4()));
let root = std::env::temp_dir().join(format!(
"sgclaw-service-disconnect-{}",
uuid::Uuid::new_v4()
));
std::fs::create_dir_all(&root).unwrap();
let config_path = root.join("sgclaw_config.json");
std::fs::write(
@@ -754,7 +1091,10 @@ fn service_binary_survives_real_client_disconnect_after_task_complete() {
}
thread::sleep(Duration::from_millis(20));
}
assert!(stderr.contains("sg_claw ready:"), "service did not report readiness; stderr={stderr}");
assert!(
stderr.contains("sg_claw ready:"),
"service did not report readiness; stderr={stderr}"
);
let mut client = std::process::Command::new(
std::env::var("CARGO_BIN_EXE_sg_claw_client").expect("sg_claw_client test binary path"),
@@ -765,7 +1105,12 @@ fn service_binary_survives_real_client_disconnect_after_task_complete() {
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
client.stdin.as_mut().unwrap().write_all("你好\n".as_bytes()).unwrap();
client
.stdin
.as_mut()
.unwrap()
.write_all("你好\n".as_bytes())
.unwrap();
let client_output = client.wait_with_output().unwrap();
assert!(
@@ -821,7 +1166,10 @@ fn service_binary_submit_flow_routes_zhihu_through_callback_host() {
let (event_tx, event_rx) = mpsc::channel();
let (browser_ws_url, browser_server) = start_callback_host_hotlist_browser_server(event_tx);
let root = std::env::temp_dir().join(format!("sgclaw-service-zhihu-submit-{}", uuid::Uuid::new_v4()));
let root = std::env::temp_dir().join(format!(
"sgclaw-service-zhihu-submit-{}",
uuid::Uuid::new_v4()
));
std::fs::create_dir_all(&root).unwrap();
let config_path = root.join("sgclaw_config.json");
std::fs::write(
@@ -872,7 +1220,10 @@ fn service_binary_submit_flow_routes_zhihu_through_callback_host() {
}
thread::sleep(Duration::from_millis(20));
}
assert!(stderr.contains("sg_claw ready:"), "service did not report readiness; stderr={stderr}");
assert!(
stderr.contains("sg_claw ready:"),
"service did not report readiness; stderr={stderr}"
);
let mut client = std::process::Command::new(
std::env::var("CARGO_BIN_EXE_sg_claw_client").expect("sg_claw_client test binary path"),
@@ -956,13 +1307,19 @@ fn service_binary_submit_flow_routes_zhihu_through_callback_host() {
other => panic!("expected open-page command envelope, got {other:?}"),
};
assert_eq!(open_page["command"]["action"], json!("sgBrowerserOpenPage"));
assert_eq!(open_page["command"]["args"][0], json!("https://www.zhihu.com/hot"));
assert_eq!(
open_page["command"]["args"][0],
json!("https://www.zhihu.com/hot")
);
let get_text = match get_text {
CallbackHostBrowserEvent::CommandEnvelope(value) => value,
other => panic!("expected getText command envelope, got {other:?}"),
};
assert_eq!(get_text["command"]["action"], json!("sgBrowserExcuteJsCodeByDomain"));
assert_eq!(
get_text["command"]["action"],
json!("sgBrowserExcuteJsCodeByDomain")
);
assert_eq!(get_text["command"]["args"][0], json!("www.zhihu.com"));
assert!(get_text["command"]["args"][1]
.as_str()
@@ -972,7 +1329,10 @@ fn service_binary_submit_flow_routes_zhihu_through_callback_host() {
CallbackHostBrowserEvent::CommandEnvelope(value) => value,
other => panic!("expected eval command envelope, got {other:?}"),
};
assert_eq!(eval["command"]["action"], json!("sgBrowserExcuteJsCodeByDomain"));
assert_eq!(
eval["command"]["action"],
json!("sgBrowserExcuteJsCodeByDomain")
);
assert_eq!(eval["command"]["args"][0], json!("www.zhihu.com"));
assert!(eval["command"]["args"][1]
.as_str()
@@ -997,6 +1357,224 @@ fn service_binary_submit_flow_routes_zhihu_through_callback_host() {
);
}
#[test]
fn service_binary_submit_flow_routes_configured_manifest_scene_through_callback_host() {
let service_listener = TcpListener::bind("127.0.0.1:0").unwrap();
let service_addr = service_listener.local_addr().unwrap();
drop(service_listener);
let (event_tx, event_rx) = mpsc::channel();
let (browser_ws_url, browser_server) =
start_callback_host_manifest_scene_browser_server(event_tx);
let skills_dir = temp_manifest_scene_skill_root();
let root = std::env::temp_dir().join(format!(
"sgclaw-service-manifest-scene-submit-{}",
uuid::Uuid::new_v4()
));
std::fs::create_dir_all(&root).unwrap();
let config_path = root.join("sgclaw_config.json");
let resources_dir = root.join("resources");
std::fs::create_dir_all(&resources_dir).unwrap();
std::fs::write(
resources_dir.join("rules.json"),
r#"{
"version": "1.0",
"domains": {
"allowed": ["manifest.example.test"]
},
"pipe_actions": {
"allowed": ["click", "type", "navigate", "getText", "eval"],
"blocked": ["executeJsInPage"]
}
}"#,
)
.unwrap();
let skills_dir_json = skills_dir.to_string_lossy().replace("\\", "/");
std::fs::write(
&config_path,
format!(
r#"{{
"apiKey": "sk-runtime",
"baseUrl": "http://127.0.0.1:9",
"model": "deepseek-chat",
"skillsDir": "{skills_dir_json}",
"browserWsUrl": "{browser_ws_url}",
"serviceWsListenAddr": "{service_addr}"
}}"#
),
)
.unwrap();
let mut service = std::process::Command::new(
std::env::var("CARGO_BIN_EXE_sg_claw").expect("sg_claw test binary path"),
)
.current_dir(&root)
.env("SGCLAW_DISABLE_POST_EXPORT_OPEN", "1")
.arg("--config-path")
.arg(&config_path)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let ws_url = format!("ws://{service_addr}");
let ready_deadline = Instant::now() + Duration::from_secs(2);
let mut stderr = String::new();
while Instant::now() < ready_deadline {
if let Some(stream) = service.stderr.as_mut() {
let mut buf = [0_u8; 1024];
match stream.read(&mut buf) {
Ok(0) => {}
Ok(n) => {
stderr.push_str(&String::from_utf8_lossy(&buf[..n]));
if stderr.contains("sg_claw ready:") {
break;
}
}
Err(_) => {}
}
}
if service.try_wait().unwrap().is_some() {
break;
}
thread::sleep(Duration::from_millis(20));
}
assert!(
stderr.contains("sg_claw ready:"),
"service did not report readiness; stderr={stderr}"
);
let mut client = std::process::Command::new(
std::env::var("CARGO_BIN_EXE_sg_claw_client").expect("sg_claw_client test binary path"),
)
.env("SG_CLAW_SERVICE_WS_URL", &ws_url)
.env("SGCLAW_DISABLE_POST_EXPORT_OPEN", "1")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
client
.stdin
.as_mut()
.unwrap()
.write_all("请执行自定义场景报表。。。\n".as_bytes())
.unwrap();
let client_output = client.wait_with_output().unwrap();
let browser_server_result = browser_server.join();
let register_result = event_rx.recv_timeout(Duration::from_secs(2));
let bootstrap_close_result = event_rx.recv_timeout(Duration::from_secs(2));
let bootstrap_result = event_rx.recv_timeout(Duration::from_secs(2));
let pre_ready_result = event_rx.recv_timeout(Duration::from_secs(2));
let first_command_result = event_rx.recv_timeout(Duration::from_secs(4));
let second_command_result = event_rx.recv_timeout(Duration::from_secs(4));
let service_status = service.try_wait().unwrap();
if service_status.is_none() {
service.kill().unwrap();
let _ = service.wait();
}
if let Some(mut stream) = service.stderr.take() {
let mut buf = Vec::new();
let _ = stream.read_to_end(&mut buf);
stderr.push_str(&String::from_utf8_lossy(&buf));
}
let combined_output = format!(
"{}\n{}\n{}",
String::from_utf8_lossy(&client_output.stdout),
String::from_utf8_lossy(&client_output.stderr),
stderr
);
assert!(
browser_server_result.is_ok(),
"manifest callback-host helper panicked; browser_server_result={browser_server_result:?} output={combined_output} register={register_result:?} bootstrap_close={bootstrap_close_result:?} bootstrap={bootstrap_result:?} pre_ready={pre_ready_result:?} first_command={first_command_result:?} second_command={second_command_result:?}"
);
let register = register_result.expect("missing register event");
let bootstrap_close = bootstrap_close_result.expect("missing bootstrap close event");
let bootstrap = bootstrap_result.expect("missing bootstrap open event");
let pre_ready = pre_ready_result.expect("missing pre-ready event");
let first_command = first_command_result.expect("missing first command event");
let register = match register {
CallbackHostBrowserEvent::BrowserFrame(value) => value,
other => panic!("expected register browser frame, got {other:?}"),
};
assert_eq!(register, json!({ "type": "register", "role": "web" }));
let bootstrap_close = match bootstrap_close {
CallbackHostBrowserEvent::BrowserFrame(value) => value,
other => panic!("expected helper close frame, got {other:?}"),
};
assert_eq!(
bootstrap_close[0],
json!("https://manifest.example.test/report")
);
assert_eq!(bootstrap_close[1], json!("sgHideBrowerserClosePage"));
let bootstrap = match bootstrap {
CallbackHostBrowserEvent::BrowserFrame(value) => value,
other => panic!("expected helper bootstrap frame, got {other:?}"),
};
assert_eq!(bootstrap[0], json!("https://manifest.example.test/report"));
assert_eq!(bootstrap[1], json!("sgHideBrowerserOpenPage"));
let pre_ready = match pre_ready {
CallbackHostBrowserEvent::CommandEnvelope(value) => value,
other => panic!("expected pre-ready command envelope, got {other:?}"),
};
assert_eq!(pre_ready, json!({ "ok": false, "command": null }));
let first_command = match first_command {
CallbackHostBrowserEvent::CommandEnvelope(value) => value,
other => panic!("expected first command envelope, got {other:?}"),
};
let eval = if first_command["command"]["action"] == json!("sgBrowerserOpenPage") {
assert_eq!(
first_command["command"]["args"][0],
json!("https://manifest.example.test/report")
);
let second_command =
second_command_result.expect("missing eval command event after open-page");
match second_command {
CallbackHostBrowserEvent::CommandEnvelope(value) => value,
other => panic!("expected eval command envelope, got {other:?}"),
}
} else {
assert!(
second_command_result.is_err(),
"did not expect a second command when the first command was already eval: {second_command_result:?}"
);
first_command
};
assert_eq!(
eval["command"]["action"],
json!("sgBrowserExcuteJsCodeByDomain")
);
assert_eq!(eval["command"]["args"][0], json!("manifest.example.test"));
assert!(eval["command"]["args"][1]
.as_str()
.is_some_and(|script| script.contains("manifest-scene-report")));
assert!(client_output.status.success());
assert!(
!combined_output.contains("compat_llm_primary"),
"manifest scene should not fall through to compat LLM: {combined_output}"
);
assert!(
!combined_output.contains(RUNTIME_DROP_PANIC_TEXT),
"service submit flow still contains runtime-drop panic: {combined_output}"
);
let _ = std::fs::remove_dir_all(skills_dir);
}
#[test]
fn service_binary_submit_flow_uses_callback_host_command_semantics_for_zhihu() {
let service_listener = TcpListener::bind("127.0.0.1:0").unwrap();
@@ -1006,7 +1584,8 @@ fn service_binary_submit_flow_uses_callback_host_command_semantics_for_zhihu() {
let (event_tx, event_rx) = mpsc::channel();
let (browser_ws_url, browser_server) = start_callback_host_hotlist_browser_server(event_tx);
let root = std::env::temp_dir().join(format!("sgclaw-service-session-{}", uuid::Uuid::new_v4()));
let root =
std::env::temp_dir().join(format!("sgclaw-service-session-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&root).unwrap();
let config_path = root.join("sgclaw_config.json");
std::fs::write(
@@ -1057,7 +1636,10 @@ fn service_binary_submit_flow_uses_callback_host_command_semantics_for_zhihu() {
}
thread::sleep(Duration::from_millis(20));
}
assert!(stderr.contains("sg_claw ready:"), "service did not report readiness; stderr={stderr}");
assert!(
stderr.contains("sg_claw ready:"),
"service did not report readiness; stderr={stderr}"
);
let mut client = std::process::Command::new(
std::env::var("CARGO_BIN_EXE_sg_claw_client").expect("sg_claw_client test binary path"),
@@ -1140,13 +1722,19 @@ fn service_binary_submit_flow_uses_callback_host_command_semantics_for_zhihu() {
other => panic!("expected open-page command envelope, got {other:?}"),
};
assert_eq!(open_page["command"]["action"], json!("sgBrowerserOpenPage"));
assert_eq!(open_page["command"]["args"][0], json!("https://www.zhihu.com/hot"));
assert_eq!(
open_page["command"]["args"][0],
json!("https://www.zhihu.com/hot")
);
let get_text = match get_text {
CallbackHostBrowserEvent::CommandEnvelope(value) => value,
other => panic!("expected getText command envelope, got {other:?}"),
};
assert_eq!(get_text["command"]["action"], json!("sgBrowserExcuteJsCodeByDomain"));
assert_eq!(
get_text["command"]["action"],
json!("sgBrowserExcuteJsCodeByDomain")
);
assert_eq!(get_text["command"]["args"][0], json!("www.zhihu.com"));
assert!(get_text["command"]["args"][1]
.as_str()
@@ -1156,7 +1744,10 @@ fn service_binary_submit_flow_uses_callback_host_command_semantics_for_zhihu() {
CallbackHostBrowserEvent::CommandEnvelope(value) => value,
other => panic!("expected eval command envelope, got {other:?}"),
};
assert_eq!(eval["command"]["action"], json!("sgBrowserExcuteJsCodeByDomain"));
assert_eq!(
eval["command"]["action"],
json!("sgBrowserExcuteJsCodeByDomain")
);
assert_eq!(eval["command"]["args"][0], json!("www.zhihu.com"));
assert!(eval["command"]["args"][1]
.as_str()
@@ -1239,7 +1830,8 @@ fn service_binary_accepts_connect_request_without_starting_browser_task() {
let service_addr = service_listener.local_addr().unwrap();
drop(service_listener);
let root = std::env::temp_dir().join(format!("sgclaw-service-connect-{}", uuid::Uuid::new_v4()));
let root =
std::env::temp_dir().join(format!("sgclaw-service-connect-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&root).unwrap();
let config_path = root.join("sgclaw_config.json");
std::fs::write(
@@ -1288,7 +1880,9 @@ fn service_binary_accepts_connect_request_without_starting_browser_task() {
let mut websocket = websocket.expect("service ws listener never became available");
websocket
.send(Message::Text(
serde_json::to_string(&ClientMessage::Connect).unwrap().into(),
serde_json::to_string(&ClientMessage::Connect)
.unwrap()
.into(),
))
.unwrap();
@@ -1340,7 +1934,10 @@ fn service_binary_survives_client_disconnect_during_task_completion_send() {
let service_addr = service_listener.local_addr().unwrap();
drop(service_listener);
let root = std::env::temp_dir().join(format!("sgclaw-service-disconnect-{}", uuid::Uuid::new_v4()));
let root = std::env::temp_dir().join(format!(
"sgclaw-service-disconnect-{}",
uuid::Uuid::new_v4()
));
std::fs::create_dir_all(&root).unwrap();
let config_path = root.join("sgclaw_config.json");
std::fs::write(
@@ -1446,7 +2043,9 @@ fn submit_task_client_message_converts_into_shared_runner_request() {
page_title: "Example".to_string(),
};
let request = message.into_submit_task_request().expect("submit task request");
let request = message
.into_submit_task_request()
.expect("submit task request");
assert_eq!(request.instruction, "continue task");
assert_eq!(request.conversation_id.as_deref(), Some("conv-1"));
@@ -1464,7 +2063,11 @@ fn ping_client_message_does_not_convert_into_submit_task_request() {
#[test]
fn lifecycle_client_messages_do_not_convert_into_submit_task_request() {
for message in [ClientMessage::Connect, ClientMessage::Start, ClientMessage::Stop] {
for message in [
ClientMessage::Connect,
ClientMessage::Start,
ClientMessage::Stop,
] {
assert!(message.into_submit_task_request().is_none());
}
}
@@ -1493,10 +2096,7 @@ fn service_messages_round_trip_with_stable_tags() {
},
r#"{"type":"status_changed","state":"started"}"#,
),
(
ServiceMessage::Pong,
r#"{"type":"pong"}"#,
),
(ServiceMessage::Pong, r#"{"type":"pong"}"#),
];
for (message, raw) in cases {