Skip to content

Commit c9569e7

Browse files
committed
test(schedule-engine): cover deploy-moment legacy-payload backward compat
Add an integration test for the path where an in-flight Redis job enqueued by the old engine (no `lastScheduleTime` in its payload) is dequeued by the new engine. The new engine must report the value persisted at `instance.lastScheduledTimestamp` as `payload.lastTimestamp` rather than reporting `undefined`, so customers don't see a one-fire gap in their lastTimestamp during the rollout. The existing integration test exercises the fresh-schedule path and the worker payload flow on subsequent fires. This new test specifically exercises the DB-column fallback in the `params.lastScheduleTime ?? instance.lastScheduledTimestamp ?? undefined` chain — the bridge that handles the legacy queue at deploy time.
1 parent cd5e629 commit c9569e7

1 file changed

Lines changed: 104 additions & 0 deletions

File tree

internal-packages/schedule-engine/test/scheduleEngine.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,4 +239,108 @@ describe("ScheduleEngine Integration", () => {
239239
}
240240
}
241241
);
242+
243+
// Deploy-moment backward compatibility. At deploy time, in-flight Redis jobs
244+
// were enqueued by the old engine — their payload has no `lastScheduleTime`
245+
// field — and `instance.lastScheduledTimestamp` is still populated (last
246+
// written by the old engine pre-deploy). The new engine must report that DB
247+
// value as `payload.lastTimestamp` so customers don't see a transient
248+
// `undefined` for the one fire per schedule that drains the legacy queue.
249+
containerTest(
250+
"should fall back to instance.lastScheduledTimestamp when payload lacks lastScheduleTime",
251+
{ timeout: 30_000 },
252+
async ({ prisma, redisOptions }) => {
253+
const triggerCalls: TriggerScheduledTaskParams[] = [];
254+
const engine = new ScheduleEngine({
255+
prisma,
256+
redis: redisOptions,
257+
distributionWindow: { seconds: 10 },
258+
worker: {
259+
concurrency: 1,
260+
disabled: true, // Don't actually run the worker — calling triggerScheduledTask directly
261+
pollIntervalMs: 1000,
262+
},
263+
tracer: trace.getTracer("test", "0.0.0"),
264+
onTriggerScheduledTask: async (params) => {
265+
triggerCalls.push(params);
266+
return { success: true };
267+
},
268+
isDevEnvironmentConnectedHandler: vi.fn().mockResolvedValue(true),
269+
});
270+
271+
try {
272+
const organization = await prisma.organization.create({
273+
data: { title: "Legacy Payload Org", slug: "legacy-payload-org" },
274+
});
275+
276+
const project = await prisma.project.create({
277+
data: {
278+
name: "Legacy Payload Project",
279+
slug: "legacy-payload-project",
280+
externalRef: "legacy-payload-ref",
281+
organizationId: organization.id,
282+
},
283+
});
284+
285+
const environment = await prisma.runtimeEnvironment.create({
286+
data: {
287+
slug: "legacy-payload-env",
288+
type: "PRODUCTION",
289+
projectId: project.id,
290+
organizationId: organization.id,
291+
apiKey: "tr_legacy_1234",
292+
pkApiKey: "pk_legacy_1234",
293+
shortcode: "legacy-short",
294+
},
295+
});
296+
297+
const taskSchedule = await prisma.taskSchedule.create({
298+
data: {
299+
friendlyId: "sched_legacy_payload",
300+
taskIdentifier: "legacy-payload-task",
301+
projectId: project.id,
302+
deduplicationKey: "legacy-payload-dedup",
303+
userProvidedDeduplicationKey: false,
304+
generatorExpression: "*/5 * * * *",
305+
generatorDescription: "Every 5 minutes",
306+
timezone: "UTC",
307+
type: "DECLARATIVE",
308+
active: true,
309+
externalId: "legacy-ext",
310+
},
311+
});
312+
313+
// Pre-populate lastScheduledTimestamp on the instance — simulates the
314+
// value the old engine wrote to the DB before this PR deployed.
315+
const preDeployLastFire = new Date("2026-04-30T10:00:00.000Z");
316+
const scheduleInstance = await prisma.taskScheduleInstance.create({
317+
data: {
318+
taskScheduleId: taskSchedule.id,
319+
environmentId: environment.id,
320+
projectId: project.id,
321+
active: true,
322+
lastScheduledTimestamp: preDeployLastFire,
323+
},
324+
});
325+
326+
// Call triggerScheduledTask directly without lastScheduleTime,
327+
// simulating an in-flight Redis job enqueued by the old engine.
328+
const exactScheduleTime = new Date("2026-04-30T10:05:00.000Z");
329+
await engine.triggerScheduledTask({
330+
instanceId: scheduleInstance.id,
331+
finalAttempt: false,
332+
exactScheduleTime,
333+
// lastScheduleTime intentionally omitted — legacy payload shape
334+
});
335+
336+
expect(triggerCalls.length).toBe(1);
337+
expect(triggerCalls[0].payload.timestamp).toEqual(exactScheduleTime);
338+
// Falls back to instance.lastScheduledTimestamp from the DB rather
339+
// than reporting undefined for this one transitional fire.
340+
expect(triggerCalls[0].payload.lastTimestamp).toEqual(preDeployLastFire);
341+
} finally {
342+
await engine.quit();
343+
}
344+
}
345+
);
242346
});

0 commit comments

Comments
 (0)