@@ -15,6 +15,7 @@ const mocks = vi.hoisted(() => ({
1515 isAsyncJobEnqueueError : vi . fn ( ) ,
1616 markFailed : vi . fn ( ) ,
1717 markPushed : vi . fn ( ) ,
18+ queueCancel : vi . fn ( ) ,
1819 queueEnqueue : vi . fn ( ) ,
1920 queueGetJob : vi . fn ( ) ,
2021 requireAttempt : vi . fn ( ) ,
@@ -31,6 +32,8 @@ vi.mock('@/lib/core/async-jobs', () => ({
3132 JOB_STATUS : {
3233 COMPLETED : 'completed' ,
3334 FAILED : 'failed' ,
35+ PENDING : 'pending' ,
36+ PROCESSING : 'processing' ,
3437 } ,
3538} ) )
3639
@@ -70,6 +73,7 @@ describe('newsletter Resend queueing', () => {
7073 vi . clearAllMocks ( )
7174 mocks . getAsyncBackendType . mockReturnValue ( 'trigger-dev' )
7275 mocks . getJobQueue . mockResolvedValue ( {
76+ cancelJob : mocks . queueCancel ,
7377 enqueue : mocks . queueEnqueue ,
7478 getJob : mocks . queueGetJob ,
7579 } )
@@ -142,23 +146,151 @@ describe('newsletter Resend queueing', () => {
142146 )
143147 } )
144148
145- it ( 'moves a newsletter run to failed when its persisted database job failed' , async ( ) => {
146- mocks . getAsyncBackendType . mockReturnValue ( 'database' )
147- mocks . claimAttempt . mockResolvedValue ( {
148- attempt : 2 ,
149- jobId : 'newsletter_resend_run-1_2' ,
150- run,
151- shouldEnqueue : false ,
152- } )
149+ it ( 'starts a new attempt when a persisted Trigger.dev job failed' , async ( ) => {
150+ mocks . claimAttempt
151+ . mockResolvedValueOnce ( {
152+ attempt : 2 ,
153+ jobId : 'trigger-run-failed' ,
154+ run,
155+ shouldEnqueue : false ,
156+ } )
157+ . mockResolvedValueOnce ( {
158+ attempt : 3 ,
159+ jobId : null ,
160+ run : { ...run , resendSyncJobId : null } ,
161+ shouldEnqueue : true ,
162+ } )
153163 mocks . queueGetJob . mockResolvedValue ( {
154- id : 'newsletter_resend_run-1_2 ' ,
164+ id : 'trigger-run-failed ' ,
155165 status : 'failed' ,
156166 error : 'worker stopped' ,
157167 } )
168+ mocks . queueEnqueue . mockResolvedValue ( 'trigger-run-retry' )
169+ mocks . setJob . mockResolvedValue ( { ...run , resendSyncJobId : 'trigger-run-retry' } )
170+
171+ const result = await enqueueNewsletterResendSync ( 'run-1' , 'admin-1' )
172+
173+ expect ( mocks . markFailed ) . toHaveBeenCalledWith ( 'run-1' , 2 , expect . any ( Error ) )
174+ expect ( mocks . queueEnqueue ) . toHaveBeenCalledWith (
175+ 'newsletter-resend-sync' ,
176+ { runId : 'run-1' , attempt : 3 , requestedById : 'admin-1' } ,
177+ expect . objectContaining ( { jobId : 'newsletter_resend_run-1_3' } )
178+ )
179+ expect ( result . jobId ) . toBe ( 'trigger-run-retry' )
180+ } )
181+
182+ it ( 'starts a new attempt when a completed job did not finalize the newsletter run' , async ( ) => {
183+ mocks . claimAttempt
184+ . mockResolvedValueOnce ( {
185+ attempt : 2 ,
186+ jobId : 'trigger-run-completed' ,
187+ run,
188+ shouldEnqueue : false ,
189+ } )
190+ . mockResolvedValueOnce ( {
191+ attempt : 3 ,
192+ jobId : null ,
193+ run : { ...run , resendSyncJobId : null } ,
194+ shouldEnqueue : true ,
195+ } )
196+ mocks . queueGetJob . mockResolvedValue ( {
197+ id : 'trigger-run-completed' ,
198+ status : 'completed' ,
199+ } )
200+ mocks . queueEnqueue . mockResolvedValue ( 'trigger-run-retry' )
201+ mocks . setJob . mockResolvedValue ( { ...run , resendSyncJobId : 'trigger-run-retry' } )
202+
203+ const result = await enqueueNewsletterResendSync ( 'run-1' , 'admin-1' )
204+
205+ expect ( mocks . markFailed ) . toHaveBeenCalledWith ( 'run-1' , 2 , expect . any ( Error ) )
206+ expect ( mocks . queueEnqueue ) . toHaveBeenCalledWith (
207+ 'newsletter-resend-sync' ,
208+ { runId : 'run-1' , attempt : 3 , requestedById : 'admin-1' } ,
209+ expect . objectContaining ( { jobId : 'newsletter_resend_run-1_3' } )
210+ )
211+ expect ( result . jobId ) . toBe ( 'trigger-run-retry' )
212+ } )
213+
214+ it . each ( [
215+ [ 'completed' , { id : 'trigger-run-existing' , status : 'completed' } ] ,
216+ [ 'processing' , { id : 'trigger-run-existing' , status : 'processing' } ] ,
217+ [ 'missing' , null ] ,
218+ ] ) (
219+ 'keeps the stored job when the newsletter run is pushed and the provider job is %s' ,
220+ async ( _providerState , providerJob ) => {
221+ const pushedRun = { ...run , status : 'pushed' }
222+ mocks . claimAttempt . mockResolvedValue ( {
223+ attempt : 2 ,
224+ jobId : 'trigger-run-existing' ,
225+ run : pushedRun ,
226+ shouldEnqueue : false ,
227+ } )
228+ mocks . queueGetJob . mockResolvedValue ( providerJob )
229+
230+ const result = await enqueueNewsletterResendSync ( 'run-1' , 'admin-1' )
231+
232+ expect ( mocks . queueGetJob ) . not . toHaveBeenCalled ( )
233+ expect ( mocks . markFailed ) . not . toHaveBeenCalled ( )
234+ expect ( mocks . queueEnqueue ) . not . toHaveBeenCalled ( )
235+ expect ( result ) . toEqual ( { run : pushedRun , jobId : 'trigger-run-existing' } )
236+ }
237+ )
238+
239+ it ( 're-enqueues when a stored Trigger.dev run no longer exists' , async ( ) => {
240+ mocks . claimAttempt
241+ . mockResolvedValueOnce ( {
242+ attempt : 2 ,
243+ jobId : 'trigger-run-missing' ,
244+ run,
245+ shouldEnqueue : false ,
246+ } )
247+ . mockResolvedValueOnce ( {
248+ attempt : 3 ,
249+ jobId : null ,
250+ run,
251+ shouldEnqueue : true ,
252+ } )
253+ mocks . queueGetJob . mockResolvedValue ( null )
254+
255+ await enqueueNewsletterResendSync ( 'run-1' , 'admin-1' )
256+
257+ expect ( mocks . markFailed ) . toHaveBeenCalledWith ( 'run-1' , 2 , expect . any ( Error ) )
258+ expect ( mocks . queueEnqueue ) . toHaveBeenCalledWith (
259+ 'newsletter-resend-sync' ,
260+ { runId : 'run-1' , attempt : 3 , requestedById : 'admin-1' } ,
261+ expect . objectContaining ( { jobId : 'newsletter_resend_run-1_3' } )
262+ )
263+ } )
264+
265+ it ( 'cancels and replaces an active Trigger.dev run when an admin resumes it' , async ( ) => {
266+ mocks . claimAttempt
267+ . mockResolvedValueOnce ( {
268+ attempt : 2 ,
269+ jobId : 'trigger-run-active' ,
270+ run,
271+ shouldEnqueue : false ,
272+ } )
273+ . mockResolvedValueOnce ( {
274+ attempt : 3 ,
275+ jobId : null ,
276+ run,
277+ shouldEnqueue : true ,
278+ } )
279+ mocks . queueGetJob . mockResolvedValue ( {
280+ id : 'trigger-run-active' ,
281+ status : 'processing' ,
282+ } )
158283
159- await expect ( enqueueNewsletterResendSync ( 'run-1' , 'admin-1' ) ) . rejects . toThrow ( 'worker stopped' )
284+ const result = await enqueueNewsletterResendSync ( 'run-1' , 'admin-1' )
285+
286+ expect ( mocks . queueCancel ) . toHaveBeenCalledWith ( 'trigger-run-active' )
160287 expect ( mocks . markFailed ) . toHaveBeenCalledWith ( 'run-1' , 2 , expect . any ( Error ) )
161- expect ( mocks . queueEnqueue ) . not . toHaveBeenCalled ( )
288+ expect ( mocks . queueEnqueue ) . toHaveBeenCalledWith (
289+ 'newsletter-resend-sync' ,
290+ { runId : 'run-1' , attempt : 3 , requestedById : 'admin-1' } ,
291+ expect . objectContaining ( { jobId : 'newsletter_resend_run-1_3' } )
292+ )
293+ expect ( result . jobId ) . toBe ( 'trigger-run-123' )
162294 } )
163295
164296 it ( 'resets failed recipients before a task retry' , async ( ) => {
0 commit comments