#[tokio::test]
async fn automations_v2_run_recover_uses_completion_assertion_node_detail() {
let state = test_state().await;
let app = app_router(state.clone());
let automation =
create_branched_test_automation_v2(&state, "auto-v2-completion-assertion").await;
let run = state
.create_automation_v2_run(&automation, "manual")
.await
.expect("run");
state
.update_automation_v2_run(&run.run_id, |row| {
row.status = crate::AutomationRunStatus::Failed;
row.detail = Some(
"automation run missing successful external action receipt for outbound node `publish`"
.to_string(),
);
row.checkpoint.completed_nodes = vec![
"research".to_string(),
"analysis".to_string(),
"draft".to_string(),
"publish".to_string(),
];
row.checkpoint.pending_nodes.clear();
for node_id in ["research", "analysis", "draft", "publish"] {
row.checkpoint.node_outputs.insert(
node_id.to_string(),
json!({"status":"completed","summary":node_id}),
);
row.checkpoint.node_attempts.insert(node_id.to_string(), 1);
}
row.checkpoint.last_failure = None;
})
.await
.expect("updated run");
let resp = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/automations/v2/runs/{}/recover", run.run_id))
.header("content-type", "application/json")
.body(Body::from(
json!({ "reason": "recover completion assertion failure" }).to_string(),
))
.expect("request"),
)
.await
.expect("response");
assert_eq!(resp.status(), StatusCode::OK);
let recovered = state
.get_automation_v2_run(&run.run_id)
.await
.expect("run after recover");
assert_eq!(recovered.status, crate::AutomationRunStatus::Queued);
for node_id in ["research", "analysis", "draft"] {
assert!(recovered
.checkpoint
.completed_nodes
.iter()
.any(|completed| completed == node_id));
assert!(recovered.checkpoint.node_outputs.contains_key(node_id));
}
assert!(!recovered
.checkpoint
.completed_nodes
.iter()
.any(|node_id| node_id == "publish"));
assert!(recovered
.checkpoint
.pending_nodes
.iter()
.any(|node_id| node_id == "publish"));
assert!(!recovered.checkpoint.node_outputs.contains_key("publish"));
assert!(!recovered.checkpoint.node_attempts.contains_key("publish"));
}
#[tokio::test]
async fn automations_v2_run_recover_resets_terminal_node_for_run_level_deliverable_assertion() {
let state = test_state().await;
let app = app_router(state.clone());
let automation =
create_branched_test_automation_v2(&state, "auto-v2-run-level-deliverable").await;
let run = state
.create_automation_v2_run(&automation, "manual")
.await
.expect("run");
state
.update_automation_v2_run(&run.run_id, |row| {
row.status = crate::AutomationRunStatus::Failed;
row.detail = Some(
"automation run deliverable `customer.incident_reported` lacks current-run output evidence"
.to_string(),
);
row.checkpoint.completed_nodes = vec![
"research".to_string(),
"analysis".to_string(),
"draft".to_string(),
"publish".to_string(),
];
row.checkpoint.pending_nodes.clear();
for node_id in ["research", "analysis", "draft", "publish"] {
row.checkpoint.node_outputs.insert(
node_id.to_string(),
json!({"status":"completed","summary":node_id}),
);
row.checkpoint.node_attempts.insert(node_id.to_string(), 1);
}
row.checkpoint.last_failure = None;
})
.await
.expect("updated run");
let resp = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/automations/v2/runs/{}/recover", run.run_id))
.header("content-type", "application/json")
.body(Body::from(
json!({ "reason": "reconcile run-level deliverables" }).to_string(),
))
.expect("request"),
)
.await
.expect("response");
assert_eq!(resp.status(), StatusCode::OK);
let recovered = state
.get_automation_v2_run(&run.run_id)
.await
.expect("run after recover");
assert_eq!(recovered.status, crate::AutomationRunStatus::Queued);
for node_id in ["research", "analysis", "draft"] {
assert!(recovered
.checkpoint
.completed_nodes
.iter()
.any(|completed| completed == node_id));
assert!(recovered.checkpoint.node_outputs.contains_key(node_id));
assert_eq!(recovered.checkpoint.node_attempts.get(node_id), Some(&1));
}
assert!(!recovered
.checkpoint
.completed_nodes
.iter()
.any(|node_id| node_id == "publish"));
assert!(recovered
.checkpoint
.pending_nodes
.iter()
.any(|node_id| node_id == "publish"));
assert!(!recovered.checkpoint.node_outputs.contains_key("publish"));
assert!(!recovered.checkpoint.node_attempts.contains_key("publish"));
assert!(recovered
.checkpoint
.lifecycle_history
.iter()
.any(|entry| entry.event == "run_recovered"));
}