@@ -2,7 +2,7 @@ import { containerTest } from "@internal/testcontainers";
22import { trace } from "@internal/tracing" ;
33import { describe , expect , vi } from "vitest" ;
44import type { TriggerScheduledTaskParams } from "../src/engine/types.js" ;
5- import { ScheduleEngine } from "../src/index.js" ;
5+ import { calculateSchedulePhase , ScheduleEngine } from "../src/index.js" ;
66
77describe ( "ScheduleEngine Integration (part 2)" , ( ) => {
88 // Deploy-moment backward compatibility. At deploy time, in-flight Redis jobs
@@ -20,6 +20,7 @@ describe("ScheduleEngine Integration (part 2)", () => {
2020 prisma,
2121 redis : redisOptions ,
2222 distributionWindow : { seconds : 10 } ,
23+ schedulePhaseSecret : "test-schedule-phase-secret" ,
2324 worker : {
2425 concurrency : 1 ,
2526 disabled : true , // Don't actually run the worker — calling triggerScheduledTask directly
@@ -108,4 +109,117 @@ describe("ScheduleEngine Integration (part 2)", () => {
108109 }
109110 }
110111 ) ;
112+
113+ containerTest (
114+ "should assign a stable schedule phase once when a window is configured" ,
115+ { timeout : 30_000 } ,
116+ async ( { prisma, redisOptions } ) => {
117+ const schedulePhaseSecret = "test-schedule-phase-secret" ;
118+ const engine = new ScheduleEngine ( {
119+ prisma,
120+ redis : redisOptions ,
121+ distributionWindow : { seconds : 10 } ,
122+ schedulePhaseSecret,
123+ worker : {
124+ concurrency : 1 ,
125+ disabled : true ,
126+ pollIntervalMs : 1000 ,
127+ } ,
128+ tracer : trace . getTracer ( "test" , "0.0.0" ) ,
129+ onTriggerScheduledTask : async ( ) => ( { success : true } ) ,
130+ isDevEnvironmentConnectedHandler : vi . fn ( ) . mockResolvedValue ( true ) ,
131+ } ) ;
132+
133+ try {
134+ const organization = await prisma . organization . create ( {
135+ data : { title : "Schedule Phase Org" , slug : "schedule-phase-org" } ,
136+ } ) ;
137+ const project = await prisma . project . create ( {
138+ data : {
139+ name : "Schedule Phase Project" ,
140+ slug : "schedule-phase-project" ,
141+ externalRef : "schedule-phase-ref" ,
142+ organizationId : organization . id ,
143+ } ,
144+ } ) ;
145+ const environment = await prisma . runtimeEnvironment . create ( {
146+ data : {
147+ slug : "schedule-phase-env" ,
148+ type : "PRODUCTION" ,
149+ projectId : project . id ,
150+ organizationId : organization . id ,
151+ apiKey : "tr_schedule_phase" ,
152+ pkApiKey : "pk_schedule_phase" ,
153+ shortcode : "phase" ,
154+ } ,
155+ } ) ;
156+ const taskSchedule = await prisma . taskSchedule . create ( {
157+ data : {
158+ friendlyId : "sched_phase" ,
159+ taskIdentifier : "schedule-phase-task" ,
160+ projectId : project . id ,
161+ deduplicationKey : "schedule-phase-dedup" ,
162+ generatorExpression : "*/5 * * * *" ,
163+ generatorDescription : "Every 5 minutes" ,
164+ timezone : "UTC" ,
165+ type : "DECLARATIVE" ,
166+ } ,
167+ } ) ;
168+ const scheduleInstance = await prisma . taskScheduleInstance . create ( {
169+ data : {
170+ taskScheduleId : taskSchedule . id ,
171+ environmentId : environment . id ,
172+ projectId : project . id ,
173+ } ,
174+ } ) ;
175+
176+ await engine . registerNextTaskScheduleInstance ( { instanceId : scheduleInstance . id } ) ;
177+
178+ const unwindowedInstance = await prisma . taskScheduleInstance . findUniqueOrThrow ( {
179+ where : { id : scheduleInstance . id } ,
180+ select : { schedulePhase : true } ,
181+ } ) ;
182+ expect ( unwindowedInstance . schedulePhase ) . toBeNull ( ) ;
183+
184+ await prisma . taskSchedule . update ( {
185+ where : { id : taskSchedule . id } ,
186+ data : { windowDurationSeconds : 60 } ,
187+ } ) ;
188+
189+ const expectedPhase = calculateSchedulePhase ( {
190+ secret : schedulePhaseSecret ,
191+ environmentId : environment . id ,
192+ deduplicationKey : taskSchedule . deduplicationKey ,
193+ } ) ;
194+
195+ await Promise . all ( [
196+ engine . registerNextTaskScheduleInstance ( { instanceId : scheduleInstance . id } ) ,
197+ engine . registerNextTaskScheduleInstance ( { instanceId : scheduleInstance . id } ) ,
198+ engine . registerNextTaskScheduleInstance ( { instanceId : scheduleInstance . id } ) ,
199+ ] ) ;
200+
201+ const assignedInstance = await prisma . taskScheduleInstance . findUniqueOrThrow ( {
202+ where : { id : scheduleInstance . id } ,
203+ select : { schedulePhase : true } ,
204+ } ) ;
205+ expect ( assignedInstance . schedulePhase ) . toBe ( expectedPhase ) ;
206+
207+ const pinnedPhase = 1_234_567_890 ;
208+ await prisma . taskScheduleInstance . update ( {
209+ where : { id : scheduleInstance . id } ,
210+ data : { schedulePhase : pinnedPhase } ,
211+ } ) ;
212+
213+ await engine . registerNextTaskScheduleInstance ( { instanceId : scheduleInstance . id } ) ;
214+
215+ const preservedInstance = await prisma . taskScheduleInstance . findUniqueOrThrow ( {
216+ where : { id : scheduleInstance . id } ,
217+ select : { schedulePhase : true } ,
218+ } ) ;
219+ expect ( preservedInstance . schedulePhase ) . toBe ( pinnedPhase ) ;
220+ } finally {
221+ await engine . quit ( ) ;
222+ }
223+ }
224+ ) ;
111225} ) ;
0 commit comments