@@ -20,12 +20,18 @@ import {
2020 deliverCredentialMail ,
2121 deliverMentionMail ,
2222 deliverRunFailureMail ,
23+ type ApprovalNotification ,
2324 type NotifyDeliveryDeps ,
25+ type NotifyDispatchStore ,
26+ type SinkDeliveryResult ,
2427} from "@corbits/notify" ;
2528
2629import { setupDatabase } from "../../../scripts/db-setup" ;
2730import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url" ;
28- import { createWorkbenchMailboxDelivery } from "../src/delivery" ;
31+ import {
32+ createResolveExistingMailIds ,
33+ createWorkbenchMailboxDelivery ,
34+ } from "../src/delivery" ;
2935import { inboxGroupOf } from "../src/group" ;
3036import { WORKBENCH_INBOX_PRIORITIES } from "../src/vocabulary" ;
3137import { dbGate } from "../../../scripts/e2e/db-gate" ;
@@ -213,5 +219,91 @@ describeIfDb(
213219 await mailboxDb . close ( ) ;
214220 }
215221 } , 15000 ) ;
222+
223+ test ( "a crash between mail write and dispatch enqueue is repaired on redelivery without doubles (CL-7238)" , async ( ) => {
224+ const mailboxDb = createMailboxDb ( scratchUrl ) ;
225+ try {
226+ const innerDispatch = createInMemoryNotifyDispatchStore ( ) ;
227+ let crashNext = true ;
228+ const dispatch : NotifyDispatchStore = {
229+ ...innerDispatch ,
230+ enqueue : async ( inputs ) => {
231+ if ( crashNext ) {
232+ crashNext = false ;
233+ throw new Error (
234+ "boom: crash between the mail write and the dispatch enqueue" ,
235+ ) ;
236+ }
237+ await innerDispatch . enqueue ( inputs ) ;
238+ } ,
239+ } ;
240+ const delivered : SinkDeliveryResult = { status : "delivered" } ;
241+ const sinks = createSinkRegistry ( ) ;
242+ sinks . register ( {
243+ name : "always" ,
244+ isEnabledFor : async ( ) => true ,
245+ deliver : async ( ) => delivered ,
246+ } ) ;
247+ const deps : NotifyDeliveryDeps = {
248+ mail : createWorkbenchMailboxDelivery ( { db : mailboxDb . db } ) ,
249+ addressing : {
250+ inbox : ( recipient ) => `${ recipient . principalId } @inbox.test` ,
251+ from : ( kind ) => `${ kind } @notify.test` ,
252+ } ,
253+ dispatch,
254+ resolveExistingMailIds : createResolveExistingMailIds ( mailboxDb . db ) ,
255+ sinks,
256+ } ;
257+ const event : ApprovalNotification = {
258+ kind : "approval" ,
259+ approvalId : generateId ( "approval" ) ,
260+ tenantId,
261+ runId : generateId ( "workflowRun" ) ,
262+ deploymentId : generateId ( "workflowRun" ) ,
263+ toolName : "quarantine_token" ,
264+ toolArguments : { repo : "corbitsdev/workbench" } ,
265+ recipients : [ { tenantId, principalId } ] ,
266+ createdAt : new Date ( ) . toISOString ( ) ,
267+ } ;
268+
269+ // The first attempt commits its mail row through the real adapter,
270+ // then dies before the dispatch enqueue — the crash window.
271+ await expect ( deliverApprovalMail ( deps , event ) ) . rejects . toThrow ( "boom" ) ;
272+
273+ // The redelivery dedupes on the mail key (no new row), resolves the
274+ // committed row's id through the adapter read-back, and queues the
275+ // dispatch the crash swallowed.
276+ const redelivery = await deliverApprovalMail ( deps , event ) ;
277+ expect ( redelivery . deliveredMailboxRowIds ) . toEqual ( [ ] ) ;
278+ expect ( redelivery . queuedDispatchCount ) . toBe ( 1 ) ;
279+
280+ const page = await listUserMailbox ( mailboxDb . db , {
281+ tenantId,
282+ principalId,
283+ limit : 10 ,
284+ view : "all" ,
285+ priorities : WORKBENCH_INBOX_PRIORITIES ,
286+ } ) ;
287+ // Exactly one mail row for the redelivered event — the crash and
288+ // both redeliveries deduped onto the first attempt's row.
289+ const matching = page . items . filter ( ( item ) =>
290+ ( item . subject ?? "" ) . includes ( "quarantine_token" ) ,
291+ ) ;
292+ expect ( matching ) . toHaveLength ( 1 ) ;
293+ const mailId = matching [ 0 ] ?. id ;
294+ if ( mailId === undefined )
295+ throw new Error ( "expected the redelivered mail row to list" ) ;
296+ expect ( await dispatch . listFor ( mailId ) ) . toHaveLength ( 1 ) ;
297+
298+ // A further benign redelivery repairs the same row again, and the
299+ // dispatch store's (mail row, sink) dedupe holds it to one row.
300+ const third = await deliverApprovalMail ( deps , event ) ;
301+ expect ( third . deliveredMailboxRowIds ) . toEqual ( [ ] ) ;
302+ expect ( third . queuedDispatchCount ) . toBe ( 1 ) ;
303+ expect ( await dispatch . listFor ( mailId ) ) . toHaveLength ( 1 ) ;
304+ } finally {
305+ await mailboxDb . close ( ) ;
306+ }
307+ } , 15000 ) ;
216308 } ,
217309) ;
0 commit comments