@@ -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
145213describe ( "dedupeSignalSnapshots" , ( ) => {
0 commit comments