Files
claw/tests/scheduled_monitoring_action_binary_wiring_test.rs

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