Skip to content

Commit b6766fc

Browse files
test(server): a missed fixed-time slot is not shown on its scheduled task
Refs #30844 AI-assisted; verified with the tests and tools listed above.
1 parent 4bf5d53 commit b6766fc

1 file changed

Lines changed: 73 additions & 0 deletions

File tree

‎apps/server/src/scheduledTasks/ScheduledTaskService.schedule.test.ts‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,3 +392,76 @@ it.effect(
392392
}).pipe(Effect.provide(ScheduledTaskService.layer.pipe(Layer.provide(layerDependencies))));
393393
}).pipe(Effect.provide(SqlitePersistence.layerMemory)),
394394
);
395+
396+
it.effect("a fixed-time slot missed while the server was off shows on the task", () =>
397+
Effect.gen(function* () {
398+
const at = (hour: number, minute = 0, day = 1) =>
399+
DateTime.makeZonedUnsafe(
400+
{ year: 2026, month: 7, day, hour, minute, second: 0, millisecond: 0 },
401+
{ timeZone: DateTime.zoneMakeLocal(), adjustForTimeZone: true },
402+
);
403+
yield* TestClock.setTime(DateTime.toEpochMillis(at(8)));
404+
const launches = yield* Ref.make(0);
405+
const layerDependencies = Layer.mergeAll(
406+
NodeCrypto.layer,
407+
Scheduler.layer,
408+
ManagedPolicy.layerTest(),
409+
Layer.mock(ThreadLaunchService.ThreadLaunchService)({
410+
launch: () =>
411+
Ref.update(launches, (count) => count + 1).pipe(
412+
Effect.as({ threadId: "thread-missed-slot", resumed: false } as never),
413+
),
414+
}),
415+
Layer.mock(ManagedProjectFolders.ManagedProjectFolders)({
416+
namedProjectsRoot: "/projects",
417+
isScratchProject: () => Effect.succeed(false),
418+
}),
419+
Layer.mock(ThreadManagementService.ThreadManagementService)({}),
420+
Layer.mock(SecretRequests.SecretRequests)({}),
421+
);
422+
const withServer = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
423+
effect.pipe(
424+
Effect.provide(ScheduledTaskService.layer.pipe(Layer.provide(layerDependencies))),
425+
);
426+
427+
const taskId = yield* withServer(
428+
Effect.gen(function* () {
429+
const service = yield* ScheduledTaskService.ScheduledTaskService;
430+
const { task } = yield* service.upsert(
431+
yield* decodeUpsertInput({
432+
title: "Morning bookkeeping",
433+
prompt: "Update yesterday's notes.",
434+
enabled: true,
435+
schedule: { type: "fixed_time", timeOfDay: "09:00" },
436+
projectId: "project-missed-slot",
437+
workspaceStrategy: { type: "root" },
438+
modelSelection: { instanceId: "codex", model: "gpt-5.4" },
439+
runtimeMode: "full-access",
440+
interactionMode: "default",
441+
}),
442+
);
443+
yield* service.runNow({ id: task.id });
444+
return task.id;
445+
}),
446+
);
447+
448+
// The server is off across the 09:00 slot and comes back at 09:30.
449+
yield* TestClock.setTime(DateTime.toEpochMillis(at(9, 30)));
450+
const [listed] = yield* withServer(
451+
Effect.gen(function* () {
452+
const service = yield* ScheduledTaskService.ScheduledTaskService;
453+
yield* TestClock.adjust("6 seconds");
454+
return (yield* service.list()).tasks;
455+
}),
456+
);
457+
458+
expect(listed?.id).toBe(taskId);
459+
expect(yield* Ref.get(launches)).toBe(1);
460+
expect(listed?.nextRunAt).toBe(DateTime.formatIso(DateTime.toUtc(at(9, 0, 2))));
461+
// The skipped slot must not read as the earlier manual run's clean success.
462+
expect({ status: listed?.lastRunStatus, error: listed?.lastRunError }).not.toEqual({
463+
status: "succeeded",
464+
error: null,
465+
});
466+
}).pipe(Effect.provide(SqlitePersistence.layerMemory)),
467+
);

0 commit comments

Comments
 (0)