diff --git a/hive-c0re/src/rebuild_queue.rs b/hive-c0re/src/rebuild_queue.rs index 20d0a13..bec9664 100644 --- a/hive-c0re/src/rebuild_queue.rs +++ b/hive-c0re/src/rebuild_queue.rs @@ -220,11 +220,18 @@ impl RebuildQueue { /// or — on dedup — the existing entry's id with the new reason /// appended). /// - /// Dedup rule: a `Queued` entry with the same `(kind, agent)` swallows - /// the new request and returns its existing id. Running and terminal - /// entries do not dedup — operators are free to re-queue a rebuild - /// that's currently running (something changed since it started) or - /// re-run one that just finished. + /// Dedup rule: + /// - `Rebuild` / `Spawn` / `Destroy`: a `Queued` entry with the same + /// `(kind, agent)` swallows the new request. + /// - `MetaUpdate`: dedup ALSO requires the `inputs` field to match — + /// two meta-updates with different input lists are distinct work + /// and must queue separately (closes #365: previously the second + /// meta-update collapsed into the first whenever it was still + /// `Queued`, losing the second's input set). + /// + /// Running and terminal entries never dedup — operators are free + /// to re-queue a rebuild that's currently running (something + /// changed since it started) or re-run one that just finished. pub fn enqueue( &self, kind: QueueKind, @@ -238,7 +245,9 @@ impl RebuildQueue { /// Same as `enqueue` but carries an `inputs` payload — used by /// `MetaUpdate` enqueues to tell the worker which meta-flake - /// inputs to bump. + /// inputs to bump. For `MetaUpdate` the `inputs` value is part of + /// the dedup key (two meta-updates with different inputs are + /// distinct operations, see #365). pub fn enqueue_with_inputs( &self, kind: QueueKind, @@ -249,9 +258,15 @@ impl RebuildQueue { inputs: Vec, ) -> u64 { let mut inner = self.inner.lock().expect("rebuild_queue mutex poisoned"); - // Dedup against a pending entry with the same (kind, agent). + // Dedup against a pending entry with the same (kind, agent) — + // and, for MetaUpdate, the same `inputs` list (see method + // docstring + #365 for why). for entry in inner.entries.iter_mut() { - if entry.state == QueueState::Queued && entry.kind == kind && entry.agent == agent { + if entry.state == QueueState::Queued + && entry.kind == kind + && entry.agent == agent + && (kind != QueueKind::MetaUpdate || entry.inputs == inputs) + { if !entry.reason.contains(&reason) { entry.reason.push_str(&format!("\nalso requested by: {reason}")); } @@ -619,6 +634,62 @@ mod tests { assert!(snap[0].reason.contains("auto sweep")); } + #[test] + fn meta_update_dedup_matches_inputs() { + // Two MetaUpdate enqueues with identical inputs → dedup (#365). + let q = RebuildQueue::new(); + let a = q.enqueue_with_inputs( + QueueKind::MetaUpdate, + "hyperhive".to_owned(), + QueueSource::Manual, + "first".to_owned(), + None, + vec!["nixpkgs".to_owned()], + ); + let b = q.enqueue_with_inputs( + QueueKind::MetaUpdate, + "hyperhive".to_owned(), + QueueSource::Manual, + "duplicate click".to_owned(), + None, + vec!["nixpkgs".to_owned()], + ); + assert_eq!(a, b, "identical-inputs meta-updates should dedup"); + assert_eq!(q.snapshot().len(), 1); + } + + #[test] + fn meta_update_dedup_separates_distinct_inputs() { + // Two MetaUpdate enqueues with DIFFERENT inputs → distinct + // entries, not deduped (the actual #365 bug). + let q = RebuildQueue::new(); + let a = q.enqueue_with_inputs( + QueueKind::MetaUpdate, + "hyperhive".to_owned(), + QueueSource::Manual, + "bump nixpkgs".to_owned(), + None, + vec!["nixpkgs".to_owned()], + ); + let b = q.enqueue_with_inputs( + QueueKind::MetaUpdate, + "hyperhive".to_owned(), + QueueSource::Manual, + "bump bitburner-agent".to_owned(), + None, + vec!["agent-bitburner/bitburner-agent".to_owned()], + ); + assert_ne!(a, b, "different-inputs meta-updates must NOT dedup"); + let snap = q.snapshot(); + assert_eq!(snap.len(), 2); + // Both inputs lists are preserved. + let inputs: Vec<&[String]> = snap.iter().map(|e| e.inputs.as_slice()).collect(); + assert!(inputs.iter().any(|i| *i == ["nixpkgs"])); + assert!(inputs + .iter() + .any(|i| *i == ["agent-bitburner/bitburner-agent"])); + } + #[test] fn dedup_does_not_apply_across_kinds_or_agents() { let q = RebuildQueue::new();