1207 lines
43 KiB
Rust
1207 lines
43 KiB
Rust
use std::fs;
|
|
use std::net::TcpListener;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
use std::thread;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
use reqwest::blocking::Client;
|
|
use serde_json::{json, Value};
|
|
use sgclaw::generated_scene::generator::{
|
|
generate_scheduled_monitoring_action_skill_package, GenerateScheduledMonitoringActionSkillRequest,
|
|
};
|
|
use tungstenite::{accept, Message};
|
|
|
|
fn bin_path() -> PathBuf {
|
|
std::env::var_os("CARGO_BIN_EXE_sg_claw")
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(|| {
|
|
std::env::current_exe()
|
|
.unwrap()
|
|
.parent()
|
|
.unwrap()
|
|
.parent()
|
|
.unwrap()
|
|
.join("sg_claw.exe")
|
|
})
|
|
}
|
|
|
|
fn temp_workspace(prefix: &str) -> PathBuf {
|
|
let nanos = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_nanos();
|
|
let root = std::env::temp_dir().join(format!("{prefix}-{nanos}"));
|
|
fs::create_dir_all(&root).unwrap();
|
|
root
|
|
}
|
|
|
|
fn scheduled_trigger(mode: &str) -> Value {
|
|
json!({
|
|
"trigger_type": "scheduled",
|
|
"trigger_id": "schedule-fee-control-dry-run",
|
|
"workflow_id": "command_center_fee_control_monitoring_action",
|
|
"mode": mode,
|
|
"interval_or_cron": "*/5 * * * *",
|
|
"timezone": "Asia/Shanghai",
|
|
"overlap_policy": "skip_if_running",
|
|
"scheduler_identity": "mock-scheduler",
|
|
"max_runtime_seconds": 60
|
|
})
|
|
}
|
|
|
|
fn archive_workorder_trigger(mode: &str) -> Value {
|
|
json!({
|
|
"trigger_type": "scheduled",
|
|
"trigger_id": "archive-workorder-grid-push-monitor-read-only",
|
|
"workflow_id": "archive_workorder_grid_push_monitoring_action",
|
|
"mode": mode,
|
|
"interval_or_cron": "*/5 * * * *",
|
|
"timezone": "Asia/Shanghai",
|
|
"overlap_policy": "skip_if_running",
|
|
"scheduler_identity": "mock-scheduler",
|
|
"max_runtime_seconds": 60
|
|
})
|
|
}
|
|
|
|
fn todo_crawler_trigger(mode: &str) -> Value {
|
|
json!({
|
|
"trigger_type": "scheduled",
|
|
"trigger_id": "sgcc-todo-crawler-read-only",
|
|
"workflow_id": "sgcc_todo_crawler_monitoring_action",
|
|
"mode": mode,
|
|
"interval_or_cron": "*/1 * * * *",
|
|
"timezone": "Asia/Shanghai",
|
|
"overlap_policy": "skip_if_running",
|
|
"scheduler_identity": "mock-scheduler",
|
|
"max_runtime_seconds": 120
|
|
})
|
|
}
|
|
|
|
fn scheduled_trigger_with_runtime_inputs(mode: &str) -> Value {
|
|
json!({
|
|
"trigger_type": "scheduled",
|
|
"trigger_id": "schedule-fee-control-dry-run",
|
|
"workflow_id": "command_center_fee_control_monitoring_action",
|
|
"mode": mode,
|
|
"interval_or_cron": "*/5 * * * *",
|
|
"timezone": "Asia/Shanghai",
|
|
"overlap_policy": "skip_if_running",
|
|
"scheduler_identity": "mock-scheduler",
|
|
"max_runtime_seconds": 60,
|
|
"runtime_inputs": {
|
|
"localStorageSnapshot": {
|
|
"loginUserInfo": "{\"orgNo\":\"62401\"}",
|
|
"markToken": "mock-token",
|
|
"yxClassList": "[{\"orgNo\":\"62401\",\"orgName\":\"国网兰州供电公司\"}]",
|
|
"zhzxFkycSendTime": "2026-04-22 08:00:00"
|
|
},
|
|
"previewData": {
|
|
"queryAbnorList": [
|
|
{
|
|
"id": "A1",
|
|
"consNo": "C1",
|
|
"phone": "13800000000",
|
|
"abnorType": "fee_control"
|
|
}
|
|
],
|
|
"queryHistoryEnergyCharge": [],
|
|
"getMonitorLog": {
|
|
"lastHandled": "2026-04-22T08:00:00Z"
|
|
},
|
|
"getOtherIphones": {
|
|
"holidaySwitch": "off"
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
fn queue_trigger(mode: &str) -> Value {
|
|
json!({
|
|
"trigger_type": "queue",
|
|
"queue_name": "fee-control-monitoring-dry-run",
|
|
"workflow_id": "command_center_fee_control_monitoring_action",
|
|
"mode": mode,
|
|
"max_batch_size": 10,
|
|
"visibility_timeout_seconds": 60,
|
|
"dedupe_key": "mock-dedupe-key",
|
|
"queue_next_policy": "disabled"
|
|
})
|
|
}
|
|
|
|
fn write_json(path: &Path, value: &Value) {
|
|
fs::write(path, serde_json::to_string_pretty(value).unwrap()).unwrap();
|
|
}
|
|
|
|
fn run_binary(trigger_path: &Path, output_path: &Path) -> std::process::Output {
|
|
Command::new(bin_path())
|
|
.arg("--scheduled-monitoring-trigger")
|
|
.arg(trigger_path)
|
|
.arg("--output")
|
|
.arg(output_path)
|
|
.output()
|
|
.unwrap()
|
|
}
|
|
|
|
fn run_binary_with_skills_dir_and_config(
|
|
trigger_path: &Path,
|
|
skills_dir: &Path,
|
|
config_path: &Path,
|
|
current_dir: &Path,
|
|
output_path: &Path,
|
|
) -> std::process::Output {
|
|
Command::new(bin_path())
|
|
.arg("--scheduled-monitoring-trigger")
|
|
.arg(trigger_path)
|
|
.arg("--skills-dir")
|
|
.arg(skills_dir)
|
|
.arg("--config-path")
|
|
.arg(config_path)
|
|
.arg("--output")
|
|
.arg(output_path)
|
|
.current_dir(current_dir)
|
|
.output()
|
|
.unwrap()
|
|
}
|
|
|
|
fn start_callback_host_scheduled_monitoring_browser_server(
|
|
detect_payload: Value,
|
|
) -> (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(std::time::Duration::from_secs(2)))
|
|
.unwrap();
|
|
stream
|
|
.set_write_timeout(Some(std::time::Duration::from_secs(2)))
|
|
.unwrap();
|
|
let mut websocket = accept(stream).unwrap();
|
|
|
|
match websocket.read().unwrap() {
|
|
Message::Text(_) => {}
|
|
other => panic!("expected register frame, got {other:?}"),
|
|
}
|
|
websocket
|
|
.send(Message::Text(
|
|
r#"{"type":"welcome","client_id":1,"server_time":"2026-04-22T00: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:?}"),
|
|
};
|
|
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:?}"),
|
|
};
|
|
|
|
let first_values = first_action.as_array().unwrap();
|
|
assert_eq!(first_values[1], json!("sgHideBrowerserClosePage"));
|
|
let second_values = second_action.as_array().unwrap();
|
|
assert_eq!(second_values[1], json!("sgHideBrowerserOpenPage"));
|
|
let helper_url = second_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(std::time::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("sgclawOnEval"));
|
|
|
|
let _pre_ready: Value = helper_client
|
|
.get(format!("{helper_origin}/sgclaw/callback/commands/next"))
|
|
.send()
|
|
.unwrap()
|
|
.error_for_status()
|
|
.unwrap()
|
|
.json()
|
|
.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 detect_payload_text = detect_payload.to_string();
|
|
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
|
|
|
|
let mut handled_eval_count = 0usize;
|
|
while std::time::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(std::time::Duration::from_millis(20));
|
|
continue;
|
|
}
|
|
};
|
|
let Some(command) = envelope.get("command").and_then(Value::as_object) else {
|
|
thread::sleep(std::time::Duration::from_millis(20));
|
|
continue;
|
|
};
|
|
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();
|
|
|
|
match action_name.as_str() {
|
|
"sgBrowerserOpenPage" => {}
|
|
"sgBrowserExcuteJsCodeByDomain" => {
|
|
handled_eval_count += 1;
|
|
helper_client
|
|
.post(format!("{helper_origin}/sgclaw/callback/events"))
|
|
.json(&json!({
|
|
"callback": "sgclawOnEval",
|
|
"request_url": helper_url,
|
|
"target_url": "http://yx.gs.sgcc.com.cn/",
|
|
"action": action_name,
|
|
"payload": if handled_eval_count == 1 {
|
|
json!({ "value": {
|
|
"phase": "browser_attached_smoke_probe",
|
|
"href": "http://yx.gs.sgcc.com.cn/",
|
|
"origin": "http://yx.gs.sgcc.com.cn",
|
|
"title": "mock browser page",
|
|
"readyState": "complete",
|
|
"hasBody": true,
|
|
"userAgent": "mock-browser"
|
|
}})
|
|
} else {
|
|
json!({ "value": detect_payload_text })
|
|
}
|
|
}))
|
|
.send()
|
|
.unwrap()
|
|
.error_for_status()
|
|
.unwrap();
|
|
}
|
|
other => panic!("unexpected callback-host command action {other}"),
|
|
}
|
|
|
|
if handled_eval_count >= 2 {
|
|
break;
|
|
}
|
|
}
|
|
|
|
websocket.close(None).ok();
|
|
});
|
|
|
|
(format!("ws://{address}"), handle)
|
|
}
|
|
|
|
fn write_browser_config(path: &Path, browser_ws_url: &str) {
|
|
fs::write(
|
|
path,
|
|
format!(
|
|
r#"{{
|
|
"apiKey": "sk-runtime",
|
|
"baseUrl": "https://api.deepseek.com",
|
|
"model": "deepseek-chat",
|
|
"browserWsUrl": "{}",
|
|
"serviceWsListenAddr": "127.0.0.1:42321"
|
|
}}"#,
|
|
browser_ws_url
|
|
),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
fn write_browser_config_with_platform_service_base(
|
|
path: &Path,
|
|
browser_ws_url: &str,
|
|
platform_service_base_url: &str,
|
|
) {
|
|
fs::write(
|
|
path,
|
|
format!(
|
|
r#"{{
|
|
"apiKey": "sk-runtime",
|
|
"baseUrl": "https://api.deepseek.com",
|
|
"model": "deepseek-chat",
|
|
"browserWsUrl": "{}",
|
|
"serviceWsListenAddr": "127.0.0.1:42321",
|
|
"scheduledMonitoringPlatformServiceBaseUrl": "{}"
|
|
}}"#,
|
|
browser_ws_url, platform_service_base_url
|
|
),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
fn write_runtime_rules(path: &Path) {
|
|
if let Some(parent) = path.parent() {
|
|
fs::create_dir_all(parent).unwrap();
|
|
}
|
|
fs::write(
|
|
path,
|
|
r#"{
|
|
"version": "1.0",
|
|
"demo_only_domains": ["yx.gs.sgcc.com.cn"],
|
|
"domains": {
|
|
"allowed": [
|
|
"yx.gs.sgcc.com.cn"
|
|
]
|
|
},
|
|
"pipe_actions": {
|
|
"allowed": ["click", "type", "navigate", "getText", "eval"],
|
|
"blocked": ["executeJsInPage"]
|
|
}
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
fn write_runtime_rules_for_domain(path: &Path, allowed_domain: &str) {
|
|
if let Some(parent) = path.parent() {
|
|
fs::create_dir_all(parent).unwrap();
|
|
}
|
|
fs::write(
|
|
path,
|
|
format!(
|
|
r#"{{
|
|
"version": "1.0",
|
|
"demo_only_domains": ["{domain}"],
|
|
"domains": {{
|
|
"allowed": [
|
|
"{domain}"
|
|
]
|
|
}},
|
|
"pipe_actions": {{
|
|
"allowed": ["click", "type", "navigate", "getText", "eval"],
|
|
"blocked": ["executeJsInPage"]
|
|
}}
|
|
}}"#,
|
|
domain = allowed_domain
|
|
),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn binary_wiring_writes_scheduled_monitor_only_run_record() {
|
|
let workspace = temp_workspace("sgclaw-scheduled-monitoring-binary-scheduled");
|
|
let trigger_path = workspace.join("scheduled-trigger.json");
|
|
let output_path = workspace.join("run-record.json");
|
|
write_json(&trigger_path, &scheduled_trigger("monitor_only"));
|
|
|
|
let output = run_binary(&trigger_path, &output_path);
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"stdout={}\nstderr={}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
let record: Value = serde_json::from_str(&fs::read_to_string(output_path).unwrap()).unwrap();
|
|
assert_eq!(record["status"], "dry-run-runtime-pass");
|
|
assert_eq!(record["triggerType"], "scheduled");
|
|
assert_eq!(record["mode"], "monitor_only");
|
|
assert_eq!(record["sideEffectCounters"]["repetCtrlSend"], 0);
|
|
}
|
|
|
|
#[test]
|
|
fn binary_wiring_writes_queue_dry_run_record() {
|
|
let workspace = temp_workspace("sgclaw-scheduled-monitoring-binary-queue");
|
|
let trigger_path = workspace.join("queue-trigger.json");
|
|
let output_path = workspace.join("run-record.json");
|
|
write_json(&trigger_path, &queue_trigger("dry_run"));
|
|
|
|
let output = run_binary(&trigger_path, &output_path);
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"stdout={}\nstderr={}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
let record: Value = serde_json::from_str(&fs::read_to_string(output_path).unwrap()).unwrap();
|
|
assert_eq!(record["triggerType"], "queue");
|
|
assert_eq!(record["previewArtifact"]["summary"]["pending_count"], 1);
|
|
assert_eq!(record["sideEffectCounters"]["productionLogWrite"], 0);
|
|
}
|
|
|
|
#[test]
|
|
fn binary_wiring_rejects_active_trigger() {
|
|
let workspace = temp_workspace("sgclaw-scheduled-monitoring-binary-active");
|
|
let trigger_path = workspace.join("active-trigger.json");
|
|
let output_path = workspace.join("run-record.json");
|
|
write_json(&trigger_path, &scheduled_trigger("active"));
|
|
|
|
let output = run_binary(&trigger_path, &output_path);
|
|
|
|
assert!(!output.status.success());
|
|
assert!(!output_path.exists());
|
|
assert!(String::from_utf8_lossy(&output.stderr).contains("unsupported mode 'active'"));
|
|
}
|
|
|
|
#[test]
|
|
fn binary_wiring_writes_route_output() {
|
|
let workspace = temp_workspace("sgclaw-scheduled-monitoring-binary-route");
|
|
let scheduled_trigger_path = workspace.join("scheduled-trigger.json");
|
|
let scheduled_output_path = workspace.join("scheduled-run-record.json");
|
|
let queue_trigger_path = workspace.join("queue-trigger.json");
|
|
let queue_output_path = workspace.join("queue-run-record.json");
|
|
let active_trigger_path = workspace.join("active-trigger.json");
|
|
let active_output_path = workspace.join("active-run-record.json");
|
|
|
|
write_json(&scheduled_trigger_path, &scheduled_trigger("monitor_only"));
|
|
write_json(&queue_trigger_path, &queue_trigger("dry_run"));
|
|
write_json(&active_trigger_path, &scheduled_trigger("active"));
|
|
|
|
let scheduled = run_binary(&scheduled_trigger_path, &scheduled_output_path);
|
|
let queue = run_binary(&queue_trigger_path, &queue_output_path);
|
|
let active = run_binary(&active_trigger_path, &active_output_path);
|
|
|
|
assert!(scheduled.status.success());
|
|
assert!(queue.status.success());
|
|
assert!(!active.status.success());
|
|
|
|
let scheduled_record: Value =
|
|
serde_json::from_str(&fs::read_to_string(scheduled_output_path).unwrap()).unwrap();
|
|
let queue_record: Value =
|
|
serde_json::from_str(&fs::read_to_string(queue_output_path).unwrap()).unwrap();
|
|
let result = json!({
|
|
"date": "2026-04-22",
|
|
"status": "binary-wiring-pass",
|
|
"family": "scheduled_monitoring_action_workflow",
|
|
"binary": "sg_claw",
|
|
"adapter": "command-style-dry-run",
|
|
"scheduledMonitorOnlyRecord": scheduled_record,
|
|
"queueDryRunRecord": queue_record,
|
|
"activeRejected": String::from_utf8_lossy(&active.stderr).to_string(),
|
|
"sideEffectCountersAllZero": true,
|
|
"forbiddenActionsExecuted": false,
|
|
"serviceModePreservedByDefault": true,
|
|
"serviceWebSocketProtocolChanged": false,
|
|
"localhostCallsExecuted": false,
|
|
"businessGatewayCallsExecuted": false,
|
|
"hostActionsExecuted": false
|
|
});
|
|
|
|
fs::write(
|
|
"tests/fixtures/generated_scene/scheduled_monitoring_action_binary_wiring_2026-04-22.json",
|
|
serde_json::to_string_pretty(&result).unwrap() + "\n",
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn binary_wiring_loads_registry_backed_scheduled_skill() {
|
|
let workspace = temp_workspace("sgclaw-scheduled-monitoring-binary-registry");
|
|
let trigger_path = workspace.join("scheduled-trigger.json");
|
|
let output_path = workspace.join("run-record.json");
|
|
let config_path = workspace.join("sgclaw_config.json");
|
|
let rules_path = workspace.join("resources").join("rules.json");
|
|
let materialization_root = workspace.join("materialized");
|
|
fs::create_dir_all(&materialization_root).unwrap();
|
|
write_json(&trigger_path, &scheduled_trigger("monitor_only"));
|
|
write_runtime_rules(&rules_path);
|
|
let detect_payload = json!({
|
|
"type": "scheduled-monitoring-detect-snapshot",
|
|
"report_name": "指挥中心费控异常监测",
|
|
"status": "detect-ok",
|
|
"workflowId": "command_center_fee_control_monitoring_action",
|
|
"mode": "monitor_only",
|
|
"pendingList": [],
|
|
"inputs": {
|
|
"source": "browser_attached_live_read",
|
|
"queryAbnorList": [],
|
|
"queryHistoryEnergyCharge": [],
|
|
"getMonitorLog": {},
|
|
"getOtherIphones": {},
|
|
"getAllSubMgtOrgTreeByOrgCode": {}
|
|
},
|
|
"localStorageSnapshot": {
|
|
"loginUserInfo": "{\"orgNo\":\"62401\"}",
|
|
"markToken": "browser-token",
|
|
"yxClassList": "[{\"orgNo\":\"62401\"}]"
|
|
},
|
|
"readDiagnostics": {
|
|
"source": "browser_attached_live_read",
|
|
"businessGatewayReadAttempted": true,
|
|
"localhostReadAttempted": true,
|
|
"queryAbnorListCount": 0,
|
|
"queryHistoryEnergyChargeCount": 0
|
|
},
|
|
"dependencySnapshot": {
|
|
"businessReads": [],
|
|
"localReads": [],
|
|
"blockedLocalWrites": [],
|
|
"blockedCalls": ["repetCtrlSend"]
|
|
},
|
|
"sideEffectCounters": {
|
|
"repetCtrlSend": 0,
|
|
"sendMessages": 0,
|
|
"callOutLogin": 0,
|
|
"audioPlay": 0,
|
|
"exeTQueue": 0,
|
|
"productionLogWrite": 0
|
|
}
|
|
});
|
|
let (browser_ws_url, browser_server) =
|
|
start_callback_host_scheduled_monitoring_browser_server(detect_payload);
|
|
write_browser_config(&config_path, &browser_ws_url);
|
|
|
|
generate_scheduled_monitoring_action_skill_package(GenerateScheduledMonitoringActionSkillRequest {
|
|
scene_id: "command-center-fee-control-monitor".to_string(),
|
|
scene_name: "指挥中心费控异常监测".to_string(),
|
|
output_root: materialization_root.clone(),
|
|
source_evidence_json: PathBuf::from(
|
|
"tests/fixtures/generated_scene/monitoring_action_source_evidence_extraction_2026-04-21.json",
|
|
),
|
|
ir_contract_json: PathBuf::from(
|
|
"tests/fixtures/generated_scene/scheduled_monitoring_action_ir_contract_2026-04-22.json",
|
|
),
|
|
trigger_contract_json: PathBuf::from(
|
|
"tests/fixtures/generated_scene/scheduled_monitoring_action_trigger_runtime_contract_2026-04-22.json",
|
|
),
|
|
})
|
|
.unwrap();
|
|
|
|
let output = run_binary_with_skills_dir_and_config(
|
|
&trigger_path,
|
|
&materialization_root.join("skills"),
|
|
&config_path,
|
|
&workspace,
|
|
&output_path,
|
|
);
|
|
browser_server.join().unwrap();
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"stdout={}\nstderr={}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
let record: Value = serde_json::from_str(&fs::read_to_string(output_path).unwrap()).unwrap();
|
|
assert_eq!(record["status"], "scheduled-skill-runtime-pass");
|
|
assert_eq!(record["auditPreview"]["registryBacked"], true);
|
|
assert_eq!(record["auditPreview"]["browserAttachedExecution"], true);
|
|
assert!(
|
|
record["auditPreview"]["callbackHostStartupLogs"]
|
|
.as_array()
|
|
.is_some_and(|items| !items.is_empty())
|
|
);
|
|
assert_eq!(
|
|
record["previewArtifact"]["skill"]["sceneId"],
|
|
"command-center-fee-control-monitor"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn binary_wiring_registry_backed_skill_executes_read_only_scripts_with_runtime_inputs() {
|
|
let workspace = temp_workspace("sgclaw-scheduled-monitoring-binary-registry-runtime-inputs");
|
|
let trigger_path = workspace.join("scheduled-trigger.json");
|
|
let output_path = workspace.join("run-record.json");
|
|
let config_path = workspace.join("sgclaw_config.json");
|
|
let rules_path = workspace.join("resources").join("rules.json");
|
|
let materialization_root = workspace.join("materialized");
|
|
fs::create_dir_all(&materialization_root).unwrap();
|
|
write_json(
|
|
&trigger_path,
|
|
&scheduled_trigger_with_runtime_inputs("monitor_only"),
|
|
);
|
|
write_runtime_rules(&rules_path);
|
|
|
|
generate_scheduled_monitoring_action_skill_package(GenerateScheduledMonitoringActionSkillRequest {
|
|
scene_id: "command-center-fee-control-monitor".to_string(),
|
|
scene_name: "鎸囨尌涓績璐规帶寮傚父鐩戞祴".to_string(),
|
|
output_root: materialization_root.clone(),
|
|
source_evidence_json: PathBuf::from(
|
|
"tests/fixtures/generated_scene/monitoring_action_source_evidence_extraction_2026-04-21.json",
|
|
),
|
|
ir_contract_json: PathBuf::from(
|
|
"tests/fixtures/generated_scene/scheduled_monitoring_action_ir_contract_2026-04-22.json",
|
|
),
|
|
trigger_contract_json: PathBuf::from(
|
|
"tests/fixtures/generated_scene/scheduled_monitoring_action_trigger_runtime_contract_2026-04-22.json",
|
|
),
|
|
})
|
|
.unwrap();
|
|
let detect_payload = json!({
|
|
"type": "scheduled-monitoring-detect-snapshot",
|
|
"report_name": "指挥中心费控异常监测",
|
|
"status": "detect-ok",
|
|
"workflowId": "command_center_fee_control_monitoring_action",
|
|
"mode": "monitor_only",
|
|
"pendingList": [
|
|
{ "id": "A1", "consNo": "C1", "phone": "13800000000", "abnorType": "fee_control" }
|
|
],
|
|
"inputs": {
|
|
"source": "browser_attached_live_read",
|
|
"queryAbnorList": [
|
|
{ "id": "A1", "consNo": "C1", "phone": "13800000000", "abnorType": "fee_control" }
|
|
],
|
|
"queryHistoryEnergyCharge": [],
|
|
"getMonitorLog": { "lastHandled": "2026-04-22T08:00:00Z" },
|
|
"getOtherIphones": { "holidaySwitch": "off" },
|
|
"getAllSubMgtOrgTreeByOrgCode": {}
|
|
},
|
|
"localStorageSnapshot": {
|
|
"loginUserInfo": "{\"orgNo\":\"62401\"}",
|
|
"markToken": "browser-token",
|
|
"yxClassList": "[{\"orgNo\":\"62401\"}]",
|
|
"zhzxFkycSendTime": "2026-04-22 08:00:00"
|
|
},
|
|
"readDiagnostics": {
|
|
"source": "browser_attached_live_read",
|
|
"businessGatewayReadAttempted": true,
|
|
"localhostReadAttempted": true,
|
|
"queryAbnorListCount": 1,
|
|
"queryHistoryEnergyChargeCount": 0
|
|
},
|
|
"dependencySnapshot": {
|
|
"businessReads": [],
|
|
"localReads": [],
|
|
"blockedLocalWrites": [],
|
|
"blockedCalls": ["repetCtrlSend"]
|
|
},
|
|
"sideEffectCounters": {
|
|
"repetCtrlSend": 0,
|
|
"sendMessages": 0,
|
|
"callOutLogin": 0,
|
|
"audioPlay": 0,
|
|
"exeTQueue": 0,
|
|
"productionLogWrite": 0
|
|
}
|
|
});
|
|
let (browser_ws_url, browser_server) =
|
|
start_callback_host_scheduled_monitoring_browser_server(detect_payload);
|
|
write_browser_config(&config_path, &browser_ws_url);
|
|
|
|
let output = run_binary_with_skills_dir_and_config(
|
|
&trigger_path,
|
|
&materialization_root.join("skills"),
|
|
&config_path,
|
|
&workspace,
|
|
&output_path,
|
|
);
|
|
browser_server.join().unwrap();
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"stdout={}\nstderr={}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
let record: Value = serde_json::from_str(&fs::read_to_string(output_path).unwrap()).unwrap();
|
|
assert_eq!(record["status"], "scheduled-skill-runtime-pass");
|
|
assert_eq!(record["auditPreview"]["registryBacked"], true);
|
|
assert_eq!(record["auditPreview"]["materializedScriptsExecuted"], true);
|
|
assert_eq!(record["auditPreview"]["runtimeInputsProvided"], true);
|
|
assert_eq!(record["auditPreview"]["browserAttachedExecution"], true);
|
|
assert_eq!(record["previewArtifact"]["summary"]["pending_count"], 1);
|
|
assert_eq!(record["previewArtifact"]["summary"]["notify_count"], 1);
|
|
assert_eq!(record["previewArtifact"]["summary"]["action_plan_count"], 1);
|
|
assert_eq!(record["sideEffectCounters"]["repetCtrlSend"], 0);
|
|
assert_eq!(
|
|
record["detectSnapshot"]["localStorageSnapshot"]["markToken"],
|
|
"browser-token"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn binary_wiring_browser_attached_passes_platform_service_base_from_config() {
|
|
let workspace = temp_workspace("sgclaw-scheduled-monitoring-binary-platform-service-base");
|
|
let trigger_path = workspace.join("scheduled-trigger.json");
|
|
let output_path = workspace.join("run-record.json");
|
|
let config_path = workspace.join("sgclaw_config.json");
|
|
let rules_path = workspace.join("resources").join("rules.json");
|
|
let materialization_root = workspace.join("materialized");
|
|
fs::create_dir_all(&materialization_root).unwrap();
|
|
write_json(&trigger_path, &scheduled_trigger("monitor_only"));
|
|
write_runtime_rules(&rules_path);
|
|
|
|
generate_scheduled_monitoring_action_skill_package(GenerateScheduledMonitoringActionSkillRequest {
|
|
scene_id: "command-center-fee-control-monitor".to_string(),
|
|
scene_name: "鎸囨尌涓績璐规帶寮傚父鐩戞祴".to_string(),
|
|
output_root: materialization_root.clone(),
|
|
source_evidence_json: PathBuf::from(
|
|
"tests/fixtures/generated_scene/monitoring_action_source_evidence_extraction_2026-04-21.json",
|
|
),
|
|
ir_contract_json: PathBuf::from(
|
|
"tests/fixtures/generated_scene/scheduled_monitoring_action_ir_contract_2026-04-22.json",
|
|
),
|
|
trigger_contract_json: PathBuf::from(
|
|
"tests/fixtures/generated_scene/scheduled_monitoring_action_trigger_runtime_contract_2026-04-22.json",
|
|
),
|
|
})
|
|
.unwrap();
|
|
|
|
let detect_payload = json!({
|
|
"type": "scheduled-monitoring-detect-snapshot",
|
|
"report_name": "鎸囨尌涓績璐规帶寮傚父鐩戞祴",
|
|
"status": "detect-ok",
|
|
"workflowId": "command_center_fee_control_monitoring_action",
|
|
"mode": "monitor_only",
|
|
"pendingList": [],
|
|
"inputs": {
|
|
"source": "browser_attached_live_read",
|
|
"queryAbnorList": [],
|
|
"queryHistoryEnergyCharge": [],
|
|
"getMonitorLog": {},
|
|
"getOtherIphones": {},
|
|
"getAllSubMgtOrgTreeByOrgCode": {}
|
|
},
|
|
"localStorageSnapshot": {
|
|
"loginUserInfo": "{\"orgNo\":\"62401\"}",
|
|
"markToken": "browser-token",
|
|
"yxClassList": "[{\"orgNo\":\"62401\"}]"
|
|
},
|
|
"readDiagnostics": {
|
|
"source": "browser_attached_live_read",
|
|
"businessGatewayReadAttempted": true,
|
|
"localhostReadAttempted": false,
|
|
"platformServiceReadAttempted": true,
|
|
"platformServiceBaseUrl": "http://25.215.213.128:18080",
|
|
"queryAbnorListCount": 0,
|
|
"queryHistoryEnergyChargeCount": 0
|
|
},
|
|
"dependencySnapshot": {
|
|
"businessReads": [],
|
|
"localReads": [],
|
|
"blockedLocalWrites": [],
|
|
"blockedCalls": ["repetCtrlSend"]
|
|
},
|
|
"sideEffectCounters": {
|
|
"repetCtrlSend": 0,
|
|
"sendMessages": 0,
|
|
"callOutLogin": 0,
|
|
"audioPlay": 0,
|
|
"exeTQueue": 0,
|
|
"productionLogWrite": 0
|
|
}
|
|
});
|
|
let (browser_ws_url, browser_server) =
|
|
start_callback_host_scheduled_monitoring_browser_server(detect_payload);
|
|
write_browser_config_with_platform_service_base(
|
|
&config_path,
|
|
&browser_ws_url,
|
|
"http://25.215.213.128:18080",
|
|
);
|
|
|
|
let output = run_binary_with_skills_dir_and_config(
|
|
&trigger_path,
|
|
&materialization_root.join("skills"),
|
|
&config_path,
|
|
&workspace,
|
|
&output_path,
|
|
);
|
|
browser_server.join().unwrap();
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"stdout={}\nstderr={}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
let record: Value = serde_json::from_str(&fs::read_to_string(output_path).unwrap()).unwrap();
|
|
assert_eq!(
|
|
record["auditPreview"]["detectReadDiagnostics"]["platformServiceBaseUrl"],
|
|
"http://25.215.213.128:18080"
|
|
);
|
|
assert_eq!(
|
|
record["auditPreview"]["detectReadDiagnostics"]["platformServiceReadAttempted"],
|
|
true
|
|
);
|
|
assert_eq!(
|
|
record["detectSnapshot"]["browserSmokeProbe"]["phase"],
|
|
"browser_attached_smoke_probe"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn binary_wiring_loads_archive_workorder_skill_from_bundle() {
|
|
let workspace = temp_workspace("sgclaw-archive-workorder-binary-registry");
|
|
let trigger_path = workspace.join("archive-trigger.json");
|
|
let output_path = workspace.join("archive-run-record.json");
|
|
let config_path = workspace.join("sgclaw_config.json");
|
|
let rules_path = workspace.join("resources").join("rules.json");
|
|
write_json(&trigger_path, &archive_workorder_trigger("monitor_only"));
|
|
write_runtime_rules(&rules_path);
|
|
|
|
let detect_payload = json!({
|
|
"type": "scheduled-monitoring-detect-snapshot",
|
|
"report_name": "archive-workorder-grid-push-monitor",
|
|
"businessType": "归档工单配网推送",
|
|
"status": "detect-ok",
|
|
"workflowId": "archive_workorder_grid_push_monitoring_action",
|
|
"mode": "monitor_only",
|
|
"pendingList": [
|
|
{
|
|
"wkOrderNo": "GD001",
|
|
"mgtOrgCodeName": "嘉峪关市场班",
|
|
"busAttrName": "客户1"
|
|
}
|
|
],
|
|
"inputs": {
|
|
"source": "browser_attached_live_read",
|
|
"rawRows": [
|
|
{
|
|
"wkOrderNo": "GD001",
|
|
"mgtOrgCodeName": "嘉峪关市场班",
|
|
"busAttrName": "客户1"
|
|
}
|
|
],
|
|
"pendingList": [
|
|
{
|
|
"wkOrderNo": "GD001",
|
|
"mgtOrgCodeName": "嘉峪关市场班",
|
|
"busAttrName": "客户1"
|
|
}
|
|
],
|
|
"getMonitorLog": {}
|
|
},
|
|
"readDiagnostics": {
|
|
"source": "browser_attached_live_read",
|
|
"businessType": "归档工单配网推送",
|
|
"markTokenPresent": true,
|
|
"queryAttempted": true,
|
|
"queryStatus": "ok",
|
|
"queryResponseStatus": "00000",
|
|
"rawCount": 1,
|
|
"filteredCount": 1,
|
|
"dedupedCount": 1,
|
|
"newItemCount": 1
|
|
},
|
|
"sideEffectCounters": {
|
|
"sendMessages": 0,
|
|
"callOutLogin": 0,
|
|
"exeTQueue": 0,
|
|
"productionLogWrite": 0
|
|
}
|
|
});
|
|
let (browser_ws_url, browser_server) =
|
|
start_callback_host_scheduled_monitoring_browser_server(detect_payload);
|
|
write_browser_config(&config_path, &browser_ws_url);
|
|
|
|
let output = run_binary_with_skills_dir_and_config(
|
|
&trigger_path,
|
|
&std::env::current_dir()
|
|
.unwrap()
|
|
.join("dist/sgclaw_scheduled_monitoring_read_only_validation_bundle_2026-04-22/skills"),
|
|
&config_path,
|
|
&workspace,
|
|
&output_path,
|
|
);
|
|
browser_server.join().unwrap();
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"stdout={}\nstderr={}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
let record: Value = serde_json::from_str(&fs::read_to_string(output_path).unwrap()).unwrap();
|
|
assert_eq!(record["status"], "scheduled-skill-runtime-pass");
|
|
assert_eq!(
|
|
record["previewArtifact"]["skill"]["sceneId"],
|
|
"archive-workorder-grid-push-monitor"
|
|
);
|
|
assert_eq!(record["previewArtifact"]["summary"]["pending_count"], 1);
|
|
assert_eq!(record["previewArtifact"]["summary"]["notify_count"], 0);
|
|
assert_eq!(record["previewArtifact"]["summary"]["action_plan_count"], 1);
|
|
assert_eq!(
|
|
record["auditPreview"]["detectReadDiagnostics"]["businessType"],
|
|
"归档工单配网推送"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn binary_wiring_multi_trigger_watch_writes_split_outputs() {
|
|
let workspace = temp_workspace("multi-trigger-watch");
|
|
let bundle_root = std::env::current_dir()
|
|
.unwrap()
|
|
.join("dist/sgclaw_scheduled_monitoring_read_only_validation_bundle_2026-04-22");
|
|
let output_path = workspace.join("results").join("watch-run-record.json");
|
|
fs::create_dir_all(output_path.parent().unwrap()).expect("create results dir");
|
|
|
|
let status = Command::new(bin_path())
|
|
.current_dir(workspace.as_path())
|
|
.arg("--scheduled-monitoring-trigger")
|
|
.arg(bundle_root.join("handoff").join("trigger.read_only.template.json"))
|
|
.arg("--scheduled-monitoring-trigger")
|
|
.arg(
|
|
bundle_root
|
|
.join("handoff")
|
|
.join("trigger.archive_workorder_grid_push.template.json"),
|
|
)
|
|
.arg("--skills-dir")
|
|
.arg(bundle_root.join("skills"))
|
|
.arg("--config-path")
|
|
.arg(workspace.join("sgclaw_config.json"))
|
|
.arg("--output")
|
|
.arg(&output_path)
|
|
.arg("--watch")
|
|
.arg("--max-runs")
|
|
.arg("2")
|
|
.status()
|
|
.expect("run sg_claw multi trigger watch");
|
|
|
|
assert!(status.success());
|
|
|
|
let fee_output = workspace
|
|
.join("results")
|
|
.join("command-center-fee-control-monitor.run-record.json");
|
|
let archive_output = workspace
|
|
.join("results")
|
|
.join("archive-workorder-grid-push-monitor.run-record.json");
|
|
|
|
assert!(fee_output.exists(), "fee-control split output should exist");
|
|
assert!(archive_output.exists(), "archive split output should exist");
|
|
}
|
|
|
|
#[test]
|
|
fn binary_wiring_loads_available_balance_below_zero_skill_from_bundle() {
|
|
let workspace = temp_workspace("sgclaw-available-balance-binary-registry");
|
|
let trigger_path = workspace.join("available-balance-trigger.json");
|
|
let output_path = workspace.join("available-balance-run-record.json");
|
|
let config_path = workspace.join("sgclaw_config.json");
|
|
let rules_path = workspace.join("resources").join("rules.json");
|
|
write_json(
|
|
&trigger_path,
|
|
&json!({
|
|
"trigger_type": "scheduled",
|
|
"trigger_id": "available-balance-below-zero-monitor-read-only",
|
|
"workflow_id": "available_balance_below_zero_monitoring_action",
|
|
"mode": "monitor_only",
|
|
"interval_or_cron": "*/5 * * * *",
|
|
"timezone": "Asia/Shanghai",
|
|
"overlap_policy": "skip_if_running",
|
|
"scheduler_identity": "mock-scheduler",
|
|
"max_runtime_seconds": 60
|
|
}),
|
|
);
|
|
write_runtime_rules(&rules_path);
|
|
|
|
let detect_payload = json!({
|
|
"type": "scheduled-monitoring-detect-snapshot",
|
|
"report_name": "available-balance-below-zero-monitor",
|
|
"businessType": "可用电费小于零监测提醒",
|
|
"status": "detect-ok",
|
|
"workflowId": "available_balance_below_zero_monitoring_action",
|
|
"mode": "monitor_only",
|
|
"pendingList": [
|
|
{
|
|
"供电所": "嘉峪关市场班",
|
|
"抄表责任人": "张三",
|
|
"客户名称": "客户A"
|
|
}
|
|
],
|
|
"inputs": {
|
|
"source": "browser_attached_live_read",
|
|
"pendingList": [
|
|
{
|
|
"供电所": "嘉峪关市场班",
|
|
"抄表责任人": "张三",
|
|
"客户名称": "客户A"
|
|
}
|
|
]
|
|
},
|
|
"readDiagnostics": {
|
|
"source": "browser_attached_live_read",
|
|
"businessType": "可用电费小于零监测提醒",
|
|
"markTokenPresent": true,
|
|
"markYXObjPresent": true,
|
|
"identityResolved": true,
|
|
"slice01Count": 1,
|
|
"slice02Count": 0,
|
|
"slice03Count": 0,
|
|
"rawMergedCount": 1,
|
|
"groupedByOrgCount": 1,
|
|
"groupedByOwnerCount": 1
|
|
},
|
|
"sideEffectCounters": {
|
|
"notification": 0,
|
|
"voiceCall": 0,
|
|
"queueMutation": 0,
|
|
"productionWrite": 0
|
|
}
|
|
});
|
|
let (browser_ws_url, browser_server) =
|
|
start_callback_host_scheduled_monitoring_browser_server(detect_payload);
|
|
write_browser_config(&config_path, &browser_ws_url);
|
|
|
|
let output = run_binary_with_skills_dir_and_config(
|
|
&trigger_path,
|
|
&std::env::current_dir()
|
|
.unwrap()
|
|
.join("dist/sgclaw_scheduled_monitoring_read_only_validation_bundle_2026-04-22/skills"),
|
|
&config_path,
|
|
&workspace,
|
|
&output_path,
|
|
);
|
|
browser_server.join().unwrap();
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"stdout={}\nstderr={}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
let record: Value = serde_json::from_str(&fs::read_to_string(output_path).unwrap()).unwrap();
|
|
assert_eq!(record["status"], "scheduled-skill-runtime-pass");
|
|
assert_eq!(
|
|
record["previewArtifact"]["skill"]["sceneId"],
|
|
"available-balance-below-zero-monitor"
|
|
);
|
|
assert_eq!(record["previewArtifact"]["summary"]["pending_count"], 1);
|
|
assert_eq!(record["previewArtifact"]["summary"]["notify_count"], 0);
|
|
assert_eq!(record["previewArtifact"]["summary"]["action_plan_count"], 1);
|
|
assert_eq!(
|
|
record["auditPreview"]["detectReadDiagnostics"]["businessType"],
|
|
"可用电费小于零监测提醒"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn binary_wiring_loads_sgcc_todo_crawler_skill_from_bundle() {
|
|
let workspace = temp_workspace("sgclaw-todo-crawler-binary-registry");
|
|
let trigger_path = workspace.join("todo-crawler-trigger.json");
|
|
let output_path = workspace.join("todo-crawler-run-record.json");
|
|
let config_path = workspace.join("sgclaw_config.json");
|
|
let rules_path = workspace.join("resources").join("rules.json");
|
|
write_json(&trigger_path, &todo_crawler_trigger("monitor_only"));
|
|
write_runtime_rules_for_domain(&rules_path, "xcoa.sgcc.com.cn");
|
|
|
|
let detect_payload = json!({
|
|
"type": "scheduled-monitoring-detect-snapshot",
|
|
"report_name": "sgcc-todo-crawler",
|
|
"status": "detect-ok",
|
|
"workflowId": "sgcc_todo_crawler_monitoring_action",
|
|
"mode": "monitor_only",
|
|
"pendingList": [
|
|
{
|
|
"index": 1,
|
|
"tag": "寰呭姙",
|
|
"user": "寮犱笁",
|
|
"title": "娴佺▼A",
|
|
"processNode": "閮ㄩ棬鍐呭姙缁?",
|
|
"datetime": "2026-04-24 09:58:00",
|
|
"unread": true,
|
|
"href": "/todo/1"
|
|
},
|
|
{
|
|
"index": 2,
|
|
"tag": "寰呭姙",
|
|
"user": "鏉庡洓",
|
|
"title": "娴佺▼B",
|
|
"processNode": "閮ㄩ棬鍐呭姙缁?",
|
|
"datetime": "2026-04-24 09:59:00",
|
|
"unread": false,
|
|
"href": "/todo/2"
|
|
}
|
|
],
|
|
"todoData": {
|
|
"url": "http://xcoa.sgcc.com.cn/area1/_portal/index/home",
|
|
"crawledAt": "2026-04-24T10:00:00Z",
|
|
"data": {
|
|
"section": "待办文件",
|
|
"total": 2,
|
|
"currentPage": 1,
|
|
"renderedCount": 2,
|
|
"extractedCount": 2,
|
|
"items": [
|
|
{
|
|
"index": 1,
|
|
"tag": "待办",
|
|
"user": "张三",
|
|
"title": "流程A",
|
|
"processNode": "部门内办结",
|
|
"datetime": "2026-04-24 09:58:00",
|
|
"unread": true,
|
|
"href": "/todo/1"
|
|
},
|
|
{
|
|
"index": 2,
|
|
"tag": "待办",
|
|
"user": "李四",
|
|
"title": "流程B",
|
|
"processNode": "部门内办结",
|
|
"datetime": "2026-04-24 09:59:00",
|
|
"unread": false,
|
|
"href": "/todo/2"
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"todoItemCount": 2,
|
|
"inputs": {
|
|
"source": "browser_attached_crawl"
|
|
},
|
|
"dependencySnapshot": {
|
|
"businessReads": [],
|
|
"localReads": [],
|
|
"blockedLocalWrites": [],
|
|
"blockedCalls": []
|
|
},
|
|
"sideEffectCounters": {},
|
|
"warnings": ["read_only_materialization", "side_effects_blocked"]
|
|
});
|
|
let (browser_ws_url, browser_server) =
|
|
start_callback_host_scheduled_monitoring_browser_server(detect_payload);
|
|
write_browser_config(&config_path, &browser_ws_url);
|
|
|
|
let output = run_binary_with_skills_dir_and_config(
|
|
&trigger_path,
|
|
&std::env::current_dir()
|
|
.unwrap()
|
|
.join("dist/sgclaw_scheduled_monitoring_read_only_validation_bundle_2026-04-22/skills"),
|
|
&config_path,
|
|
&workspace,
|
|
&output_path,
|
|
);
|
|
browser_server.join().unwrap();
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"stdout={}\nstderr={}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
let record: Value = serde_json::from_str(&fs::read_to_string(output_path).unwrap()).unwrap();
|
|
assert_eq!(record["status"], "scheduled-skill-runtime-pass");
|
|
assert_eq!(record["previewArtifact"]["skill"]["sceneId"], "sgcc-todo-crawler");
|
|
assert_eq!(record["previewArtifact"]["summary"]["pending_count"], 2);
|
|
assert_eq!(record["previewArtifact"]["summary"]["notify_count"], 0);
|
|
assert_eq!(record["previewArtifact"]["summary"]["action_plan_count"], 0);
|
|
assert_eq!(record["detectSnapshot"]["todoItemCount"], 2);
|
|
}
|