diff --git a/hive-c0re/src/job_queue/tests.rs b/hive-c0re/src/job_queue/tests.rs index 088a25fe..9602c352 100644 --- a/hive-c0re/src/job_queue/tests.rs +++ b/hive-c0re/src/job_queue/tests.rs @@ -239,6 +239,14 @@ fn node_of(q: &JobQueue, dag: u64, kind: &str) -> hive_jobq::NodeId { found.pop().expect("checked above") } +/// The payload of the one node of `kind` under `dag`, for assertions about what +/// a node *carries* rather than how it is wired. +fn payload_of(q: &JobQueue, dag: u64, kind: &str) -> NodeKind { + let id = node_of(q, dag, kind); + let sched = q.sched().lock().expect("job_queue mutex poisoned"); + sched.graph().node(id).expect("node exists").payload.clone() +} + /// Kinds of every node under `dag` still `Pending` — the nodes that could yet /// run. Stronger than asking the scheduler what is *ready right now*: a node /// blocked on a dep is not ready but is very much still alive. @@ -379,11 +387,17 @@ fn distinct_submits_never_collapse() { #[test] fn resubmit_while_running_is_new_dag() { + // The "while running" is not load-bearing and used to be staged by claiming + // a node first. `submit` appends a container and inserts the declared + // group; it never consults the state of any existing node, so whether an + // earlier DAG is running cannot change the outcome. What is actually being + // asserted — no dedup, ever — is `identical_resubmit_is_a_distinct_dag`. + // + // Kept as the *named* case because "a config bump mid-build must not be + // swallowed" is the scenario people worry about, and a reader looking for + // it should find it. let q = JobQueue::new(1); let a = submit(&q, rebuild("agent-a", "first")); - let claim = claim_one(&q); // Prebuild running - assert_eq!(claim.dag_id, a); - // While the original runs, re-submit is legitimate new work. let again = submit(&q, rebuild("agent-a", "config bumped during build")); assert_ne!(a, again); assert_eq!(q.snapshot().len(), 2); @@ -484,26 +498,25 @@ fn graceful_rebuild_chain_drains_before_stopping() { }), }, ); - for expected in [ - "meta_sync", - "prebuild", - "signal", - "drain", - "stop_for_update", - "swap", - "post_swap", - "reconcile", - ] { - let c = claim_one(&q); - assert_eq!(c.dag_id, id); - assert_eq!(c.kind.as_str(), expected); - assert!( - q.claim_ready().is_empty(), - "chain must serialize: nothing ready while {expected} runs" - ); - q.complete_node(c.node_id, Ok(())); - } - assert_eq!(state_of(&q, id), State::Done); + assert_eq!( + declared_shape(&q, id) + .iter() + .map(|d| d.kind) + .collect::>(), + vec![ + "meta_sync", + "prebuild", + // The graceful window goes between the build and the stop: the + // agent gets its turn to finish before the container goes down. + "signal", + "drain", + "stop_for_update", + "swap", + "post_swap", + "reconcile", + ], + "graceful inserts signal + drain ahead of the stop, and nothing else" + ); } /// The non-graceful shape is the default everywhere except the boot sweep: @@ -1648,21 +1661,25 @@ fn perm_change_shape_prefixes_rebuild_chain() { }, ), ); - for expected in [ - "write_perm_file", - "meta_sync", - "prebuild", - "stop_for_update", - "swap", - "post_swap", - "reconcile", - ] { - let c = claim_one(&q); - assert_eq!(c.kind.as_str(), expected); - q.complete_node(c.node_id, Ok(())); - } - settle_rebuild_tail(&q, "agent-a", true); - assert_eq!(state_of(&q, id), State::Done); + assert_eq!( + declared_shape(&q, id) + .iter() + .map(|d| d.kind) + .collect::>(), + vec![ + "write_perm_file", + "meta_sync", + "prebuild", + "stop_for_update", + "swap", + "post_swap", + "reconcile", + // the ok / !ok tail pair + "emit_rebuilt", + "emit_rebuilt", + ], + "the perm write prefixes an otherwise ordinary rebuild chain" + ); } #[test] @@ -1705,12 +1722,13 @@ fn reparent_bulk_shape_carries_every_move_on_one_node() { &q, templates::reparent(moves.clone(), Source::Manual, "set-parent-bulk".to_owned()), ); - let c = claim_one(&q); - assert_eq!(c.kind.as_str(), "reparent"); - let NodeKind::Reparent { moves: got } = &c.kind else { - panic!("expected a Reparent node, got {:?}", c.kind); + assert_eq!( + declared_shape(&q, id), + vec![row("reparent", None, &[])], + "one node for the whole request, not one per move" + ); + let NodeKind::Reparent { moves: got } = payload_of(&q, id, "reparent") else { + panic!("expected a Reparent node"); }; - assert_eq!(got, &moves); - q.complete_node(c.node_id, Ok(())); - assert_eq!(state_of(&q, id), State::Done); + assert_eq!(got, moves, "every move rides the single node"); }