From f12cee12e647ebda665dedf70e68b42a47904c66 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 06:38:27 +0000 Subject: [PATCH] fix(spec): stamp both sides of the browser-reachable freshness fixture from one anchor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The self-test case "accepts a build newer than its sources" left the source file at whatever mtime the OS wrote and stamped only the bundle, with `new Date()`. The two clocks do not share a resolution and the mismatch runs in the direction that fails a correct tree: `writeFileSync` stamps to the nanosecond on ext4/tmpfs, `new Date()` carries whole milliseconds, so `utimesSync(bundle, new Date())` can land up to 1 ms BEHIND a source written microseconds earlier. `bundlesAreStale` then reads a bundle touched later in wall-clock time as the older of the two and the case fails. Nothing in the old code bought margin against that; the margin was whatever the five syscalls in between happened to cost — 2.5-9.5 ms cold on this box, under 1 ms when replayed hot, where the same fixture fails 378/500. One such run reddened a merge-queue candidate and evicted a PR that cannot influence this gate. Both sides are now stamped from one integer-millisecond anchor and every case states its own offset, the discipline dist-freshness.test.ts already applies to this same library. Two cases join it: a bundle one millisecond older reads stale (the reject side at its finest grain, which reddens if anyone ever settles a flake here by widening the comparison into a tolerance window), and an exact mtime tie reads fresh (the `>` boundary, previously unpinned). Every freshness case now passes a detail so a failure prints the mtimes instead of a bare cross. No production behaviour changes: `bundlesAreStale` is untouched. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt --- .../check-browser-reachable-entries.ts | 93 +++++++++++++++++-- 1 file changed, 83 insertions(+), 10 deletions(-) diff --git a/packages/spec/scripts/check-browser-reachable-entries.ts b/packages/spec/scripts/check-browser-reachable-entries.ts index 683fa9fd82..f04cf86047 100644 --- a/packages/spec/scripts/check-browser-reachable-entries.ts +++ b/packages/spec/scripts/check-browser-reachable-entries.ts @@ -112,6 +112,7 @@ import { mkdtempSync, readFileSync, rmSync, + statSync, utimesSync, writeFileSync, } from 'node:fs'; @@ -627,10 +628,56 @@ function selfTest(): never { // observe the refusal on the real tree is to break the real tree. Both // verdicts are pinned: a guard only ever seen green cannot be told apart from // one that matches nothing. + // + // ## Why every mtime below is stamped, the SOURCE's included (#10511) + // + // The acceptance case used to leave the source at whatever mtime the OS wrote + // and stamp only the bundle, with `new Date()`. Those two clocks do not have + // the same resolution, and the mismatch runs in the direction that FAILS a + // correct tree: `writeFileSync` stamps to the nanosecond (ext4, tmpfs) while + // `new Date()` carries whole milliseconds, so `utimesSync(bundle, new Date())` + // can land up to 1 ms BEHIND a source written microseconds earlier. + // `bundlesAreStale` then reads a bundle touched LATER in wall-clock time as + // the older of the two, and "accepts a build newer than its sources" fails. + // Measured on this fixture: src mtime `…539.612`, bundle `…539.000`, margin + // −0.612 ms. + // + // Nothing in the old code bought any margin against that — the margin was + // whatever the five syscalls in between happened to cost. Cold, that is + // 2.5–9.5 ms and the case passes; replayed hot it collapses below 1 ms and + // the case fails 378/500. One such run reddened a merge-queue candidate and + // evicted a PR that cannot influence this gate. + // + // So both sides are stamped from ONE integer-millisecond anchor and every + // case states its own offset — the discipline `dist-freshness.test.ts` already + // applies to this very library ("explicit stamps rather than sleeps … a test + // that races them is a test that gets `.skip`ped later"). const fresh = mkdtempSync(join(tmpdir(), 'os-browser-reachable-fresh-')); try { + const srcFile = join(fresh, 'src', 'a.ts'); + const bundleFile = join(fresh, 'dist', 'index.mjs'); + const configFile = join(fresh, 'tsup.config.ts'); + + // `Date.now()` is whole milliseconds, which is exactly the point: every + // stamp below is an integer offset from it, so no comparison this fixture + // makes ever crosses two timestamp resolutions. + const ANCHOR = Date.now(); + const stamp = (file: string, offsetMs: number): void => { + const at = new Date(ANCHOR + offsetMs); + utimesSync(file, at, at); + }; + /** The stamps as the rule sees them, so a failure prints the numbers. */ + const mtimes = (): string => + JSON.stringify({ + anchor: ANCHOR, + srcOffsetMs: statSync(srcFile).mtimeMs - ANCHOR, + bundleOffsetMs: existsSync(bundleFile) ? statSync(bundleFile).mtimeMs - ANCHOR : null, + configOffsetMs: existsSync(configFile) ? statSync(configFile).mtimeMs - ANCHOR : null, + }); + mkdirSync(join(fresh, 'src'), { recursive: true }); - writeFileSync(join(fresh, 'src', 'a.ts'), 'export const a = 1;\n'); + writeFileSync(srcFile, 'export const a = 1;\n'); + stamp(srcFile, 0); const missing = inspectBundleFreshness(fresh, 'check', RERUN); check( @@ -641,9 +688,8 @@ function selfTest(): never { // A bundle older than the source it claims to describe. mkdirSync(join(fresh, 'dist'), { recursive: true }); - writeFileSync(join(fresh, 'dist', 'index.mjs'), 'export const a = 1;\n'); - const past = new Date(Date.now() - 60_000); - utimesSync(join(fresh, 'dist', 'index.mjs'), past, past); + writeFileSync(bundleFile, 'export const a = 1;\n'); + stamp(bundleFile, -60_000); const stale = inspectBundleFreshness(fresh, 'check', RERUN); check( @@ -652,19 +698,46 @@ function selfTest(): never { JSON.stringify(stale), ); + // The reject side at its finest grain, and the case that reddens if anyone + // ever settles a flake here by widening the comparison into a tolerance + // window: a bundle one millisecond behind its sources IS stale, and a rule + // that shrugs at 1 ms answers about the wrong build for as long as the + // window lasts. Refusing at 60 s above cannot tell that apart. + stamp(bundleFile, -1); + const barelyStale = inspectBundleFreshness(fresh, 'check', RERUN); + check( + 'refuses a build ONE MILLISECOND older than its sources', + !barelyStale.fresh && barelyStale.state === 'stale', + mtimes(), + ); + + // The boundary itself. `bundlesAreStale` asks whether the sources are NEWER + // (`>`), so an exact tie is not "older" and reads fresh. Pinned because its + // two neighbours sit one millisecond either side and nothing else in this + // file says which way the equality falls. + stamp(bundleFile, 0); + check( + 'accepts a build whose mtime EQUALS its newest source (the > boundary)', + inspectBundleFreshness(fresh, 'check', RERUN).fresh, + mtimes(), + ); + // …and passes once the bundle is the newer of the two. - const now = new Date(); - utimesSync(join(fresh, 'dist', 'index.mjs'), now, now); - check('accepts a build newer than its sources', inspectBundleFreshness(fresh, 'check', RERUN).fresh); + stamp(bundleFile, 60_000); + check( + 'accepts a build newer than its sources', + inspectBundleFreshness(fresh, 'check', RERUN).fresh, + mtimes(), + ); // The bundler config is an input too: editing it without rebuilding must // read as stale, because it decides the entries and the externals. - const later = new Date(Date.now() + 60_000); - writeFileSync(join(fresh, 'tsup.config.ts'), 'export default {};\n'); - utimesSync(join(fresh, 'tsup.config.ts'), later, later); + writeFileSync(configFile, 'export default {};\n'); + stamp(configFile, 120_000); check( 'an edited-but-unbuilt tsup.config.ts reads as stale', !inspectBundleFreshness(fresh, 'check', RERUN).fresh, + mtimes(), ); } finally { rmSync(fresh, { recursive: true, force: true });