@@ -62,6 +62,17 @@ function buildFailureDestinationOnlyJob(name: string): CronAddInput {
6262 } ;
6363}
6464
65+ function buildFailureAlertOnlyJob ( name : string ) : CronAddInput {
66+ return {
67+ ...buildIsolatedAgentTurnJob ( name ) ,
68+ failureAlert : {
69+ after : 1 ,
70+ channel : "forum" ,
71+ to : "123" ,
72+ } ,
73+ } ;
74+ }
75+
6576function buildBestEffortFailureDestinationOnlyJob ( name : string ) : CronAddInput {
6677 return {
6778 ...buildFailureDestinationOnlyJob ( name ) ,
@@ -92,6 +103,7 @@ function createIsolatedCronWithFinishedBarrier(params: {
92103 status ?: "ok" | "error" ;
93104 delivered ?: boolean ;
94105 error ?: string ;
106+ sendCronFailureAlert ?: ConstructorParameters < typeof CronService > [ 0 ] [ "sendCronFailureAlert" ] ;
95107 onFinished ?: ( evt : {
96108 jobId : string ;
97109 delivered ?: boolean ;
@@ -116,6 +128,9 @@ function createIsolatedCronWithFinishedBarrier(params: {
116128 ...( params . error === undefined ? { } : { error : params . error } ) ,
117129 ...( params . delivered === undefined ? { } : { delivered : params . delivered } ) ,
118130 } ) ) ,
131+ ...( params . sendCronFailureAlert === undefined
132+ ? { }
133+ : { sendCronFailureAlert : params . sendCronFailureAlert } ) ,
119134 onEvent : ( evt ) => {
120135 if ( evt . action === "finished" ) {
121136 params . onFinished ?.( {
@@ -190,6 +205,7 @@ async function runIsolatedJobAndReadState(params: {
190205 status ?: "ok" | "error" ;
191206 delivered ?: boolean ;
192207 error ?: string ;
208+ sendCronFailureAlert ?: ConstructorParameters < typeof CronService > [ 0 ] [ "sendCronFailureAlert" ] ;
193209 onFinished ?: ( evt : {
194210 jobId : string ;
195211 delivered ?: boolean ;
@@ -208,6 +224,9 @@ async function runIsolatedJobAndReadState(params: {
208224 ...( params . status !== undefined ? { status : params . status } : { } ) ,
209225 ...( params . delivered !== undefined ? { delivered : params . delivered } : { } ) ,
210226 ...( params . error !== undefined ? { error : params . error } : { } ) ,
227+ ...( params . sendCronFailureAlert !== undefined
228+ ? { sendCronFailureAlert : params . sendCronFailureAlert }
229+ : { } ) ,
211230 onFinished : ( evt ) => {
212231 params . onFinished ?.( evt ) ;
213232 finishedEvents . get ( evt . jobId ) ?.( evt ) ;
@@ -326,6 +345,102 @@ describe("CronService persists delivered status", () => {
326345 expect ( capturedEvent ?. failureNotificationDelivery ) . toEqual ( { status : "unknown" } ) ;
327346 } ) ;
328347
348+ it ( "persists delivered status for requested per-job failureAlert" , async ( ) => {
349+ const sendCronFailureAlert = vi . fn ( async ( ) => undefined ) ;
350+ let capturedEvent :
351+ | {
352+ delivered ?: boolean ;
353+ deliveryStatus ?: string ;
354+ failureNotificationDelivery ?: {
355+ delivered ?: boolean ;
356+ status : string ;
357+ error ?: string ;
358+ } ;
359+ }
360+ | undefined ;
361+ const updated = await runIsolatedJobAndReadState ( {
362+ job : buildFailureAlertOnlyJob ( "failure-alert-only" ) ,
363+ status : "error" ,
364+ error : "Agent couldn't generate a response." ,
365+ sendCronFailureAlert,
366+ onFinished : ( evt ) => {
367+ capturedEvent = evt ;
368+ } ,
369+ } ) ;
370+
371+ expect ( sendCronFailureAlert ) . toHaveBeenCalledTimes ( 1 ) ;
372+ expect ( updated ?. state . lastRunStatus ) . toBe ( "error" ) ;
373+ expect ( updated ?. state . lastDeliveryStatus ) . toBe ( "not-requested" ) ;
374+ expect ( updated ?. state . lastFailureNotificationDelivered ) . toBe ( true ) ;
375+ expect ( updated ?. state . lastFailureNotificationDeliveryStatus ) . toBe ( "delivered" ) ;
376+ expect ( updated ?. state . lastFailureNotificationDeliveryError ) . toBeUndefined ( ) ;
377+ expect ( capturedEvent ?. deliveryStatus ) . toBe ( "not-requested" ) ;
378+ expect ( capturedEvent ?. failureNotificationDelivery ) . toEqual ( {
379+ delivered : true ,
380+ status : "delivered" ,
381+ } ) ;
382+ } ) ;
383+
384+ it ( "persists not-delivered status when per-job failureAlert send fails" , async ( ) => {
385+ const sendCronFailureAlert = vi . fn ( async ( ) => {
386+ throw new Error ( "channel offline" ) ;
387+ } ) ;
388+ let capturedEvent :
389+ | {
390+ failureNotificationDelivery ?: {
391+ delivered ?: boolean ;
392+ status : string ;
393+ error ?: string ;
394+ } ;
395+ }
396+ | undefined ;
397+ const updated = await runIsolatedJobAndReadState ( {
398+ job : buildFailureAlertOnlyJob ( "failure-alert-send-fails" ) ,
399+ status : "error" ,
400+ error : "Agent couldn't generate a response." ,
401+ sendCronFailureAlert,
402+ onFinished : ( evt ) => {
403+ capturedEvent = evt ;
404+ } ,
405+ } ) ;
406+
407+ expect ( sendCronFailureAlert ) . toHaveBeenCalledTimes ( 1 ) ;
408+ expect ( updated ?. state . lastRunStatus ) . toBe ( "error" ) ;
409+ expect ( updated ?. state . lastDeliveryStatus ) . toBe ( "not-requested" ) ;
410+ expect ( updated ?. state . lastFailureNotificationDelivered ) . toBe ( false ) ;
411+ expect ( updated ?. state . lastFailureNotificationDeliveryStatus ) . toBe ( "not-delivered" ) ;
412+ expect ( updated ?. state . lastFailureNotificationDeliveryError ) . toBe ( "Error: channel offline" ) ;
413+ expect ( capturedEvent ?. failureNotificationDelivery ) . toEqual ( {
414+ delivered : false ,
415+ status : "not-delivered" ,
416+ error : "Error: channel offline" ,
417+ } ) ;
418+ } ) ;
419+
420+ it ( "persists unknown status when per-job failureAlert send times out" , async ( ) => {
421+ const sendCronFailureAlert = vi . fn ( ( ) => new Promise < never > ( ( ) => { } ) ) ;
422+ const updatedPromise = runIsolatedJobAndReadState ( {
423+ job : buildFailureAlertOnlyJob ( "failure-alert-send-times-out" ) ,
424+ status : "error" ,
425+ error : "Agent couldn't generate a response." ,
426+ sendCronFailureAlert,
427+ } ) ;
428+
429+ await vi . runOnlyPendingTimersAsync ( ) ;
430+ await vi . waitFor ( ( ) => expect ( sendCronFailureAlert ) . toHaveBeenCalledTimes ( 1 ) ) ;
431+ await vi . advanceTimersByTimeAsync ( 10_000 ) ;
432+
433+ const updated = await updatedPromise ;
434+ expect ( sendCronFailureAlert ) . toHaveBeenCalledTimes ( 1 ) ;
435+ expect ( updated ?. state . lastRunStatus ) . toBe ( "error" ) ;
436+ expect ( updated ?. state . lastDeliveryStatus ) . toBe ( "not-requested" ) ;
437+ expect ( updated ?. state . lastFailureNotificationDelivered ) . toBeUndefined ( ) ;
438+ expect ( updated ?. state . lastFailureNotificationDeliveryStatus ) . toBe ( "unknown" ) ;
439+ expect ( updated ?. state . lastFailureNotificationDeliveryError ) . toBe (
440+ "failure alert delivery timed out after 10000ms" ,
441+ ) ;
442+ } ) ;
443+
329444 it ( "does not treat primary error delivery as alternate failure-destination delivery" , async ( ) => {
330445 let capturedEvent :
331446 | {
0 commit comments