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 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 validation_bundle_skills_dir() -> PathBuf { std::env::current_dir() .unwrap() .join("dist/sgclaw_scheduled_monitoring_read_only_validation_bundle_2026-04-22/skills") } 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::(&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::(&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"); 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); let output = run_binary_with_skills_dir_and_config( &trigger_path, &validation_bundle_skills_dir(), &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"); write_json( &trigger_path, &scheduled_trigger_with_runtime_inputs("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": [ { "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, &validation_bundle_skills_dir(), &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 command_center_preview_reflects_automation_semantics() { let workspace = temp_workspace("sgclaw-command-center-preview-automation-semantics"); 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"); write_json( &trigger_path, &scheduled_trigger_with_runtime_inputs("monitor_only"), ); write_runtime_rules(&rules_path); let detect_payload = json!({ "type": "scheduled-monitoring-detect-snapshot", "report_name": "command-center-fee-control-monitor", "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, &validation_bundle_skills_dir(), &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(); let preview_artifact = &record["previewArtifact"]; assert_eq!( preview_artifact["actionPlan"][0]["actionContractRef"], "dispatch_exception_order" ); assert_eq!(preview_artifact["summary"]["queue_transition_count"], 1); assert_eq!( preview_artifact["queueTransitions"][0]["transitionId"], "queue_continue_on_done" ); assert_eq!( preview_artifact["logWritePreview"][0]["logId"], "dispose_log_after_dispatch" ); } #[test] fn command_center_empty_pending_list_does_not_emit_log_write_preview() { let workspace = temp_workspace("sgclaw-command-center-empty-preview-semantics"); 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"); write_json( &trigger_path, &scheduled_trigger_with_runtime_inputs("monitor_only"), ); write_runtime_rules(&rules_path); let detect_payload = json!({ "type": "scheduled-monitoring-detect-snapshot", "report_name": "command-center-fee-control-monitor", "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\"}]", "zhzxFkycSendTime": "2026-04-22 08:00:00" }, "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); let output = run_binary_with_skills_dir_and_config( &trigger_path, &validation_bundle_skills_dir(), &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(); let preview_artifact = &record["previewArtifact"]; assert_eq!(preview_artifact["summary"]["pending_count"], 0); assert_eq!(preview_artifact["summary"]["queue_transition_count"], 1); assert_eq!( preview_artifact["queueTransitions"][0]["transitionId"], "queue_continue_on_empty" ); assert_eq!( preview_artifact["logWritePreview"].as_array().unwrap().len(), 0 ); } #[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"); 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": 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, &validation_bundle_skills_dir(), &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"], 0); 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"], 0); 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); }