diff --git a/hive-c0re/src/job_queue/tests.rs b/hive-c0re/src/job_queue/tests.rs index ba46586c..53d2ee55 100644 --- a/hive-c0re/src/job_queue/tests.rs +++ b/hive-c0re/src/job_queue/tests.rs @@ -926,6 +926,88 @@ fn cancelled_dag_finalizes_with_terminal_rollup() { ); } +// ---- approval deploy subtree ---- + +/// The config-PR deploy is a subtree, not one opaque node. The +/// resource-holding root completes immediately (its `Finishing` state is the +/// parent gate that releases the children), then the phases run strictly in +/// order — and the `AfterAny` tail still runs when the irreversible half fails, +/// because it's the node that compensates for it. +#[test] +fn deploy_dag_runs_phases_in_order_and_tails_a_failed_apply() { + let q = JobQueue::new(1); + let id = submit( + &q, + templates::approval_deploy("agent-a", 7, "approval #7".to_owned()), + ); + + let root = claim_one(&q); + assert!( + matches!(root.kind, NodeKind::DeployWindow { .. }), + "root claims first: it holds the meta window for the whole subtree" + ); + q.complete_node(id, root.node_id, Ok(())); + + let verify = claim_one(&q); + assert!(matches!(verify.kind, NodeKind::MergeVerify { .. })); + q.complete_node(id, verify.node_id, Ok(())); + + let apply = claim_one(&q); + assert!(matches!(apply.kind, NodeKind::DeployApply { .. })); + q.complete_node( + id, + apply.node_id, + Err("nixos-container update blew up".into()), + ); + + let tail = claim_one(&q); + assert!( + matches!(tail.kind, NodeKind::DeployTail { .. }), + "AfterAny tail runs on a failed apply — that's the whole point of it" + ); + q.complete_node(id, tail.node_id, Ok(())); + + let summary = q.terminal_summary(id).expect("dag terminal"); + assert_eq!( + summary.state, + State::Failed, + "an Ok tail must not launder a failed deploy into a success" + ); + assert_eq!(summary.approval_id, Some(7)); +} + +/// A pre-merge rejection (drift gate, eval failure) cancel-cascades the +/// irreversible half via its `AfterOk` edge, but the tail is still reached — +/// it owns the forge mirror, not just compensation. +#[test] +fn deploy_dag_skips_apply_but_still_runs_tail_when_verify_fails() { + let q = JobQueue::new(1); + let id = submit( + &q, + templates::approval_deploy("agent-a", 9, "approval #9".to_owned()), + ); + + let root = claim_one(&q); + q.complete_node(id, root.node_id, Ok(())); + let verify = claim_one(&q); + q.complete_node( + id, + verify.node_id, + Err("PR head drifted since review".into()), + ); + + let tail = claim_one(&q); + assert!( + matches!(tail.kind, NodeKind::DeployTail { .. }), + "apply is cancel-cascaded, so the tail is the next claimable node" + ); + q.complete_node(id, tail.node_id, Ok(())); + + let summary = q.terminal_summary(id).expect("dag terminal"); + assert_eq!(summary.state, State::Failed); + assert_eq!(summary.approval_id, Some(9)); +} + // ---- steps, build logs, history ---- #[test]