Compare commits

...
8 changed files with 230 additions and 9 deletions

No files matched your search

+1 -1
View File
@@ -463,7 +463,7 @@ do not establish connectivity to a live peer.
| `rig queue create` | — | `--source <session>`<br>`--destination <session>` **required**<br>`--body <text>`<br>`--body-file <path>`<br>`--body-context <ref>`<br>`--mission <id>`<br>`--slice <id>`<br>`--gate <role>`<br>`--priority <priority>`<br>`--tier <tier>`<br>`--tags <tags>`<br>`--expires-at <iso>`<br>`--id <qitemId>`<br>`--target-repo <name>`<br>`--summary <text>`<br>`--human-intent <intent>`<br>`--alert`<br>`--human-detail-file <path>`<br>`--reply-to <qitemId>`<br>`--human-questions-file <path>`<br>`--evidence-ref <path>`<br>`--host <id>`<br>`--no-nudge`<br>`--verify`<br>`--full`<br>`--json` |
| `rig queue claim <qitemId>` | — | `--destination <session>`<br>`--full`<br>`--json` |
| `rig queue unclaim <qitemId>` | — | `--destination <session>`<br>`--reason <text>`<br>`--full`<br>`--json` |
| `rig queue update <qitemId>` | — | `--actor <session>`<br>`--state <state>`<br>`--reopen`<br>`--closure-reason <reason>`<br>`--closure-target <target>`<br>`--resolution <text>`<br>`--blocked-on <blocker>`<br>`--wake-watchdog <jobId>`<br>`--wake-after <duration>`<br>`--summary <text>`<br>`--evidence-ref <path>`<br>`--note <text>`<br>`--full`<br>`--json` |
| `rig queue update <qitemId>` | — | `--actor <session>`<br>`--state <state>`<br>`--reopen`<br>`--closure-reason <reason>`<br>`--closure-target <target>`<br>`--closure-required-at <iso>`<br>`--resolution <text>`<br>`--blocked-on <blocker>`<br>`--wake-watchdog <jobId>`<br>`--wake-after <duration>`<br>`--summary <text>`<br>`--evidence-ref <path>`<br>`--note <text>`<br>`--full`<br>`--json` |
| `rig queue block <qitemId>` | — | `--on <blocker>` **required**<br>`--actor <session>`<br>`--summary <text>`<br>`--evidence-ref <path>`<br>`--note <text>`<br>`--continuation <text>`<br>`--wake-watchdog <jobId>`<br>`--wake-after <duration>`<br>`--full`<br>`--json` |
| `rig queue resolve <qitemId>` | — | `--decision <text>` **required**<br>`--actor <session>`<br>`--bearer <token>`<br>`--no-notify`<br>`--json` |
| `rig queue handoff <qitemId>` | — | `--from <session>`<br>`--to <session>` **required**<br>`--body <text>`<br>`--body-file <path>`<br>`--note <text>`<br>`--priority <priority>`<br>`--tier <tier>`<br>`--tags <tags>`<br>`--gate <role>`<br>`--target-repo <name>`<br>`--summary <text>`<br>`--evidence-ref <path>`<br>`--alert`<br>`--host <id>`<br>`--no-nudge`<br>`--full`<br>`--json` |
+3
View File
@@ -1240,6 +1240,7 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
.option("--reopen", "Explicitly acknowledge a deliberate terminal-to-active repair; requires --state and --note")
.option("--closure-reason <reason>", "Required for state=done. --state canceled takes no reason, except 'superseded' (with --closure-target = the successor), which records a supersession, distinct from an abandoned cancel")
.option("--closure-target <target>", "Required for handed_off_to, escalation, and superseded; blocked_on park records use state=blocked with --blocked-on")
.option("--closure-required-at <iso>", "Accept a revised closure deadline on an in-progress item; keeps the claim and records the old/new time and accepting actor")
.option("--resolution <text>", "The outcome, stored on the row and shown by rig queue show. Needs --state done, canceled, denied, failed or handed-off, and non-blank text. Stored when the update closes an open row, after anything the row already has (a fallback line, an earlier outcome); a row that is already closed keeps what it has")
.option("--blocked-on <blocker>", "For state=blocked: the blocker — a qitem id (must exist and be live), a human seat (FR-6 park; requires summary + evidence_ref), or a typed non-qitem gate 'fold:<what>' / 'auth:<what>' / 'external:<what>'")
.option("--wake-watchdog <jobId>", "For state=blocked: attach an existing live watchdog id targeting the row owner")
@@ -1255,6 +1256,7 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
reopen?: boolean;
closureReason?: string;
closureTarget?: string;
closureRequiredAt?: string;
resolution?: string;
blockedOn?: string;
wakeWatchdog?: string;
@@ -1291,6 +1293,7 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
reopen: opts.reopen,
closureReason: opts.closureReason,
closureTarget: opts.closureTarget,
...(opts.closureRequiredAt !== undefined ? { closureRequiredAt: opts.closureRequiredAt } : {}),
resolution: opts.resolution,
blockedOn: opts.blockedOn,
wakeWatchdogId: opts.wakeWatchdog,
+11
View File
@@ -1204,6 +1204,17 @@ describe("rig queue CLI", () => {
});
});
it("update forwards an explicitly accepted closure deadline without a state change", async () => {
const { deps, calls } = makeDeps();
const program = createProgram({ queueDeps: deps });
program.exitOverride();
await program.parseAsync(["node", "rig", "queue", "update", "qitem-estimate", "--closure-required-at", "2026-11-04T10:00:00.000Z", "--note", "Coordinator accepted the revised estimate", "--json"]);
expect(calls.find(c => c.path === "/api/queue/qitem-estimate/update")?.body).toMatchObject({
closureRequiredAt: "2026-11-04T10:00:00.000Z", transitionNote: "Coordinator accepted the revised estimate",
});
expect(calls.find(c => c.path === "/api/queue/qitem-estimate/update")?.body).toHaveProperty("state", undefined);
});
it("update forwards explicit reopen acknowledgment", async () => {
const { deps, calls } = makeDeps();
const program = createProgram({ queueDeps: deps });
+5 -2
View File
@@ -14,8 +14,11 @@ function failedAttempt(db: Database.Database, qitemId: string): string | null {
// the unclaimed baton's retry path.
return (db.prepare("SELECT last_nudge_attempt AS at FROM queue_items WHERE qitem_id = ? AND (last_nudge_result LIKE 'failed:%' OR last_nudge_result LIKE 'retained:%')").get(qitemId) as { at: string | null } | undefined)?.at ?? null;
}
export function recoveryId(db: Database.Database, qitemId: string): string {
return `qitem-recovery-${createHash("sha256").update(JSON.stringify([qitemId, lastMeaningfulTransition(db, qitemId)?.id ?? null, failedAttempt(db, qitemId)])).digest("hex").slice(0, 16)}`;
export function recoveryId(db: Database.Database, qitemId: string, overdueDeadline?: string): string {
const evidence: unknown[] = [qitemId, lastMeaningfulTransition(db, qitemId)?.id ?? null, failedAttempt(db, qitemId)];
// Keep existing IDs byte-for-byte unless a newer deadline needs its own recovery episode.
if (overdueDeadline !== undefined) evidence.push(["overdue-claim", overdueDeadline]);
return `qitem-recovery-${createHash("sha256").update(JSON.stringify(evidence)).digest("hex").slice(0, 16)}`;
}
/** Rows whose tag text could hold a recovery tag: valid JSON stores a string verbatim or with
* backslash escapes, so this superset loses no exact match. It repeats the first group of
+24 -3
View File
@@ -417,6 +417,8 @@ export interface QueueUpdateInput {
qitemId: string;
actorSession: string;
state?: QueueState;
/** The actor accepts this revised closure deadline without replacing the claim. */
closureRequiredAt?: string;
/** Explicit acknowledgment for a deliberate terminal → active repair. */
reopen?: boolean;
/**
@@ -3005,15 +3007,28 @@ export class QueueRepository {
`qitem ${input.qitemId} not found`
);
}
const hasReestimate = input.closureRequiredAt !== undefined;
let closureRequiredAt: string | undefined;
if (hasReestimate) {
if (qitem.state !== "in-progress" || (input.state !== undefined && input.state !== "in-progress")) {
throw new QueueRepositoryError("qitem_not_in_progress", "a revised closure deadline updates an in-progress item without changing its state; nothing was written");
}
const deadline = typeof input.closureRequiredAt === "string" ? Date.parse(input.closureRequiredAt) : NaN;
if (!Number.isFinite(deadline)) {
throw new QueueRepositoryError("note_append_fields_not_admitted", "--closure-required-at needs a valid timestamp; nothing was written");
}
closureRequiredAt = new Date(deadline).toISOString();
}
const hasNote = typeof input.transitionNote === "string" && input.transitionNote.trim().length > 0;
const hasValidSummary = typeof input.summary === "string" && input.summary.trim().length > 0;
const isGuardedTerminal = (["done", "canceled", "handed-off"] as const).includes(
qitem.state as "done" | "canceled" | "handed-off",
);
const isStatePreservingAppend = input.state === undefined || (isGuardedTerminal && input.state === qitem.state);
const isStatePreservingAppend = input.state === undefined || (isGuardedTerminal && input.state === qitem.state)
|| (hasReestimate && input.state === qitem.state);
if (isStatePreservingAppend) {
if (input.state === undefined && !hasNote && !hasValidSummary) {
if (input.state === undefined && !hasNote && !hasValidSummary && !hasReestimate) {
throw new QueueRepositoryError(
"state_or_note_required",
"queue update requires --state, a non-empty --note, or a non-empty --summary; nothing was written",
@@ -3056,12 +3071,18 @@ export class QueueRepository {
this.persistSummary(input.qitemId, input.summary!);
}
const note = hasValidSummary
let note = hasValidSummary
? (hasNote
? (input.transitionNote!.includes("(summary updated)") ? input.transitionNote : `${input.transitionNote} (summary updated)`)
: "summary updated")
: input.transitionNote;
if (hasReestimate) {
this.db.prepare("UPDATE queue_items SET closure_required_at = ? WHERE qitem_id = ?").run(closureRequiredAt!, input.qitemId);
const receipt = `closure deadline re-estimated: ${qitem.closureRequiredAt ?? "(none)"} -> ${closureRequiredAt}; accepted by ${input.actorSession}`;
note = hasNote || hasValidSummary ? `${note}\n${receipt}` : receipt;
}
this.transitionLog.append({
qitemId: input.qitemId,
state: qitem.state,
@@ -933,8 +933,14 @@ export async function runStuckSweep(deps: StuckSweepDeps): Promise<StuckSweepRes
for (const c of [...new Map(candidates.map(c => [c.row.qitemId, c])).values()]) {
const dedupTag = findingDedupTag(c.kind, c.row.qitemId);
const shared = findQueueRecovery(deps.db, c.row.qitemId);
if (shared) {
const existingTags = deps.queueRepo.getById(shared.qitemId)?.tags ?? [];
const sharedRow = shared ? deps.queueRepo.getById(shared.qitemId) : null;
// A later accepted deadline can expire after the previous finding closed.
// Its newer time is fresh overdue evidence; an OPEN recovery still owns custody.
const renewedDeadline = c.kind === "overdue-claim" && sharedRow
&& !["pending", "in-progress", "blocked"].includes(sharedRow.state)
&& evidenceIsNewer(c.row.closureRequiredAt ?? "", sharedRow.tsUpdated);
if (shared && !renewedDeadline) {
const existingTags = sharedRow?.tags ?? [];
for (const tag of existingTags) if (tag.startsWith("stuck-sweep:")) liveDedupTags.add(tag);
if (["pending", "in-progress", "blocked"].includes(shared.state)) findings.push({ kind: c.kind, qitemId: c.row.qitemId, findingQitemId: shared.qitemId, action: "refreshed" });
continue;
@@ -959,7 +965,7 @@ export async function runStuckSweep(deps: StuckSweepDeps): Promise<StuckSweepRes
findings.push({ kind: c.kind, qitemId: c.row.qitemId, findingQitemId: existing.qitem_id, action: "refreshed" });
} else if (!existing || evidenceIsNewer(c.evidenceAt, existing.ts_updated)) {
const input = {
qitemId: recoveryId(deps.db, c.row.qitemId),
qitemId: recoveryId(deps.db, c.row.qitemId, renewedDeadline ? c.row.closureRequiredAt! : undefined),
// The detector is machinery, not a seat: the obligation's own creator is the
// finding's source (the workflow-exception precedent).
sourceSession: c.row.sourceSession,
+2
View File
@@ -706,6 +706,7 @@ export function queueRoutes(): Hono {
classificationBinding?: unknown;
state?: QueueState;
reopen?: boolean;
closureRequiredAt?: string;
transitionNote?: string;
closureReason?: string;
closureTarget?: string;
@@ -733,6 +734,7 @@ export function queueRoutes(): Hono {
actorSession,
state: body.state,
reopen: body.reopen,
closureRequiredAt: body.closureRequiredAt,
transitionNote: body.transitionNote,
closureReason: body.closureReason,
closureTarget: body.closureTarget,
@@ -0,0 +1,175 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import Database from "better-sqlite3";
import { Hono } from "hono";
import { ALL_MIGRATIONS } from "../src/db/all-migrations.js";
import { migrate } from "../src/db/migrate.js";
import { EventBus } from "../src/domain/event-bus.js";
import { OutboxHandler } from "../src/domain/outbox-handler.js";
import { QueueRepository } from "../src/domain/queue-repository.js";
import { runStuckSweep } from "../src/domain/queue-stuck-sweep.js";
import { recoveryId } from "../src/domain/queue-recovery.js";
import { lastMeaningfulTransition } from "../src/domain/queue-waiting.js";
import { queueRoutes } from "../src/routes/queue.js";
describe("accepted queue re-estimates (#2453)", () => {
let db: Database.Database, repo: QueueRepository, app: Hono;
const later = "2026-11-03T13:00:00.000Z";
beforeEach(() => {
vi.useFakeTimers({ toFake: ["Date"] });
vi.setSystemTime(new Date("2026-11-03T12:00:00.000Z"));
db = new Database(":memory:");
migrate(db, ALL_MIGRATIONS);
const bus = new EventBus(db), outbox = new OutboxHandler(db);
repo = new QueueRepository(db, bus, {
validateRig: () => true, resolveOccupantGeneration: () => "estimate-generation",
loadHumanRegistry: () => ({ ok: true, entities: [] }),
});
repo.attachOutbox(outbox);
app = new Hono();
app.use("*", async (c, next) => {
c.set("queueRepo" as never, repo); c.set("eventBus" as never, bus); c.set("outboxHandler" as never, outbox);
await next();
});
app.route("/api/queue", queueRoutes());
});
afterEach(() => { db.close(); vi.useRealTimers(); vi.unstubAllEnvs(); });
async function overdue() {
const row = await repo.create({ sourceSession: "coordinator@fixture", destinationSession: "worker@fixture", body: "work", tier: "fast", nudge: false });
repo.claim({ qitemId: row.qitemId, destinationSession: row.destinationSession });
db.prepare("UPDATE queue_items SET closure_required_at = ? WHERE qitem_id = ?").run("2026-11-03T11:59:00.000Z", row.qitemId);
return repo.getById(row.qitemId)!;
}
const claim = (id: string) => db.prepare("SELECT state, claimed_at, claimed_by_generation_uuid FROM queue_items WHERE qitem_id = ?").get(id);
const post = (id: string, body: Record<string, unknown>) => app.request(`/api/queue/${id}/update`, {
method: "POST", headers: { "Content-Type": "application/json", "X-OpenRig-Session": "coordinator@fixture" }, body: JSON.stringify(body),
});
const accept = (id: string) => post(id, { closureRequiredAt: later, actorSession: "body-claim@fixture", transitionNote: "Accepted an extra hour for the remaining work" });
const sweep = () => runStuckSweep({
db, queueRepo: repo, now: new Date(), unclaimedAgeMinutes: 60,
resolveOrchestrator: () => null, sessionConfirmedMissing: () => false,
resolveRecoveryAgentSeat: () => null, resolveOperatorSeat: () => null, isRegisteredHost: () => false, log: () => {},
});
it("accepts a deadline through the real update route with old/new/actor provenance and the original claim", async () => {
const row = await overdue(), before = claim(row.qitemId);
await post(row.qitemId, { transitionNote: `Accepted ETA ${later}, still working` });
expect(repo.getById(row.qitemId)?.closureRequiredAt).toBe(row.closureRequiredAt);
const response = await accept(row.qitemId);
expect(response.status).toBe(200);
expect(await response.json()).toMatchObject({ closureRequiredAt: later });
expect(claim(row.qitemId)).toEqual(before);
const receipt = repo.transitionLog.listForQitem(row.qitemId).at(-1)!;
expect(receipt).toMatchObject({ state: "in-progress", actorSession: "coordinator@fixture", identityProvenance: "transport:v1" });
expect(receipt.transitionNote).toContain(row.closureRequiredAt);
expect(receipt.transitionNote).toContain(later);
expect(receipt.transitionNote).toContain("Accepted an extra hour");
});
it("stops reporting overdue until the accepted deadline actually expires", async () => {
const row = await overdue();
expect(repo.findOverdue().map(r => r.qitemId)).toContain(row.qitemId);
expect((await accept(row.qitemId)).status).toBe(200);
expect(repo.findOverdue().map(r => r.qitemId)).not.toContain(row.qitemId);
expect((await sweep()).findings.filter(f => f.qitemId === row.qitemId && f.kind === "overdue-claim")).toEqual([]);
repo.attachActivityReader(() => ({ activity: "working", needsInput: { count: 0, reason: null }, decidedBy: "lifecycle-hooks" }));
vi.setSystemTime(new Date(Date.parse(later) + 1));
expect(repo.getById(row.qitemId)?.pickup?.state).toBe("working");
expect(repo.findOverdue().map(r => r.qitemId)).toContain(row.qitemId);
expect((await sweep()).findings).toContainEqual(expect.objectContaining({ qitemId: row.qitemId, kind: "overdue-claim", action: "created" }));
});
it("closes an already-open overdue finding on the next sweep without closing or reclaiming the work", async () => {
const row = await overdue(), before = claim(row.qitemId);
const initial = await sweep();
const finding = initial.findings.find(f => f.qitemId === row.qitemId && f.kind === "overdue-claim")!;
expect(finding.action).toBe("created");
const owner = repo.getById(finding.findingQitemId)!;
expect((await accept(row.qitemId)).status).toBe(200);
expect((await sweep()).findings).toContainEqual({ ...finding, action: "closed" });
expect(repo.getById(finding.findingQitemId)).toMatchObject({ state: "done", closureReason: "no-follow-on", sourceSession: owner.sourceSession, destinationSession: owner.destinationSession });
expect(claim(row.qitemId)).toEqual(before);
repo.attachActivityReader(() => ({ activity: "working", needsInput: { count: 0, reason: null }, decidedBy: "lifecycle-hooks" }));
vi.setSystemTime(new Date(Date.parse(later) + 1));
expect(repo.getById(row.qitemId)?.pickup?.state).toBe("working");
const expired = (await sweep()).findings.find(f => f.qitemId === row.qitemId && f.kind === "overdue-claim")!;
expect(expired.action).toBe("created");
expect(expired.findingQitemId).not.toBe(finding.findingQitemId);
expect(claim(row.qitemId)).toEqual(before);
});
it("renews a passed deadline after its prior finding closes even when the owner has no activity telemetry", async () => {
vi.stubEnv("OPENRIG_QUEUE_PICKUP_STALL_THRESHOLD_MINUTES", "3");
const row = await overdue(), before = claim(row.qitemId);
const finding = (await sweep()).findings.find(f => f.qitemId === row.qitemId && f.kind === "overdue-claim")!;
expect(finding.action).toBe("created");
expect((await accept(row.qitemId)).status).toBe(200);
expect((await sweep()).findings).toContainEqual({ ...finding, action: "closed" });
expect(repo.getById(finding.findingQitemId)?.state).toBe("done");
vi.setSystemTime(new Date(Date.parse(later) + 1));
expect(repo.getById(row.qitemId)?.pickup?.state).toBe("stalled-after-claim");
expect(repo.findOverdue().map(r => r.qitemId)).toContain(row.qitemId);
const expired = await sweep();
const live = repo.list({ limit: 100 }).filter(r => ["pending", "in-progress", "blocked"].includes(r.state) && r.tags?.includes(`recovery-for:${row.qitemId}`));
expect(live).toHaveLength(1);
expect(expired.findings).toContainEqual({ kind: "stalled-after-claim", qitemId: row.qitemId, findingQitemId: live[0]!.qitemId, action: "created" });
expect(live[0]!.qitemId).not.toBe(finding.findingQitemId);
expect((await sweep()).findings).toContainEqual({ kind: "stalled-after-claim", qitemId: row.qitemId, findingQitemId: live[0]!.qitemId, action: "refreshed" });
expect(claim(row.qitemId)).toEqual(before);
});
it("creates one live overdue after a post-estimate pickup recovery closes before the revised deadline", async () => {
vi.stubEnv("OPENRIG_QUEUE_PICKUP_STALL_THRESHOLD_MINUTES", "3");
const row = await overdue(), before = claim(row.qitemId);
expect((await accept(row.qitemId)).status).toBe(200);
const accepted = lastMeaningfulTransition(db, row.qitemId);
vi.setSystemTime(new Date("2026-11-03T12:10:00.000Z"));
const pickup = (await sweep()).findings.find(f => f.qitemId === row.qitemId && f.kind === "stalled-after-claim")!;
expect(pickup.action).toBe("created");
expect(pickup.findingQitemId).toBe(recoveryId(db, row.qitemId));
repo.update({ qitemId: pickup.findingQitemId, actorSession: "coordinator@fixture", state: "done", closureReason: "no-follow-on", transitionNote: "Owner is working; pickup recovery resolved" });
repo.attachActivityReader(() => ({ activity: "working", needsInput: { count: 0, reason: null }, decidedBy: "lifecycle-hooks" }));
vi.setSystemTime(new Date(Date.parse(later) + 1));
expect(repo.getById(row.qitemId)?.pickup?.state).toBe("working");
const expired = (await sweep()).findings.find(f => f.qitemId === row.qitemId && f.kind === "overdue-claim")!;
expect(expired.action).toBe("created");
expect(lastMeaningfulTransition(db, row.qitemId)).toEqual(accepted);
expect(recoveryId(db, row.qitemId)).toBe(pickup.findingQitemId);
const live = repo.list({ limit: 100 }).filter(r => ["pending", "in-progress", "blocked"].includes(r.state) && r.tags?.includes(`recovery-for:${row.qitemId}`));
expect(live).toHaveLength(1);
expect(live[0]!.qitemId).toBe(expired.findingQitemId);
expect(expired.findingQitemId).not.toBe(pickup.findingQitemId);
expect(repo.getById(pickup.findingQitemId)?.state).toBe("done");
expect((await sweep()).findings).toContainEqual({ ...expired, action: "refreshed" });
expect(claim(row.qitemId)).toEqual(before);
});
it("can add an estimate to a claim without a tier, without requiring a note or resetting its generation", async () => {
const row = await repo.create({ sourceSession: "coordinator@fixture", destinationSession: "worker@fixture", body: "untimed", nudge: false });
repo.claim({ qitemId: row.qitemId, destinationSession: row.destinationSession });
const before = claim(row.qitemId);
expect(repo.getById(row.qitemId)?.closureRequiredAt).toBeNull();
expect((await post(row.qitemId, { state: "in-progress", closureRequiredAt: "2026-11-03T14:00:00+01:00" })).status).toBe(200);
expect(repo.getById(row.qitemId)?.closureRequiredAt).toBe(later);
expect(claim(row.qitemId)).toEqual(before);
expect(repo.transitionLog.listForQitem(row.qitemId).at(-1)?.transitionNote).toContain(`(none) -> ${later}; accepted by coordinator@fixture`);
});
it("rejects a non-string deadline without changing the row or ledger", async () => {
const row = await overdue(), count = repo.transitionLog.listForQitem(row.qitemId).length;
expect((await post(row.qitemId, { closureRequiredAt: 123, transitionNote: "request" })).status).toBe(400);
expect(repo.getById(row.qitemId)).toEqual(row);
expect(repo.transitionLog.listForQitem(row.qitemId)).toHaveLength(count);
});
it("does not combine re-estimation with closing work or silently set a pending row's deadline", async () => {
const row = await overdue();
expect((await post(row.qitemId, { closureRequiredAt: later, state: "done", closureReason: "no-follow-on" })).status).toBe(409);
expect(repo.getById(row.qitemId)).toEqual(row);
const pending = await repo.create({ sourceSession: "coordinator@fixture", destinationSession: "worker@fixture", body: "pending", nudge: false });
expect((await accept(pending.qitemId)).status).toBe(409);
expect(repo.getById(pending.qitemId)).toEqual(pending);
});
});