Skip to content

service-job: db-job-adapter.timeout.test.ts reds CI whenever the run crosses a minute boundary — it registers a real every-minute cron and asserts exactly one run row #8628

Description

@os-zhuang

Found while verifying an unrelated change (#8460 / PR #8620), where this test reddened Test Core (2/3) on a diff that cannot reach it. Filing rather than fixing — out of scope for that card.

The failure

FAIL  src/db-job-adapter.timeout.test.ts
  > the timeout policy still applies through an injected cron adapter (#7734)
  > a cron-scheduled run lands a timeout row even though the adapter no longer sees the policy

AssertionError: expected [ { …(10) }, { …(7) } ] to have a length of 1 but got 2
 ❯ src/db-job-adapter.timeout.test.ts:221:18

Why it flakes

The case registers a real CronJobAdapter (croner) on an every-minute expression, then fires one explicit trigger and asserts exactly one persisted run row:

const CRON = { type: 'cron', expression: '* * * * *' } as const;  // line 48
...
const cron = new CronJobAdapter();
const adapter = new DbJobAdapter({ engine, cron });
await adapter.schedule('cronic', CRON, handler, { timeout: TIMEOUT_MS });
await cron.trigger('cronic');            // the explicit fire the case is about

const runs = engine.tables.get('sys_job_run') ?? [];
expect(runs).toHaveLength(1);            // line 221 — fails as 2

Nothing suppresses the cron's own schedule. If the wall clock crosses a minute boundary between adapter.schedule(...) and the assertion, croner fires the registration independently and a second sys_job_run row lands — so the count is 2, not 1.

The failing CI run makes the mechanism explicit rather than inferred:

  • test file Start at 03:02:55
  • assertion failure stamped 03:03:00.17

i.e. the run straddled 03:03:00, which is exactly when * * * * * fires.

Evidence it is timing, not a real regression

  • Passes locally 5/5 in isolation, and the whole package passes (8 files / 71 tests) on the same commit that reds it in CI.
  • @objectstack/service-job does not depend on @objectstack/objectql (dependencies: core, metadata-core, platform-objects, spec), so the change it reddened has no dependency path to it at all.
  • The case never touches the object fold, labels, or metadata reads — it drives a fake in-memory engine.

Why it matters

It is a wall-clock-positional flake, not a random one: any CI run whose service-job shard happens to straddle a minute boundary reds, and the red points at a package the offending PR often has not touched. That is expensive to diagnose — it cost one dev-agent cycle here, and the shard attestation surfaces it a second time as an aggregate Test Core red, which reads like two failures.

Suggested direction (not prescriptive)

Make the case's cron unable to self-fire, so the explicit trigger is the only writer — e.g. a far-future/never expression, faking timers around the registration, or registering through a cron double whose schedule is inert while trigger() still works. The case's stated intent (#7734: the timeout policy survives the policy-free registration handed to the timer adapter) does not depend on the cron actually being schedulable.

⚠️ Any in-memory matches(row, where) double introduced here must satisfy pnpm check:where-matcher or throw.

Repro

pnpm --filter @objectstack/service-job test

straddling a minute boundary — or set the clock so adapter.schedule lands within a few hundred ms of :00.


Generated by Claude Code

Activity

  1. added theissue type on Aug 14, 2026
  2. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    Triage: lands in packages/services/service-job (test file only) ⇒ domain:services, pm:queue, type Task (test-only repair — no product code is implicated; the case's own real-cron registration self-fires across a minute boundary).

    Verified on origin/main before routing: db-job-adapter.timeout.test.ts:48 registers '* * * * *' on a real CronJobAdapter, and the toHaveLength(1) assertion follows with nothing suppressing the cron's own schedule — mechanism as filed, and the CI timestamps in the card (start 03:02:55, failure 03:03:00.17) straddle the minute boundary exactly.

    Merge-queue-health note: this is a wall-clock-positional flake that reds Test Core (2/3) for PRs with no dependency path to service-job. Lane may reasonably take it ahead of same-age queue peers (a red shard taxes every lane); until it lands, the queue steward should treat this failure signature as known-timing (re-enqueue), per the card's evidence.

    Constraint already on the card, repeated because it is a gate: any in-memory matches(row, where) double introduced by the fix must satisfy pnpm check:where-matcher or throw.

    Size/model suggestion: S (mechanical test repair) · sonnet · mode:subagent.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions