Skip to content

Commit 6402a23

Browse files
RealDiligentclaude
andcommitted
test(retention): cover pruneExpiredRecords's two defensive ?? 0 arms
The dry-run `row?.n ?? 0` and delete-loop `result.meta?.changes ?? 0` guards had no direct coverage, unlike the identical pattern on the sibling dedupeSignalSnapshots, whose arms are already tested in this file. Test-only: pruneExpiredRecords's logic is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9b9924b commit 6402a23

1 file changed

Lines changed: 68 additions & 0 deletions

File tree

test/unit/retention.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,74 @@ describe("pruneExpiredRecords", () => {
140140
const rows = await env.DB.prepare("SELECT delivery_id FROM webhook_events").all<{ delivery_id: string }>();
141141
expect(rows.results.map((row) => row.delivery_id)).toEqual(["wh-recent"]);
142142
});
143+
144+
// #8370: the same defensive-arm coverage dedupeSignalSnapshots already has below, for this function's
145+
// identical `?? 0` guards against a D1 driver returning an unexpected row/meta shape.
146+
it("dry-run falls back to 0 when the count query returns no row (defensive ?? 0 arm)", async () => {
147+
const noRowEnv = {
148+
DB: {
149+
prepare: (_sql: string) => ({
150+
bind: (..._binds: unknown[]) => ({ first: async () => undefined }), // no row → `row?.n ?? 0` fires
151+
}),
152+
},
153+
} as unknown as Env;
154+
const results = await pruneExpiredRecords(noRowEnv, {
155+
dryRun: true,
156+
nowMs: NOW,
157+
policy: [{ table: "webhook_events", column: "received_at", days: 90 }],
158+
});
159+
expect(results).toEqual([{ table: "webhook_events", column: "received_at", cutoff: daysAgo(90), deleted: 0 }]);
160+
});
161+
162+
it("dry-run falls back to 0 when the count row carries a null n (defensive ?? 0 arm, present-row side)", async () => {
163+
const nullCountEnv = {
164+
DB: {
165+
prepare: (_sql: string) => ({
166+
bind: (..._binds: unknown[]) => ({ first: async () => ({ n: null }) }), // row present, n null → same fallback
167+
}),
168+
},
169+
} as unknown as Env;
170+
const results = await pruneExpiredRecords(nullCountEnv, {
171+
dryRun: true,
172+
nowMs: NOW,
173+
policy: [{ table: "webhook_events", column: "received_at", days: 90 }],
174+
});
175+
expect(results[0]?.deleted).toBe(0);
176+
// Number(null) is 0, but Number(undefined) is NaN — the point of the guard is that neither shape yields NaN.
177+
expect(Number.isNaN(results[0]?.deleted)).toBe(false);
178+
});
179+
180+
it("falls back to 0 changes when a delete run() result lacks meta (defensive ?? 0 arm)", async () => {
181+
const noMetaEnv = {
182+
DB: {
183+
prepare: (_sql: string) => ({
184+
// no meta → `result.meta?.changes ?? 0` fires, so changes = 0 < batchSize and the loop exits at once
185+
bind: (..._binds: unknown[]) => ({ run: async () => ({}) }),
186+
}),
187+
},
188+
} as unknown as Env;
189+
const results = await pruneExpiredRecords(noMetaEnv, {
190+
nowMs: NOW,
191+
policy: [{ table: "webhook_events", column: "received_at", days: 90 }],
192+
});
193+
expect(results).toEqual([{ table: "webhook_events", column: "received_at", cutoff: daysAgo(90), deleted: 0 }]);
194+
});
195+
196+
it("falls back to 0 changes when meta is present but changes is null (defensive ?? 0 arm, present-meta side)", async () => {
197+
const nullChangesEnv = {
198+
DB: {
199+
prepare: (_sql: string) => ({
200+
bind: (..._binds: unknown[]) => ({ run: async () => ({ meta: { changes: null } }) }),
201+
}),
202+
},
203+
} as unknown as Env;
204+
const results = await pruneExpiredRecords(nullChangesEnv, {
205+
nowMs: NOW,
206+
policy: [{ table: "webhook_events", column: "received_at", days: 90 }],
207+
});
208+
expect(results[0]?.deleted).toBe(0);
209+
expect(Number.isNaN(results[0]?.deleted)).toBe(false);
210+
});
143211
});
144212

145213
describe("dedupeSignalSnapshots", () => {

0 commit comments

Comments
 (0)