From 558745772c6b0f7a0db238d86c2785f574ef5a5d Mon Sep 17 00:00:00 2001 From: shin-core <153108882+shin-core@users.noreply.github.com> Date: Sat, 25 Jul 2026 00:54:58 +0900 Subject: [PATCH] test(retention): cover pruneExpiredRecords's two defensive ?? 0 fallback arms pruneExpiredRecords has two defensive `?? 0` arms -- the dry-run count path (`Number(row?.n ?? 0)`) and the delete-loop path (`Number(result.meta?.changes ?? 0)`) -- with no direct test coverage, unlike the identical pattern on its sibling dedupeSignalSnapshots (which has dedicated tests). Add two tests mirroring that sibling's approach: mock env.DB so the count query returns no row / the delete run() result lacks meta, and assert the function falls back to 0 rather than producing NaN. No source change -- coverage-only for existing, correct code. --- test/unit/retention.test.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/unit/retention.test.ts b/test/unit/retention.test.ts index 086758da92..8e1c96914a 100644 --- a/test/unit/retention.test.ts +++ b/test/unit/retention.test.ts @@ -140,6 +140,39 @@ describe("pruneExpiredRecords", () => { const rows = await env.DB.prepare("SELECT delivery_id FROM webhook_events").all<{ delivery_id: string }>(); expect(rows.results.map((row) => row.delivery_id)).toEqual(["wh-recent"]); }); + + it("dry-run falls back to 0 when the count query returns no row (defensive ?? 0 arm, #8370)", async () => { + // Mirrors dedupeSignalSnapshots's existing equivalent test: the count `first()` returns undefined, so + // `Number(row?.n ?? 0)` must fall back to 0 rather than producing NaN. + const noRowEnv = { + DB: { + prepare: (_sql: string) => ({ + bind: (..._binds: unknown[]) => ({ first: async () => undefined }), + }), + }, + } as unknown as Env; + const results = await pruneExpiredRecords(noRowEnv, { + dryRun: true, + policy: [{ table: "audit_events", column: "created_at", days: 90 }], + }); + expect(results).toEqual([{ table: "audit_events", column: "created_at", cutoff: expect.any(String), deleted: 0 }]); + }); + + it("delete loop falls back to 0 changes when a run() result lacks meta (defensive ?? 0 arm, #8370)", async () => { + // The delete `run()` returns an object with no `meta`, so `Number(result.meta?.changes ?? 0)` must fall back + // to 0 -- which is < batchSize, so the batch loop terminates immediately with deleted: 0 (no NaN, no spin). + const noMetaEnv = { + DB: { + prepare: (_sql: string) => ({ + bind: (..._binds: unknown[]) => ({ run: async () => ({}) }), + }), + }, + } as unknown as Env; + const results = await pruneExpiredRecords(noMetaEnv, { + policy: [{ table: "audit_events", column: "created_at", days: 90 }], + }); + expect(results).toEqual([{ table: "audit_events", column: "created_at", cutoff: expect.any(String), deleted: 0 }]); + }); }); describe("dedupeSignalSnapshots", () => {