use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; use std::time::{SystemTime, UNIX_EPOCH}; use serde_json::{json, Value}; use sgclaw::generated_scene::generator::{ generate_scheduled_monitoring_action_skill_package, GenerateScheduledMonitoringActionSkillRequest, }; use sgclaw::generated_scene::ir::MonitoringDependencyIr; use sgclaw::generated_scene::scheduled_monitoring_runtime::{ run_scheduled_monitoring_skill_command_adapter, ScheduledMonitoringSkillCommandAdapterRequest, }; 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 write_json(path: &Path, value: &Value) { if let Some(parent) = path.parent() { fs::create_dir_all(parent).unwrap(); } fs::write(path, serde_json::to_string_pretty(value).unwrap()).unwrap(); } fn minimal_trigger_contract() -> Value { json!({ "family": "scheduled_monitoring_action_workflow", "activeModeEnabled": false, "queueProcessModeEnabled": false }) } fn run_node_script(script_path: &Path) -> std::process::Output { Command::new("node").arg(script_path).output().unwrap() } fn write_runtime_rules(path: &Path) { if let Some(parent) = path.parent() { fs::create_dir_all(parent).unwrap(); } fs::write( path, serde_json::to_string_pretty(&json!({ "mac": { "sendMessages": { "enabled": false }, "callOutLogin": { "enabled": false }, "audioPlay": { "enabled": false }, "exeTQueue": { "enabled": false } } })) .unwrap(), ) .unwrap(); } fn normalize_monitoring_dependency_classification( dependency: &MonitoringDependencyIr, ) -> String { dependency.normalized_classification() } #[test] fn localhost_host_runtime_dependency_classification() { let localhost_dependency = MonitoringDependencyIr { name: "getMonitorLog".to_string(), url: "http://localhost:13313/MonitorServices/getMonitorLog".to_string(), classification: "read_state".to_string(), side_effect: false, blocked_by_default: false, }; let remote_platform_dependency = MonitoringDependencyIr { name: "remoteMonitorLog".to_string(), url: "http://25.215.213.128:18080/MonitorServices/getMonitorLog".to_string(), classification: "read_state".to_string(), side_effect: false, blocked_by_default: false, }; assert_eq!( normalize_monitoring_dependency_classification(&localhost_dependency), "host_runtime_local_service" ); assert_eq!( normalize_monitoring_dependency_classification(&remote_platform_dependency), "remote_platform_service" ); } #[test] fn available_balance_materialization_emits_storage_slice_encryption_and_timeout_contracts() { let root = temp_workspace("sgclaw-available-balance-hardening"); let source_evidence = root.join("source-evidence.json"); let contract_path = root.join("available-balance-contract.json"); let trigger_path = root.join("trigger-contract.json"); write_json(&source_evidence, &json!({})); write_json( &contract_path, &json!({ "workflowId": "available_balance_below_zero_monitoring_action", "displayName": "available balance below zero", "defaultMode": "monitor_only", "archetype": "business_page_report_monitor", "runtimeContext": { "runtimeContextUrl": "http://yx.gs.sgcc.com.cn/", "expectedDomain": "yx.gs.sgcc.com.cn", "gatewayDomain": "yxgateway.gs.sgcc.com.cn", "localhostServiceBase": "http://localhost:13313", "browserAttachedRequired": true, "hostBridgeRequired": true, "executionContextMode": "attached_page_direct", "requestClientMode": "isolated_xhr", "encryptionMode": "window_encrypt_old", "attachedPageBrowserActionPolicy": "forbid_secondary_jump", "platformWritePolicy": "skip_when_zero" }, "businessApiDependencies": [ { "name": "load_report", "url": "http://yxgateway.gs.sgcc.com.cn/report/load", "classification": "read_report", "sideEffect": false } ], "storageReads": [ { "key": "markToken", "source": "localStorage", "fallbackOrder": ["sessionStorage"], "required": true, "parseMode": "raw" }, { "key": "markYXObj", "source": "localStorage", "fallbackOrder": ["sessionStorage"], "required": true, "parseMode": "json" }, { "key": "loginUserInfo", "source": "sessionStorage", "fallbackOrder": ["localStorage"], "required": true, "parseMode": "json" } ], "readSlices": [ { "name": "slice_01", "endpointBinding": "load_report", "requestTemplateOverride": { "sliceType": "01" }, "responsePath": "data.tableData", "timeoutMs": 11111, "mergeRole": "concat", "required": true }, { "name": "slice_02", "endpointBinding": "load_report", "requestTemplateOverride": { "sliceType": "02" }, "responsePath": "data.tableData", "timeoutMs": 22222, "mergeRole": "concat", "required": false }, { "name": "slice_03", "endpointBinding": "load_report", "requestTemplateOverride": { "sliceType": "03" }, "responsePath": "data.tableData", "timeoutMs": 33333, "mergeRole": "fail_partial", "required": false } ], "encryptionResolution": { "primaryMethod": "window.encrypt_old", "fallbackMethods": [ "EmssLib.dataEncrypt_CBC_New", "EmssLib.dataEncrypt_PUB" ], "requiredContext": ["business_page"], "hardFail": true }, "timeoutContract": { "perStepTimeoutMs": 11111, "overallDetectTimeoutMs": 45000, "statusOnTimeout": "timeout", "statusOnPartial": "partial" }, "sideEffectPolicy": { "blockedCallSignatures": [ "mac.sendMessages", "mac.callOutLogin" ] } }), ); write_json(&trigger_path, &minimal_trigger_contract()); let skill_root = generate_scheduled_monitoring_action_skill_package( GenerateScheduledMonitoringActionSkillRequest { scene_id: "available-balance-below-zero-monitor".to_string(), scene_name: "available-balance-below-zero-monitor".to_string(), output_root: root.clone(), source_evidence_json: source_evidence, ir_contract_json: contract_path, trigger_contract_json: trigger_path, }, ) .unwrap(); let scene_toml = fs::read_to_string(skill_root.join("scene.toml")).unwrap(); assert!(scene_toml.contains("[[runtime_context.storage_reads]]")); assert!(scene_toml.contains("key = \"markToken\"")); assert!(scene_toml.contains("[[runtime_context.read_slices]]")); assert!(scene_toml.contains("name = \"slice_01\"")); assert!(scene_toml.contains("[runtime_context.encryption_resolution]")); assert!(scene_toml.contains("primary_method = \"window.encrypt_old\"")); assert!(scene_toml.contains("[runtime_context.timeout_contract]")); assert!(scene_toml.contains("overall_detect_timeout_ms = 45000")); let workflow_ir = fs::read_to_string(skill_root.join("references/workflow-ir.json")).unwrap(); assert!(workflow_ir.contains("\"storageReads\"")); assert!(workflow_ir.contains("\"readSlices\"")); assert!(workflow_ir.contains("\"encryptionResolution\"")); assert!(workflow_ir.contains("\"timeoutContract\"")); let detect_script = fs::read_to_string(skill_root.join("scripts/detect.js")).unwrap(); assert!(detect_script.contains("slice_01")); assert!(detect_script.contains("slice_02")); assert!(detect_script.contains("slice_03")); assert!(detect_script.contains("EmssLib.dataEncrypt_CBC_New")); assert!(detect_script.contains("EmssLib.dataEncrypt_PUB")); assert!(detect_script.contains("overallDetectTimeoutMs")); assert!(detect_script.contains("storageReads")); assert!(detect_script.contains("read_slices") || detect_script.contains("readSlices")); assert!(detect_script.contains("mergeRole") || detect_script.contains("merge_role")); } #[test] fn fee_control_materialization_emits_timeout_contract_in_generated_artifacts() { let root = temp_workspace("sgclaw-fee-control-timeout-hardening"); let skill_root = generate_scheduled_monitoring_action_skill_package( GenerateScheduledMonitoringActionSkillRequest { scene_id: "command-center-fee-control-monitor".to_string(), scene_name: "command-center-fee-control-monitor".to_string(), output_root: 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 scene_toml = fs::read_to_string(skill_root.join("scene.toml")).unwrap(); assert!(scene_toml.contains("[runtime_context.timeout_contract]")); let workflow_ir = fs::read_to_string(skill_root.join("references/workflow-ir.json")).unwrap(); assert!(workflow_ir.contains("\"timeoutContract\"")); let detect_script = fs::read_to_string(skill_root.join("scripts/detect.js")).unwrap(); assert!(detect_script.contains("overallDetectTimeoutMs")); assert!(detect_script.contains("statusOnTimeout")); } #[test] fn command_center_workflow_ir_includes_automation_semantics() { let root = temp_workspace("sgclaw-command-center-automation-semantics"); let skill_root = generate_scheduled_monitoring_action_skill_package( GenerateScheduledMonitoringActionSkillRequest { scene_id: "command-center-fee-control-monitor".to_string(), scene_name: "command-center-fee-control-monitor".to_string(), output_root: 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 workflow_ir: Value = serde_json::from_str( &fs::read_to_string(skill_root.join("references/workflow-ir.json")).unwrap(), ) .unwrap(); assert!( workflow_ir.get("actionContracts").is_some(), "workflow-ir.json must include actionContracts" ); assert!( workflow_ir.get("iterationContract").is_some(), "workflow-ir.json must include iterationContract" ); assert!( workflow_ir.get("executionFlow").is_some(), "workflow-ir.json must include executionFlow" ); assert!( workflow_ir.get("resultStateMachines").is_some(), "workflow-ir.json must include resultStateMachines" ); assert!( workflow_ir.get("queueTransitionRules").is_some(), "workflow-ir.json must include queueTransitionRules" ); assert!( workflow_ir.get("logWriteContracts").is_some(), "workflow-ir.json must include logWriteContracts" ); } #[test] fn available_balance_and_archive_materialization_emit_explicit_dependency_classification_in_references() { let example_output = Command::new("cargo") .args([ "run", "--example", "refresh_scheduled_monitoring_reference_metadata", ]) .current_dir(std::env::current_dir().unwrap()) .output() .unwrap(); assert!( example_output.status.success(), "refresh_scheduled_monitoring_skill failed: stdout={}\nstderr={}", String::from_utf8_lossy(&example_output.stdout), String::from_utf8_lossy(&example_output.stderr) ); let skills_root = PathBuf::from( "dist/sgclaw_scheduled_monitoring_read_only_validation_bundle_2026-04-22/skills", ); for scene_id in [ "available-balance-below-zero-monitor", "archive-workorder-grid-push-monitor", ] { let references_root = skills_root.join(scene_id).join("references"); let workflow_ir = fs::read_to_string(references_root.join("workflow-ir.json")).unwrap_or_default(); let platform_dependencies = fs::read_to_string(references_root.join("platform-dependencies.json")) .unwrap_or_default(); let source_evidence = fs::read_to_string(references_root.join("source-evidence.json")).unwrap_or_default(); assert!( workflow_ir.contains("normalizedClassification"), "{scene_id} workflow-ir.json must emit normalizedClassification" ); assert!( workflow_ir.contains("dependencyClassificationSummary"), "{scene_id} workflow-ir.json must emit dependencyClassificationSummary" ); assert!( platform_dependencies.contains("normalizedClassification"), "{scene_id} platform-dependencies.json must emit normalizedClassification" ); assert!( platform_dependencies.contains("dependencyClassificationSummary"), "{scene_id} platform-dependencies.json must emit dependencyClassificationSummary" ); assert!( source_evidence.contains("normalizedClassification"), "{scene_id} source-evidence.json must emit normalizedClassification" ); assert!( source_evidence.contains("dependencyClassificationSummary"), "{scene_id} source-evidence.json must emit dependencyClassificationSummary" ); assert!( workflow_ir.contains("host_runtime_local_service") || platform_dependencies.contains("host_runtime_local_service") || source_evidence.contains("host_runtime_local_service"), "{scene_id} references must include host_runtime_local_service" ); assert!( workflow_ir.contains("business_gateway_service") || platform_dependencies.contains("business_gateway_service") || source_evidence.contains("business_gateway_service"), "{scene_id} references must include business_gateway_service" ); } } #[test] fn fee_control_materialization_emits_explicit_localhost_vs_platform_classification_in_references() { let root = temp_workspace("sgclaw-fee-control-reference-classification"); let skill_root = generate_scheduled_monitoring_action_skill_package( GenerateScheduledMonitoringActionSkillRequest { scene_id: "command-center-fee-control-monitor".to_string(), scene_name: "command-center-fee-control-monitor".to_string(), output_root: 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 workflow_ir = fs::read_to_string(skill_root.join("references/workflow-ir.json")).unwrap(); let platform_dependencies = fs::read_to_string(skill_root.join("references/platform-dependencies.json")).unwrap(); let source_evidence = fs::read_to_string(skill_root.join("references/source-evidence.json")).unwrap(); assert!(workflow_ir.contains("host_runtime_local_service")); assert!(platform_dependencies.contains("host_runtime_local_service")); assert!(source_evidence.contains("host_runtime_local_service")); assert!(workflow_ir.contains("business_gateway_service")); assert!(platform_dependencies.contains("business_gateway_service")); assert!(source_evidence.contains("business_gateway_service")); } #[test] fn archive_workorder_materialization_emits_delta_contract_and_identity_based_dedupe() { let root = temp_workspace("sgclaw-archive-workorder-hardening"); let source_evidence = root.join("source-evidence.json"); let contract_path = root.join("archive-workorder-contract.json"); let trigger_path = root.join("trigger-contract.json"); write_json(&source_evidence, &json!({})); write_json( &contract_path, &json!({ "workflowId": "archive_workorder_grid_push_monitoring_action", "displayName": "archive workorder grid push", "defaultMode": "monitor_only", "archetype": "list_dedupe_monitor", "runtimeContext": { "runtimeContextUrl": "http://yx.gs.sgcc.com.cn/", "expectedDomain": "yx.gs.sgcc.com.cn", "gatewayDomain": "yxgateway.gs.sgcc.com.cn", "localhostServiceBase": "http://localhost:13313", "browserAttachedRequired": true, "hostBridgeRequired": true, "executionContextMode": "attached_page_direct", "requestClientMode": "isolated_xhr", "encryptionMode": "window_encrypt_old", "attachedPageBrowserActionPolicy": "forbid_secondary_jump", "platformWritePolicy": "skip_when_zero" }, "businessApiDependencies": [ { "name": "get_workorders", "url": "http://yxgateway.gs.sgcc.com.cn/workorders", "classification": "read_workorders", "sideEffect": false } ], "storageReads": [ { "key": "markToken", "source": "localStorage", "fallbackOrder": ["sessionStorage"], "required": true, "parseMode": "raw" } ], "deltaState": { "identityFields": ["wkOrderNo", "mgtOrgCodeName"], "stateSidecarPath": "state/archive-workorder-grid-push-monitor.state.json", "comparisonMode": "new_rows_only", "emitPolicy": "new_rows_only" }, "timeoutContract": { "perStepTimeoutMs": 15000, "overallDetectTimeoutMs": 30000, "statusOnTimeout": "timeout", "statusOnPartial": "partial" }, "sideEffectPolicy": { "blockedCallSignatures": [ "mac.sendMessages", "_this.autoTask" ] } }), ); write_json(&trigger_path, &minimal_trigger_contract()); let skill_root = generate_scheduled_monitoring_action_skill_package( GenerateScheduledMonitoringActionSkillRequest { scene_id: "archive-workorder-grid-push-monitor".to_string(), scene_name: "archive-workorder-grid-push-monitor".to_string(), output_root: root.clone(), source_evidence_json: source_evidence, ir_contract_json: contract_path, trigger_contract_json: trigger_path, }, ) .unwrap(); let scene_toml = fs::read_to_string(skill_root.join("scene.toml")).unwrap(); assert!(scene_toml.contains("[runtime_context.output_contract.delta_state]")); assert!(scene_toml.contains("identity_fields = [\"wkOrderNo\", \"mgtOrgCodeName\"]")); assert!(scene_toml.contains("emit_policy = \"new_rows_only\"")); let workflow_ir = fs::read_to_string(skill_root.join("references/workflow-ir.json")).unwrap(); assert!(workflow_ir.contains("\"deltaState\"")); assert!(workflow_ir.contains("\"identityFields\"")); let detect_script = fs::read_to_string(skill_root.join("scripts/detect.js")).unwrap(); assert!(detect_script.contains("wkOrderNo")); assert!(detect_script.contains("mgtOrgCodeName")); assert!(detect_script.contains("new_rows_only")); } #[test] fn business_page_report_detect_executes_all_configured_slices_and_merges_rows() { let root = temp_workspace("sgclaw-business-page-slices"); let source_evidence = root.join("source-evidence.json"); let contract_path = root.join("contract.json"); let trigger_path = root.join("trigger-contract.json"); write_json(&source_evidence, &json!({})); write_json( &contract_path, &json!({ "workflowId": "available_balance_below_zero_monitoring_action", "displayName": "available balance below zero", "defaultMode": "monitor_only", "archetype": "business_page_report_monitor", "runtimeContext": { "runtimeContextUrl": "http://yx.gs.sgcc.com.cn/", "expectedDomain": "yx.gs.sgcc.com.cn", "gatewayDomain": "yxgateway.gs.sgcc.com.cn", "localhostServiceBase": "http://localhost:13313", "browserAttachedRequired": true, "hostBridgeRequired": true, "executionContextMode": "attached_page_direct", "requestClientMode": "isolated_xhr", "encryptionMode": "window_encrypt_old", "attachedPageBrowserActionPolicy": "forbid_secondary_jump", "platformWritePolicy": "skip_when_zero", "storageReads": [ { "key": "markToken", "source": "localStorage", "fallbackOrder": ["sessionStorage"], "required": true, "parseMode": "raw" } ], "readSlices": [ { "name": "slice_01", "endpointBinding": "load_report", "requestTemplateOverride": { "sliceType": "01" }, "responsePath": "data.items", "timeoutMs": 1000, "mergeRole": "concat", "required": true }, { "name": "slice_02", "endpointBinding": "load_report", "requestTemplateOverride": { "sliceType": "02" }, "responsePath": "data.items", "timeoutMs": 1000, "mergeRole": "concat", "required": true }, { "name": "slice_03", "endpointBinding": "load_report", "requestTemplateOverride": { "sliceType": "03" }, "responsePath": "data.items", "timeoutMs": 1000, "mergeRole": "concat", "required": true } ], "encryptionResolution": { "primaryMethod": "deps.encrypt_old", "fallbackMethods": [], "requiredContext": [], "hardFail": true }, "timeoutContract": { "perStepTimeoutMs": 1000, "overallDetectTimeoutMs": 3000, "statusOnTimeout": "timeout", "statusOnPartial": "partial" } }, "businessApiDependencies": [ { "name": "load_report", "url": "http://yxgateway.gs.sgcc.com.cn/report/load", "classification": "read_report", "sideEffect": false } ], "sideEffectPolicy": { "blockedCallSignatures": ["mac.sendMessages"] } }), ); write_json(&trigger_path, &minimal_trigger_contract()); let skill_root = generate_scheduled_monitoring_action_skill_package( GenerateScheduledMonitoringActionSkillRequest { scene_id: "available-balance-below-zero-monitor".to_string(), scene_name: "available-balance-below-zero-monitor".to_string(), output_root: root.clone(), source_evidence_json: source_evidence, ir_contract_json: contract_path, trigger_contract_json: trigger_path, }, ) .unwrap(); let runner_path = root.join("run-detect.js"); fs::write( &runner_path, format!( r#"const detect = require({detect_path:?}); let calls = 0; global.fetch = async (_url, _opts) => {{ calls += 1; const payloads = [ {{ data: {{ items: [{{ id: 'A1' }}] }} }}, {{ data: {{ items: [{{ id: 'A2' }}] }} }}, {{ data: {{ items: [{{ id: 'A3' }}] }} }} ]; return {{ ok: true, status: 200, text: async () => JSON.stringify(payloads[calls - 1] || {{ data: {{ items: [] }} }}), json: async () => (payloads[calls - 1] || {{ data: {{ items: [] }} }}) }}; }}; (async () => {{ const result = await detect.detect({{ mode: 'monitor_only' }}, {{ localStorage: {{ getItem: () => 'mock-token' }}, encrypt_old: (value) => value }}); if (calls !== 3) {{ throw new Error(`expected 3 slice reads, got ${{calls}}`); }} if (!Array.isArray(result.pendingList) || result.pendingList.length !== 3) {{ throw new Error(`expected merged pendingList length 3, got ${{result.pendingList && result.pendingList.length}}`); }} if (!result.readDiagnostics || !Array.isArray(result.readDiagnostics.readStepTraces) || result.readDiagnostics.readStepTraces.length !== 3) {{ throw new Error('expected three readStepTraces entries'); }} }})().catch((err) => {{ console.error(err.stack || String(err)); process.exit(1); }}); "#, detect_path = skill_root.join("scripts").join("detect.js") ), ) .unwrap(); let output = run_node_script(&runner_path); assert!( output.status.success(), "stdout={}\nstderr={}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); } #[test] fn business_page_report_detect_marks_timeout_as_structured_root_cause() { let root = temp_workspace("sgclaw-business-page-timeout-root-cause"); let source_evidence = root.join("source-evidence.json"); let contract_path = root.join("contract.json"); let trigger_path = root.join("trigger-contract.json"); write_json(&source_evidence, &json!({})); write_json( &contract_path, &json!({ "workflowId": "available_balance_timeout_monitoring_action", "displayName": "available balance timeout", "defaultMode": "monitor_only", "archetype": "business_page_report_monitor", "runtimeContext": { "runtimeContextUrl": "http://yx.gs.sgcc.com.cn/", "expectedDomain": "yx.gs.sgcc.com.cn", "gatewayDomain": "yxgateway.gs.sgcc.com.cn", "localhostServiceBase": "http://localhost:13313", "browserAttachedRequired": true, "hostBridgeRequired": true, "executionContextMode": "attached_page_direct", "requestClientMode": "isolated_xhr", "encryptionMode": "window_encrypt_old", "attachedPageBrowserActionPolicy": "forbid_secondary_jump", "platformWritePolicy": "skip_when_zero", "readSlices": [ { "name": "slice_timeout", "endpointBinding": "load_report", "requestTemplateOverride": { "sliceType": "01" }, "responsePath": "data.items", "timeoutMs": 1, "mergeRole": "concat", "required": true } ], "encryptionResolution": { "primaryMethod": "deps.encrypt_old", "fallbackMethods": [], "requiredContext": [], "hardFail": true }, "timeoutContract": { "perStepTimeoutMs": 1, "overallDetectTimeoutMs": 10, "statusOnTimeout": "timeout", "statusOnPartial": "partial" } }, "businessApiDependencies": [ { "name": "load_report", "url": "http://yxgateway.gs.sgcc.com.cn/report/load", "classification": "read_report", "sideEffect": false } ], "sideEffectPolicy": { "blockedCallSignatures": ["mac.sendMessages"] } }), ); write_json(&trigger_path, &minimal_trigger_contract()); let skill_root = generate_scheduled_monitoring_action_skill_package( GenerateScheduledMonitoringActionSkillRequest { scene_id: "available-balance-timeout".to_string(), scene_name: "available-balance-timeout".to_string(), output_root: root.clone(), source_evidence_json: source_evidence, ir_contract_json: contract_path, trigger_contract_json: trigger_path, }, ) .unwrap(); let runner_path = root.join("run-timeout-detect.js"); fs::write( &runner_path, format!( r#"const detect = require({detect_path:?}); global.fetch = async () => {{ await new Promise((resolve) => setTimeout(resolve, 30)); return {{ ok: true, status: 200, json: async () => ({{ data: {{ items: [{{ id: 'late' }}] }} }}), text: async () => JSON.stringify({{ data: {{ items: [{{ id: 'late' }}] }} }}) }}; }}; (async () => {{ const result = await detect.detect({{ mode: 'monitor_only' }}, {{ localStorage: {{ getItem: () => 'mock-token' }}, encrypt_old: (value) => value }}); if (result.status !== 'timeout') {{ throw new Error(`expected timeout status, got ${{result.status}}`); }} if (!Array.isArray(result.readDiagnostics.readStepTraces) || result.readDiagnostics.readStepTraces[0].status !== 'timeout') {{ throw new Error('expected timeout trace'); }} }})().catch((err) => {{ console.error(err.stack || String(err)); process.exit(1); }}); "#, detect_path = skill_root.join("scripts").join("detect.js") ), ) .unwrap(); let output = run_node_script(&runner_path); assert!( output.status.success(), "stdout={}\nstderr={}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); } #[test] fn runtime_preserves_detect_root_cause_when_pending_count_is_zero() { let root = temp_workspace("sgclaw-runtime-root-cause"); let source_evidence = root.join("source-evidence.json"); let contract_path = root.join("contract.json"); let trigger_contract_path = root.join("trigger-contract.json"); let trigger_path = root.join("scheduled-trigger.json"); let output_path = root.join("results").join("available-balance-root-cause.run-record.json"); let rules_path = root.join("resources").join("rules.json"); write_json(&source_evidence, &json!({})); write_json( &contract_path, &json!({ "workflowId": "available_balance_root_cause_monitoring_action", "displayName": "available balance root cause", "defaultMode": "monitor_only", "archetype": "business_page_report_monitor", "runtimeContext": { "runtimeContextUrl": "http://yx.gs.sgcc.com.cn/", "expectedDomain": "yx.gs.sgcc.com.cn", "gatewayDomain": "yxgateway.gs.sgcc.com.cn", "localhostServiceBase": "http://localhost:13313", "browserAttachedRequired": false, "hostBridgeRequired": false, "executionContextMode": "attached_page_direct", "requestClientMode": "isolated_xhr", "encryptionMode": "window_encrypt_old", "attachedPageBrowserActionPolicy": "forbid_secondary_jump", "platformWritePolicy": "skip_when_zero", "readSlices": [ { "name": "slice_timeout", "endpointBinding": "load_report", "requestTemplateOverride": { "sliceType": "01" }, "responsePath": "data.items", "timeoutMs": 1, "mergeRole": "concat", "required": true } ], "encryptionResolution": { "primaryMethod": "deps.encrypt_old", "fallbackMethods": [], "requiredContext": [], "hardFail": true }, "timeoutContract": { "perStepTimeoutMs": 1, "overallDetectTimeoutMs": 10, "statusOnTimeout": "timeout", "statusOnPartial": "partial" } }, "businessApiDependencies": [ { "name": "load_report", "url": "http://127.0.0.1:9/report/load", "classification": "read_report", "sideEffect": false } ], "sideEffectPolicy": { "blockedCallSignatures": ["mac.sendMessages"] } }), ); write_json(&trigger_contract_path, &minimal_trigger_contract()); write_json( &trigger_path, &json!({ "trigger_type": "scheduled", "trigger_id": "available-balance-root-cause-read-only", "workflow_id": "available_balance_root_cause_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 materialization_root = root.join("materialized"); fs::create_dir_all(&materialization_root).unwrap(); generate_scheduled_monitoring_action_skill_package( GenerateScheduledMonitoringActionSkillRequest { scene_id: "available-balance-root-cause".to_string(), scene_name: "available-balance-root-cause".to_string(), output_root: materialization_root.clone(), source_evidence_json: source_evidence, ir_contract_json: contract_path, trigger_contract_json: trigger_contract_path, }, ) .unwrap(); let current_dir = std::env::current_dir().unwrap(); std::env::set_current_dir(&root).unwrap(); let record = run_scheduled_monitoring_skill_command_adapter( ScheduledMonitoringSkillCommandAdapterRequest { trigger_path: &trigger_path, skills_dir: &materialization_root.join("skills"), config_path: None, output_path: &output_path, watch: false, max_runs: None, }, ) .unwrap(); std::env::set_current_dir(current_dir).unwrap(); assert_eq!(record["previewArtifact"]["summary"]["pending_count"], 0); assert_eq!( record["previewArtifact"]["summary"]["detect_root_cause"], "soft_error" ); assert_eq!(record["auditPreview"]["detectRootCause"], "soft_error"); assert_eq!(record["previewArtifact"]["status"], "soft_error"); } #[test] fn materialization_rejects_business_page_report_without_slices() { let root = temp_workspace("sgclaw-missing-slices"); let source_evidence = root.join("source-evidence.json"); let contract_path = root.join("contract.json"); let trigger_path = root.join("trigger-contract.json"); write_json(&source_evidence, &json!({})); write_json( &contract_path, &json!({ "workflowId": "missing_slices_monitoring_action", "displayName": "missing slices", "defaultMode": "monitor_only", "archetype": "business_page_report_monitor", "runtimeContext": { "runtimeContextUrl": "http://yx.gs.sgcc.com.cn/", "expectedDomain": "yx.gs.sgcc.com.cn", "gatewayDomain": "yxgateway.gs.sgcc.com.cn", "localhostServiceBase": "http://localhost:13313", "browserAttachedRequired": true, "hostBridgeRequired": true }, "sideEffectPolicy": { "blockedCallSignatures": ["mac.sendMessages"] } }), ); write_json(&trigger_path, &minimal_trigger_contract()); let error = generate_scheduled_monitoring_action_skill_package( GenerateScheduledMonitoringActionSkillRequest { scene_id: "missing-slices".to_string(), scene_name: "missing-slices".to_string(), output_root: root.clone(), source_evidence_json: source_evidence, ir_contract_json: contract_path, trigger_contract_json: trigger_path, }, ) .unwrap_err(); assert!(error.to_string().contains("requires at least one read slice")); } #[test] fn materialization_rejects_delta_monitor_without_identity_fields() { let root = temp_workspace("sgclaw-missing-delta-identity"); let source_evidence = root.join("source-evidence.json"); let contract_path = root.join("contract.json"); let trigger_path = root.join("trigger-contract.json"); write_json(&source_evidence, &json!({})); write_json( &contract_path, &json!({ "workflowId": "missing_delta_identity_monitoring_action", "displayName": "missing delta identity", "defaultMode": "monitor_only", "archetype": "list_dedupe_monitor", "runtimeContext": { "runtimeContextUrl": "http://yx.gs.sgcc.com.cn/", "expectedDomain": "yx.gs.sgcc.com.cn", "gatewayDomain": "yxgateway.gs.sgcc.com.cn", "localhostServiceBase": "http://localhost:13313", "browserAttachedRequired": true, "hostBridgeRequired": true, "outputContract": { "deltaState": { "identityFields": [], "stateSidecarPath": "state/test.json", "comparisonMode": "new_rows_only", "emitPolicy": "new_rows_only" } } }, "sideEffectPolicy": { "blockedCallSignatures": ["mac.sendMessages"] } }), ); write_json(&trigger_path, &minimal_trigger_contract()); let error = generate_scheduled_monitoring_action_skill_package( GenerateScheduledMonitoringActionSkillRequest { scene_id: "missing-delta-identity".to_string(), scene_name: "missing-delta-identity".to_string(), output_root: root.clone(), source_evidence_json: source_evidence, ir_contract_json: contract_path, trigger_contract_json: trigger_path, }, ) .unwrap_err(); assert!(error.to_string().contains("delta_state.identity_fields")); } #[test] fn materialization_rejects_empty_encryption_resolution() { let root = temp_workspace("sgclaw-empty-encryption"); let source_evidence = root.join("source-evidence.json"); let contract_path = root.join("contract.json"); let trigger_path = root.join("trigger-contract.json"); write_json(&source_evidence, &json!({})); write_json( &contract_path, &json!({ "workflowId": "empty_encryption_monitoring_action", "displayName": "empty encryption", "defaultMode": "monitor_only", "archetype": "business_page_report_monitor", "runtimeContext": { "runtimeContextUrl": "http://yx.gs.sgcc.com.cn/", "expectedDomain": "yx.gs.sgcc.com.cn", "gatewayDomain": "yxgateway.gs.sgcc.com.cn", "localhostServiceBase": "http://localhost:13313", "browserAttachedRequired": true, "hostBridgeRequired": true, "encryptionMode": "", "readSlices": [ { "name": "slice_01", "endpointBinding": "load_report", "requestTemplateOverride": {}, "responsePath": "data.items", "timeoutMs": 1000, "mergeRole": "concat", "required": true } ], "encryptionResolution": { "primaryMethod": "", "fallbackMethods": [], "requiredContext": [], "hardFail": true } }, "businessApiDependencies": [ { "name": "load_report", "url": "http://yxgateway.gs.sgcc.com.cn/report/load", "classification": "read_report", "sideEffect": false } ], "sideEffectPolicy": { "blockedCallSignatures": ["mac.sendMessages"] } }), ); write_json(&trigger_path, &minimal_trigger_contract()); let error = generate_scheduled_monitoring_action_skill_package( GenerateScheduledMonitoringActionSkillRequest { scene_id: "empty-encryption".to_string(), scene_name: "empty-encryption".to_string(), output_root: root.clone(), source_evidence_json: source_evidence, ir_contract_json: contract_path, trigger_contract_json: trigger_path, }, ) .unwrap_err(); assert!(error.to_string().contains("encryption resolution")); }