You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Flaky: scheduler cron-window test races a real 500ms timer under a frozen clock #452
packages/core/src/scheduler/__tests__/scheduler.test.ts — "triggers cron
schedules when time is within trigger window" — fails intermittently on CI with:
expected 0 to be greater than 0
Seen on the first Unit Tests run of PR #450; passed on re-run, and passes locally
(51/51). So it is a flake, not a regression.
Why it flakes
The test pins the clock but not the waiting:
Date.now=()=>fakeNow;// frozen, 5s past a minute boundaryconststartPromise=scheduler.start(agents);awaitwait(500);// ← REAL setTimeoutexpect(triggers.length).toBeGreaterThan(0);
The scheduler's own check loop runs on real timers and does async file I/O. The
assertion therefore depends on the loop getting scheduled, running, and
completing inside a 500 ms wall-clock budget on a shared runner. The comment
says "500ms gives ample headroom for slow CI runners doing async file I/O" —
which is the tell: the headroom is a guess, and a loaded runner spends it on
something else.
This is the classic frozen-Date.now + real-setTimeout split. Freezing the
clock makes the decision deterministic while leaving the delivery racy, so
the test is only as reliable as the runner is idle.
Suggested fix
Wait on the condition rather than on a duration — poll triggers.length up to a
generous ceiling, so a fast runner finishes in ~10 ms and a slow one still
passes:
Better still, drive the loop's timer with fake timers so no wall-clock wait is
involved at all — though that is the larger change, since the scheduler mixes
timers with real async I/O.
Worth a sweep for siblings: any other test in this file doing await wait(<n>)
followed by an assertion on work done by a real timer has the same shape.
Found while landing #449 / PR #450; unrelated to that change.
Still live, and it now reproduces more broadly than the single cron-window test this issue was filed for.
Seen on both CI attempts of #472 (a dependency bump that does not touch the scheduler), on the self-hosted runner. The failing subset moved between the two attempts of the same commit:
Attempt 1:triggers cron schedules when time is within trigger window (the expected 0 to be greater than 0 in this issue), skips cron schedules missing cron expression, updates schedule state on trigger, records error in schedule state on trigger failure. The first failure also left ENOTEMPTY: directory not empty on temp-dir cleanup.
Attempt 2:calculates next trigger time for cron schedule after completion, catches up with a single trigger when cron runs were missed, triggers cron schedule on subsequent run when due time arrives, getTotalRunningJobCount returns correct count.
So the frozen-clock/real-timer split diagnosed here is not confined to the one test — the whole schedule checking block (plus at least one graceful shutdown test) shares the pattern and races the same way under runner load. Worth widening the suggested fix to poll on the condition across that describe block rather than patching just the one await wait(500).
Two further notes for whoever picks this up:
The global Date.now = vi.fn(...) stub in the cron-window test is restored in a finally, but when the assertion inside the try fails, in-flight scheduler async work can still observe the frozen clock — which is a plausible mechanism for the downstream cascade seen in attempt 1, including the dirty temp dir.
Data point, in the other direction: scheduler.test.ts went 51/51 green on the self-hosted runner in run 39054880725 (#472, retargeted to SDK 0.3.302).
Same branch, same test file, no scheduler code changed between that commit and the two failing attempts reported above — the only diff is packages/core/package.json, pnpm-lock.yaml and a changeset. So the earlier red runs were this flake rather than anything in that PR, which is the useful half of the result.
The unuseful half: it means a green Unit Tests run tells you nothing about whether this is fixed. The race is load-dependent, so it will keep alternating until the await wait(500) pattern is replaced with polling on the condition. Worth keeping this issue open until that lands.
Activity
edspencer commentedon Sep 30, 2026
Still live, and it now reproduces more broadly than the single cron-window test this issue was filed for.
Seen on both CI attempts of #472 (a dependency bump that does not touch the scheduler), on the
self-hostedrunner. The failing subset moved between the two attempts of the same commit:triggers cron schedules when time is within trigger window(theexpected 0 to be greater than 0in this issue),skips cron schedules missing cron expression,updates schedule state on trigger,records error in schedule state on trigger failure. The first failure also leftENOTEMPTY: directory not emptyon temp-dir cleanup.calculates next trigger time for cron schedule after completion,catches up with a single trigger when cron runs were missed,triggers cron schedule on subsequent run when due time arrives,getTotalRunningJobCount returns correct count.So the frozen-clock/real-timer split diagnosed here is not confined to the one test — the whole
schedule checkingblock (plus at least onegraceful shutdowntest) shares the pattern and races the same way under runner load. Worth widening the suggested fix to poll on the condition across that describe block rather than patching just the oneawait wait(500).Two further notes for whoever picks this up:
Date.now = vi.fn(...)stub in the cron-window test is restored in afinally, but when the assertion inside thetryfails, in-flight scheduler async work can still observe the frozen clock — which is a plausible mechanism for the downstream cascade seen in attempt 1, including the dirty temp dir.edspencer commentedon Oct 17, 2026
Data point, in the other direction:
scheduler.test.tswent 51/51 green on the self-hosted runner in run 39054880725 (#472, retargeted to SDK 0.3.302).Same branch, same test file, no scheduler code changed between that commit and the two failing attempts reported above — the only diff is
packages/core/package.json,pnpm-lock.yamland a changeset. So the earlier red runs were this flake rather than anything in that PR, which is the useful half of the result.The unuseful half: it means a green Unit Tests run tells you nothing about whether this is fixed. The race is load-dependent, so it will keep alternating until the
await wait(500)pattern is replaced with polling on the condition. Worth keeping this issue open until that lands.