@@ -4,8 +4,17 @@ import { describe, expect, vi } from "vitest";
44import type { AuthenticatedEnvironment } from "~/services/apiAuth.server" ;
55import { syncDeclarativeSchedules } from "~/v3/services/createBackgroundWorker.server" ;
66
7+ const { registerNextTaskScheduleInstance } = vi . hoisted ( ( ) => ( {
8+ registerNextTaskScheduleInstance : vi . fn ( ) . mockResolvedValue ( undefined ) ,
9+ } ) ) ;
10+
11+ vi . mock ( "~/v3/scheduleEngine.server" , ( ) => ( {
12+ scheduleEngine : { registerNextTaskScheduleInstance } ,
13+ } ) ) ;
14+
715vi . setConfig ( { testTimeout : 60_000 } ) ;
816
17+ type TasksArg = Parameters < typeof syncDeclarativeSchedules > [ 0 ] ;
918type WorkerArg = Parameters < typeof syncDeclarativeSchedules > [ 1 ] ;
1019const noWorker = { } as unknown as WorkerArg ;
1120
@@ -82,6 +91,82 @@ function countingPrisma(prisma: PrismaClient) {
8291const asEnv = ( env : { id : string ; projectId : string ; type : string } ) =>
8392 env as unknown as AuthenticatedEnvironment ;
8493
94+ function declarativeTasks ( schedule : { cron : string ; timezone : string ; window ?: string } ) : TasksArg {
95+ return [ { id : "my-task" , schedule } ] as TasksArg ;
96+ }
97+
98+ async function seedScheduledTask (
99+ prisma : PrismaClient ,
100+ projectId : string ,
101+ runtimeEnvironmentId : string
102+ ) {
103+ const worker = await prisma . backgroundWorker . create ( {
104+ data : {
105+ friendlyId : `worker_${ runtimeEnvironmentId } ` ,
106+ contentHash : `hash_${ runtimeEnvironmentId } ` ,
107+ version : "20260811.1" ,
108+ metadata : { } ,
109+ projectId,
110+ runtimeEnvironmentId,
111+ } ,
112+ } ) ;
113+
114+ await prisma . backgroundWorkerTask . create ( {
115+ data : {
116+ friendlyId : `task_${ runtimeEnvironmentId } ` ,
117+ slug : "my-task" ,
118+ filePath : "src/trigger/my-task.ts" ,
119+ workerId : worker . id ,
120+ projectId,
121+ runtimeEnvironmentId,
122+ triggerSource : "SCHEDULED" ,
123+ } ,
124+ } ) ;
125+ }
126+
127+ describe ( "syncDeclarativeSchedules registration" , ( ) => {
128+ containerTest (
129+ "preserves an existing Redis job when declarative timing is unchanged" ,
130+ async ( { prisma } ) => {
131+ registerNextTaskScheduleInstance . mockClear ( ) ;
132+ const { project, prodEnv } = await seedProjectWithEnvs ( prisma ) ;
133+ const schedule = await makeDeclarativeSchedule ( prisma , project . id , [ prodEnv . id ] ) ;
134+ await seedScheduledTask ( prisma , project . id , prodEnv . id ) ;
135+
136+ await syncDeclarativeSchedules (
137+ declarativeTasks ( { cron : "0 * * * *" , timezone : "UTC" } ) ,
138+ noWorker ,
139+ asEnv ( prodEnv ) ,
140+ prisma
141+ ) ;
142+
143+ expect ( registerNextTaskScheduleInstance ) . toHaveBeenCalledWith ( {
144+ instanceId : schedule . instances [ 0 ] . id ,
145+ preserveExistingJob : true ,
146+ } ) ;
147+ }
148+ ) ;
149+
150+ containerTest ( "replaces the Redis job when declarative timing changes" , async ( { prisma } ) => {
151+ registerNextTaskScheduleInstance . mockClear ( ) ;
152+ const { project, prodEnv } = await seedProjectWithEnvs ( prisma ) ;
153+ const schedule = await makeDeclarativeSchedule ( prisma , project . id , [ prodEnv . id ] ) ;
154+ await seedScheduledTask ( prisma , project . id , prodEnv . id ) ;
155+
156+ await syncDeclarativeSchedules (
157+ declarativeTasks ( { cron : "30 * * * *" , timezone : "UTC" , window : "30m" } ) ,
158+ noWorker ,
159+ asEnv ( prodEnv ) ,
160+ prisma
161+ ) ;
162+
163+ expect ( registerNextTaskScheduleInstance ) . toHaveBeenCalledWith ( {
164+ instanceId : schedule . instances [ 0 ] . id ,
165+ preserveExistingJob : false ,
166+ } ) ;
167+ } ) ;
168+ } ) ;
169+
85170describe ( "syncDeclarativeSchedules deletion path" , ( ) => {
86171 containerTest (
87172 "does not issue any instance delete when the env owns no instance of the missing schedules" ,
0 commit comments