From 3130e56cfb8be7ca27ba9fc332b3e8f6178251f5 Mon Sep 17 00:00:00 2001 From: damocles Date: Tue, 9 Jun 2026 09:19:40 +0200 Subject: [PATCH] refactor(#1474): replace too_many_arguments allows on pub fns with param structs --- hive-ag3nt/src/bin/hive.rs | 14 +- hive-ag3nt/src/serve_common.rs | 42 ++-- hive-c0re/src/actions.rs | 96 +++++---- hive-c0re/src/agent_server.rs | 51 +++-- hive-c0re/src/coordinator.rs | 80 ++++--- hive-c0re/src/dashboard/approvals.rs | 16 +- hive-c0re/src/manager_server.rs | 99 +++++---- hive-c0re/src/questions.rs | 26 ++- hive-c0re/src/rebuild_queue.rs | 307 ++++++++++++++------------- 9 files changed, 407 insertions(+), 324 deletions(-) diff --git a/hive-ag3nt/src/bin/hive.rs b/hive-ag3nt/src/bin/hive.rs index 45e76995..5193631f 100644 --- a/hive-ag3nt/src/bin/hive.rs +++ b/hive-ag3nt/src/bin/hive.rs @@ -533,17 +533,17 @@ async fn handle_turn( let ended_at = serve_common::now_unix(); let duration_ms = i64::try_from(started_instant.elapsed().as_millis()).unwrap_or(i64::MAX); let (open_threads, open_reminders) = S::post_turn_counts(socket).await; - let row = serve_common::build_row( + let row = serve_common::build_row(serve_common::TurnRowArgs { started_at, ended_at, duration_ms, - model_at_start, - from.clone(), - &outcome, + model: model_at_start, + wake_from: from.clone(), + outcome: &outcome, bus, - open_threads, - open_reminders, - ); + open_threads_count: open_threads, + open_reminders_count: open_reminders, + }); stats.record(&row); } let pending = S::inbox_unread(socket).await; diff --git a/hive-ag3nt/src/serve_common.rs b/hive-ag3nt/src/serve_common.rs index eaf8a529..b669c8e4 100644 --- a/hive-ag3nt/src/serve_common.rs +++ b/hive-ag3nt/src/serve_common.rs @@ -36,26 +36,36 @@ pub fn now_unix() -> i64 { .unwrap_or(0) } +/// Field-named args for [`build_row`]. Mirrors the turn-stats row +/// columns; `outcome` and `bus` borrow for the duration of the call. +pub struct TurnRowArgs<'a> { + pub started_at: i64, + pub ended_at: i64, + pub duration_ms: i64, + pub model: String, + pub wake_from: String, + pub outcome: &'a TurnOutcome, + pub bus: &'a Bus, + pub open_threads_count: Option, + pub open_reminders_count: Option, +} + /// Assemble a `TurnStatRow` from the harness's per-turn state. Used by both /// the agent and manager serve loops — the shape is identical, only the /// post-turn count fetch helpers differ (and those stay in each binary). #[must_use] -#[allow( - clippy::too_many_arguments, - reason = "args mirror the turn-stats row columns 1:1; a builder struct used \ - only here would just relabel the same fields" -)] -pub fn build_row( - started_at: i64, - ended_at: i64, - duration_ms: i64, - model: String, - wake_from: String, - outcome: &TurnOutcome, - bus: &Bus, - open_threads_count: Option, - open_reminders_count: Option, -) -> TurnStatRow { +pub fn build_row(args: TurnRowArgs<'_>) -> TurnStatRow { + let TurnRowArgs { + started_at, + ended_at, + duration_ms, + model, + wake_from, + outcome, + bus, + open_threads_count, + open_reminders_count, + } = args; // Prefer the API-resolved model id (e.g. `claude-opus-4-8`) captured // from this turn's assistant events over the requested `--model` // name/alias, so the model-mix + cost rollup label the concrete diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index c1f8bb44..1cd0c79d 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -46,17 +46,19 @@ pub async fn approve(coord: Arc, id: i64) -> Result<()> { run_approval_init_config(&coord, approval, proposed_dir, claude_dir, notes_dir).await } ApprovalKind::ApplyCommit => { - coord.rebuild_queue.enqueue_full( - crate::rebuild_queue::QueueKind::Rebuild, - approval.agent.clone(), - crate::rebuild_queue::QueueSource::Approval, - format!("approval #{id} apply commit"), - None, - Vec::new(), - Some(id), - None, - Vec::new(), - ); + coord + .rebuild_queue + .enqueue_full(crate::rebuild_queue::FullEnqueue { + kind: crate::rebuild_queue::QueueKind::Rebuild, + agent: approval.agent.clone(), + source: crate::rebuild_queue::QueueSource::Approval, + reason: format!("approval #{id} apply commit"), + parent_id: None, + inputs: Vec::new(), + approval_id: Some(id), + perm_payload: None, + depends_on: Vec::new(), + }); coord.emit_rebuild_queue_snapshot(); Ok(()) } @@ -66,17 +68,19 @@ pub async fn approve(coord: Arc, id: i64) -> Result<()> { // dashboard can show *which* inputs are about to bump. let inputs: Vec = serde_json::from_str(&approval.commit_ref).unwrap_or_default(); - let parent_id = coord.rebuild_queue.enqueue_full( - crate::rebuild_queue::QueueKind::MetaUpdate, - approval.agent.clone(), - crate::rebuild_queue::QueueSource::Approval, - format!("approval #{id} meta input update"), - None, - inputs.clone(), - Some(id), - None, - Vec::new(), - ); + let parent_id = coord + .rebuild_queue + .enqueue_full(crate::rebuild_queue::FullEnqueue { + kind: crate::rebuild_queue::QueueKind::MetaUpdate, + agent: approval.agent.clone(), + source: crate::rebuild_queue::QueueSource::Approval, + reason: format!("approval #{id} meta input update"), + parent_id: None, + inputs: inputs.clone(), + approval_id: Some(id), + perm_payload: None, + depends_on: Vec::new(), + }); // Pre-enqueue cascade rebuilds in topological order so // agents depending on updated inputs are rebuilt after the // lock bump, matching the dashboard post_meta_update path. @@ -95,17 +99,19 @@ pub async fn approve(coord: Arc, id: i64) -> Result<()> { Ok(()) } ApprovalKind::Spawn => { - coord.rebuild_queue.enqueue_full( - crate::rebuild_queue::QueueKind::Spawn, - approval.agent.clone(), - crate::rebuild_queue::QueueSource::Approval, - format!("approval #{id} spawn"), - None, - Vec::new(), - Some(id), - None, - Vec::new(), - ); + coord + .rebuild_queue + .enqueue_full(crate::rebuild_queue::FullEnqueue { + kind: crate::rebuild_queue::QueueKind::Spawn, + agent: approval.agent.clone(), + source: crate::rebuild_queue::QueueSource::Approval, + reason: format!("approval #{id} spawn"), + parent_id: None, + inputs: Vec::new(), + approval_id: Some(id), + perm_payload: None, + depends_on: Vec::new(), + }); coord.emit_rebuild_queue_snapshot(); Ok(()) } @@ -363,15 +369,15 @@ fn finish_approval( .as_deref() .map(|s| s[..s.len().min(12)].to_owned()); let status_str = if ok { "approved" } else { "failed" }; - coord.emit_approval_resolved( - approval.id, - &approval.agent, + coord.emit_approval_resolved(crate::coordinator::ApprovalResolved { + id: approval.id, + agent: &approval.agent, approval_kind, sha_short, - status_str, - note.clone(), - approval.description.clone(), - ); + status: status_str, + note: note.clone(), + description: approval.description.clone(), + }); // For spawn/rebuild/init_config approvals, also surface the underlying // action so the manager knows whether the lifecycle step succeeded. // The ApprovalResolved event already carries the same `ok` signal but @@ -756,15 +762,15 @@ pub async fn deny(coord: &Coordinator, id: i64, note: Option<&str>) -> Result<() sha, tag, }); - coord.emit_approval_resolved( + coord.emit_approval_resolved(crate::coordinator::ApprovalResolved { id, - &agent_owned, + agent: &agent_owned, approval_kind, sha_short, - "denied", - note.map(String::from), + status: "denied", + note: note.map(String::from), description, - ); + }); } Ok(()) } diff --git a/hive-c0re/src/agent_server.rs b/hive-c0re/src/agent_server.rs index 2949de69..0a6345c1 100644 --- a/hive-c0re/src/agent_server.rs +++ b/hive-c0re/src/agent_server.rs @@ -298,7 +298,19 @@ pub(crate) async fn dispatch_shared( since, until, } => { - dispatch_host_journal(agent, unit, container, lines, priority, grep, since, until).await + dispatch_host_journal( + agent, + HostJournalArgs { + unit, + container, + lines, + priority, + grep, + since, + until, + }, + ) + .await } // Not a shared variant. _ => return None, @@ -561,21 +573,28 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc) -> /// /// The manager is not exempt - grant `read_host_journal` in /// `meta/capabilities.json` to enable it for any agent including the manager. -#[allow( - clippy::too_many_arguments, - reason = "args mirror the GetHostJournal wire variant 1:1 at this single \ - call site; a params struct would just relabel the same fields" -)] -pub async fn dispatch_host_journal( - agent: &str, - unit: &Option, - container: &Option, - lines: &Option, - priority: &Option, - grep: &Option, - since: &Option, - until: &Option, -) -> AgentResponse { +/// Field-named journal-query knobs for [`dispatch_host_journal`]. +/// Borrows straight from the matched `GetHostJournal` request variant. +pub struct HostJournalArgs<'a> { + pub unit: &'a Option, + pub container: &'a Option, + pub lines: &'a Option, + pub priority: &'a Option, + pub grep: &'a Option, + pub since: &'a Option, + pub until: &'a Option, +} + +pub async fn dispatch_host_journal(agent: &str, args: HostJournalArgs<'_>) -> AgentResponse { + let HostJournalArgs { + unit, + container, + lines, + priority, + grep, + since, + until, + } = args; if !crate::capabilities::has_cap(agent, hive_sh4re::Capability::ReadHostJournal) { return AgentResponse::Err { message: "agent does not have the read_host_journal capability".to_owned(), diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index 60a0286e..69487a03 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -349,6 +349,33 @@ impl TransientKind { } } +/// Field-named payload for [`Coordinator::emit_approval_resolved`]. +/// Mirrors the `ApprovalResolved` dashboard-event fields. `agent` +/// borrows from the caller; `approval_kind` / `status` are +/// compile-time constants. +pub struct ApprovalResolved<'a> { + pub id: i64, + pub agent: &'a str, + pub approval_kind: &'static str, + pub sha_short: Option, + pub status: &'static str, + pub note: Option, + pub description: Option, +} + +/// Field-named payload for [`Coordinator::emit_question_added`]. +/// Mirrors the `QuestionAdded` dashboard-event fields; all references +/// share the caller's lifetime. +pub struct QuestionAdded<'a> { + pub id: i64, + pub asker: &'a str, + pub question: &'a str, + pub options: &'a [String], + pub multi: bool, + pub deadline_at: Option, + pub target: Option<&'a str>, +} + impl Coordinator { pub fn open( db_path: &Path, @@ -652,21 +679,19 @@ impl Coordinator { /// already have an authoritative timestamp from the db update, /// the tiny skew between "row updated" and "event emitted" is /// presentation-only and doesn't matter to clients. - #[allow( - clippy::too_many_arguments, - reason = "args mirror the approval-resolved event payload fields; \ - bundling them into a struct used only here adds no clarity" - )] - pub fn emit_approval_resolved( - &self, - id: i64, - agent: &str, - approval_kind: &'static str, - sha_short: Option, - status: &'static str, - note: Option, - description: Option, - ) { + /// + /// Takes [`ApprovalResolved`] rather than a positional arg list so + /// the seven fields are named at every call site. + pub fn emit_approval_resolved(&self, ev: ApprovalResolved<'_>) { + let ApprovalResolved { + id, + agent, + approval_kind, + sha_short, + status, + note, + description, + } = ev; let resolved_at = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .ok() @@ -689,21 +714,16 @@ impl Coordinator { /// both operator-targeted (`target = None`) and peer-to-peer /// (`target = Some(agent)`) threads — the dashboard surfaces /// both, distinguishing visually + offering operator override. - #[allow( - clippy::too_many_arguments, - reason = "args mirror the question-added event payload fields; \ - bundling them into a struct used only here adds no clarity" - )] - pub fn emit_question_added( - &self, - id: i64, - asker: &str, - question: &str, - options: &[String], - multi: bool, - deadline_at: Option, - target: Option<&str>, - ) { + pub fn emit_question_added(&self, ev: &QuestionAdded<'_>) { + let &QuestionAdded { + id, + asker, + question, + options, + multi, + deadline_at, + target, + } = ev; let asked_at = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .ok() diff --git a/hive-c0re/src/dashboard/approvals.rs b/hive-c0re/src/dashboard/approvals.rs index 3990678c..c523f728 100644 --- a/hive-c0re/src/dashboard/approvals.rs +++ b/hive-c0re/src/dashboard/approvals.rs @@ -82,15 +82,15 @@ pub(super) fn gc_orphans(coord: &Coordinator, approvals: Vec) -> Vec) -> ManagerResp coord, MANAGER_AGENT, *id, - body.clone(), - description.clone(), - *interval_seconds, - *next_fire_at_unix, - targets_add.clone(), - targets_remove.clone(), + EditSchedulePatch { + body: body.clone(), + description: description.clone(), + interval_seconds: *interval_seconds, + next_fire_at_unix: *next_fire_at_unix, + targets_add: targets_add.clone(), + targets_remove: targets_remove.clone(), + }, ), ManagerRequest::ListSchedules => match coord.scheduled_prompts.list() { Ok(schedules) => ManagerResponse::Schedules { @@ -425,15 +427,15 @@ pub(crate) async fn submit_apply_commit( // explanation of why the approval can't be approved. let note = format!("{e:#}"); let _ = coord.approvals.mark_failed(id, ¬e); - coord.emit_approval_resolved( + coord.emit_approval_resolved(crate::coordinator::ApprovalResolved { id, agent, - "apply_commit", - None, - "failed", - Some(note), - description.map(str::to_owned), - ); + approval_kind: "apply_commit", + sha_short: None, + status: "failed", + note: Some(note), + description: description.map(str::to_owned), + }); return Err(anyhow::anyhow!("git_fetch_to_tag: {e:#}")); } }; @@ -455,29 +457,29 @@ pub(crate) async fn submit_apply_commit( if let Err(e) = crate::flake_check::check_lock_in_sync(&applied_dir, &tag, id).await { let note = format!("{e:#}"); let _ = coord.approvals.mark_failed(id, ¬e); - coord.emit_approval_resolved( + coord.emit_approval_resolved(crate::coordinator::ApprovalResolved { id, agent, - "apply_commit", - Some(sha_short.clone()), - "failed", - Some(note), - description.map(str::to_owned), - ); + approval_kind: "apply_commit", + sha_short: Some(sha_short.clone()), + status: "failed", + note: Some(note), + description: description.map(str::to_owned), + }); return Err(anyhow::anyhow!("flake lock-sync check: {e:#}")); } if let Err(e) = crate::flake_check::check_no_duplicate_inputs(&applied_dir, &tag).await { let note = format!("{e:#}"); let _ = coord.approvals.mark_failed(id, ¬e); - coord.emit_approval_resolved( + coord.emit_approval_resolved(crate::coordinator::ApprovalResolved { id, agent, - "apply_commit", - Some(sha_short.clone()), - "failed", - Some(note), - description.map(str::to_owned), - ); + approval_kind: "apply_commit", + sha_short: Some(sha_short.clone()), + status: "failed", + note: Some(note), + description: description.map(str::to_owned), + }); return Err(anyhow::anyhow!("flake dedup check: {e:#}")); } // Mirror the freshly-planted proposal/ tag to the forge. @@ -660,6 +662,24 @@ async fn handle_fire_schedule_now( } } +/// Field-named PATCH payload for [`handle_edit_schedule`]. Every +/// field is "leave alone" when `None`; the double-`Option` fields +/// additionally distinguish clear (`Some(None)`) from set +/// (`Some(Some(v))`). +#[allow( + clippy::option_option, + reason = "double-Option carries three-state PATCH semantics: outer None = \ + leave alone, Some(None) = clear, Some(Some(v)) = set" +)] +struct EditSchedulePatch { + body: Option, + description: Option>, + interval_seconds: Option>, + next_fire_at_unix: Option, + targets_add: Option>, + targets_remove: Option>, +} + /// Authorize + dispatch a `EditSchedule` patch. Same ownership /// rules as `CancelSchedule` — the manager can edit /// schedules it owns + any owned by an agent in its subtree. @@ -668,27 +688,20 @@ async fn handle_fire_schedule_now( /// zero-interval validation. Returns `Ok` on a clean update; /// `Err` with the underlying message on any auth / validation /// failure so the dashboard can surface it verbatim. -#[allow( - clippy::too_many_arguments, - reason = "args mirror the edit-schedule PATCH fields 1:1; bundling them into \ - a struct used only here adds no clarity" -)] -#[allow( - clippy::option_option, - reason = "double-Option carries three-state PATCH semantics: outer None = \ - leave alone, Some(None) = clear, Some(Some(v)) = set" -)] fn handle_edit_schedule( coord: &Arc, requester: &str, schedule_id: i64, - body: Option, - description: Option>, - interval_seconds: Option>, - next_fire_at_unix: Option, - targets_add: Option>, - targets_remove: Option>, + patch: EditSchedulePatch, ) -> ManagerResponse { + let EditSchedulePatch { + body, + description, + interval_seconds, + next_fire_at_unix, + targets_add, + targets_remove, + } = patch; let schedule = match coord.scheduled_prompts.get(schedule_id) { Ok(Some(s)) => s, Ok(None) => { diff --git a/hive-c0re/src/questions.rs b/hive-c0re/src/questions.rs index 1d91647b..8e719d35 100644 --- a/hive-c0re/src/questions.rs +++ b/hive-c0re/src/questions.rs @@ -90,7 +90,15 @@ pub fn handle_ask( } // Always fire on the dashboard channel — both operator-targeted // and peer threads now surface in the dashboard's questions pane. - coord.emit_question_added(id, asker, question, options, multi, deadline_at, target); + coord.emit_question_added(&crate::coordinator::QuestionAdded { + id, + asker, + question, + options, + multi, + deadline_at, + target, + }); if let Some(t) = ttl { spawn_question_watchdog(coord, id, t); } @@ -195,15 +203,15 @@ pub fn handle_cancel_loose_end( .fetched_sha .as_deref() .map(|s| s[..s.len().min(12)].to_owned()); - coord.emit_approval_resolved( - approval.id, - &approval.agent, - kind_to_str(approval.kind), + coord.emit_approval_resolved(crate::coordinator::ApprovalResolved { + id: approval.id, + agent: &approval.agent, + approval_kind: kind_to_str(approval.kind), sha_short, - "cancelled", - approval.note, - approval.description, - ); + status: "cancelled", + note: approval.note, + description: approval.description, + }); Ok(()) } } diff --git a/hive-c0re/src/rebuild_queue.rs b/hive-c0re/src/rebuild_queue.rs index 97e91c6d..6d8d6735 100644 --- a/hive-c0re/src/rebuild_queue.rs +++ b/hive-c0re/src/rebuild_queue.rs @@ -259,6 +259,22 @@ impl Default for RebuildQueue { } } +/// Full-shape submit spec for [`RebuildQueue::enqueue_full`] — every +/// `QueueEntry` field settable at submit time. The thinner `enqueue` +/// / `enqueue_with_inputs` / `enqueue_with_perm` wrappers build this +/// for the common cases. +pub struct FullEnqueue { + pub kind: QueueKind, + pub agent: String, + pub source: QueueSource, + pub reason: String, + pub parent_id: Option, + pub inputs: Vec, + pub approval_id: Option, + pub perm_payload: Option, + pub depends_on: Vec, +} + impl RebuildQueue { pub fn new() -> Self { Self::default() @@ -294,17 +310,17 @@ impl RebuildQueue { reason: String, parent_id: Option, ) -> u64 { - self.enqueue_full( + self.enqueue_full(FullEnqueue { kind, agent, source, reason, parent_id, - Vec::new(), - None, - None, - Vec::new(), - ) + inputs: Vec::new(), + approval_id: None, + perm_payload: None, + depends_on: Vec::new(), + }) } /// Same as `enqueue` but carries an `inputs` payload — used by @@ -321,17 +337,17 @@ impl RebuildQueue { parent_id: Option, inputs: Vec, ) -> u64 { - self.enqueue_full( + self.enqueue_full(FullEnqueue { kind, agent, source, reason, parent_id, inputs, - None, - None, - Vec::new(), - ) + approval_id: None, + perm_payload: None, + depends_on: Vec::new(), + }) } /// Enqueue a `PermChange` entry for `agent`. The worker applies the @@ -344,17 +360,17 @@ impl RebuildQueue { reason: String, payload: PermPayload, ) -> u64 { - self.enqueue_full( - QueueKind::PermChange, + self.enqueue_full(FullEnqueue { + kind: QueueKind::PermChange, agent, source, reason, - None, - Vec::new(), - None, - Some(payload), - Vec::new(), - ) + parent_id: None, + inputs: Vec::new(), + approval_id: None, + perm_payload: Some(payload), + depends_on: Vec::new(), + }) } /// Full-shape enqueue — every `QueueEntry` field that's settable @@ -362,27 +378,18 @@ impl RebuildQueue { /// `enqueue_with_perm` delegate to this; the approval-driven POST /// handlers call it directly with the source row's id so the /// worker can re-fetch the kind-specific payload. - // 10 args: the queue entry has 6 independent submit-time fields plus - // four kind-specific payload fields (inputs, approval_id, perm_payload, - // depends_on). A builder struct would obscure the call sites; the - // shorter wrappers already cover all common cases. - #[allow( - clippy::too_many_arguments, - reason = "args mirror the queue-entry fields the row is built from; the \ - thinner enqueue helpers wrap this for the common cases" - )] - pub fn enqueue_full( - &self, - kind: QueueKind, - agent: String, - source: QueueSource, - reason: String, - parent_id: Option, - inputs: Vec, - approval_id: Option, - perm_payload: Option, - depends_on: Vec, - ) -> u64 { + pub fn enqueue_full(&self, spec: FullEnqueue) -> u64 { + let FullEnqueue { + kind, + agent, + source, + reason, + parent_id, + inputs, + approval_id, + perm_payload, + depends_on, + } = spec; let mut inner = self.inner.lock().expect("rebuild_queue mutex poisoned"); // Dedup against a pending entry with the same (kind, agent) — // and, for MetaUpdate, the same `inputs` list (see method @@ -1213,17 +1220,17 @@ mod tests { #[test] fn approval_entries_keep_approval_id() { let q = RebuildQueue::new(); - let id = q.enqueue_full( - QueueKind::Rebuild, - "agent-a".to_owned(), - QueueSource::Approval, - "approval #42 apply commit".to_owned(), - None, - Vec::new(), - Some(42), - None, - Vec::new(), - ); + let id = q.enqueue_full(FullEnqueue { + kind: QueueKind::Rebuild, + agent: "agent-a".to_owned(), + source: QueueSource::Approval, + reason: "approval #42 apply commit".to_owned(), + parent_id: None, + inputs: Vec::new(), + approval_id: Some(42), + perm_payload: None, + depends_on: Vec::new(), + }); let snap = q.snapshot(); let entry = snap.iter().find(|e| e.id == id).expect("entry present"); assert_eq!(entry.approval_id, Some(42)); @@ -1237,43 +1244,43 @@ mod tests { // approve click is a separate piece of work even when the // (kind, agent) pair matches. let q = RebuildQueue::new(); - let a = q.enqueue_full( - QueueKind::Rebuild, - "agent-a".to_owned(), - QueueSource::Approval, - "approval #1".to_owned(), - None, - Vec::new(), - Some(1), - None, - Vec::new(), - ); - let b = q.enqueue_full( - QueueKind::Rebuild, - "agent-a".to_owned(), - QueueSource::Approval, - "approval #2".to_owned(), - None, - Vec::new(), - Some(2), - None, - Vec::new(), - ); + let a = q.enqueue_full(FullEnqueue { + kind: QueueKind::Rebuild, + agent: "agent-a".to_owned(), + source: QueueSource::Approval, + reason: "approval #1".to_owned(), + parent_id: None, + inputs: Vec::new(), + approval_id: Some(1), + perm_payload: None, + depends_on: Vec::new(), + }); + let b = q.enqueue_full(FullEnqueue { + kind: QueueKind::Rebuild, + agent: "agent-a".to_owned(), + source: QueueSource::Approval, + reason: "approval #2".to_owned(), + parent_id: None, + inputs: Vec::new(), + approval_id: Some(2), + perm_payload: None, + depends_on: Vec::new(), + }); assert_ne!(a, b); assert_eq!(q.snapshot().len(), 2); // Same approval_id submitted twice DOES dedup (rapid double- // click on the dashboard's approve button is a single op). - let c = q.enqueue_full( - QueueKind::Rebuild, - "agent-a".to_owned(), - QueueSource::Approval, - "approval #1 (duplicate)".to_owned(), - None, - Vec::new(), - Some(1), - None, - Vec::new(), - ); + let c = q.enqueue_full(FullEnqueue { + kind: QueueKind::Rebuild, + agent: "agent-a".to_owned(), + source: QueueSource::Approval, + reason: "approval #1 (duplicate)".to_owned(), + parent_id: None, + inputs: Vec::new(), + approval_id: Some(1), + perm_payload: None, + depends_on: Vec::new(), + }); assert_eq!(a, c); assert_eq!(q.snapshot().len(), 2); } @@ -1459,17 +1466,17 @@ mod tests { "first".to_owned(), None, ); - let b = q.enqueue_full( - QueueKind::Rebuild, - "agent-b".to_owned(), - QueueSource::Manual, - "second (blocked on a)".to_owned(), - None, - Vec::new(), - None, - None, - vec![a], - ); + let b = q.enqueue_full(FullEnqueue { + kind: QueueKind::Rebuild, + agent: "agent-b".to_owned(), + source: QueueSource::Manual, + reason: "second (blocked on a)".to_owned(), + parent_id: None, + inputs: Vec::new(), + approval_id: None, + perm_payload: None, + depends_on: vec![a], + }); // B depends on A — take_next should give A first. let first = q.take_next().expect("a is ready"); assert_eq!(first.id, a); @@ -1529,17 +1536,17 @@ mod tests { "dep must be evicted from history" ); // An entry that depends on the (evicted) dep must be immediately runnable. - let downstream = q.enqueue_full( - QueueKind::Rebuild, - "downstream".to_owned(), - QueueSource::Manual, - "downstream (dep evicted = resolved)".to_owned(), - None, - Vec::new(), - None, - None, - vec![dep], - ); + let downstream = q.enqueue_full(FullEnqueue { + kind: QueueKind::Rebuild, + agent: "downstream".to_owned(), + source: QueueSource::Manual, + reason: "downstream (dep evicted = resolved)".to_owned(), + parent_id: None, + inputs: Vec::new(), + approval_id: None, + perm_payload: None, + depends_on: vec![dep], + }); let got = q.take_next().expect("downstream runnable when dep evicted"); assert_eq!(got.id, downstream); } @@ -1563,42 +1570,42 @@ mod tests { "d2".to_owned(), None, ); - let a = q.enqueue_full( - QueueKind::Rebuild, - "target".to_owned(), - QueueSource::Manual, - "r".to_owned(), - None, - Vec::new(), - None, - None, - vec![dep1], - ); + let a = q.enqueue_full(FullEnqueue { + kind: QueueKind::Rebuild, + agent: "target".to_owned(), + source: QueueSource::Manual, + reason: "r".to_owned(), + parent_id: None, + inputs: Vec::new(), + approval_id: None, + perm_payload: None, + depends_on: vec![dep1], + }); // Same kind+agent but different depends_on — must NOT dedup. - let b = q.enqueue_full( - QueueKind::Rebuild, - "target".to_owned(), - QueueSource::Manual, - "r".to_owned(), - None, - Vec::new(), - None, - None, - vec![dep2], - ); + let b = q.enqueue_full(FullEnqueue { + kind: QueueKind::Rebuild, + agent: "target".to_owned(), + source: QueueSource::Manual, + reason: "r".to_owned(), + parent_id: None, + inputs: Vec::new(), + approval_id: None, + perm_payload: None, + depends_on: vec![dep2], + }); assert_ne!(a, b, "different depends_on must produce distinct entries"); // Same depends_on as a — must dedup. - let c = q.enqueue_full( - QueueKind::Rebuild, - "target".to_owned(), - QueueSource::Manual, - "r again".to_owned(), - None, - Vec::new(), - None, - None, - vec![dep1], - ); + let c = q.enqueue_full(FullEnqueue { + kind: QueueKind::Rebuild, + agent: "target".to_owned(), + source: QueueSource::Manual, + reason: "r again".to_owned(), + parent_id: None, + inputs: Vec::new(), + approval_id: None, + perm_payload: None, + depends_on: vec![dep1], + }); assert_eq!(a, c, "identical depends_on must dedup"); } @@ -1615,17 +1622,17 @@ mod tests { "a".to_owned(), None, ); - let b = q.enqueue_full( - QueueKind::Rebuild, - "b".to_owned(), - QueueSource::Manual, - "b (blocked on a)".to_owned(), - None, - Vec::new(), - None, - None, - vec![a], - ); + let b = q.enqueue_full(FullEnqueue { + kind: QueueKind::Rebuild, + agent: "b".to_owned(), + source: QueueSource::Manual, + reason: "b (blocked on a)".to_owned(), + parent_id: None, + inputs: Vec::new(), + approval_id: None, + perm_payload: None, + depends_on: vec![a], + }); q.take_next(); // pop a, mark Running q.finish(a, QueueState::Failed, Some("nix build exploded".to_owned())); let got = q.take_next().expect("b runnable after a failed");