@@ -272,6 +272,13 @@ async function runsOf(scheduleId: string) {
272272 . where ( eq ( jobExecutionLogs . scheduleId , scheduleId ) )
273273}
274274
275+ /** The usage gate's answer when the payer's usage can't be read. */
276+ const usageOutage = {
277+ isExceeded : true ,
278+ message : 'Usage could not be checked.' ,
279+ reason : 'usage_unavailable' as const ,
280+ }
281+
275282/** Makes a schedule due again now, as its next occurrence arriving would. */
276283async function dueAgain ( scheduleId : string ) {
277284 const dueAt = new Date ( Date . now ( ) - 1_000 )
@@ -647,26 +654,48 @@ describe.runIf(Boolean(redisUrl))('the scheduled job lane', () => {
647654 await reconcileUntilSettled ( occurrenceId )
648655 } )
649656
650- it ( 'retries an occurrence whose usage could not be read, and skips one whose billing is blocked ' , async ( ) => {
657+ it ( 'retries an occurrence whose usage could not be read, and runs it once usage reads again ' , async ( ) => {
651658 setEnvFlags ( { isHosted : true } )
652659 const { scheduleId, occurrenceId } = await seedJob ( )
653- vi . mocked ( checkAttributedUsageLimits ) . mockResolvedValueOnce ( {
654- isExceeded : true ,
655- message : 'Usage could not be checked.' ,
656- reason : 'usage_unavailable' ,
657- } )
660+ vi . mocked ( checkAttributedUsageLimits ) . mockResolvedValueOnce ( usageOutage )
658661 await runJobLane ( new Date ( ) )
659662 expect ( await runsOf ( scheduleId ) ) . toEqual ( [
660663 expect . objectContaining ( { status : 'queued' , outcome : null , dispatchFailures : 1 } ) ,
661664 ] )
662665
666+ vi . mocked ( checkAttributedUsageLimits ) . mockResolvedValueOnce ( { isExceeded : false } )
667+ await reconcileJobRuns ( new Date ( Date . now ( ) + JOB_DISPATCH_GRACE_MS + 1_000 ) )
668+ await reconcileUntilSettled ( occurrenceId )
669+ expect ( legsFor ( occurrenceId ) ) . toHaveLength ( 1 )
670+ expect ( await runsOf ( scheduleId ) ) . toEqual ( [
671+ expect . objectContaining ( { status : 'complete' , outcome : 'not_met' , dispatchFailures : 1 } ) ,
672+ ] )
673+ } )
674+
675+ it ( 'ends an occurrence whose usage stays unreadable at its last attempt, and skips one whose billing is blocked' , async ( ) => {
676+ setEnvFlags ( { isHosted : true } )
677+ const outage = await seedJob ( )
678+ await claimDueJobOccurrences ( new Date ( ) , 10 )
679+ for ( let attempt = 1 ; attempt <= 5 ; attempt ++ ) {
680+ vi . mocked ( checkAttributedUsageLimits ) . mockResolvedValueOnce ( usageOutage )
681+ expect ( await dispatchJobOccurrence ( outage . occurrenceId ) ) . toBe (
682+ attempt < 5 ? 'retry' : 'failed'
683+ )
684+ }
685+ expect ( await runsOf ( outage . scheduleId ) ) . toEqual ( [
686+ expect . objectContaining ( { outcome : 'failed' , summary : 'The run could not be started.' } ) ,
687+ ] )
688+ expect ( legsFor ( outage . occurrenceId ) ) . toHaveLength ( 0 )
689+
690+ const blocked = await seedJob ( )
691+ await claimDueJobOccurrences ( new Date ( ) , 10 )
663692 vi . mocked ( checkAttributedUsageLimits ) . mockResolvedValueOnce ( {
664693 isExceeded : true ,
665694 message : 'Billing is blocked for this account.' ,
666695 reason : 'billing_blocked' ,
667696 } )
668- expect ( await dispatchJobOccurrence ( occurrenceId ) ) . toBe ( 'skipped_budget' )
669- expect ( await runsOf ( scheduleId ) ) . toEqual ( [
697+ expect ( await dispatchJobOccurrence ( blocked . occurrenceId ) ) . toBe ( 'skipped_budget' )
698+ expect ( await runsOf ( blocked . scheduleId ) ) . toEqual ( [
670699 expect . objectContaining ( {
671700 outcome : 'skipped_budget' ,
672701 summary : 'Billing is blocked for this account.' ,
0 commit comments