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