From 9fe736fef2d0dfca61711971bb0e41447d1cc144 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 8 Sep 2026 10:54:11 -0400 Subject: [PATCH 1/6] Pin the suite's clock so a rendered elapsed time stops drifting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- packages/host/tests/helpers/adapter.ts | 5 ++-- packages/host/tests/helpers/test-clock.ts | 28 +++++++++++++++++++++++ packages/host/tests/test-helper.js | 6 +++++ 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 packages/host/tests/helpers/test-clock.ts diff --git a/packages/host/tests/helpers/adapter.ts b/packages/host/tests/helpers/adapter.ts index f9659d233f4..4f0273bcad3 100644 --- a/packages/host/tests/helpers/adapter.ts +++ b/packages/host/tests/helpers/adapter.ts @@ -36,6 +36,7 @@ import type { RealmEventContent, } from '@cardstack/base/matrix-event'; import type ms from 'ms'; +import { TEST_CLOCK_INSTANT } from './test-clock'; interface Dir { kind: 'directory'; @@ -80,7 +81,7 @@ export class TestRealmAdapter implements RealmAdapter { this.#paths = new RealmPaths(realmURL); this.#mockMatrixUtils = mockMatrixUtils; - let now = unixTime(Date.now()); + let now = unixTime(TEST_CLOCK_INSTANT); for (let [path, content] of Object.entries(contents)) { let segments = path.split('/'); @@ -311,7 +312,7 @@ export class TestRealmAdapter implements RealmAdapter { let updateEvent: FileWatcherEventContent; - let lastModified = unixTime(Date.now()); + let lastModified = unixTime(TEST_CLOCK_INSTANT); this.#lastModified.set(this.#paths.fileURL(path).href, lastModified); if (dir.contents[name]) { diff --git a/packages/host/tests/helpers/test-clock.ts b/packages/host/tests/helpers/test-clock.ts new file mode 100644 index 00000000000..bc8af228d0b --- /dev/null +++ b/packages/host/tests/helpers/test-clock.ts @@ -0,0 +1,28 @@ +// The instant the suite reports as "now". +// +// Card code reads the clock through `@cardstack/base/helpers/clock`, which +// falls through to the real one unless something pins it. Pinning it here +// makes what a card renders a function of its own data: "3d ago", a countdown, +// an age and an "expires soon" warning all stop depending on when the suite +// happened to run, so a visual comparison of them is a comparison of the code +// rather than a race against whichever threshold each is nearest. +// +// The value is arbitrary — determinism is the point, not realism — but it is +// not free of consequences, so it is chosen rather than picked. Two things +// follow from it: +// +// Realms the tests build in the browser stamp their files with this same +// instant (see `adapter.ts`), so those files read as `today`, which is what +// they read as when both sides were the real clock. Nothing about them moves. +// +// Realms served from the index cache carry mtimes that +// `scripts/normalize-realm-mtimes.mjs` derives from file content, so they are +// fixed dates scattered across decades. Their rendered ages are measured from +// this instant instead of from today, which shifts some of them by a unit the +// first time this lands. That is a one-time baseline change, and it buys the +// property that they never shift again on their own. +export const TEST_CLOCK_INSTANT = Date.UTC(2026, 8, 1, 0, 0, 0); + +export function pinTestClock() { + (globalThis as { __boxelNow?: number }).__boxelNow = TEST_CLOCK_INSTANT; +} diff --git a/packages/host/tests/test-helper.js b/packages/host/tests/test-helper.js index 5a40afe693d..ba21b53a9cb 100644 --- a/packages/host/tests/test-helper.js +++ b/packages/host/tests/test-helper.js @@ -8,11 +8,17 @@ import { start as examStart } from 'ember-exam/test-support'; // eslint-disable-next-line ember/no-test-import-export import { loadRealmTests } from './live-test'; import { setupQUnit } from './helpers/setup-qunit'; +import { pinTestClock } from './helpers/test-clock'; import { registerShardWarmup } from './helpers/shard-warmup'; import { selectShardModules } from './helpers/shard-modules'; import testModuleTimings from './test-module-timings.json'; export async function start(examOptions) { + // Before anything renders: card code reads the clock through a seam, and + // pinning it here is what stops a rendered elapsed time depending on when + // the suite ran. + pinTestClock(); + // ember-basic-dropdown 9 has no boot-time initializer, and in a test // build it prefers the page-level wormhole div, which sits outside the // test root where scoped DOM helpers cannot see teleported content. From 040e8dcb119c184463b36a820d138008267d666e Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 8 Sep 2026 11:46:45 -0400 Subject: [PATCH 2/6] Advance fixture mtimes, and measure the calendar test from the pinned clock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- packages/host/tests/helpers/adapter.ts | 6 ++-- packages/host/tests/helpers/test-clock.ts | 34 ++++++++++++++++++- .../integration/date-time-fields-test.gts | 6 +++- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/packages/host/tests/helpers/adapter.ts b/packages/host/tests/helpers/adapter.ts index 4f0273bcad3..b69e2120cb0 100644 --- a/packages/host/tests/helpers/adapter.ts +++ b/packages/host/tests/helpers/adapter.ts @@ -36,7 +36,7 @@ import type { RealmEventContent, } from '@cardstack/base/matrix-event'; import type ms from 'ms'; -import { TEST_CLOCK_INSTANT } from './test-clock'; +import { nextFixtureMtime } from './test-clock'; interface Dir { kind: 'directory'; @@ -81,7 +81,7 @@ export class TestRealmAdapter implements RealmAdapter { this.#paths = new RealmPaths(realmURL); this.#mockMatrixUtils = mockMatrixUtils; - let now = unixTime(TEST_CLOCK_INSTANT); + let now = nextFixtureMtime(); for (let [path, content] of Object.entries(contents)) { let segments = path.split('/'); @@ -312,7 +312,7 @@ export class TestRealmAdapter implements RealmAdapter { let updateEvent: FileWatcherEventContent; - let lastModified = unixTime(TEST_CLOCK_INSTANT); + let lastModified = nextFixtureMtime(); this.#lastModified.set(this.#paths.fileURL(path).href, lastModified); if (dir.contents[name]) { diff --git a/packages/host/tests/helpers/test-clock.ts b/packages/host/tests/helpers/test-clock.ts index bc8af228d0b..b5a90bcca6f 100644 --- a/packages/host/tests/helpers/test-clock.ts +++ b/packages/host/tests/helpers/test-clock.ts @@ -21,8 +21,40 @@ // this instant instead of from today, which shifts some of them by a unit the // first time this lands. That is a one-time baseline change, and it buys the // property that they never shift again on their own. -export const TEST_CLOCK_INSTANT = Date.UTC(2026, 8, 1, 0, 0, 0); +// Mid-month and mid-day 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, which is a quieter way to lose it than leaving it broken. +export const TEST_CLOCK_INSTANT = Date.UTC(2026, 8, 15, 12, 0, 0); export function pinTestClock() { (globalThis as { __boxelNow?: number }).__boxelNow = TEST_CLOCK_INSTANT; } + +// A fixture file's recorded mtime. +// +// These cannot all be the pinned instant. The indexer decides what a +// from-scratch pass has to 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 a change the indexer cannot see — which is what +// `scripts/normalize-realm-mtimes.mjs` exists to keep working, and what a +// single frozen stamp would defeat. +// +// So they advance, one second per stamp, from far enough below the pinned +// instant that a suite would have to write tens of thousands of files in one +// page load to reach it. Staying below matters: a mtime after the pinned +// instant is in the future, and a file with a future mtime renders as an +// absolute date rather than an age. Staying within the same day matters too — +// that is what keeps these files reading as `today`, which is what they read +// as when both the clock and the stamp were the real one. +const FIXTURE_MTIME_FLOOR = + Math.floor(TEST_CLOCK_INSTANT / 1000) - 12 * 60 * 60; +const FIXTURE_MTIME_CEILING = Math.floor(TEST_CLOCK_INSTANT / 1000) - 1; +let fixtureMtimeCounter = 0; + +export function nextFixtureMtime(): number { + return Math.min( + FIXTURE_MTIME_FLOOR + fixtureMtimeCounter++, + FIXTURE_MTIME_CEILING, + ); +} diff --git a/packages/host/tests/integration/date-time-fields-test.gts b/packages/host/tests/integration/date-time-fields-test.gts index 0ab3b4f0a32..f7b3fcf0d17 100644 --- a/packages/host/tests/integration/date-time-fields-test.gts +++ b/packages/host/tests/integration/date-time-fields-test.gts @@ -26,6 +26,7 @@ import { buildField, } from '../helpers/field-test-helpers'; import { setupRenderingTest } from '../helpers/setup'; +import { TEST_CLOCK_INSTANT } from '../helpers/test-clock'; module('Integration | date-time fields', function (hooks) { setupRenderingTest(hooks); @@ -485,7 +486,10 @@ module('Integration | date-time fields', function (hooks) { `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; // Anchor on mid-month days so all asserted cells are guaranteed to be // rendered in the calendar pane that centers on the current month. - let now = new Date(); + // The same instant the calendar measures from: the field reads the clock + // through the seam, so a test that asked the real clock what "today" is + // would be asserting against a different day than the component rendered. + let now = new Date(TEST_CLOCK_INSTANT); let day14 = new Date(now.getFullYear(), now.getMonth(), 14); let day15 = new Date(now.getFullYear(), now.getMonth(), 15); let day16 = new Date(now.getFullYear(), now.getMonth(), 16); From 8f2f649b7eb0a3e2b95e3b0298f6342aea0771a0 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 8 Sep 2026 12:35:33 -0400 Subject: [PATCH 3/6] Share one clock across the card-loader boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- packages/base/helpers/clock.ts | 15 +++-------- .../host/app/resources/last-modified-date.ts | 7 ++++- packages/runtime-common/clock.ts | 27 +++++++++++++++++++ packages/runtime-common/index.ts | 1 + 4 files changed, 37 insertions(+), 13 deletions(-) create mode 100644 packages/runtime-common/clock.ts diff --git a/packages/base/helpers/clock.ts b/packages/base/helpers/clock.ts index bdb2aef0b70..f04b5d30f47 100644 --- a/packages/base/helpers/clock.ts +++ b/packages/base/helpers/clock.ts @@ -15,15 +15,6 @@ // reach card code through whichever loader instance rendered it, which no call // site knows about. `__boxelRenderMode` and its neighbours already work this // way. -export function now(): number { - let pinned = (globalThis as { __boxelNow?: unknown }).__boxelNow; - return typeof pinned === 'number' && Number.isFinite(pinned) - ? pinned - : Date.now(); -} - -// The same instant as a `Date`, for callers that would otherwise write -// `new Date()`. -export function nowDate(): Date { - return new Date(now()); -} +// The implementation lives in runtime-common so the host app can read the +// same instant; this is the path card code imports it by. +export { now, nowDate } from '@cardstack/runtime-common'; diff --git a/packages/host/app/resources/last-modified-date.ts b/packages/host/app/resources/last-modified-date.ts index 84037d9a595..7475cc09b7a 100644 --- a/packages/host/app/resources/last-modified-date.ts +++ b/packages/host/app/resources/last-modified-date.ts @@ -2,6 +2,8 @@ import { registerDestructor } from '@ember/destroyable'; import { tracked } from '@glimmer/tracking'; import { formatDistance } from 'date-fns'; + +import { now as clockNow } from '@cardstack/runtime-common'; import { Resource } from 'ember-modify-based-class-resource'; import type { Ready as ReadyFile } from '@cardstack/host/resources/file'; @@ -12,7 +14,10 @@ interface Args { export const LAST_SAVED_JUST_NOW_THRESHOLD_MS = 60 * 1000; -export function formatLastSavedText(date: Date, now = Date.now()) { +// Defaults to the shared clock rather than the real one: a test that pins the +// instant also pins the timestamps its fixtures carry, and measuring those +// against the wall clock reports a file saved in the future. +export function formatLastSavedText(date: Date, now = clockNow()) { if (Math.abs(now - date.getTime()) < LAST_SAVED_JUST_NOW_THRESHOLD_MS) { return 'Last saved just now'; } diff --git a/packages/runtime-common/clock.ts b/packages/runtime-common/clock.ts new file mode 100644 index 00000000000..2a358fc3517 --- /dev/null +++ b/packages/runtime-common/clock.ts @@ -0,0 +1,27 @@ +// The instant anything measuring elapsed time should measure from. +// +// Unset, this is the real clock. Setting a number on `globalThis.__boxelNow` +// pins it, so what gets rendered becomes a function of the data rather than of +// when it was rendered. +// +// That matters wherever output is an age — "3d ago", a countdown, "Last saved +// 6 days ago". Each changes on a schedule nobody chose, so a visual comparison +// of one differs between two runs over identical data, and the usual way to +// quiet that is to stop comparing the element, which trades the coverage away. +// +// It lives here rather than in either consumer because both sides of the card +// loader need it: card code in `packages/base` reaches it through +// `helpers/clock`, and the host app imports it directly. A global is what +// crosses that boundary — the loader gives card code its own module instances, +// so a shared module-level variable would not be shared at all. +// `__boxelRenderMode` and its neighbours already work this way. +export function now(): number { + let pinned = (globalThis as { __boxelNow?: unknown }).__boxelNow; + return typeof pinned === 'number' && Number.isFinite(pinned) + ? pinned + : Date.now(); +} + +export function nowDate(): Date { + return new Date(now()); +} diff --git a/packages/runtime-common/index.ts b/packages/runtime-common/index.ts index 1f93dffccba..292bc73aefe 100644 --- a/packages/runtime-common/index.ts +++ b/packages/runtime-common/index.ts @@ -20,6 +20,7 @@ import { rri, type RealmResourceIdentifier } from './realm-identifiers.ts'; import type { RealmEventContent } from '@cardstack/base/matrix-event'; import type { FileDef } from '@cardstack/base/file-api'; +export { now, nowDate } from './clock'; export interface LooseSingleResourceDocument { data: LooseLinkableResource; included?: LooseLinkableResource[]; From c92ad4813abec0f65926ed79bc401a049242071a Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 8 Sep 2026 12:50:23 -0400 Subject: [PATCH 4/6] Give the clock re-export the file extension this package requires MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 --- packages/runtime-common/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-common/index.ts b/packages/runtime-common/index.ts index 292bc73aefe..1ce0118bae3 100644 --- a/packages/runtime-common/index.ts +++ b/packages/runtime-common/index.ts @@ -20,7 +20,7 @@ import { rri, type RealmResourceIdentifier } from './realm-identifiers.ts'; import type { RealmEventContent } from '@cardstack/base/matrix-event'; import type { FileDef } from '@cardstack/base/file-api'; -export { now, nowDate } from './clock'; +export { now, nowDate } from './clock.ts'; export interface LooseSingleResourceDocument { data: LooseLinkableResource; included?: LooseLinkableResource[]; From bb4300d5af7e7181bdc0228b49a1c41939aa27f1 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 8 Sep 2026 13:13:11 -0400 Subject: [PATCH 5/6] Keep seeded fixture mtimes inside the just-now window, per realm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../host/app/resources/last-modified-date.ts | 2 +- packages/host/tests/helpers/adapter.ts | 8 +-- packages/host/tests/helpers/test-clock.ts | 51 ++++++++++--------- .../host/tests/unit/file-view-model-test.ts | 8 ++- 4 files changed, 39 insertions(+), 30 deletions(-) diff --git a/packages/host/app/resources/last-modified-date.ts b/packages/host/app/resources/last-modified-date.ts index 7475cc09b7a..b27ccfcdb4e 100644 --- a/packages/host/app/resources/last-modified-date.ts +++ b/packages/host/app/resources/last-modified-date.ts @@ -2,9 +2,9 @@ import { registerDestructor } from '@ember/destroyable'; import { tracked } from '@glimmer/tracking'; import { formatDistance } from 'date-fns'; +import { Resource } from 'ember-modify-based-class-resource'; import { now as clockNow } from '@cardstack/runtime-common'; -import { Resource } from 'ember-modify-based-class-resource'; import type { Ready as ReadyFile } from '@cardstack/host/resources/file'; diff --git a/packages/host/tests/helpers/adapter.ts b/packages/host/tests/helpers/adapter.ts index b69e2120cb0..d455e94297b 100644 --- a/packages/host/tests/helpers/adapter.ts +++ b/packages/host/tests/helpers/adapter.ts @@ -29,6 +29,7 @@ import type { import { WebMessageStream, messageCloseHandler } from './stream'; import { createJWT, testRealmURL } from '.'; +import { createFixtureMtimeSequence } from './test-clock'; import type { MockUtils } from './mock-matrix/_utils'; import type { @@ -36,7 +37,6 @@ import type { RealmEventContent, } from '@cardstack/base/matrix-event'; import type ms from 'ms'; -import { nextFixtureMtime } from './test-clock'; interface Dir { kind: 'directory'; @@ -66,6 +66,8 @@ export class TestRealmAdapter implements RealmAdapter { #subscriber: ((message: FileWatcherEventContent) => void) | undefined; #loader: Loader | undefined; // Will be set in the realm's constructor - needed for openFile for shimming purposes #ready = new Deferred(); + // Per adapter, so each realm's files stay inside the just-now window. + #nextMtime = createFixtureMtimeSequence(); #potentialModulesAndInstances: { content: any; url: URL }[] = []; #mockMatrixUtils: MockUtils; @@ -81,7 +83,7 @@ export class TestRealmAdapter implements RealmAdapter { this.#paths = new RealmPaths(realmURL); this.#mockMatrixUtils = mockMatrixUtils; - let now = nextFixtureMtime(); + let now = this.#nextMtime(); for (let [path, content] of Object.entries(contents)) { let segments = path.split('/'); @@ -312,7 +314,7 @@ export class TestRealmAdapter implements RealmAdapter { let updateEvent: FileWatcherEventContent; - let lastModified = nextFixtureMtime(); + let lastModified = this.#nextMtime(); this.#lastModified.set(this.#paths.fileURL(path).href, lastModified); if (dir.contents[name]) { diff --git a/packages/host/tests/helpers/test-clock.ts b/packages/host/tests/helpers/test-clock.ts index b5a90bcca6f..2d5f6b0e03c 100644 --- a/packages/host/tests/helpers/test-clock.ts +++ b/packages/host/tests/helpers/test-clock.ts @@ -31,30 +31,31 @@ export function pinTestClock() { (globalThis as { __boxelNow?: number }).__boxelNow = TEST_CLOCK_INSTANT; } -// A fixture file's recorded mtime. -// -// These cannot all be the pinned instant. The indexer decides what a -// from-scratch pass has to 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 a change the indexer cannot see — which is what -// `scripts/normalize-realm-mtimes.mjs` exists to keep working, and what a -// single frozen stamp would defeat. -// -// So they advance, one second per stamp, from far enough below the pinned -// instant that a suite would have to write tens of thousands of files in one -// page load to reach it. Staying below matters: a mtime after the pinned -// instant is in the future, and a file with a future mtime renders as an -// absolute date rather than an age. Staying within the same day matters too — -// that is what keeps these files reading as `today`, which is what they read -// as when both the clock and the stamp were the real one. -const FIXTURE_MTIME_FLOOR = - Math.floor(TEST_CLOCK_INSTANT / 1000) - 12 * 60 * 60; -const FIXTURE_MTIME_CEILING = Math.floor(TEST_CLOCK_INSTANT / 1000) - 1; -let fixtureMtimeCounter = 0; +// A realm's fixture mtimes, as a fresh sequence per adapter. +// +// Two constraints pull against each other here. +// +// They must advance: the indexer decides what a from-scratch pass has to +// 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. +// +// They must also 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 earlier. +// +// A minute of one-second steps is only sixty values, which a whole shard would +// exhaust immediately. 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 — the comparison that matters is between a file and its own +// index row. +const FIXTURE_MTIME_SPAN_S = 59; -export function nextFixtureMtime(): number { - return Math.min( - FIXTURE_MTIME_FLOOR + fixtureMtimeCounter++, - FIXTURE_MTIME_CEILING, - ); +export function createFixtureMtimeSequence(): () => number { + let pinnedSeconds = Math.floor(TEST_CLOCK_INSTANT / 1000); + let step = 0; + return () => + Math.min(pinnedSeconds - FIXTURE_MTIME_SPAN_S + step++, pinnedSeconds - 1); } diff --git a/packages/host/tests/unit/file-view-model-test.ts b/packages/host/tests/unit/file-view-model-test.ts index 418de71b1eb..d41f584293a 100644 --- a/packages/host/tests/unit/file-view-model-test.ts +++ b/packages/host/tests/unit/file-view-model-test.ts @@ -415,11 +415,17 @@ module('Unit | file-formats', function (hooks) { const PINNED = 1_768_478_400; const DAY = 86_400; + // Restored rather than deleted: the suite pins the clock once at + // startup, so deleting it here would drop every later test in the shard + // back onto the wall clock while their fixtures still carry pinned + // timestamps. + let suitePin: number | undefined; hooks.beforeEach(function () { + suitePin = (globalThis as { __boxelNow?: number }).__boxelNow; (globalThis as { __boxelNow?: number }).__boxelNow = PINNED * 1000; }); hooks.afterEach(function () { - delete (globalThis as { __boxelNow?: number }).__boxelNow; + (globalThis as { __boxelNow?: number }).__boxelNow = suitePin; }); test('measures from the pinned instant rather than the real clock', function (assert) { From 766394024bf96b504b0d295ab9beabd651e22942 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 8 Sep 2026 13:46:36 -0400 Subject: [PATCH 6/6] Group the test-clock import with the other sibling imports `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 --- packages/host/tests/helpers/adapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/host/tests/helpers/adapter.ts b/packages/host/tests/helpers/adapter.ts index d455e94297b..20251182065 100644 --- a/packages/host/tests/helpers/adapter.ts +++ b/packages/host/tests/helpers/adapter.ts @@ -27,9 +27,9 @@ import type { } from '@cardstack/runtime-common/realm'; import { WebMessageStream, messageCloseHandler } from './stream'; +import { createFixtureMtimeSequence } from './test-clock'; import { createJWT, testRealmURL } from '.'; -import { createFixtureMtimeSequence } from './test-clock'; import type { MockUtils } from './mock-matrix/_utils'; import type {