Skip to content

service-job: cron-job-adapter.test.ts carries the same minute-boundary self-fire hazard fixed in #8628 — four cases register a real every-minute cron and assert exact counts #8748

Description

@qq9340100

Found while fixing #8628 (db-job-adapter.timeout.test.ts). That card's mechanism is not confined to the one file it named: the sibling suite has the same shape in four cases, and the PM scoped the sweep out of #8628 deliberately, so this is the follow-up card.

The mechanism (identical to #8628)

CronJobAdapter.schedule() constructs a real croner job. Nothing suppresses that registration's own schedule, so on an every-minute expression croner fires it independently of the explicit trigger(). Any case that then asserts an exact call/execution count reds whenever the run straddles :00.

This was reproduced on demand while fixing #8628 — with only Date faked (real timers, croner's real scheduling path) and the registration positioned 2/5/10/15 ms before a minute boundary, the #8628 case produced 2 rows instead of 1, reproducing the CI signature exactly.

Cases that will red on a boundary

All in packages/services/service-job/src/cron-job-adapter.test.ts, all registering a real CronJobAdapter on '* * * * *':

linecaseexact-count assertion
36records executionsexpect(execs).toHaveLength(1) (L41)
74retries a failing handler per retryPolicy and succeedsexpect(calls).toBe(3) (L87), expect(execs).toHaveLength(1) (L89)
93exhausts retries and records the last failureexpect(calls).toBe(3) (L103)
109does not retry when no retryPolicy is given (legacy behavior)expect(calls).toBe(1) (L117)

Same mechanism, far rarer window

These use a daily expression, so the straddle window is one instant per day rather than one per minute — same defect, roughly 1440x less likely to be observed:

  • cron-job-adapter.test.ts:11'0 0 * * *', expect(calls).toBe(1) (L18)
  • cron-job-adapter.test.ts:208DAILY, expect(calls).toBe(1) (L225)
  • db-job-adapter.test.ts:195DAILY ('0 8 * * *'), expect(fired).toEqual(['new-kernel']) (L215)

Checked and NOT hazardous

  • db-job-adapter.degraded-outcome.test.ts:237 — registers a real cron on '* * * * *' but asserts only [0].status, and a self-fire runs the same handler to the same verdict. Latent, not currently failing; it would become a defect the moment an exact-count assertion is added.
  • interval-job-adapter.test.ts — its '* * * * *' usages are inert: IntervalJobAdapter stores cron schedules but never executes them (interval-job-adapter.ts:81-89).
  • db-job-adapter.timeout.test.ts first describe block — DbJobAdapter with no injected cron only warns and never registers a real croner (db-job-adapter.ts:106-109).

Suggested direction (not prescriptive)

The route taken in #8628 applies unchanged and needs no production seam: replace the fixture expression with one croner parses but can never fire — '0 0 30 2 *' (February 30th) has nextRun() === null. Note '0 0 29 2 *' is NOT a substitute: croner resolves Feb 29 to the next leap year (measured: 2028-02-29T00:00:00.000Z).

⛔ Do not loosen the exact-count assertions to "at least one" — they are the only thing that can catch a genuine double-scheduling regression, which is what these suites exist to catch.

Worth carrying over from #8628: that fix pinned the property with assertions (nextRun() must be null for the fixture, and for the croner job the case actually registers) so a later tidy-up back to an every-minute spelling reds immediately and locally instead of intermittently in CI.

Why it matters

Same as #8628: a wall-clock-positional flake reds Test Core for PRs with no dependency path to service-job, and a red shared merge queue taxes every lane at once.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions