@@ -58,6 +58,7 @@ vi.mock('@/lib/newsletters/runs', () => ({
5858 updateRecipientSyncStatus : mocks . updateRecipient ,
5959} ) )
6060
61+ import { pushNewsletterRunResponseSchema } from '@/lib/api/contracts/newsletters'
6162import { enqueueNewsletterResendSync , runNewsletterResendSync } from '@/lib/newsletters/push-resend'
6263
6364const run = {
@@ -86,6 +87,7 @@ describe('newsletter Resend queueing', () => {
8687 } )
8788 mocks . queueEnqueue . mockResolvedValue ( 'trigger-run-123' )
8889 mocks . setJob . mockResolvedValue ( { ...run , resendSyncJobId : 'trigger-run-123' } )
90+ mocks . setSegment . mockResolvedValue ( run )
8991 mocks . isAsyncJobEnqueueError . mockReturnValue ( false )
9092 } )
9193
@@ -238,6 +240,55 @@ describe('newsletter Resend queueing', () => {
238240 }
239241 )
240242
243+ it ( 'does not enqueue a pushed run without a stored job id' , async ( ) => {
244+ const pushedRun = { ...run , status : 'pushed' }
245+ mocks . claimAttempt . mockResolvedValue ( {
246+ attempt : 2 ,
247+ jobId : null ,
248+ run : pushedRun ,
249+ shouldEnqueue : false ,
250+ } )
251+
252+ const result = await enqueueNewsletterResendSync ( 'run-1' , 'admin-1' )
253+
254+ expect ( mocks . getJobQueue ) . not . toHaveBeenCalled ( )
255+ expect ( mocks . queueEnqueue ) . not . toHaveBeenCalled ( )
256+ expect ( result ) . toEqual ( { run : pushedRun , jobId : null } )
257+ expect ( pushNewsletterRunResponseSchema . shape . jobId . parse ( result . jobId ) ) . toBeNull ( )
258+ } )
259+
260+ it . each ( [
261+ [ 'completed' , { id : 'trigger-run-existing' , status : 'completed' } ] ,
262+ [ 'failed' , { id : 'trigger-run-existing' , status : 'failed' , error : 'worker failed' } ] ,
263+ [ 'processing' , { id : 'trigger-run-existing' , status : 'processing' } ] ,
264+ ] ) (
265+ 'does not enqueue when reconciliation of a %s job refreshes to pushed' ,
266+ async ( _providerState , providerJob ) => {
267+ const pushedRun = { ...run , status : 'pushed' }
268+ mocks . claimAttempt
269+ . mockResolvedValueOnce ( {
270+ attempt : 2 ,
271+ jobId : 'trigger-run-existing' ,
272+ run,
273+ shouldEnqueue : false ,
274+ } )
275+ . mockResolvedValueOnce ( {
276+ attempt : 2 ,
277+ jobId : null ,
278+ run : pushedRun ,
279+ shouldEnqueue : false ,
280+ } )
281+ mocks . queueGetJob . mockResolvedValue ( providerJob )
282+
283+ const result = await enqueueNewsletterResendSync ( 'run-1' , 'admin-1' )
284+
285+ expect ( mocks . markFailed ) . toHaveBeenCalledWith ( 'run-1' , 2 , expect . any ( Error ) )
286+ expect ( mocks . queueEnqueue ) . not . toHaveBeenCalled ( )
287+ expect ( mocks . setJob ) . not . toHaveBeenCalled ( )
288+ expect ( result ) . toEqual ( { run : pushedRun , jobId : null } )
289+ }
290+ )
291+
241292 it ( 're-enqueues when a stored Trigger.dev run no longer exists' , async ( ) => {
242293 mocks . claimAttempt
243294 . mockResolvedValueOnce ( {
@@ -342,6 +393,78 @@ describe('newsletter Resend queueing', () => {
342393 expect ( mocks . markFailed ) . not . toHaveBeenCalled ( )
343394 } )
344395
396+ it ( 'stops when the authoritative post-reset read is already pushed' , async ( ) => {
397+ mocks . requireAttempt
398+ . mockResolvedValueOnce ( run )
399+ . mockResolvedValueOnce ( { ...run , status : 'pushed' } )
400+
401+ await runNewsletterResendSync ( {
402+ runId : 'run-1' ,
403+ attempt : 2 ,
404+ requestedById : 'admin-1' ,
405+ } )
406+
407+ expect ( mocks . createSegment ) . not . toHaveBeenCalled ( )
408+ expect ( mocks . setSegment ) . not . toHaveBeenCalled ( )
409+ expect ( mocks . ensureProperties ) . not . toHaveBeenCalled ( )
410+ expect ( mocks . markFailed ) . not . toHaveBeenCalled ( )
411+ } )
412+
413+ it ( 'uses the segment from the authoritative post-reset read' , async ( ) => {
414+ const staleRun = {
415+ ...run ,
416+ resendSegmentId : null ,
417+ resendSegmentName : null ,
418+ }
419+ const currentRun = {
420+ ...run ,
421+ resendSegmentId : 'segment-current' ,
422+ resendSegmentName : 'Current segment' ,
423+ }
424+ mocks . requireAttempt . mockResolvedValueOnce ( staleRun ) . mockResolvedValueOnce ( currentRun )
425+ mocks . setSegment . mockResolvedValue ( currentRun )
426+ mocks . getExcludedEmails . mockResolvedValue ( new Set ( ) )
427+ mocks . getPendingRecipients . mockResolvedValue ( [ ] )
428+ mocks . countByStatus . mockResolvedValue ( { } )
429+
430+ await runNewsletterResendSync ( {
431+ runId : 'run-1' ,
432+ attempt : 2 ,
433+ requestedById : 'admin-1' ,
434+ } )
435+
436+ expect ( mocks . createSegment ) . not . toHaveBeenCalled ( )
437+ expect ( mocks . setSegment ) . toHaveBeenCalledWith ( 'run-1' , 2 , 'segment-current' , 'Current segment' )
438+ expect ( mocks . markPushed ) . toHaveBeenCalledWith ( 'run-1' , 2 , 'segment-current' , 'Current segment' )
439+ } )
440+
441+ it ( 'stops when another worker pushes while a segment is being created' , async ( ) => {
442+ const runWithoutSegment = {
443+ ...run ,
444+ resendSegmentId : null ,
445+ resendSegmentName : null ,
446+ }
447+ mocks . requireAttempt . mockResolvedValue ( runWithoutSegment )
448+ mocks . createSegment . mockResolvedValue ( { id : 'segment-new' , name : 'New segment' } )
449+ mocks . setSegment . mockResolvedValue ( {
450+ ...run ,
451+ status : 'pushed' ,
452+ resendSegmentId : 'segment-winner' ,
453+ resendSegmentName : 'Winner segment' ,
454+ } )
455+
456+ await runNewsletterResendSync ( {
457+ runId : 'run-1' ,
458+ attempt : 2 ,
459+ requestedById : 'admin-1' ,
460+ } )
461+
462+ expect ( mocks . setSegment ) . toHaveBeenCalledWith ( 'run-1' , 2 , 'segment-new' , 'New segment' )
463+ expect ( mocks . ensureProperties ) . not . toHaveBeenCalled ( )
464+ expect ( mocks . markPushed ) . not . toHaveBeenCalled ( )
465+ expect ( mocks . markFailed ) . not . toHaveBeenCalled ( )
466+ } )
467+
345468 it ( 'does not fail the newsletter attempt when its database claim is aborted' , async ( ) => {
346469 const controller = new AbortController ( )
347470 controller . abort ( 'claim lost' )
0 commit comments