Files
clawmates/crates/cm-db/tests/self_driven_runs.rs
T
Omar Sobh 66f730ad16 test(db): a regression net for the three-minute bug, with its negative control
The #54 fix had no test that could see it. Its defining property is that it only
appears past 180 seconds, and `verify-mission-delivery.sh microvm` runs a
90-second mission — so the end-to-end harness written to catch silent failure was
structurally blind to this one. A unit test asserting the allowlist's membership
helps, but would not notice a NEW sweeper added without the filter.

`crates/cm-db/tests/self_driven_runs.rs` tests the real SQL against a migrated
database, in milliseconds instead of eight minutes:

  - a `microvm` and a `session` run, 30 minutes idle and still `running`, must be
    left alone by `requeue_stale` — that is the bug, in one assertion
  - a `team` run in the SAME state must still be requeued, so the fix is "sweep the
    right rows" and not "stop sweeping"
  - the worker must not CLAIM a queued self-driven row, which is what turned a
    healthy run into "missing or invalid graph"
  - the allowlist names only worker-driven tiers

NEGATIVE CONTROL, run rather than assumed: with the tier filter removed from
`requeue_stale`, `requeue_stale_leaves_self_driven_runs_alone` FAILS; restored, it
passes. A guard that cannot detect the bug it was written for is decoration, and
this project has shipped one of those before.

489 tests pass, clippy clean.
2026-08-06 09:52:58 -07:00

160 lines
5.9 KiB
Rust

//! The sweepers must leave SELF-DRIVEN runs alone.
//!
//! A `tier='microvm'` or `tier='session'` run is inserted directly as `running` by
//! `phase_runner` and owned start-to-finish by its own `tokio::spawn`. Nothing
//! touches its `updated_at` or `checkpoint` while it is in flight, because there
//! is no per-step loop to hook.
//!
//! Both sweepers were written when every `running` row was a `cm_orchestrator` job
//! that checkpointed after each step, and neither filtered on tier. The result,
//! measured in production on 2026-08-06: `requeue_stale` declared a healthy microVM
//! run stale at 180 seconds, the worker claimed it, failed to deserialize its graph
//! placeholder, and killed the phase with "missing or invalid graph" — while the
//! agent went on working and its VM was orphaned for over an hour.
//!
//! **Every microVM mission that appeared to work did so by finishing inside three
//! minutes.** The end-to-end harness runs a 90-second mission, so it cannot see
//! this class at all — which is why the guard lives here, against the real SQL, and
//! costs milliseconds instead of eight minutes.
use cm_db::repo::topology_runs;
use cm_domain::WorkspaceId;
use uuid::Uuid;
/// Insert a run that is `running` and has looked idle for a long time — exactly
/// the shape a long agent turn presents.
async fn stale_running_run(pool: &sqlx::PgPool, ws: WorkspaceId, tier: &str) -> Uuid {
let id = Uuid::now_v7();
sqlx::query(
"INSERT INTO topology_runs
(id, workspace_id, task, kind, status, graph, tier,
created_at, updated_at)
VALUES ($1, $2, 'long turn', 'run', 'running', $3, $4,
now() - interval '30 minutes', now() - interval '30 minutes')",
)
.bind(id)
.bind(ws.as_uuid())
// The placeholder a self-driven run carries: no `kind`, so `TopologyGraph`
// cannot parse it. That is what turned a requeue into a hard failure.
.bind(serde_json::json!({ "nodes": [], "edges": [], "executor": tier }))
.bind(tier)
// `mission_id` is left NULL: it has an FK to `missions`, and `requeue_stale`
// does not look at it. The reaper DOES filter on `mission_id IS NOT NULL` —
// which is precisely what used to be mistaken for "orchestrator-driven" — and
// it now shares the same tier allowlist, asserted below.
.execute(pool)
.await
.expect("insert run");
id
}
async fn status_of(pool: &sqlx::PgPool, id: Uuid) -> String {
sqlx::query_scalar::<_, String>("SELECT status FROM topology_runs WHERE id = $1")
.bind(id)
.fetch_one(pool)
.await
.expect("read status")
}
async fn workspace(pool: &sqlx::PgPool) -> WorkspaceId {
let ws = cm_domain::Workspace {
id: WorkspaceId::new(),
name: "Sweeper".into(),
plan: "team".into(),
};
cm_db::repo::workspaces::insert(pool, &ws)
.await
.expect("workspace");
ws.id
}
/// The bug, in one assertion: 30 minutes idle and it must still be `running`.
#[tokio::test]
async fn requeue_stale_leaves_self_driven_runs_alone() {
let pool = cm_testkit::test_pool().await;
let ws = workspace(&pool).await;
let microvm = stale_running_run(&pool, ws, "microvm").await;
let session = stale_running_run(&pool, ws, "session").await;
let moved = topology_runs::requeue_stale(&pool, 180.0)
.await
.expect("requeue");
assert_eq!(
status_of(&pool, microvm).await,
"running",
"a microvm run was requeued out from under a live VM ({moved} rows moved)"
);
assert_eq!(
status_of(&pool, session).await,
"running",
"a session run was requeued out from under a live agent"
);
}
/// And a worker-driven run in the same state MUST still be requeued, or the fix
/// would have been "stop sweeping" rather than "sweep the right rows".
#[tokio::test]
async fn requeue_stale_still_rescues_worker_driven_runs() {
let pool = cm_testkit::test_pool().await;
let ws = workspace(&pool).await;
let team = stale_running_run(&pool, ws, "team").await;
topology_runs::requeue_stale(&pool, 180.0).await.expect("requeue");
assert_eq!(
status_of(&pool, team).await,
"queued",
"a genuinely stalled team run must still be recovered"
);
}
/// Defence in depth: even handed a queued self-driven row, the worker must not
/// adopt a job it cannot execute. Claiming one is what produced the
/// "missing or invalid graph" failure on a run that was perfectly healthy.
#[tokio::test]
async fn the_worker_will_not_claim_a_self_driven_run() {
let pool = cm_testkit::test_pool().await;
let ws = workspace(&pool).await;
let id = Uuid::now_v7();
topology_runs::enqueue_run_tier(
&pool,
id,
ws,
"should never be claimed",
&serde_json::json!({ "nodes": [], "edges": [], "executor": "microvm" }),
"microvm",
)
.await
.expect("enqueue");
let claimed = topology_runs::claim_next_queued(&pool).await.expect("claim");
assert!(
claimed.is_none(),
"the worker claimed a microvm run: {:?}",
claimed.map(|c| c.tier)
);
assert_eq!(status_of(&pool, id).await, "queued", "and it must be left as it was");
}
/// The allowlist is the single place this policy lives, so assert its membership
/// directly — a new self-driven tier added without touching it would otherwise be
/// exposed exactly as microvm was.
#[test]
fn the_allowlist_names_only_worker_driven_tiers() {
for driven in ["team", "swarm", "company", "org"] {
assert!(
topology_runs::WORKER_DRIVEN_TIERS.contains(&driven),
"{driven} is driven by the worker and must be sweepable"
);
}
for self_driven in ["microvm", "session"] {
assert!(
!topology_runs::WORKER_DRIVEN_TIERS.contains(&self_driven),
"{self_driven} owns its own lifecycle; sweeping it kills live work"
);
}
}