1098 lines
44 KiB
Rust
1098 lines
44 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 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"));
|
|
}
|