refactor(#2441): move agent field from DAG onto Node; drop dedup
Agent was a single field on Dag/DagSpec, making a DAG structurally one-agent — a multi-agent op could only ever be N separate DAGs. Move it onto Node/NodeSpec (and the NodeView wire type), drop it from Dag/DagSpec (and DagView): a DAG can now span agents. - lifecycle lease keys on the node's agent, still globally exclusive per agent across all DAGs (Inner.leases unchanged in shape). A DAG holds one lease per distinct agent it touches; settle() frees each at DAG-terminal (per-agent-subgraph early release is a follow-up, only observable with multi-agent DAGs). - transient guard keyed (dag_id, agent); cancel-revert + Rebuilt events walk TerminalDag.agents. - submit-time dedup removed (a multi-agent DAG has no single agent to key on); every submit enqueues a fresh DAG. Whether dedup needs reintroducing is tracked in a follow-up sub-issue. - templates gain a node(agent, kind, deps) helper stamping the agent onto every node; meta templates stamp "hyperhive". Templates stay single-agent in this PR — behaviour is unchanged, only the representation + wire shape. Multi-agent DAG emission (restart/restart-all/ broad stop+start as one DAG) and the SetWanted-as-a-node change are follow-ups off #2439.
This commit is contained in:
parent
91607f3896
commit
2a59f2f5fc
7 changed files with 223 additions and 356 deletions
|
|
@ -1,4 +1,4 @@
|
|||
//! Queue-core unit tests: dedup, cycle rejection, resource
|
||||
//! Queue-core unit tests: submit / no-dedup, cycle rejection, resource
|
||||
//! serialization (build slots / per-agent leases), lease-exempt
|
||||
//! overlap, FIFO fairness, cancel semantics, `AfterAny` failure
|
||||
//! routing, fan-out, and history retention. All synchronous — the
|
||||
|
|
@ -35,7 +35,7 @@ fn state_of(q: &JobQueue, dag_id: u64) -> State {
|
|||
.state
|
||||
}
|
||||
|
||||
// ---- submit / dedup ----
|
||||
// ---- submit (dedup removed — every submit is a fresh DAG) ----
|
||||
|
||||
#[test]
|
||||
fn submit_assigns_distinct_ids() {
|
||||
|
|
@ -46,20 +46,22 @@ fn submit_assigns_distinct_ids() {
|
|||
assert_eq!(q.snapshot().len(), 2);
|
||||
}
|
||||
|
||||
/// Submit-time dedup was removed with the agent-per-node refactor (a
|
||||
/// multi-agent DAG has no single agent to key a dedup on), so an identical
|
||||
/// resubmit — same template + agent, still queued — now enqueues a distinct
|
||||
/// DAG instead of collapsing into the pending one. Whether any dedup needs
|
||||
/// reintroducing is tracked as a follow-up.
|
||||
#[test]
|
||||
fn dedup_pending_same_template_and_agent() {
|
||||
fn identical_resubmit_is_a_distinct_dag() {
|
||||
let q = JobQueue::new(1);
|
||||
let a = submit(&q, rebuild("agent-a", "first"));
|
||||
let b = submit(&q, rebuild("agent-a", "auto sweep"));
|
||||
assert_eq!(a, b, "dedup should return existing id");
|
||||
let snap = q.snapshot();
|
||||
assert_eq!(snap.len(), 1);
|
||||
assert!(snap[0].reason.contains("first"));
|
||||
assert!(snap[0].reason.contains("auto sweep"));
|
||||
let b = submit(&q, rebuild("agent-a", "again"));
|
||||
assert_ne!(a, b, "no dedup: identical resubmit is a new DAG");
|
||||
assert_eq!(q.snapshot().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dedup_does_not_apply_across_templates_or_agents() {
|
||||
fn distinct_submits_never_collapse() {
|
||||
let q = JobQueue::new(1);
|
||||
let a = submit(&q, rebuild("agent-a", "r"));
|
||||
let b = submit(&q, rebuild("agent-b", "r"));
|
||||
|
|
@ -73,7 +75,7 @@ fn dedup_does_not_apply_across_templates_or_agents() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn dedup_skips_running_dags() {
|
||||
fn resubmit_while_running_is_new_dag() {
|
||||
let q = JobQueue::new(1);
|
||||
let a = submit(&q, rebuild("agent-a", "first"));
|
||||
let claim = claim_one(&q); // Prebuild running
|
||||
|
|
@ -84,126 +86,6 @@ fn dedup_skips_running_dags() {
|
|||
assert_eq!(q.snapshot().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn meta_update_dedup_matches_inputs() {
|
||||
let q = JobQueue::new(1);
|
||||
let a = submit(
|
||||
&q,
|
||||
templates::meta_update(
|
||||
vec!["nixpkgs".to_owned()],
|
||||
Source::Manual,
|
||||
"first".to_owned(),
|
||||
None,
|
||||
),
|
||||
);
|
||||
let b = submit(
|
||||
&q,
|
||||
templates::meta_update(
|
||||
vec!["nixpkgs".to_owned()],
|
||||
Source::Manual,
|
||||
"duplicate click".to_owned(),
|
||||
None,
|
||||
),
|
||||
);
|
||||
assert_eq!(a, b, "identical-inputs meta-updates should dedup");
|
||||
let c = submit(
|
||||
&q,
|
||||
templates::meta_update(
|
||||
vec!["agent-bitburner/bitburner-agent".to_owned()],
|
||||
Source::Manual,
|
||||
"bump agent".to_owned(),
|
||||
None,
|
||||
),
|
||||
);
|
||||
assert_ne!(a, c, "different-inputs meta-updates must NOT dedup");
|
||||
assert_eq!(q.snapshot().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approval_dags_dedup_only_on_matching_id() {
|
||||
let q = JobQueue::new(1);
|
||||
let a = submit(
|
||||
&q,
|
||||
templates::approval_deploy("agent-a", 1, "approval #1".to_owned()),
|
||||
);
|
||||
let b = submit(
|
||||
&q,
|
||||
templates::approval_deploy("agent-a", 2, "approval #2".to_owned()),
|
||||
);
|
||||
assert_ne!(a, b, "distinct approvals must not collapse");
|
||||
// Rapid double-click on the same approval IS a single op.
|
||||
let c = submit(
|
||||
&q,
|
||||
templates::approval_deploy("agent-a", 1, "approval #1 (dup)".to_owned()),
|
||||
);
|
||||
assert_eq!(a, c);
|
||||
assert_eq!(q.snapshot().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perm_change_dedup_respects_perm_type() {
|
||||
let q = JobQueue::new(1);
|
||||
let groups = templates::perm_change(
|
||||
"agent-a",
|
||||
Source::Manual,
|
||||
"groups".to_owned(),
|
||||
PermPayload::ToolGroups { groups: vec![] },
|
||||
);
|
||||
let caps = templates::perm_change(
|
||||
"agent-a",
|
||||
Source::Manual,
|
||||
"caps".to_owned(),
|
||||
PermPayload::Capabilities { caps: vec![] },
|
||||
);
|
||||
let a = submit(&q, groups.clone());
|
||||
let b = submit(&q, caps);
|
||||
assert_ne!(a, b, "tool-groups vs capabilities must not collapse");
|
||||
let c = submit(&q, groups);
|
||||
assert_eq!(a, c, "same perm type dedups");
|
||||
}
|
||||
|
||||
/// A `MetaUpdate` cascade `Rebuild` (with `parent_id = Some(meta_id)`)
|
||||
/// must NOT dedup into a queued `Rebuild` with a different
|
||||
/// `parent_id` (e.g. from a startup sweep) — without the guard the
|
||||
/// cascade child would be swallowed and the agent never rebuilt
|
||||
/// against the post-bump meta.
|
||||
#[test]
|
||||
fn dedup_respects_parent_id() {
|
||||
let q = JobQueue::new(1);
|
||||
let sweep = submit(&q, templates::startup_sweep("boot".to_owned(), vec![]));
|
||||
let sweep_child = submit(
|
||||
&q,
|
||||
templates::rebuild(
|
||||
"alice",
|
||||
Source::StartupSweep,
|
||||
"startup sweep".to_owned(),
|
||||
Some(sweep),
|
||||
true,
|
||||
),
|
||||
);
|
||||
let meta = submit(
|
||||
&q,
|
||||
templates::meta_update(vec![], Source::Manual, "bump".to_owned(), None),
|
||||
);
|
||||
let cascade_child = submit(
|
||||
&q,
|
||||
templates::rebuild(
|
||||
"alice",
|
||||
Source::MetaUpdate,
|
||||
"meta-update cascade".to_owned(),
|
||||
Some(meta),
|
||||
false,
|
||||
),
|
||||
);
|
||||
assert_ne!(sweep_child, cascade_child);
|
||||
let rebuilds = q
|
||||
.snapshot()
|
||||
.iter()
|
||||
.filter(|d| d.kind == Template::Rebuild && d.agent == "alice")
|
||||
.count();
|
||||
assert_eq!(rebuilds, 2, "both rebuilds must be present");
|
||||
}
|
||||
|
||||
// ---- cycle rejection ----
|
||||
|
||||
#[test]
|
||||
|
|
@ -213,6 +95,7 @@ fn cyclic_dag_is_rejected_at_submit() {
|
|||
// 0 → 1 → 0 cycle.
|
||||
spec.nodes = vec![
|
||||
NodeSpec {
|
||||
agent: "agent-a".to_owned(),
|
||||
kind: NodeKind::StopForUpdate,
|
||||
deps: vec![Dep {
|
||||
on: 1,
|
||||
|
|
@ -220,6 +103,7 @@ fn cyclic_dag_is_rejected_at_submit() {
|
|||
}],
|
||||
},
|
||||
NodeSpec {
|
||||
agent: "agent-a".to_owned(),
|
||||
kind: NodeKind::Reconcile,
|
||||
deps: vec![Dep {
|
||||
on: 0,
|
||||
|
|
@ -236,6 +120,7 @@ fn unknown_dep_is_rejected_at_submit() {
|
|||
let q = JobQueue::new(1);
|
||||
let mut spec = rebuild("agent-a", "bad dep");
|
||||
spec.nodes = vec![NodeSpec {
|
||||
agent: "agent-a".to_owned(),
|
||||
kind: NodeKind::Reconcile,
|
||||
deps: vec![Dep {
|
||||
on: 9,
|
||||
|
|
@ -578,7 +463,7 @@ fn cancel_children_skips_running_child() {
|
|||
// ---- fan-out ----
|
||||
|
||||
#[test]
|
||||
fn append_children_sets_parent_and_dedups() {
|
||||
fn append_children_sets_parent() {
|
||||
let q = JobQueue::new(1);
|
||||
let meta = submit(
|
||||
&q,
|
||||
|
|
@ -599,7 +484,7 @@ fn append_children_sets_parent_and_dedups() {
|
|||
Some(meta),
|
||||
false,
|
||||
),
|
||||
// Duplicate — must coalesce into the first alice child.
|
||||
// No dedup: a second alice child is its own DAG now.
|
||||
templates::rebuild(
|
||||
"alice",
|
||||
Source::MetaUpdate,
|
||||
|
|
@ -610,10 +495,10 @@ fn append_children_sets_parent_and_dedups() {
|
|||
];
|
||||
let ids = q.append_children(specs);
|
||||
assert_eq!(ids.len(), 3);
|
||||
assert_eq!(ids[0], ids[2], "duplicate child dedups");
|
||||
assert_ne!(ids[0], ids[2], "no dedup: duplicate child is distinct");
|
||||
let snap = q.snapshot();
|
||||
let children: Vec<_> = snap.iter().filter(|d| d.parent_id == Some(meta)).collect();
|
||||
assert_eq!(children.len(), 2);
|
||||
assert_eq!(children.len(), 3);
|
||||
}
|
||||
|
||||
// ---- terminal reporting + lease release ----
|
||||
|
|
|
|||
Loading…
Reference in a new issue