@@ -12,10 +12,10 @@ const daysAgo = (n: number) => new Date(NOW - n * 86_400_000).toISOString();
1212
1313async function seed ( env : Env ) {
1414 const db = getDb ( env . DB ) ;
15- // webhook_events are durable replay/idempotency records and must not be pruned .
15+ // webhook_events: 90d window (#8381) — seed past-cutoff + recent rows .
1616 await db . insert ( webhookEvents ) . values ( [
17- { deliveryId : "wh-old-1" , eventName : "push" , payloadHash : "h" , status : "processed" , receivedAt : daysAgo ( 40 ) } ,
18- { deliveryId : "wh-old-2" , eventName : "push" , payloadHash : "h" , status : "processed" , receivedAt : daysAgo ( 35 ) } ,
17+ { deliveryId : "wh-old-1" , eventName : "push" , payloadHash : "h" , status : "processed" , receivedAt : daysAgo ( 100 ) } ,
18+ { deliveryId : "wh-old-2" , eventName : "push" , payloadHash : "h" , status : "processed" , receivedAt : daysAgo ( 95 ) } ,
1919 { deliveryId : "wh-recent" , eventName : "push" , payloadHash : "h" , status : "processed" , receivedAt : daysAgo ( 1 ) } ,
2020 ] ) ;
2121 // ai_usage_events window = 90d; one old + one recent.
@@ -48,7 +48,7 @@ describe("pruneExpiredRecords", () => {
4848 await seed ( env ) ;
4949 const results = await pruneExpiredRecords ( env , { dryRun : true , nowMs : NOW } ) ;
5050 const ai = results . find ( ( r ) => r . table === "ai_usage_events" ) ;
51- expect ( results . find ( ( r ) => r . table === "webhook_events" ) ) . toBeUndefined ( ) ;
51+ expect ( results . find ( ( r ) => r . table === "webhook_events" ) ?. deleted ) . toBe ( 2 ) ;
5252 expect ( ai ?. deleted ) . toBe ( 1 ) ;
5353 expect ( await countWebhook ( env ) ) . toBe ( 3 ) ; // nothing actually deleted
5454 } ) ;
@@ -57,9 +57,9 @@ describe("pruneExpiredRecords", () => {
5757 const env = createTestEnv ( ) ;
5858 await seed ( env ) ;
5959 const results = await pruneExpiredRecords ( env , { nowMs : NOW } ) ;
60- expect ( results . find ( ( r ) => r . table === "webhook_events" ) ) . toBeUndefined ( ) ;
60+ expect ( results . find ( ( r ) => r . table === "webhook_events" ) ?. deleted ) . toBe ( 2 ) ;
6161 expect ( results . find ( ( r ) => r . table === "ai_usage_events" ) ?. deleted ) . toBe ( 1 ) ;
62- expect ( await countWebhook ( env ) ) . toBe ( 3 ) ;
62+ expect ( await countWebhook ( env ) ) . toBe ( 1 ) ;
6363 const aiCount = await env . DB . prepare ( "SELECT count(*) AS n FROM ai_usage_events" ) . first < { n : number } > ( ) ;
6464 expect ( aiCount ?. n ) . toBe ( 1 ) ;
6565 } ) ;
@@ -123,10 +123,23 @@ describe("pruneExpiredRecords", () => {
123123
124124 it ( "the policy only targets append-only/log/snapshot tables (no current-state tables)" , ( ) => {
125125 const tables = RETENTION_POLICY . map ( ( r ) => r . table ) ;
126- for ( const protectedTable of [ "webhook_events" , "repositories" , "repository_settings" , "pull_requests" , "issues" , "repository_ai_keys" , "contributors" ] ) {
126+ expect ( tables ) . toContain ( "webhook_events" ) ;
127+ for ( const protectedTable of [ "repositories" , "repository_settings" , "pull_requests" , "issues" , "repository_ai_keys" , "contributors" ] ) {
127128 expect ( tables ) . not . toContain ( protectedTable ) ;
128129 }
129130 } ) ;
131+
132+ it ( "prunes webhook_events older than 90d and keeps recent deliveries (#8381)" , async ( ) => {
133+ const env = createTestEnv ( ) ;
134+ await seed ( env ) ;
135+ const results = await pruneExpiredRecords ( env , {
136+ nowMs : NOW ,
137+ policy : [ { table : "webhook_events" , column : "received_at" , days : 90 } ] ,
138+ } ) ;
139+ expect ( results [ 0 ] ?. deleted ) . toBe ( 2 ) ;
140+ const rows = await env . DB . prepare ( "SELECT delivery_id FROM webhook_events" ) . all < { delivery_id : string } > ( ) ;
141+ expect ( rows . results . map ( ( row ) => row . delivery_id ) ) . toEqual ( [ "wh-recent" ] ) ;
142+ } ) ;
130143} ) ;
131144
132145describe ( "dedupeSignalSnapshots" , ( ) => {
@@ -281,7 +294,7 @@ describe("runRetentionPrune + processJob", () => {
281294 await insertSignalSnapshot ( env , "s-1" , "repo-culture-profile" , "JSONbored/loopover" , "2026-06-01T00:00:00.000Z" ) ;
282295 await insertSignalSnapshot ( env , "s-2" , "repo-culture-profile" , "JSONbored/loopover" , "2026-06-02T00:00:00.000Z" ) ;
283296 await processJob ( env , { type : "prune-retention" , requestedBy : "schedule" } ) ;
284- expect ( await countWebhook ( env ) ) . toBe ( 3 ) ;
297+ expect ( await countWebhook ( env ) ) . toBe ( 1 ) ;
285298 expect ( await countSignalSnapshots ( env , "repo-culture-profile" ) ) . toBe ( 1 ) ;
286299 const audit = await env . DB . prepare ( "SELECT outcome, detail FROM audit_events WHERE event_type = ?" ) . bind ( "retention.prune" ) . first < { outcome : string ; detail : string } > ( ) ;
287300 expect ( audit ?. outcome ) . toBe ( "success" ) ;
@@ -305,7 +318,7 @@ describe("retention preview route", () => {
305318 signalSnapshotDuplicates : Array < { signalType : string ; deleted : number } > ;
306319 } ;
307320 expect ( body . totalEligible ) . toBeGreaterThanOrEqual ( 1 ) ;
308- expect ( body . eligible . find ( ( r ) => r . table === "webhook_events" ) ) . toBeUndefined ( ) ;
321+ expect ( body . eligible . find ( ( r ) => r . table === "webhook_events" ) ?. deleted ) . toBe ( 2 ) ;
309322 expect ( body . totalSignalSnapshotDuplicates ) . toBe ( 1 ) ;
310323 expect ( body . signalSnapshotDuplicates ) . toEqual ( [ { signalType : "repo-culture-profile" , deleted : 1 } ] ) ;
311324 expect ( await countWebhook ( env ) ) . toBe ( 3 ) ; // preview is read-only
0 commit comments