generated-scene: add command-center automation semantics
This commit is contained in:
@@ -13,13 +13,15 @@ use crate::generated_scene::analyzer::{
|
|||||||
use crate::generated_scene::ir::{
|
use crate::generated_scene::ir::{
|
||||||
ApiEndpointIr, ArtifactContractIr, BootstrapIr, EnrichmentRequestIr, EvidenceIr, ExportPlanIr,
|
ApiEndpointIr, ArtifactContractIr, BootstrapIr, EnrichmentRequestIr, EvidenceIr, ExportPlanIr,
|
||||||
LegacySceneInfoJson, MainRequestIr, MergeFieldMappingIr, MergePlanIr, ModeConditionIr, ModeIr,
|
LegacySceneInfoJson, MainRequestIr, MergeFieldMappingIr, MergePlanIr, ModeConditionIr, ModeIr,
|
||||||
MonitoringActionWorkflowIr, MonitoringDeltaStateIr, MonitoringDependencyIr,
|
MonitoringActionContractIr, MonitoringActionWorkflowIr, MonitoringDeltaStateIr,
|
||||||
MonitoringEncryptionResolutionIr, MonitoringOutputContractIr, MonitoringReadSliceIr,
|
MonitoringDependencyIr, MonitoringEncryptionResolutionIr, MonitoringExecutionFlowIr,
|
||||||
MonitoringRuntimeContextIr, MonitoringSideEffectIr, MonitoringSideEffectPolicyIr,
|
MonitoringExecutionStepIr, MonitoringIterationContractIr, MonitoringLogWriteContractIr,
|
||||||
MonitoringSidecarOutputIr, MonitoringStorageReadIr, MonitoringTimeoutContractIr,
|
MonitoringOutputContractIr, MonitoringQueueTransitionRuleIr, MonitoringReadSliceIr,
|
||||||
NormalizeRulesIr, PaginationPlanIr, ParamIr, ReadinessGateIr, ReadinessIr,
|
MonitoringResultStateMachineIr, MonitoringRuntimeContextIr, MonitoringSideEffectIr,
|
||||||
RequestFieldMappingIr, RuntimeDependencyIr, SceneIdDiagnosticsIr, SceneIr, ValidationHintsIr,
|
MonitoringSideEffectPolicyIr, MonitoringSidecarOutputIr, MonitoringStorageReadIr,
|
||||||
WorkflowArchetype, WorkflowEvidenceIr, WorkflowStepIr,
|
MonitoringTimeoutContractIr, NormalizeRulesIr, PaginationPlanIr, ParamIr, ReadinessGateIr,
|
||||||
|
ReadinessIr, RequestFieldMappingIr, RuntimeDependencyIr, SceneIdDiagnosticsIr, SceneIr,
|
||||||
|
ValidationHintsIr, WorkflowArchetype, WorkflowEvidenceIr, WorkflowStepIr,
|
||||||
};
|
};
|
||||||
use crate::generated_scene::lessons::{
|
use crate::generated_scene::lessons::{
|
||||||
load_generation_lessons, GenerationLessons, BUILTIN_REPORT_COLLECTION_LESSONS,
|
load_generation_lessons, GenerationLessons, BUILTIN_REPORT_COLLECTION_LESSONS,
|
||||||
@@ -312,7 +314,13 @@ pub fn generate_scheduled_monitoring_action_skill_package(
|
|||||||
let trigger_contract =
|
let trigger_contract =
|
||||||
read_json_file(&request.trigger_contract_json, "scheduled monitoring trigger contract")?;
|
read_json_file(&request.trigger_contract_json, "scheduled monitoring trigger contract")?;
|
||||||
|
|
||||||
let workflow = monitoring_workflow_from_contract(&ir_contract);
|
let mut workflow = monitoring_workflow_from_contract(&ir_contract);
|
||||||
|
enrich_command_center_automation_semantics(
|
||||||
|
&request.scene_id,
|
||||||
|
&source_evidence,
|
||||||
|
&trigger_contract,
|
||||||
|
&mut workflow,
|
||||||
|
);
|
||||||
let scene_ir = monitoring_action_scene_ir(
|
let scene_ir = monitoring_action_scene_ir(
|
||||||
&GenerateMonitoringActionPreviewRequest {
|
&GenerateMonitoringActionPreviewRequest {
|
||||||
scene_id: request.scene_id.clone(),
|
scene_id: request.scene_id.clone(),
|
||||||
@@ -1227,6 +1235,12 @@ fn monitoring_workflow_from_contract(contract: &Value) -> MonitoringActionWorkfl
|
|||||||
})
|
})
|
||||||
.unwrap_or_else(|| monitoring_blocked_actions_from_platform_dependencies(platform_dependencies)),
|
.unwrap_or_else(|| monitoring_blocked_actions_from_platform_dependencies(platform_dependencies)),
|
||||||
},
|
},
|
||||||
|
action_contracts: Vec::new(),
|
||||||
|
iteration_contract: None,
|
||||||
|
execution_flow: None,
|
||||||
|
result_state_machines: Vec::new(),
|
||||||
|
queue_transition_rules: Vec::new(),
|
||||||
|
log_write_contracts: Vec::new(),
|
||||||
archetype: contract["archetype"]
|
archetype: contract["archetype"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.unwrap_or("marketing_gateway_monitor")
|
.unwrap_or("marketing_gateway_monitor")
|
||||||
@@ -1234,6 +1248,278 @@ fn monitoring_workflow_from_contract(contract: &Value) -> MonitoringActionWorkfl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn enrich_command_center_automation_semantics(
|
||||||
|
scene_id: &str,
|
||||||
|
source_evidence: &Value,
|
||||||
|
trigger_contract: &Value,
|
||||||
|
workflow: &mut MonitoringActionWorkflowIr,
|
||||||
|
) {
|
||||||
|
if !is_command_center_monitoring_scene(scene_id, &workflow.workflow_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dispatch_dependency = workflow
|
||||||
|
.business_api_dependencies
|
||||||
|
.iter()
|
||||||
|
.find(|item| {
|
||||||
|
item.url.contains("repetCtrlSend")
|
||||||
|
|| item.classification == "dispatch_exception_order"
|
||||||
|
|| item.classification == "business_dispatch"
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| MonitoringDependencyIr {
|
||||||
|
name: "repetCtrlSend".to_string(),
|
||||||
|
url: "http://yxgateway.gs.sgcc.com.cn/emss-chargacctgf-paysrv-front/member/acctabnor/repetCtrlSend"
|
||||||
|
.to_string(),
|
||||||
|
classification: "dispatch_exception_order".to_string(),
|
||||||
|
side_effect: true,
|
||||||
|
blocked_by_default: true,
|
||||||
|
});
|
||||||
|
let dispose_log_dependency = workflow
|
||||||
|
.local_service_dependencies
|
||||||
|
.iter()
|
||||||
|
.find(|item| {
|
||||||
|
item.url.contains("setDisposeLog")
|
||||||
|
|| item.classification == "write_dispose_log"
|
||||||
|
|| item.classification == "dispose_log_write"
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| MonitoringDependencyIr {
|
||||||
|
name: "setDisposeLog".to_string(),
|
||||||
|
url: "http://localhost:13313/MonitorServices/setDisposeLog".to_string(),
|
||||||
|
classification: "write_dispose_log".to_string(),
|
||||||
|
side_effect: true,
|
||||||
|
blocked_by_default: true,
|
||||||
|
});
|
||||||
|
let queue_action_ref = trigger_contract["platformRuntimeCapabilities"]["hostActionBridge"]
|
||||||
|
.as_array()
|
||||||
|
.and_then(|items| {
|
||||||
|
items.iter().find_map(|item| {
|
||||||
|
if item["name"].as_str().unwrap_or_default() == "mac.exeTQueue" {
|
||||||
|
Some(item["name"].as_str().unwrap_or_default().to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "mac.exeTQueue".to_string());
|
||||||
|
|
||||||
|
let dispatch_dependency_ref = if dispatch_dependency.url.is_empty() {
|
||||||
|
dispatch_dependency.classification.clone()
|
||||||
|
} else {
|
||||||
|
dispatch_dependency.url.clone()
|
||||||
|
};
|
||||||
|
let dispose_log_dependency_ref = if dispose_log_dependency.url.is_empty() {
|
||||||
|
dispose_log_dependency.classification.clone()
|
||||||
|
} else {
|
||||||
|
dispose_log_dependency.url.clone()
|
||||||
|
};
|
||||||
|
let queue_required_mode = trigger_contract["triggerContracts"]["queue"]["futureModes"]
|
||||||
|
.as_array()
|
||||||
|
.and_then(|items| items.first())
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.unwrap_or("queue_process")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let mut dispatch_field_bindings = Map::new();
|
||||||
|
dispatch_field_bindings.insert("chooseList".to_string(), json!(["$current_item"]));
|
||||||
|
dispatch_field_bindings.insert(
|
||||||
|
"itemIdentity".to_string(),
|
||||||
|
json!(["consNo", "custNo", "sendBeginTime", "createTime"]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut dispose_log_field_bindings = Map::new();
|
||||||
|
dispose_log_field_bindings.insert(
|
||||||
|
"orderID".to_string(),
|
||||||
|
json!({
|
||||||
|
"from": ["current_item.consNo", "current_item.custNo", "current_item.sendBeginTime", "current_item.createTime"],
|
||||||
|
"strategy": "join_non_empty_with_underscore"
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
dispose_log_field_bindings.insert(
|
||||||
|
"name".to_string(),
|
||||||
|
json!({
|
||||||
|
"from": ["current_item.orgName", "current_item.consName", "current_item.phone"],
|
||||||
|
"strategy": "first_non_empty"
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
dispose_log_field_bindings.insert("time".to_string(), json!({"from": "runtime.now"}));
|
||||||
|
|
||||||
|
let action_contract = MonitoringActionContractIr {
|
||||||
|
action_id: "dispatch_exception_order".to_string(),
|
||||||
|
action_type: "business_dispatch".to_string(),
|
||||||
|
dependency_ref: dispatch_dependency_ref.clone(),
|
||||||
|
target_endpoint_or_host_call: if dispatch_dependency.url.is_empty() {
|
||||||
|
"repetCtrlSend".to_string()
|
||||||
|
} else {
|
||||||
|
dispatch_dependency.url.clone()
|
||||||
|
},
|
||||||
|
execution_context: "attached_page_http_post".to_string(),
|
||||||
|
input_source: "current_item".to_string(),
|
||||||
|
request_template: json!({
|
||||||
|
"busType": "03",
|
||||||
|
"chooseList": ["$current_item"]
|
||||||
|
}),
|
||||||
|
field_bindings: dispatch_field_bindings,
|
||||||
|
auth_binding: json!({
|
||||||
|
"source": "localStorage.markToken",
|
||||||
|
"targetField": "auth_token"
|
||||||
|
}),
|
||||||
|
encryption_binding: json!({
|
||||||
|
"mode": workflow.runtime_context.encryption_mode,
|
||||||
|
"payloadSource": "request_body_json"
|
||||||
|
}),
|
||||||
|
result_channel: "browser_callback_js_result".to_string(),
|
||||||
|
};
|
||||||
|
let iteration_contract = MonitoringIterationContractIr {
|
||||||
|
iteration_id: "pending_dispatch_loop".to_string(),
|
||||||
|
source_collection: source_evidence["queueDependencies"]
|
||||||
|
.as_array()
|
||||||
|
.and_then(|items| {
|
||||||
|
items.iter().find_map(|item| {
|
||||||
|
if item["name"].as_str().unwrap_or_default() == "pendingList" {
|
||||||
|
Some("pendingList".to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "pendingList".to_string()),
|
||||||
|
iteration_mode: "sequential_per_item".to_string(),
|
||||||
|
item_alias: "current_item".to_string(),
|
||||||
|
on_empty_transition: "queue_continue_on_empty".to_string(),
|
||||||
|
on_item_complete_transition: "next_item".to_string(),
|
||||||
|
on_all_complete_transition: "queue_continue_on_done".to_string(),
|
||||||
|
};
|
||||||
|
let execution_flow = MonitoringExecutionFlowIr {
|
||||||
|
flow_id: "command_center_dispatch_preview_flow".to_string(),
|
||||||
|
entry_step: "dispatch_loop".to_string(),
|
||||||
|
steps: vec![
|
||||||
|
MonitoringExecutionStepIr {
|
||||||
|
step_id: "dispatch_loop".to_string(),
|
||||||
|
step_type: "iterate".to_string(),
|
||||||
|
iteration_ref: "pending_dispatch_loop".to_string(),
|
||||||
|
next_on_empty: "queue_continue_empty".to_string(),
|
||||||
|
next_on_done: "queue_continue_done".to_string(),
|
||||||
|
next_on_success: "dispatch_current_item".to_string(),
|
||||||
|
..MonitoringExecutionStepIr::default()
|
||||||
|
},
|
||||||
|
MonitoringExecutionStepIr {
|
||||||
|
step_id: "dispatch_current_item".to_string(),
|
||||||
|
step_type: "action".to_string(),
|
||||||
|
action_contract_ref: "dispatch_exception_order".to_string(),
|
||||||
|
next_on_success: "write_dispose_log".to_string(),
|
||||||
|
next_on_failure: "write_dispose_log".to_string(),
|
||||||
|
..MonitoringExecutionStepIr::default()
|
||||||
|
},
|
||||||
|
MonitoringExecutionStepIr {
|
||||||
|
step_id: "write_dispose_log".to_string(),
|
||||||
|
step_type: "log_write".to_string(),
|
||||||
|
log_write_contract_ref: "dispose_log_after_dispatch".to_string(),
|
||||||
|
next_on_success: "dispatch_loop".to_string(),
|
||||||
|
next_on_failure: "dispatch_loop".to_string(),
|
||||||
|
..MonitoringExecutionStepIr::default()
|
||||||
|
},
|
||||||
|
MonitoringExecutionStepIr {
|
||||||
|
step_id: "queue_continue_empty".to_string(),
|
||||||
|
step_type: "queue_transition".to_string(),
|
||||||
|
queue_transition_ref: "queue_continue_on_empty".to_string(),
|
||||||
|
next_on_success: "done".to_string(),
|
||||||
|
..MonitoringExecutionStepIr::default()
|
||||||
|
},
|
||||||
|
MonitoringExecutionStepIr {
|
||||||
|
step_id: "queue_continue_done".to_string(),
|
||||||
|
step_type: "queue_transition".to_string(),
|
||||||
|
queue_transition_ref: "queue_continue_on_done".to_string(),
|
||||||
|
next_on_success: "done".to_string(),
|
||||||
|
..MonitoringExecutionStepIr::default()
|
||||||
|
},
|
||||||
|
MonitoringExecutionStepIr {
|
||||||
|
step_id: "done".to_string(),
|
||||||
|
step_type: "terminal".to_string(),
|
||||||
|
..MonitoringExecutionStepIr::default()
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let result_state_machine = MonitoringResultStateMachineIr {
|
||||||
|
state_machine_id: "dispatch_result_state_machine".to_string(),
|
||||||
|
action_contract_ref: "dispatch_exception_order".to_string(),
|
||||||
|
success_match: json!({
|
||||||
|
"allOf": [
|
||||||
|
{"field": "code", "equals": "00000"},
|
||||||
|
{"field": "message", "equals": "success"}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
failure_match: json!({
|
||||||
|
"fallback": "not_success_match"
|
||||||
|
}),
|
||||||
|
state_on_success: "success".to_string(),
|
||||||
|
state_on_failure: "failure".to_string(),
|
||||||
|
post_success_log_contract_ref: "dispose_log_after_dispatch".to_string(),
|
||||||
|
post_failure_log_contract_ref: "dispose_log_after_dispatch".to_string(),
|
||||||
|
continue_policy: "continue_next_item".to_string(),
|
||||||
|
};
|
||||||
|
let queue_transition_rules = vec![
|
||||||
|
MonitoringQueueTransitionRuleIr {
|
||||||
|
transition_id: "queue_continue_on_empty".to_string(),
|
||||||
|
queue_action_ref: queue_action_ref.clone(),
|
||||||
|
trigger_point: "on_empty_collection".to_string(),
|
||||||
|
required_mode: queue_required_mode.clone(),
|
||||||
|
blocked_by_default: true,
|
||||||
|
},
|
||||||
|
MonitoringQueueTransitionRuleIr {
|
||||||
|
transition_id: "queue_continue_on_done".to_string(),
|
||||||
|
queue_action_ref,
|
||||||
|
trigger_point: "on_all_items_done".to_string(),
|
||||||
|
required_mode: queue_required_mode,
|
||||||
|
blocked_by_default: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let log_write_contract = MonitoringLogWriteContractIr {
|
||||||
|
log_id: "dispose_log_after_dispatch".to_string(),
|
||||||
|
dependency_ref: dispose_log_dependency_ref,
|
||||||
|
target_endpoint_or_host_call: if dispose_log_dependency.url.is_empty() {
|
||||||
|
"setDisposeLog".to_string()
|
||||||
|
} else {
|
||||||
|
dispose_log_dependency.url.clone()
|
||||||
|
},
|
||||||
|
emit_phase: "after_dispatch_result".to_string(),
|
||||||
|
payload_template: json!({
|
||||||
|
"type": "fee_control_exception_dispose",
|
||||||
|
"orderID": "$derived.orderID",
|
||||||
|
"name": "$derived.name",
|
||||||
|
"time": "$runtime.now",
|
||||||
|
"state": "$dispatch.state"
|
||||||
|
}),
|
||||||
|
field_bindings: dispose_log_field_bindings,
|
||||||
|
state_binding: "dispatch_result_state".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if workflow.action_contracts.is_empty() {
|
||||||
|
workflow.action_contracts = vec![action_contract];
|
||||||
|
}
|
||||||
|
if workflow.iteration_contract.is_none() {
|
||||||
|
workflow.iteration_contract = Some(iteration_contract);
|
||||||
|
}
|
||||||
|
if workflow.execution_flow.is_none() {
|
||||||
|
workflow.execution_flow = Some(execution_flow);
|
||||||
|
}
|
||||||
|
if workflow.result_state_machines.is_empty() {
|
||||||
|
workflow.result_state_machines = vec![result_state_machine];
|
||||||
|
}
|
||||||
|
if workflow.queue_transition_rules.is_empty() {
|
||||||
|
workflow.queue_transition_rules = queue_transition_rules;
|
||||||
|
}
|
||||||
|
if workflow.log_write_contracts.is_empty() {
|
||||||
|
workflow.log_write_contracts = vec![log_write_contract];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_command_center_monitoring_scene(scene_id: &str, workflow_id: &str) -> bool {
|
||||||
|
scene_id == "command-center-fee-control-monitor"
|
||||||
|
|| workflow_id == "command_center_fee_control_monitoring_action"
|
||||||
|
}
|
||||||
|
|
||||||
fn monitoring_dependencies(value: &Value) -> Vec<MonitoringDependencyIr> {
|
fn monitoring_dependencies(value: &Value) -> Vec<MonitoringDependencyIr> {
|
||||||
value
|
value
|
||||||
.as_array()
|
.as_array()
|
||||||
@@ -7257,14 +7543,28 @@ if (typeof module !== 'undefined') {{
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn compile_scheduled_monitoring_action_plan_script(scene_ir: &SceneIr) -> String {
|
fn compile_scheduled_monitoring_action_plan_script(scene_ir: &SceneIr) -> String {
|
||||||
|
let workflow = scene_ir
|
||||||
|
.monitoring_action_workflow
|
||||||
|
.as_ref()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
let blocked = scene_ir
|
let blocked = scene_ir
|
||||||
.monitoring_action_workflow
|
.monitoring_action_workflow
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|workflow| workflow.side_effect_policy.blocked_call_signatures.clone())
|
.map(|workflow| workflow.side_effect_policy.blocked_call_signatures.clone())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let blocked_json = serde_json::to_string_pretty(&blocked).unwrap_or_else(|_| "[]".to_string());
|
let blocked_json = serde_json::to_string_pretty(&blocked).unwrap_or_else(|_| "[]".to_string());
|
||||||
|
let action_contracts_json =
|
||||||
|
serde_json::to_string_pretty(&workflow.action_contracts).unwrap_or_else(|_| "[]".to_string());
|
||||||
|
let queue_transitions_json = serde_json::to_string_pretty(&workflow.queue_transition_rules)
|
||||||
|
.unwrap_or_else(|_| "[]".to_string());
|
||||||
|
let log_write_contracts_json =
|
||||||
|
serde_json::to_string_pretty(&workflow.log_write_contracts).unwrap_or_else(|_| "[]".to_string());
|
||||||
format!(
|
format!(
|
||||||
r#"const BLOCKED_CALL_SIGNATURES = {blocked_json};
|
r#"const BLOCKED_CALL_SIGNATURES = {blocked_json};
|
||||||
|
const ACTION_CONTRACTS = {action_contracts_json};
|
||||||
|
const QUEUE_TRANSITION_RULES = {queue_transitions_json};
|
||||||
|
const LOG_WRITE_CONTRACTS = {log_write_contracts_json};
|
||||||
|
|
||||||
function normalizeDecision(decision) {{
|
function normalizeDecision(decision) {{
|
||||||
if (typeof decision === 'string') {{
|
if (typeof decision === 'string') {{
|
||||||
@@ -7276,22 +7576,42 @@ function normalizeDecision(decision) {{
|
|||||||
function buildActionPlan(decision = {{}}, args = {{}}) {{
|
function buildActionPlan(decision = {{}}, args = {{}}) {{
|
||||||
const source = normalizeDecision(decision);
|
const source = normalizeDecision(decision);
|
||||||
const pendingList = Array.isArray(source.pendingList) ? source.pendingList : [];
|
const pendingList = Array.isArray(source.pendingList) ? source.pendingList : [];
|
||||||
|
const dispatchContract = ACTION_CONTRACTS.find(item => item.actionId === 'dispatch_exception_order') || ACTION_CONTRACTS[0] || {{}};
|
||||||
const actionPlan = pendingList.map((item, index) => ({{
|
const actionPlan = pendingList.map((item, index) => ({{
|
||||||
itemId: String(item.id || item.consNo || item.custNo || item.workOrderId || index),
|
itemId: String(item.id || item.consNo || item.custNo || item.workOrderId || index),
|
||||||
actionType: 'business_dispatch',
|
actionType: dispatchContract.actionType || 'business_dispatch',
|
||||||
targetEndpointOrHostCall: 'repetCtrlSend',
|
actionContractRef: dispatchContract.actionId || 'dispatch_exception_order',
|
||||||
|
targetEndpointOrHostCall: dispatchContract.targetEndpointOrHostCall || 'repetCtrlSend',
|
||||||
blockedByDefault: true,
|
blockedByDefault: true,
|
||||||
requiresFutureGate: 'dispatch_gate',
|
requiresFutureGate: 'dispatch_gate',
|
||||||
reason: 'scheduled monitoring preview only'
|
reason: 'scheduled monitoring preview only'
|
||||||
}}));
|
}}));
|
||||||
|
const queueTransitions = pendingList.length > 0
|
||||||
|
? QUEUE_TRANSITION_RULES.filter(item => item.transitionId === 'queue_continue_on_done')
|
||||||
|
: QUEUE_TRANSITION_RULES.filter(item => item.transitionId === 'queue_continue_on_empty');
|
||||||
|
const logWritePreview = pendingList.length > 0
|
||||||
|
? LOG_WRITE_CONTRACTS.map(item => ({{
|
||||||
|
logId: item.logId,
|
||||||
|
targetEndpointOrHostCall: item.targetEndpointOrHostCall,
|
||||||
|
emitPhase: item.emitPhase,
|
||||||
|
blockedByDefault: true
|
||||||
|
}}))
|
||||||
|
: [];
|
||||||
return {{
|
return {{
|
||||||
type: 'scheduled-monitoring-action-plan-preview',
|
type: 'scheduled-monitoring-action-plan-preview',
|
||||||
status: 'action-plan-ok',
|
status: 'action-plan-ok',
|
||||||
mode: args.mode || source.mode || 'monitor_only',
|
mode: args.mode || source.mode || 'monitor_only',
|
||||||
actionPlan,
|
actionPlan,
|
||||||
|
queueTransitions,
|
||||||
|
logWritePreview,
|
||||||
blockedSideEffects: {{
|
blockedSideEffects: {{
|
||||||
blockedCallSignatures: BLOCKED_CALL_SIGNATURES
|
blockedCallSignatures: BLOCKED_CALL_SIGNATURES
|
||||||
}},
|
}},
|
||||||
|
summary: {{
|
||||||
|
action_plan_count: actionPlan.length,
|
||||||
|
queue_transition_count: queueTransitions.length,
|
||||||
|
log_write_preview_count: logWritePreview.length
|
||||||
|
}},
|
||||||
sideEffectCounters: {{
|
sideEffectCounters: {{
|
||||||
repetCtrlSend: 0,
|
repetCtrlSend: 0,
|
||||||
sendMessages: 0,
|
sendMessages: 0,
|
||||||
@@ -7308,7 +7628,10 @@ if (typeof module !== 'undefined') {{
|
|||||||
module.exports = {{ buildActionPlan }};
|
module.exports = {{ buildActionPlan }};
|
||||||
}}
|
}}
|
||||||
"#,
|
"#,
|
||||||
blocked_json = blocked_json
|
blocked_json = blocked_json,
|
||||||
|
action_contracts_json = action_contracts_json,
|
||||||
|
queue_transitions_json = queue_transitions_json,
|
||||||
|
log_write_contracts_json = log_write_contracts_json
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8867,6 +9190,12 @@ fn scheduled_monitoring_generation_report(
|
|||||||
"sidecarOutputs": workflow.runtime_context.output_contract.sidecar_outputs,
|
"sidecarOutputs": workflow.runtime_context.output_contract.sidecar_outputs,
|
||||||
"deltaState": workflow.runtime_context.output_contract.delta_state,
|
"deltaState": workflow.runtime_context.output_contract.delta_state,
|
||||||
},
|
},
|
||||||
|
"actionContracts": workflow.action_contracts,
|
||||||
|
"iterationContract": workflow.iteration_contract,
|
||||||
|
"executionFlow": workflow.execution_flow,
|
||||||
|
"resultStateMachines": workflow.result_state_machines,
|
||||||
|
"queueTransitionRules": workflow.queue_transition_rules,
|
||||||
|
"logWriteContracts": workflow.log_write_contracts,
|
||||||
"triggerContractStatus": trigger_contract["status"],
|
"triggerContractStatus": trigger_contract["status"],
|
||||||
"readiness": scene_ir.readiness,
|
"readiness": scene_ir.readiness,
|
||||||
})
|
})
|
||||||
@@ -9129,4 +9458,40 @@ mod tests {
|
|||||||
assert!(rendered.contains("[[runtime_context.output_contract.sidecar_outputs]]"));
|
assert!(rendered.contains("[[runtime_context.output_contract.sidecar_outputs]]"));
|
||||||
assert!(rendered.contains("archetype = \"business_page_report_monitor\""));
|
assert!(rendered.contains("archetype = \"business_page_report_monitor\""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_center_enrichment_backfills_missing_semantics_without_overwriting_existing_ones() {
|
||||||
|
let mut workflow = test_workflow("marketing_gateway_monitor");
|
||||||
|
workflow.workflow_id = "command_center_fee_control_monitoring_action".to_string();
|
||||||
|
workflow.action_contracts.push(MonitoringActionContractIr {
|
||||||
|
action_id: "preexisting_dispatch".to_string(),
|
||||||
|
..MonitoringActionContractIr::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
enrich_command_center_automation_semantics(
|
||||||
|
"command-center-fee-control-monitor",
|
||||||
|
&json!({}),
|
||||||
|
&json!({
|
||||||
|
"triggerContracts": {
|
||||||
|
"queue": {
|
||||||
|
"futureModes": ["queue_process"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"platformRuntimeCapabilities": {
|
||||||
|
"hostActionBridge": [
|
||||||
|
{ "name": "mac.exeTQueue" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
&mut workflow,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(workflow.action_contracts.len(), 1);
|
||||||
|
assert_eq!(workflow.action_contracts[0].action_id, "preexisting_dispatch");
|
||||||
|
assert!(workflow.iteration_contract.is_some());
|
||||||
|
assert!(workflow.execution_flow.is_some());
|
||||||
|
assert_eq!(workflow.result_state_machines.len(), 1);
|
||||||
|
assert_eq!(workflow.queue_transition_rules.len(), 2);
|
||||||
|
assert_eq!(workflow.log_write_contracts.len(), 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -477,6 +477,138 @@ pub struct MonitoringSideEffectPolicyIr {
|
|||||||
pub blocked_actions: Vec<MonitoringSideEffectIr>,
|
pub blocked_actions: Vec<MonitoringSideEffectIr>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub struct MonitoringActionContractIr {
|
||||||
|
#[serde(rename = "actionId", default)]
|
||||||
|
pub action_id: String,
|
||||||
|
#[serde(rename = "actionType", default)]
|
||||||
|
pub action_type: String,
|
||||||
|
#[serde(rename = "dependencyRef", default)]
|
||||||
|
pub dependency_ref: String,
|
||||||
|
#[serde(rename = "targetEndpointOrHostCall", default)]
|
||||||
|
pub target_endpoint_or_host_call: String,
|
||||||
|
#[serde(rename = "executionContext", default)]
|
||||||
|
pub execution_context: String,
|
||||||
|
#[serde(rename = "inputSource", default)]
|
||||||
|
pub input_source: String,
|
||||||
|
#[serde(rename = "requestTemplate", default)]
|
||||||
|
pub request_template: Value,
|
||||||
|
#[serde(rename = "fieldBindings", default)]
|
||||||
|
pub field_bindings: Map<String, Value>,
|
||||||
|
#[serde(rename = "authBinding", default)]
|
||||||
|
pub auth_binding: Value,
|
||||||
|
#[serde(rename = "encryptionBinding", default)]
|
||||||
|
pub encryption_binding: Value,
|
||||||
|
#[serde(rename = "resultChannel", default)]
|
||||||
|
pub result_channel: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub struct MonitoringIterationContractIr {
|
||||||
|
#[serde(rename = "iterationId", default)]
|
||||||
|
pub iteration_id: String,
|
||||||
|
#[serde(rename = "sourceCollection", default)]
|
||||||
|
pub source_collection: String,
|
||||||
|
#[serde(rename = "iterationMode", default)]
|
||||||
|
pub iteration_mode: String,
|
||||||
|
#[serde(rename = "itemAlias", default)]
|
||||||
|
pub item_alias: String,
|
||||||
|
#[serde(rename = "onEmptyTransition", default)]
|
||||||
|
pub on_empty_transition: String,
|
||||||
|
#[serde(rename = "onItemCompleteTransition", default)]
|
||||||
|
pub on_item_complete_transition: String,
|
||||||
|
#[serde(rename = "onAllCompleteTransition", default)]
|
||||||
|
pub on_all_complete_transition: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub struct MonitoringExecutionStepIr {
|
||||||
|
#[serde(rename = "stepId", default)]
|
||||||
|
pub step_id: String,
|
||||||
|
#[serde(rename = "stepType", default)]
|
||||||
|
pub step_type: String,
|
||||||
|
#[serde(rename = "iterationRef", default)]
|
||||||
|
pub iteration_ref: String,
|
||||||
|
#[serde(rename = "actionContractRef", default)]
|
||||||
|
pub action_contract_ref: String,
|
||||||
|
#[serde(rename = "logWriteContractRef", default)]
|
||||||
|
pub log_write_contract_ref: String,
|
||||||
|
#[serde(rename = "queueTransitionRef", default)]
|
||||||
|
pub queue_transition_ref: String,
|
||||||
|
#[serde(rename = "nextOnSuccess", default)]
|
||||||
|
pub next_on_success: String,
|
||||||
|
#[serde(rename = "nextOnFailure", default)]
|
||||||
|
pub next_on_failure: String,
|
||||||
|
#[serde(rename = "nextOnEmpty", default)]
|
||||||
|
pub next_on_empty: String,
|
||||||
|
#[serde(rename = "nextOnDone", default)]
|
||||||
|
pub next_on_done: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub struct MonitoringExecutionFlowIr {
|
||||||
|
#[serde(rename = "flowId", default)]
|
||||||
|
pub flow_id: String,
|
||||||
|
#[serde(rename = "entryStep", default)]
|
||||||
|
pub entry_step: String,
|
||||||
|
#[serde(rename = "steps", default)]
|
||||||
|
pub steps: Vec<MonitoringExecutionStepIr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub struct MonitoringResultStateMachineIr {
|
||||||
|
#[serde(rename = "stateMachineId", default)]
|
||||||
|
pub state_machine_id: String,
|
||||||
|
#[serde(rename = "actionContractRef", default)]
|
||||||
|
pub action_contract_ref: String,
|
||||||
|
#[serde(rename = "successMatch", default)]
|
||||||
|
pub success_match: Value,
|
||||||
|
#[serde(rename = "failureMatch", default)]
|
||||||
|
pub failure_match: Value,
|
||||||
|
#[serde(rename = "stateOnSuccess", default)]
|
||||||
|
pub state_on_success: String,
|
||||||
|
#[serde(rename = "stateOnFailure", default)]
|
||||||
|
pub state_on_failure: String,
|
||||||
|
#[serde(rename = "postSuccessLogContractRef", default)]
|
||||||
|
pub post_success_log_contract_ref: String,
|
||||||
|
#[serde(rename = "postFailureLogContractRef", default)]
|
||||||
|
pub post_failure_log_contract_ref: String,
|
||||||
|
#[serde(rename = "continuePolicy", default)]
|
||||||
|
pub continue_policy: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub struct MonitoringQueueTransitionRuleIr {
|
||||||
|
#[serde(rename = "transitionId", default)]
|
||||||
|
pub transition_id: String,
|
||||||
|
#[serde(rename = "queueActionRef", default)]
|
||||||
|
pub queue_action_ref: String,
|
||||||
|
#[serde(rename = "triggerPoint", default)]
|
||||||
|
pub trigger_point: String,
|
||||||
|
#[serde(rename = "requiredMode", default)]
|
||||||
|
pub required_mode: String,
|
||||||
|
#[serde(rename = "blockedByDefault", default)]
|
||||||
|
pub blocked_by_default: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub struct MonitoringLogWriteContractIr {
|
||||||
|
#[serde(rename = "logId", default)]
|
||||||
|
pub log_id: String,
|
||||||
|
#[serde(rename = "dependencyRef", default)]
|
||||||
|
pub dependency_ref: String,
|
||||||
|
#[serde(rename = "targetEndpointOrHostCall", default)]
|
||||||
|
pub target_endpoint_or_host_call: String,
|
||||||
|
#[serde(rename = "emitPhase", default)]
|
||||||
|
pub emit_phase: String,
|
||||||
|
#[serde(rename = "payloadTemplate", default)]
|
||||||
|
pub payload_template: Value,
|
||||||
|
#[serde(rename = "fieldBindings", default)]
|
||||||
|
pub field_bindings: Map<String, Value>,
|
||||||
|
#[serde(rename = "stateBinding", default)]
|
||||||
|
pub state_binding: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct MonitoringActionWorkflowIr {
|
pub struct MonitoringActionWorkflowIr {
|
||||||
#[serde(rename = "workflowId", default)]
|
#[serde(rename = "workflowId", default)]
|
||||||
@@ -505,6 +637,18 @@ pub struct MonitoringActionWorkflowIr {
|
|||||||
pub preview_schema: Vec<String>,
|
pub preview_schema: Vec<String>,
|
||||||
#[serde(rename = "sideEffectPolicy", default)]
|
#[serde(rename = "sideEffectPolicy", default)]
|
||||||
pub side_effect_policy: MonitoringSideEffectPolicyIr,
|
pub side_effect_policy: MonitoringSideEffectPolicyIr,
|
||||||
|
#[serde(rename = "actionContracts", default)]
|
||||||
|
pub action_contracts: Vec<MonitoringActionContractIr>,
|
||||||
|
#[serde(rename = "iterationContract", default)]
|
||||||
|
pub iteration_contract: Option<MonitoringIterationContractIr>,
|
||||||
|
#[serde(rename = "executionFlow", default)]
|
||||||
|
pub execution_flow: Option<MonitoringExecutionFlowIr>,
|
||||||
|
#[serde(rename = "resultStateMachines", default)]
|
||||||
|
pub result_state_machines: Vec<MonitoringResultStateMachineIr>,
|
||||||
|
#[serde(rename = "queueTransitionRules", default)]
|
||||||
|
pub queue_transition_rules: Vec<MonitoringQueueTransitionRuleIr>,
|
||||||
|
#[serde(rename = "logWriteContracts", default)]
|
||||||
|
pub log_write_contracts: Vec<MonitoringLogWriteContractIr>,
|
||||||
#[serde(rename = "archetype", default = "default_monitoring_archetype")]
|
#[serde(rename = "archetype", default = "default_monitoring_archetype")]
|
||||||
pub archetype: String,
|
pub archetype: String,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1301,6 +1301,16 @@ fn run_scheduled_monitoring_skill_runtime(
|
|||||||
.and_then(Value::as_array)
|
.and_then(Value::as_array)
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
let queue_transitions = action_plan_preview
|
||||||
|
.get("queueTransitions")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let log_write_preview = action_plan_preview
|
||||||
|
.get("logWritePreview")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
let detect_root_cause = detect_status_root_cause(&json!({
|
let detect_root_cause = detect_status_root_cause(&json!({
|
||||||
"detectSnapshot": detect_snapshot.clone()
|
"detectSnapshot": detect_snapshot.clone()
|
||||||
}));
|
}));
|
||||||
@@ -1322,11 +1332,14 @@ fn run_scheduled_monitoring_skill_runtime(
|
|||||||
"pending_count": pending_count,
|
"pending_count": pending_count,
|
||||||
"notify_count": notify_candidates.len(),
|
"notify_count": notify_candidates.len(),
|
||||||
"action_plan_count": action_plan.len(),
|
"action_plan_count": action_plan.len(),
|
||||||
|
"queue_transition_count": queue_transitions.len(),
|
||||||
"detect_root_cause": detect_root_cause
|
"detect_root_cause": detect_root_cause
|
||||||
},
|
},
|
||||||
"pendingList": pending_list,
|
"pendingList": pending_list,
|
||||||
"notifyCandidates": notify_candidates,
|
"notifyCandidates": notify_candidates,
|
||||||
"actionPlan": action_plan,
|
"actionPlan": action_plan,
|
||||||
|
"queueTransitions": queue_transitions,
|
||||||
|
"logWritePreview": log_write_preview,
|
||||||
"blockedSideEffects": {
|
"blockedSideEffects": {
|
||||||
"blockedCallSignatures": blocked_call_signatures
|
"blockedCallSignatures": blocked_call_signatures
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
],
|
],
|
||||||
"source": "scheduled_monitoring_action_trigger_runtime_contract"
|
"source": "scheduled_monitoring_action_trigger_runtime_contract"
|
||||||
},
|
},
|
||||||
"finishedAt": "2026-04-26T05:30:42.489979300+00:00",
|
"finishedAt": "2026-05-06T04:36:29.926950300+00:00",
|
||||||
"mode": "dry_run",
|
"mode": "dry_run",
|
||||||
"previewArtifact": {
|
"previewArtifact": {
|
||||||
"actionPlan": [
|
"actionPlan": [
|
||||||
@@ -112,7 +112,7 @@
|
|||||||
"repetCtrlSend": 0,
|
"repetCtrlSend": 0,
|
||||||
"sendMessages": 0
|
"sendMessages": 0
|
||||||
},
|
},
|
||||||
"startedAt": "2026-04-26T05:30:42.489979300+00:00",
|
"startedAt": "2026-05-06T04:36:29.926950300+00:00",
|
||||||
"status": "dry-run-runtime-pass",
|
"status": "dry-run-runtime-pass",
|
||||||
"triggerType": "queue",
|
"triggerType": "queue",
|
||||||
"warnings": [
|
"warnings": [
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
],
|
],
|
||||||
"source": "scheduled_monitoring_action_trigger_runtime_contract"
|
"source": "scheduled_monitoring_action_trigger_runtime_contract"
|
||||||
},
|
},
|
||||||
"finishedAt": "2026-04-26T05:30:42.260973600+00:00",
|
"finishedAt": "2026-05-06T04:36:29.680658700+00:00",
|
||||||
"mode": "monitor_only",
|
"mode": "monitor_only",
|
||||||
"previewArtifact": {
|
"previewArtifact": {
|
||||||
"actionPlan": [
|
"actionPlan": [
|
||||||
@@ -226,7 +226,7 @@
|
|||||||
"repetCtrlSend": 0,
|
"repetCtrlSend": 0,
|
||||||
"sendMessages": 0
|
"sendMessages": 0
|
||||||
},
|
},
|
||||||
"startedAt": "2026-04-26T05:30:42.260973600+00:00",
|
"startedAt": "2026-05-06T04:36:29.680658700+00:00",
|
||||||
"status": "dry-run-runtime-pass",
|
"status": "dry-run-runtime-pass",
|
||||||
"triggerType": "scheduled",
|
"triggerType": "scheduled",
|
||||||
"warnings": [
|
"warnings": [
|
||||||
|
|||||||
@@ -3893,6 +3893,84 @@ fn generator_emits_scheduled_monitoring_action_skill_package() {
|
|||||||
assert!(test_status.success());
|
assert!(test_status.success());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_center_materializes_automation_semantics_into_workflow_ir() {
|
||||||
|
let output_root = temp_workspace("sgclaw-command-center-automation-workflow-ir");
|
||||||
|
let skill_root = generate_scheduled_monitoring_action_skill_package(
|
||||||
|
GenerateScheduledMonitoringActionSkillRequest {
|
||||||
|
scene_id: "command-center-fee-control-monitor".to_string(),
|
||||||
|
scene_name: "指挥中心费控异常监测".to_string(),
|
||||||
|
output_root: output_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: serde_json::Value = serde_json::from_str(
|
||||||
|
&fs::read_to_string(skill_root.join("references/workflow-ir.json")).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let action_contracts = workflow_ir["actionContracts"]
|
||||||
|
.as_array()
|
||||||
|
.expect("expected actionContracts array");
|
||||||
|
assert!(
|
||||||
|
action_contracts.iter().any(|item| {
|
||||||
|
item["targetEndpointOrHostCall"] == "repetCtrlSend"
|
||||||
|
|| item["actionId"] == "dispatch_exception_order"
|
||||||
|
}),
|
||||||
|
"expected action contract for repetCtrlSend dispatch"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
workflow_ir["iterationContract"]["sourceCollection"],
|
||||||
|
"pendingList",
|
||||||
|
"expected iteration contract over pendingList"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
workflow_ir["iterationContract"]["iterationMode"],
|
||||||
|
"sequential_per_item",
|
||||||
|
"expected sequential per-item iteration"
|
||||||
|
);
|
||||||
|
|
||||||
|
let queue_transition_rules = workflow_ir["queueTransitionRules"]
|
||||||
|
.as_array()
|
||||||
|
.expect("expected queueTransitionRules array");
|
||||||
|
assert!(
|
||||||
|
queue_transition_rules.iter().any(|item| {
|
||||||
|
item["triggerPoint"] == "on_empty_collection"
|
||||||
|
|| item["transitionId"] == "queue_continue_on_empty"
|
||||||
|
}),
|
||||||
|
"expected queue continue transition for empty collection"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
queue_transition_rules.iter().any(|item| {
|
||||||
|
item["triggerPoint"] == "on_all_items_done"
|
||||||
|
|| item["transitionId"] == "queue_continue_on_done"
|
||||||
|
}),
|
||||||
|
"expected queue continue transition for completed collection"
|
||||||
|
);
|
||||||
|
|
||||||
|
let log_write_contracts = workflow_ir["logWriteContracts"]
|
||||||
|
.as_array()
|
||||||
|
.expect("expected logWriteContracts array");
|
||||||
|
assert!(
|
||||||
|
log_write_contracts.iter().any(|item| {
|
||||||
|
item["targetEndpointOrHostCall"] == "setDisposeLog"
|
||||||
|
|| item["logId"] == "dispose_log_after_dispatch"
|
||||||
|
}),
|
||||||
|
"expected dispose-log contract"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn generator_preserves_localhost_dependency_as_host_runtime_evidence() {
|
fn generator_preserves_localhost_dependency_as_host_runtime_evidence() {
|
||||||
let analysis = analyze_scene_source(Path::new(
|
let analysis = analyze_scene_source(Path::new(
|
||||||
|
|||||||
@@ -737,6 +737,233 @@ fn binary_wiring_registry_backed_skill_executes_read_only_scripts_with_runtime_i
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_center_preview_reflects_automation_semantics() {
|
||||||
|
let workspace = temp_workspace("sgclaw-command-center-preview-automation-semantics");
|
||||||
|
let trigger_path = workspace.join("scheduled-trigger.json");
|
||||||
|
let output_path = workspace.join("run-record.json");
|
||||||
|
let config_path = workspace.join("sgclaw_config.json");
|
||||||
|
let rules_path = workspace.join("resources").join("rules.json");
|
||||||
|
let materialization_root = workspace.join("materialized");
|
||||||
|
fs::create_dir_all(&materialization_root).unwrap();
|
||||||
|
write_json(
|
||||||
|
&trigger_path,
|
||||||
|
&scheduled_trigger_with_runtime_inputs("monitor_only"),
|
||||||
|
);
|
||||||
|
write_runtime_rules(&rules_path);
|
||||||
|
|
||||||
|
generate_scheduled_monitoring_action_skill_package(
|
||||||
|
GenerateScheduledMonitoringActionSkillRequest {
|
||||||
|
scene_id: "command-center-fee-control-monitor".to_string(),
|
||||||
|
scene_name: "command-center-fee-control-monitor".to_string(),
|
||||||
|
output_root: materialization_root.clone(),
|
||||||
|
source_evidence_json: PathBuf::from(
|
||||||
|
"tests/fixtures/generated_scene/monitoring_action_source_evidence_extraction_2026-04-21.json",
|
||||||
|
),
|
||||||
|
ir_contract_json: PathBuf::from(
|
||||||
|
"tests/fixtures/generated_scene/scheduled_monitoring_action_ir_contract_2026-04-22.json",
|
||||||
|
),
|
||||||
|
trigger_contract_json: PathBuf::from(
|
||||||
|
"tests/fixtures/generated_scene/scheduled_monitoring_action_trigger_runtime_contract_2026-04-22.json",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let detect_payload = json!({
|
||||||
|
"type": "scheduled-monitoring-detect-snapshot",
|
||||||
|
"report_name": "command-center-fee-control-monitor",
|
||||||
|
"status": "detect-ok",
|
||||||
|
"workflowId": "command_center_fee_control_monitoring_action",
|
||||||
|
"mode": "monitor_only",
|
||||||
|
"pendingList": [
|
||||||
|
{ "id": "A1", "consNo": "C1", "phone": "13800000000", "abnorType": "fee_control" }
|
||||||
|
],
|
||||||
|
"inputs": {
|
||||||
|
"source": "browser_attached_live_read",
|
||||||
|
"queryAbnorList": [
|
||||||
|
{ "id": "A1", "consNo": "C1", "phone": "13800000000", "abnorType": "fee_control" }
|
||||||
|
],
|
||||||
|
"queryHistoryEnergyCharge": [],
|
||||||
|
"getMonitorLog": { "lastHandled": "2026-04-22T08:00:00Z" },
|
||||||
|
"getOtherIphones": { "holidaySwitch": "off" },
|
||||||
|
"getAllSubMgtOrgTreeByOrgCode": {}
|
||||||
|
},
|
||||||
|
"localStorageSnapshot": {
|
||||||
|
"loginUserInfo": "{\"orgNo\":\"62401\"}",
|
||||||
|
"markToken": "browser-token",
|
||||||
|
"yxClassList": "[{\"orgNo\":\"62401\"}]",
|
||||||
|
"zhzxFkycSendTime": "2026-04-22 08:00:00"
|
||||||
|
},
|
||||||
|
"readDiagnostics": {
|
||||||
|
"source": "browser_attached_live_read",
|
||||||
|
"businessGatewayReadAttempted": true,
|
||||||
|
"localhostReadAttempted": true,
|
||||||
|
"queryAbnorListCount": 1,
|
||||||
|
"queryHistoryEnergyChargeCount": 0
|
||||||
|
},
|
||||||
|
"dependencySnapshot": {
|
||||||
|
"businessReads": [],
|
||||||
|
"localReads": [],
|
||||||
|
"blockedLocalWrites": [],
|
||||||
|
"blockedCalls": ["repetCtrlSend"]
|
||||||
|
},
|
||||||
|
"sideEffectCounters": {
|
||||||
|
"repetCtrlSend": 0,
|
||||||
|
"sendMessages": 0,
|
||||||
|
"callOutLogin": 0,
|
||||||
|
"audioPlay": 0,
|
||||||
|
"exeTQueue": 0,
|
||||||
|
"productionLogWrite": 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let (browser_ws_url, browser_server) =
|
||||||
|
start_callback_host_scheduled_monitoring_browser_server(detect_payload);
|
||||||
|
write_browser_config(&config_path, &browser_ws_url);
|
||||||
|
|
||||||
|
let output = run_binary_with_skills_dir_and_config(
|
||||||
|
&trigger_path,
|
||||||
|
&materialization_root.join("skills"),
|
||||||
|
&config_path,
|
||||||
|
&workspace,
|
||||||
|
&output_path,
|
||||||
|
);
|
||||||
|
browser_server.join().unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"stdout={}\nstderr={}",
|
||||||
|
String::from_utf8_lossy(&output.stdout),
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
let record: Value = serde_json::from_str(&fs::read_to_string(output_path).unwrap()).unwrap();
|
||||||
|
let preview_artifact = &record["previewArtifact"];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
preview_artifact["actionPlan"][0]["actionContractRef"],
|
||||||
|
"dispatch_exception_order"
|
||||||
|
);
|
||||||
|
assert_eq!(preview_artifact["summary"]["queue_transition_count"], 1);
|
||||||
|
assert_eq!(
|
||||||
|
preview_artifact["queueTransitions"][0]["transitionId"],
|
||||||
|
"queue_continue_on_done"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
preview_artifact["logWritePreview"][0]["logId"],
|
||||||
|
"dispose_log_after_dispatch"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_center_empty_pending_list_does_not_emit_log_write_preview() {
|
||||||
|
let workspace = temp_workspace("sgclaw-command-center-empty-preview-semantics");
|
||||||
|
let trigger_path = workspace.join("scheduled-trigger.json");
|
||||||
|
let output_path = workspace.join("run-record.json");
|
||||||
|
let config_path = workspace.join("sgclaw_config.json");
|
||||||
|
let rules_path = workspace.join("resources").join("rules.json");
|
||||||
|
let materialization_root = workspace.join("materialized");
|
||||||
|
fs::create_dir_all(&materialization_root).unwrap();
|
||||||
|
write_json(
|
||||||
|
&trigger_path,
|
||||||
|
&scheduled_trigger_with_runtime_inputs("monitor_only"),
|
||||||
|
);
|
||||||
|
write_runtime_rules(&rules_path);
|
||||||
|
|
||||||
|
generate_scheduled_monitoring_action_skill_package(
|
||||||
|
GenerateScheduledMonitoringActionSkillRequest {
|
||||||
|
scene_id: "command-center-fee-control-monitor".to_string(),
|
||||||
|
scene_name: "command-center-fee-control-monitor".to_string(),
|
||||||
|
output_root: materialization_root.clone(),
|
||||||
|
source_evidence_json: PathBuf::from(
|
||||||
|
"tests/fixtures/generated_scene/monitoring_action_source_evidence_extraction_2026-04-21.json",
|
||||||
|
),
|
||||||
|
ir_contract_json: PathBuf::from(
|
||||||
|
"tests/fixtures/generated_scene/scheduled_monitoring_action_ir_contract_2026-04-22.json",
|
||||||
|
),
|
||||||
|
trigger_contract_json: PathBuf::from(
|
||||||
|
"tests/fixtures/generated_scene/scheduled_monitoring_action_trigger_runtime_contract_2026-04-22.json",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let detect_payload = json!({
|
||||||
|
"type": "scheduled-monitoring-detect-snapshot",
|
||||||
|
"report_name": "command-center-fee-control-monitor",
|
||||||
|
"status": "detect-ok",
|
||||||
|
"workflowId": "command_center_fee_control_monitoring_action",
|
||||||
|
"mode": "monitor_only",
|
||||||
|
"pendingList": [],
|
||||||
|
"inputs": {
|
||||||
|
"source": "browser_attached_live_read",
|
||||||
|
"queryAbnorList": [],
|
||||||
|
"queryHistoryEnergyCharge": [],
|
||||||
|
"getMonitorLog": {},
|
||||||
|
"getOtherIphones": {},
|
||||||
|
"getAllSubMgtOrgTreeByOrgCode": {}
|
||||||
|
},
|
||||||
|
"localStorageSnapshot": {
|
||||||
|
"loginUserInfo": "{\"orgNo\":\"62401\"}",
|
||||||
|
"markToken": "browser-token",
|
||||||
|
"yxClassList": "[{\"orgNo\":\"62401\"}]",
|
||||||
|
"zhzxFkycSendTime": "2026-04-22 08:00:00"
|
||||||
|
},
|
||||||
|
"readDiagnostics": {
|
||||||
|
"source": "browser_attached_live_read",
|
||||||
|
"businessGatewayReadAttempted": true,
|
||||||
|
"localhostReadAttempted": true,
|
||||||
|
"queryAbnorListCount": 0,
|
||||||
|
"queryHistoryEnergyChargeCount": 0
|
||||||
|
},
|
||||||
|
"dependencySnapshot": {
|
||||||
|
"businessReads": [],
|
||||||
|
"localReads": [],
|
||||||
|
"blockedLocalWrites": [],
|
||||||
|
"blockedCalls": ["repetCtrlSend"]
|
||||||
|
},
|
||||||
|
"sideEffectCounters": {
|
||||||
|
"repetCtrlSend": 0,
|
||||||
|
"sendMessages": 0,
|
||||||
|
"callOutLogin": 0,
|
||||||
|
"audioPlay": 0,
|
||||||
|
"exeTQueue": 0,
|
||||||
|
"productionLogWrite": 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let (browser_ws_url, browser_server) =
|
||||||
|
start_callback_host_scheduled_monitoring_browser_server(detect_payload);
|
||||||
|
write_browser_config(&config_path, &browser_ws_url);
|
||||||
|
|
||||||
|
let output = run_binary_with_skills_dir_and_config(
|
||||||
|
&trigger_path,
|
||||||
|
&materialization_root.join("skills"),
|
||||||
|
&config_path,
|
||||||
|
&workspace,
|
||||||
|
&output_path,
|
||||||
|
);
|
||||||
|
browser_server.join().unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"stdout={}\nstderr={}",
|
||||||
|
String::from_utf8_lossy(&output.stdout),
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
let record: Value = serde_json::from_str(&fs::read_to_string(output_path).unwrap()).unwrap();
|
||||||
|
let preview_artifact = &record["previewArtifact"];
|
||||||
|
|
||||||
|
assert_eq!(preview_artifact["summary"]["pending_count"], 0);
|
||||||
|
assert_eq!(preview_artifact["summary"]["queue_transition_count"], 1);
|
||||||
|
assert_eq!(
|
||||||
|
preview_artifact["queueTransitions"][0]["transitionId"],
|
||||||
|
"queue_continue_on_empty"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
preview_artifact["logWritePreview"].as_array().unwrap().len(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn binary_wiring_browser_attached_passes_platform_service_base_from_config() {
|
fn binary_wiring_browser_attached_passes_platform_service_base_from_config() {
|
||||||
let workspace = temp_workspace("sgclaw-scheduled-monitoring-binary-platform-service-base");
|
let workspace = temp_workspace("sgclaw-scheduled-monitoring-binary-platform-service-base");
|
||||||
|
|||||||
@@ -279,6 +279,58 @@ fn fee_control_materialization_emits_timeout_contract_in_generated_artifacts() {
|
|||||||
assert!(detect_script.contains("statusOnTimeout"));
|
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]
|
#[test]
|
||||||
fn available_balance_and_archive_materialization_emit_explicit_dependency_classification_in_references() {
|
fn available_balance_and_archive_materialization_emit_explicit_dependency_classification_in_references() {
|
||||||
let example_output = Command::new("cargo")
|
let example_output = Command::new("cargo")
|
||||||
|
|||||||
Reference in New Issue
Block a user