From 2b0fa7000acfa558d57d044a35d549e196a3437d Mon Sep 17 00:00:00 2001 From: dev-guard Date: Thu, 29 Oct 2026 17:49:30 +0000 Subject: [PATCH] test: cover quota-full and terminal reminder recovery --- .../daemon/test/parked-owner-consumer.test.ts | 72 +++++++++++++++++++ .../test/watchdog-delivery-guarded.test.ts | 35 ++++++++- 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/packages/daemon/test/parked-owner-consumer.test.ts b/packages/daemon/test/parked-owner-consumer.test.ts index 0bbe401c9..32a40d842 100644 --- a/packages/daemon/test/parked-owner-consumer.test.ts +++ b/packages/daemon/test/parked-owner-consumer.test.ts @@ -736,3 +736,75 @@ describe("#1933: the first reminder waits a grace after the seat starts waiting" expect((await run(new Date(NOW - GRACE_MS - 1_000).toISOString())).action).toBe("send"); }); }); + + +describe("parked reminders after a positive no-input quota rejection", () => { + const quotaState = "guard-revision-a:100:8192"; + const failedQuota = (result: Awaited["evaluate"]>>, phase: string | undefined = "not_reached") => { + const entry = sentHistory({ episodeKey: String(result.notes?.["episodeKey"]), primaryRow: String(result.notes?.["primaryRow"]), deliveryStatus: "failed", deliveryReason: "Held-message quota is full; no input was written." }); + entry.evaluationNotes = { ...entry.evaluationNotes, deliveryCode: "retained_quota_full", ...(phase ? { deliveryInputPhase: phase } : {}) }; + return entry; + }; + const policy = (store: RowStore, history: WatchdogHistoryEntry[], state = quotaState, terminal = false) => + makeParkedOwnerConsumerPolicy({ ...makeDeps([parkedSeat()], store, history), retentionState: () => state, isTerminalSeat: () => terminal } as ParkedOwnerConsumerDeps); + + it("bounds unchanged attempts AND row bookkeeping across restart and history pruning", async () => { + const store = new RowStore(); + const first = await policy(store, []).evaluate(makeJob()); + expect(first.action).toBe("send"); + const history = [failedQuota(first)]; + const next = await policy(store, history).evaluate(makeJob()); + expect(next).toMatchObject({ action: "skip", notes: { skippedSeats: [{ seat: SEAT, why: "retention-quota-unchanged" }] } }); + const writes = store.appended.length; + const nudges = store.nudges.length; + expect(store.nudges[0]?.result).toContain("retained_quota_full"); + expect(store.nudges[0]?.result).toContain("not_reached"); + for (let i = 0; i < 100; i++) { + expect((await policy(store, i < 50 ? history : []).evaluate(makeJob())).action).toBe("skip"); + } + expect(store.appended).toHaveLength(writes); + expect(store.nudges).toHaveLength(nudges); + expect(nudges).toBe(1); + }); + + it.each(["quota-released", "guard-revision-b:off"])("rechecks admission when %s changes", async state => { + const store = new RowStore(); + const first = await policy(store, []).evaluate(makeJob()); + const history = [failedQuota(first)]; + expect((await policy(store, history).evaluate(makeJob())).action).toBe("skip"); + const retry = await policy(store, history, state).evaluate(makeJob()); + expect(retry.action).toBe("send"); + const delivered = sentHistory({ episodeKey: String(retry.notes?.["episodeKey"]), primaryRow: String(retry.notes?.["primaryRow"]) }); + expect((await policy(store, [delivered, ...history], state).evaluate(makeJob())).action).toBe("skip"); + }); + + it.each([undefined, "attempted", "written"])("does not turn phase %s into positive no-input admission", async phase => { + const store = new RowStore(); + const first = await policy(store, []).evaluate(makeJob()); + const entry = failedQuota(first, phase); + if (phase === undefined) delete entry.evaluationNotes!["deliveryInputPhase"]; + expect((await policy(store, [entry]).evaluate(makeJob())).action).toBe("send"); + }); + + it("leaves an ordinary transient failure retryable", async () => { + const store = new RowStore(); + const first = await policy(store, []).evaluate(makeJob()); + const entry = failedQuota(first); + delete entry.evaluationNotes!["deliveryCode"]; + expect((await policy(store, [entry]).evaluate(makeJob())).action).toBe("send"); + }); + + it("records terminal-only custody once without reserving recurring input attempts", async () => { + const store = new RowStore(); + const first = await policy(store, [], quotaState, true).evaluate(makeJob()); + expect(first).toMatchObject({ action: "skip", notes: { skippedSeats: [{ seat: SEAT, why: "terminal-seat" }] } }); + expect(store.nudges).toHaveLength(1); + expect(store.nudges[0]?.result).toContain("failed:terminal-seat:"); + expect(store.nudges[0]?.result).toContain("route it to an agent seat"); + const writes = store.appended.length; + for (let i = 0; i < 100; i++) expect((await policy(store, [], quotaState, true).evaluate(makeJob())).action).toBe("skip"); + expect(store.appended).toHaveLength(writes); + expect(store.nudges).toHaveLength(1); + expect((await policy(store, [], quotaState, false).evaluate(makeJob())).action).toBe("send"); + }); +}); diff --git a/packages/daemon/test/watchdog-delivery-guarded.test.ts b/packages/daemon/test/watchdog-delivery-guarded.test.ts index 8be1f7546..665222c18 100644 --- a/packages/daemon/test/watchdog-delivery-guarded.test.ts +++ b/packages/daemon/test/watchdog-delivery-guarded.test.ts @@ -12,7 +12,7 @@ import { TmuxAdapter } from "../src/adapters/tmux.js"; import { ALL_MIGRATIONS } from "../src/db/all-migrations.js"; import { migrate } from "../src/db/migrate.js"; import type { ComposerSnapshot } from "../src/domain/composer-prompts.js"; -import { OutboxHandler } from "../src/domain/outbox-handler.js"; +import { OutboxHandler, OutboxHandlerError } from "../src/domain/outbox-handler.js"; import { RigRepository } from "../src/domain/rig-repository.js"; import { SeatDeliveryGuard, resolveGuardTarget } from "../src/domain/seat-delivery-guard.js"; import { SessionRegistry } from "../src/domain/session-registry.js"; @@ -368,3 +368,36 @@ describe("current parked-owner reminders", () => { expect(f.submissions).toHaveLength(2); }); }); + + +describe("watchdog positive no-input outcomes", () => { + it("preserves the typed quota rejection through the real engine history", async () => { + const f = fixture(); + const jobs = new WatchdogJobsRepository(f.db); + const history = new WatchdogHistoryLog(f.db); + const deliver = makeWatchdogDeliver({ sessionTransport: { send: async () => { throw new OutboxHandlerError("retained_quota_full", "Held-message quota is full; no input was written."); } } }); + const engine = new WatchdogPolicyEngine({ jobsRepo: jobs, historyLog: history, eventBus: new EventBus(f.db), deliver }); + const job = jobs.register({ policy: "periodic-reminder", targetSession: seat, intervalSeconds: 60, registeredBySession: seat, specYaml: "message: Still owed\n" }); + await engine.evaluate(job); + expect(history.listForJob(job.jobId, 1)[0]).toMatchObject({ deliveryStatus: "failed", evaluationNotes: { deliveryCode: "retained_quota_full", deliveryInputPhase: "not_reached" } }); + expect(f.pastes).toHaveLength(0); + }); + + it("does not infer no input from an untyped error with the same text", async () => { + const deliver = makeWatchdogDeliver({ sessionTransport: { send: async () => { throw new Error("Held-message quota is full; no input was written."); } } }); + const result = await deliver({ targetSession: seat, message: "Still owed" }, { jobId: "quota-control", policy: PARKED_OWNER_POLICY_NAME, occurrenceId: "one" }); + expect(result).not.toHaveProperty("code"); + expect(result).not.toHaveProperty("inputPhase"); + }); + + it("excludes bound terminal-only parked reminders while preserving other deliberate deliveries", async () => { + const sends = vi.fn().mockResolvedValue({ ok: true }); + const deliver = makeWatchdogDeliver({ sessionTransport: { send: sends }, isTerminalSeat: () => true } as Parameters[0]); + const refused = await deliver({ targetSession: seat, message: "Parked wake" }, { jobId: "terminal-policy", policy: PARKED_OWNER_POLICY_NAME, occurrenceId: "one" }); + expect(refused).toMatchObject({ status: "failed", code: "terminal_seat", inputPhase: "not_reached" }); + expect(sends).not.toHaveBeenCalled(); + const deliberate = await deliver({ targetSession: seat, message: "Explicitly requested terminal text" }, { jobId: "terminal-explicit", policy: "periodic-reminder", occurrenceId: "two" }); + expect(deliberate.status).toBe("ok"); + expect(sends).toHaveBeenCalledTimes(1); + }); +});