LearnNewsExamplesServices
Frontmatter
id18075
titleA frozen NOW in the data-sync spec ages into a permanent CI red
stateClosed
labels
bugaitesting
assigneesneo-opus-grace
createdAtSep 2, 2026, 1:45 PM
updatedAtSep 2, 2026, 2:07 PM
githubUrlhttps://github.com/neomjs/neo/issues/18075
authorneo-opus-grace
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 2, 2026, 2:07 PM

A frozen NOW in the data-sync spec ages into a permanent CI red

neo-opus-grace
neo-opus-grace commented on Sep 2, 2026, 1:45 PM

Context

Engine Tests is red on dev at 5ae9358063 (run 33624118983), so the unit job now fails on every branch and every PR is gated — including the demo chain (#18059, #18063).

This is not the three PRs merged this morning: #18066, #18061 and #18068 each had gh pr checks exit 0 at review time. It crossed over during the following hour.

It is also not the production corpus guard misbehaving. That guard is working exactly as designed, and the Data Sync Pipeline workflow failing is the finding it exists to surface — already tracked by #17920 (the GitHub corpus stage lost in the devindex split) with the staleness alarm on #17834. Nothing here should quiet that.

The Problem

test/playwright/unit/buildScripts/dataSyncPipeline.spec.mjs:17-18 derives every fixture date from a frozen constant:

NOW   = new Date('2026-08-31T12:00:00Z'),
hours = n => new Date(NOW.getTime() - n * 3_600_000).toISOString(),

The pure-function arms are hermetic because they hand that same constant to the subject — assertCorpusFreshness({facetCommitDates, now: NOW}).

The RUN-path arms under test.describe('the refusal reaches the RUN path, not only the pure function') are not. They call emitGeneratedData, which invokes assertCorpusFreshness({facetCommitDates}) with no now, so the subject falls back to new Date() — the real clock — while the fixture date stays pinned to 2026-08-31.

The measured age is therefore realNow − 2026-08-31T11:00Z, not the injected value:

injected measured at 2026-09-02T11:21Z
runWith({corpusAgeHours: 1}) 1h 48.35h

against a 48h threshold — which is the 48.4h in the failure text, to the decimal:

Error: dataSyncPipeline: refusing to derive the portal from a stale corpus —
  `issues` is 48.4h old (threshold 48h), `pulls` is 48.4h old (threshold 48h),
  `discussions` is 48.4h old (threshold 48h)
    at assertCorpusFreshness (buildScripts/dataSyncPipeline.mjs:705:15)
    at emitGeneratedData (buildScripts/dataSyncPipeline.mjs:781:13)
    at test/playwright/unit/buildScripts/dataSyncPipeline.spec.mjs:320:13

So a fresh corpus lets the derivation stage run has not been testing a fresh corpus since it was written. It passed for ~48 hours because the wall clock happened to sit inside the threshold of its own frozen constant, and it is now permanently red and worsening — the gap grows an hour per hour.

The sibling arm (a stale corpus stops the derivation stage from running at all, corpusAgeHours: 120) still passes, but for the wrong reason: it asserts a refusal, and it now gets one whatever the fixture says. It has stopped discriminating.

The Architectural Reality

  • buildScripts/dataSyncPipeline.mjs:697 — assertCorpusFreshness({facetCommitDates, maxAgeHours, now = new Date()}) already exposes now as a seam.
  • buildScripts/dataSyncPipeline.mjs:781 — emitGeneratedData calls it without now, which is correct for production and is what leaves the RUN path clock-dependent under test.
  • test/.../dataSyncPipeline.spec.mjs:285 — runWith stubs execute so any git call returns hours(corpusAgeHours). The stub works; the constant it is built from is the defect.

Introduced by #17938 (A missing emission stage or a stale corpus must fail the run), which added both guards and these arms.

The Fix

Make the RUN-path arms hermetic. The fixture date must be relative to whatever clock the subject will read, so an injected age is the measured age.

Smallest correct change: give the RUN-path harness a real-clock-relative helper rather than the frozen NOW, leaving the pure-function arms and their pinned NOW exactly as they are. Production is untouched, so the Data Sync workflow keeps failing loudly and #17920 stays visible.

The alternative — threading a now seam through emitGeneratedData — is rejected here: it widens a production signature for test convenience when the subject already exposes the seam one level down, and the harness is the thing that is wrong.

Acceptance Criteria

  • a fresh corpus lets the derivation stage run passes regardless of wall-clock date, verified by running it with the system clock unchanged and again against a date well past the current threshold window.
  • The refusal arm is restored to a discriminating test: it must fail if the guard stops refusing, rather than passing on a refusal it would now get anyway. Red-first evidence required — mutate assertCorpusFreshness to never throw and show that arm going red.
  • No change to buildScripts/dataSyncPipeline.mjs; the production guard's behaviour and the Data Sync Pipeline workflow's failure are unchanged and still surface #17920.
  • npm run test-unit green on dev.
  • A note in the spec naming why the RUN-path arms cannot share the pinned NOW, so the next author does not re-freeze them.

Out of Scope

  • Restoring the corpus stage or any part of #17920 — the production red is correct and must remain.
  • The staleness alarm on #17834.
  • Changing the 48h threshold. The threshold is not the problem and lowering or raising it would only move the bomb.

Related

  • #17938 — introduced the guards and these arms
  • #17920 — the real corpus outage the production guard is correctly reporting
  • #17834 — the consecutive-failure alarm for that outage

Live latest-open sweep: open issues matching data.?sync|corpus|stale|portal|pipeline checked 2026-09-02 ~11:40 UTC — #17920, #17834, #17416, #17238, #17170, #16532, #17877 are all the corpus/production outage or unrelated; none covers this spec's clock dependence. Closed sweep surfaced #17938 as the introducing change. A2A claim sweep: none; I broadcast the claim before filing.

Origin Session ID: fb58e855-74b2-4367-9f83-5877f151f024

Retrieval Hint: "dataSyncPipeline spec frozen NOW 48h threshold time bomb hermetic run path" · Engine Tests red on dev 2026-09-02