@@ -88,13 +88,25 @@ const { redisUrl, inheritedEnv, worker } = await vi.hoisted(async () => {
8888 const inheritedEnv = {
8989 REDIS_URL : process . env . REDIS_URL ,
9090 SIM_AGENT_API_URL : process . env . SIM_AGENT_API_URL ,
91+ COPILOT_API_KEY : process . env . COPILOT_API_KEY ,
9192 }
9293 if ( url ) process . env . REDIS_URL = url
94+ process . env . COPILOT_API_KEY = 'job-lane-fixture-key'
9395 process . env . SIM_AGENT_API_URL = `http://127.0.0.1:${ port } `
9496 return { redisUrl : url , inheritedEnv, worker : Object . assign ( worker , { server, finish } ) }
9597} )
9698
9799vi . mock ( '@/lib/core/config/env-flags' , ( ) => envFlagsMock )
100+
101+ /** Spied so the idle tick can prove it neither claims nor reconciles. */
102+ vi . mock ( '@/lib/mothership/jobs/claim' , async ( importOriginal ) => {
103+ const actual = await importOriginal < typeof import ( '@/lib/mothership/jobs/claim' ) > ( )
104+ return { ...actual , claimDueJobOccurrences : vi . fn ( actual . claimDueJobOccurrences ) }
105+ } )
106+ vi . mock ( '@/lib/mothership/jobs/reconcile' , async ( importOriginal ) => {
107+ const actual = await importOriginal < typeof import ( '@/lib/mothership/jobs/reconcile' ) > ( )
108+ return { ...actual , reconcileJobRuns : vi . fn ( actual . reconcileJobRuns ) }
109+ } )
98110vi . mock ( '@/lib/core/config/feature-flags' , ( ) => featureFlagsMock )
99111
100112const { controllers } = vi . hoisted ( ( ) => ( { controllers : new Map < string , AbortController > ( ) } ) )
@@ -122,15 +134,15 @@ import {
122134} from '@sim / db / schema '
123135import { sleep } from '@sim/utils/helpers'
124136import { generateId } from '@sim/utils/id'
125- import { eq , sql } from 'drizzle-orm'
137+ import { eq , inArray , sql } from 'drizzle-orm'
126138import { closeRedisConnection , getRedisClient } from '@/lib/core/config/redis'
127139import { ABANDONED_RUN_CURSOR_KEY } from '@/lib/mothership/async-runs/orphaned-runs'
128140import { chatPubSub } from '@/lib/mothership/chat-status'
129141import { recordJobRunSpend } from '@/lib/mothership/jobs/budget'
130142import { claimDueJobOccurrences } from '@/lib/mothership/jobs/claim'
131143import { dispatchJobOccurrence } from '@/lib/mothership/jobs/dispatch'
132144import { finalizeJobRun } from '@/lib/mothership/jobs/finalize'
133- import { runJobLane } from '@/lib/mothership/jobs/lane'
145+ import { jobLaneWorkQuery , runJobLane } from '@/lib/mothership/jobs/lane'
134146import { jobOccurrenceId , MIN_JOB_CADENCE_MS } from '@/lib/mothership/jobs/occurrence'
135147import { JOB_DISPATCH_GRACE_MS , reconcileJobRuns } from '@/lib/mothership/jobs/reconcile'
136148import { takeOverAbandonedRuns } from '@/lib/mothership/request/application/abandoned-runs'
@@ -140,6 +152,18 @@ import {
140152 StreamControllerSupersededError ,
141153} from '@/lib/mothership/request/session/controller-lease'
142154
155+ interface QueryPlan {
156+ 'Node Type' : string
157+ 'Index Name' ?: string
158+ 'Shared Hit Blocks' : number
159+ 'Shared Read Blocks' : number
160+ Plans ?: QueryPlan [ ]
161+ }
162+
163+ function planNodes ( plan : QueryPlan ) : QueryPlan [ ] {
164+ return [ plan , ...( plan . Plans ?? [ ] ) . flatMap ( planNodes ) ]
165+ }
166+
143167const userId = generateId ( )
144168const workspaceId = generateId ( )
145169const chatIds : string [ ] = [ ]
@@ -316,6 +340,61 @@ describe.runIf(Boolean(redisUrl))('the scheduled job lane', () => {
316340 await closeRedisConnection ( )
317341 } )
318342
343+ it ( 'does nothing but one indexed probe on a tick with no live jobs' , async ( ) => {
344+ /** What the archive migration leaves behind: retired jobs and their finished run logs. */
345+ const archivedIds = Array . from ( { length : 2_000 } , ( ) => generateId ( ) )
346+ await db . insert ( workflowSchedule ) . values (
347+ archivedIds . map ( ( id ) => ( {
348+ id,
349+ triggerType : 'schedule' ,
350+ sourceType : 'job' ,
351+ status : 'disabled' ,
352+ archivedAt : new Date ( ) ,
353+ nextRunAt : new Date ( Date . now ( ) - 60_000 ) ,
354+ sourceUserId : userId ,
355+ sourceWorkspaceId : workspaceId ,
356+ } ) )
357+ )
358+ await db . insert ( jobExecutionLogs ) . values (
359+ Array . from ( { length : 5_000 } , ( ) => {
360+ const id = generateId ( )
361+ return {
362+ id,
363+ executionId : id ,
364+ workspaceId,
365+ level : 'info' ,
366+ status : 'completed' ,
367+ trigger : 'mothership' ,
368+ startedAt : new Date ( Date . now ( ) - 60_000 ) ,
369+ }
370+ } )
371+ )
372+ await db . execute ( sql `ANALYZE workflow_schedule, job_execution_logs` )
373+ const now = new Date ( )
374+ vi . mocked ( claimDueJobOccurrences ) . mockClear ( )
375+ vi . mocked ( reconcileJobRuns ) . mockClear ( )
376+
377+ try {
378+ expect ( await runJobLane ( now ) ) . toEqual ( { claimed : 0 } )
379+ expect ( claimDueJobOccurrences ) . not . toHaveBeenCalled ( )
380+ expect ( reconcileJobRuns ) . not . toHaveBeenCalled ( )
381+
382+ const plans = await db . execute < { 'QUERY PLAN' : { Plan : QueryPlan } [ ] } > (
383+ sql `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${ jobLaneWorkQuery ( now ) } `
384+ )
385+ const plan = plans [ 0 ] [ 'QUERY PLAN' ] [ 0 ] . Plan
386+ const nodes = planNodes ( plan )
387+ expect ( nodes . some ( ( node ) => node [ 'Node Type' ] === 'Seq Scan' ) ) . toBe ( false )
388+ expect ( nodes . map ( ( node ) => node [ 'Index Name' ] ) . filter ( Boolean ) ) . toEqual (
389+ expect . arrayContaining ( [ 'workflow_schedule_due_job_idx' , 'job_execution_logs_open_idx' ] )
390+ )
391+ expect ( plan [ 'Shared Hit Blocks' ] + plan [ 'Shared Read Blocks' ] ) . toBeLessThan ( 20 )
392+ } finally {
393+ await db . delete ( jobExecutionLogs ) . where ( eq ( jobExecutionLogs . workspaceId , workspaceId ) )
394+ await db . delete ( workflowSchedule ) . where ( inArray ( workflowSchedule . id , archivedIds ) )
395+ }
396+ } )
397+
319398 it ( 'records an occurrence once when two ticks claim it at the same time, and starts it once' , async ( ) => {
320399 const { scheduleId, occurrenceId, dueAt } = await seedJob ( )
321400 const now = new Date ( )
0 commit comments