Files
claw/tests/scheduled_monitoring_generated_scene_hardening_test.rs

1046 lines
42 KiB
Rust

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