Skip to content

Pin the suite's clock so a rendered elapsed time stops drifting - #6043

Open
backspace wants to merge 7 commits into
cs-12770-guard-ambient-clockfrom
cs-12770-pin-test-clock
Open

Pin the suite's clock so a rendered elapsed time stops drifting#6043
backspace wants to merge 7 commits into
cs-12770-guard-ambient-clockfrom
cs-12770-pin-test-clock

Conversation

@backspace

Copy link
Copy Markdown
Contributor

The seam beneath this gave card code one clock to read and kept it that way, but changed nothing on its own: unpinned, now() is Date.now(), so every rendered age still depended on when the suite ran. This sets the pin, which is what the other two were for.

"3d ago", a countdown, an age and an "expires soon" warning become functions of their own data. A visual comparison of them is then a comparison of the code, rather than a race against whichever threshold each value happens to be nearest — and the way to quiet one stops being to hide it, which is what trades the coverage away.

The parts that are not obvious

Fixture stamps have to move with the clock, and they cannot all be one value. Realms the tests build in the browser stamp their files with the pinned instant, so they still read today exactly as they did when both sides were the real clock. But the indexer decides what a from-scratch pass must revisit by comparing a file's mtime against the one on its index row and skipping where they match, so a write that leaves the mtime alone is an edit it cannot see. Stamps therefore advance a second at a time from twelve hours below the pinned instant: below, because a mtime after it is in the future and renders as an absolute date rather than an age; within the same day, because that is what keeps them reading as today.

The clock has to cross the card-loader boundary. The host app renders elapsed time from the same timestamps card code does — formatLastSavedText among them — so a pin that reached only card code produced "Last saved in 6 days", a saved timestamp in the future. The implementation lives in runtime-common, which both sides already depend on, and a global is what actually crosses, because the loader hands card code its own module instances.

The instant is mid-month on purpose. A calendar test asserts that days earlier in the current month are disabled by a today sentinel, and guards itself with getDate() > 1. Pinned to the first, that assertion would skip rather than fail — a quieter way to lose a check than leaving it broken.

The adapter's token-expiry comparison stays on the real clock. It is auth arithmetic rather than a rendered value, and freezing it changes when a test's tokens are considered live, which is a different question from what a card displays.

What this costs

Nothing, so far. Every Percy build on this branch that produced snapshots reported changed=0 — the pinned instant is close enough to the present that no content-derived mtime crossed a unit boundary. If a later instant ever does move a baseline, that is the one-time price of those labels never moving again on their own.

Verification state

Two runs found real bugs, both fixed here: fixture mtimes defeating the indexer's change detection, and the calendar test measuring from a different clock than the component it renders.

Three further runs produced no test evidence at all — two lost to GitHub artifact upload failures, one to a missing file extension in a re-export, fixed. So the host-side clock fix and that extension are correct by inspection but have not yet had a full green run behind them. That is what this PR's first run is for.

backspace and others added 4 commits September 8, 2026 10:54
The seam and the guard gave card code one clock to read and kept it that way.
Neither changed anything on its own: unpinned, `now()` is `Date.now()`, so
every rendered age still depended on when the suite ran. This sets the pin,
which is the point of the other two.

What it buys is that "3d ago", a countdown, an age and an "expires soon"
warning become functions of their own data. A visual comparison of them is then
a comparison of the code, rather than a race against whichever threshold each
value happens to be nearest — and the way to quiet one stops being to hide it,
which is what trades the coverage away.

Realms the tests build in the browser stamp their files with the same instant,
so they still read as `today`, exactly as they did when both sides were the
real clock. Nothing about those moves. Realms served from the index cache carry
content-derived mtimes — fixed dates scattered across decades — and their ages
are now measured from the pinned instant rather than from today, which shifts
some by a unit once. That is the one-time cost of their never shifting again.

The adapter's token-expiry comparison is deliberately left on the real clock.
It is auth arithmetic rather than a rendered value, and freezing it would
change when a test's tokens are considered live, which is a different question
from what a card displays.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… clock

Two things the first run found.

Stamping every fixture write with the pinned instant broke change detection.
The indexer decides what a from-scratch pass must revisit by comparing a file's
mtime against the one on its index row and skipping where they match, so a
write that leaves the mtime alone is an edit it cannot see —
`full indexing skips over unchanged items in index` caught exactly that. Stamps
now advance a second at a time from twelve hours below the pinned instant.
Below matters: a mtime after it is in the future, which renders as an absolute
date rather than an age. Within the same day matters too, since that is what
keeps these files reading as `today`, as they did when both the clock and the
stamp were the real one.

The calendar test asked the real clock what "today" is while the field it
renders asked the pinned one, so the two disagreed about which days a `today`
sentinel disables. It reads the pin now. The instant moves to mid-month for the
same test: its `getDate() > 1` guard means that pinned to the first, the
assertion would have skipped rather than failed, which is a quieter way to lose
a check than leaving it broken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pinning the suite's clock made a file read "Last saved in 6 days" — a saved
timestamp in the future. The fixtures carried the pinned instant while
`formatLastSavedText` measured them against the wall clock, so the two
disagreed by exactly the distance between them.

The seam covered card code and stopped there, but the host app renders elapsed
time from the same timestamps. So the implementation moves to runtime-common,
which both sides already depend on: `base/helpers/clock` is now the path card
code imports it by, and the host app imports it directly. A global is still
what actually crosses the boundary, because the card loader hands card code its
own module instances and a module-level variable would not be shared at all.

`formatLastSavedText` already took its `now` as a parameter; only the default
changes, so nothing that passes one behaves differently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`runtime-common` is loaded as Node ESM, where a relative specifier resolves
literally, so `./clock` names a file that does not exist. Every other relative
export in the barrel carries `.ts` — 126 of them — and this one did not, which
is the whole failure: the prerender service could not start, and twenty jobs
reported failure without running a single assertion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-08T17:03:44.908736Z c92ad48 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c92ad4813a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/host/tests/helpers/test-clock.ts Outdated
Comment thread packages/host/tests/test-helper.js
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files  ±  0      1 suites  ±0   2h 43m 22s ⏱️ + 23m 11s
4 728 tests +120  4 713 ✅ +119  14 💤 ±0  0 ❌ ±0  1 🔥 +1 
4 743 runs  +120  4 727 ✅ +118  14 💤 ±0  1 ❌ +1  1 🔥 +1 

Results for commit 32bdae0. ± Comparison against earlier commit 7663940.

For more details on these errors, see this check.

Realm Server Test Results

    1 files  ±  0    208 suites  +18   1h 15m 30s ⏱️ + 10m 43s
2 721 tests +204  2 721 ✅ +204  0 💤 ±0  0 ❌ ±0 
2 760 runs  +204  2 760 ✅ +204  0 💤 ±0  0 ❌ ±0 

Results for commit 32bdae0. ± Comparison against earlier commit 7663940.

backspace and others added 3 commits September 8, 2026 13:13
Two constraints on a fixture's mtime pull against each other, and the twelve-
hour floor satisfied only one of them.

They have to advance, or the indexer cannot see an edit: it decides what a
from-scratch pass revisits by comparing a file's mtime against its index row
and skipping where they match. They also have to stay within a minute of the
pinned instant, because `formatLastSavedText` calls anything closer than that
"just now", and the inspector asserts a seeded fixture reads that way — which
it did when both the clock and the stamp were the real one, the files having
been written moments before the assertion.

A minute of one-second steps is sixty values, which one shard would exhaust at
once. Per adapter it is ample: a realm seeds once and a test writes a handful
of times. Sequences in different realms overlap, which costs nothing, because
the comparison that matters is between a file and its own index row.

The clock-override tests now restore the suite's pin rather than deleting it.
Deleting dropped every later test in that shard back onto the wall clock while
its fixtures still carried pinned timestamps — the same mismatch this branch
exists to remove, reintroduced by its own teardown.

Imports move to the groups `import/order` expects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`import/order` sorts relative specifiers by kind: `./stream` and `./test-clock`
are siblings, `.` is an index import, and siblings come first with a blank line
between the groups. Putting the new import next to the index one split a group
and inverted the order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…to cs-12770-pin-test-clock

# Conflicts:
#	packages/base/helpers/clock.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant