Skip to content

Commit b5f562a

Browse files
claude[bot]claude
andauthored
fix(spec): stamp both sides of the browser-reachable freshness fixture from one anchor (#10626)
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. Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6b0be02 commit b5f562a

1 file changed

Lines changed: 83 additions & 10 deletions

File tree

packages/spec/scripts/check-browser-reachable-entries.ts

Lines changed: 83 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ import {
112112
mkdtempSync,
113113
readFileSync,
114114
rmSync,
115+
statSync,
115116
utimesSync,
116117
writeFileSync,
117118
} from 'node:fs';
@@ -627,10 +628,56 @@ function selfTest(): never {
627628
// observe the refusal on the real tree is to break the real tree. Both
628629
// verdicts are pinned: a guard only ever seen green cannot be told apart from
629630
// one that matches nothing.
631+
//
632+
// ## Why every mtime below is stamped, the SOURCE's included (#10511)
633+
//
634+
// The acceptance case used to leave the source at whatever mtime the OS wrote
635+
// and stamp only the bundle, with `new Date()`. Those two clocks do not have
636+
// the same resolution, and the mismatch runs in the direction that FAILS a
637+
// correct tree: `writeFileSync` stamps to the nanosecond (ext4, tmpfs) while
638+
// `new Date()` carries whole milliseconds, so `utimesSync(bundle, new Date())`
639+
// can land up to 1 ms BEHIND a source written microseconds earlier.
640+
// `bundlesAreStale` then reads a bundle touched LATER in wall-clock time as
641+
// the older of the two, and "accepts a build newer than its sources" fails.
642+
// Measured on this fixture: src mtime `…539.612`, bundle `…539.000`, margin
643+
// −0.612 ms.
644+
//
645+
// Nothing in the old code bought any margin against that — the margin was
646+
// whatever the five syscalls in between happened to cost. Cold, that is
647+
// 2.5–9.5 ms and the case passes; replayed hot it collapses below 1 ms and
648+
// the case fails 378/500. One such run reddened a merge-queue candidate and
649+
// evicted a PR that cannot influence this gate.
650+
//
651+
// So both sides are stamped from ONE integer-millisecond anchor and every
652+
// case states its own offset — the discipline `dist-freshness.test.ts` already
653+
// applies to this very library ("explicit stamps rather than sleeps … a test
654+
// that races them is a test that gets `.skip`ped later").
630655
const fresh = mkdtempSync(join(tmpdir(), 'os-browser-reachable-fresh-'));
631656
try {
657+
const srcFile = join(fresh, 'src', 'a.ts');
658+
const bundleFile = join(fresh, 'dist', 'index.mjs');
659+
const configFile = join(fresh, 'tsup.config.ts');
660+
661+
// `Date.now()` is whole milliseconds, which is exactly the point: every
662+
// stamp below is an integer offset from it, so no comparison this fixture
663+
// makes ever crosses two timestamp resolutions.
664+
const ANCHOR = Date.now();
665+
const stamp = (file: string, offsetMs: number): void => {
666+
const at = new Date(ANCHOR + offsetMs);
667+
utimesSync(file, at, at);
668+
};
669+
/** The stamps as the rule sees them, so a failure prints the numbers. */
670+
const mtimes = (): string =>
671+
JSON.stringify({
672+
anchor: ANCHOR,
673+
srcOffsetMs: statSync(srcFile).mtimeMs - ANCHOR,
674+
bundleOffsetMs: existsSync(bundleFile) ? statSync(bundleFile).mtimeMs - ANCHOR : null,
675+
configOffsetMs: existsSync(configFile) ? statSync(configFile).mtimeMs - ANCHOR : null,
676+
});
677+
632678
mkdirSync(join(fresh, 'src'), { recursive: true });
633-
writeFileSync(join(fresh, 'src', 'a.ts'), 'export const a = 1;\n');
679+
writeFileSync(srcFile, 'export const a = 1;\n');
680+
stamp(srcFile, 0);
634681

635682
const missing = inspectBundleFreshness(fresh, 'check', RERUN);
636683
check(
@@ -641,9 +688,8 @@ function selfTest(): never {
641688

642689
// A bundle older than the source it claims to describe.
643690
mkdirSync(join(fresh, 'dist'), { recursive: true });
644-
writeFileSync(join(fresh, 'dist', 'index.mjs'), 'export const a = 1;\n');
645-
const past = new Date(Date.now() - 60_000);
646-
utimesSync(join(fresh, 'dist', 'index.mjs'), past, past);
691+
writeFileSync(bundleFile, 'export const a = 1;\n');
692+
stamp(bundleFile, -60_000);
647693

648694
const stale = inspectBundleFreshness(fresh, 'check', RERUN);
649695
check(
@@ -652,19 +698,46 @@ function selfTest(): never {
652698
JSON.stringify(stale),
653699
);
654700

701+
// The reject side at its finest grain, and the case that reddens if anyone
702+
// ever settles a flake here by widening the comparison into a tolerance
703+
// window: a bundle one millisecond behind its sources IS stale, and a rule
704+
// that shrugs at 1 ms answers about the wrong build for as long as the
705+
// window lasts. Refusing at 60 s above cannot tell that apart.
706+
stamp(bundleFile, -1);
707+
const barelyStale = inspectBundleFreshness(fresh, 'check', RERUN);
708+
check(
709+
'refuses a build ONE MILLISECOND older than its sources',
710+
!barelyStale.fresh && barelyStale.state === 'stale',
711+
mtimes(),
712+
);
713+
714+
// The boundary itself. `bundlesAreStale` asks whether the sources are NEWER
715+
// (`>`), so an exact tie is not "older" and reads fresh. Pinned because its
716+
// two neighbours sit one millisecond either side and nothing else in this
717+
// file says which way the equality falls.
718+
stamp(bundleFile, 0);
719+
check(
720+
'accepts a build whose mtime EQUALS its newest source (the > boundary)',
721+
inspectBundleFreshness(fresh, 'check', RERUN).fresh,
722+
mtimes(),
723+
);
724+
655725
// …and passes once the bundle is the newer of the two.
656-
const now = new Date();
657-
utimesSync(join(fresh, 'dist', 'index.mjs'), now, now);
658-
check('accepts a build newer than its sources', inspectBundleFreshness(fresh, 'check', RERUN).fresh);
726+
stamp(bundleFile, 60_000);
727+
check(
728+
'accepts a build newer than its sources',
729+
inspectBundleFreshness(fresh, 'check', RERUN).fresh,
730+
mtimes(),
731+
);
659732

660733
// The bundler config is an input too: editing it without rebuilding must
661734
// read as stale, because it decides the entries and the externals.
662-
const later = new Date(Date.now() + 60_000);
663-
writeFileSync(join(fresh, 'tsup.config.ts'), 'export default {};\n');
664-
utimesSync(join(fresh, 'tsup.config.ts'), later, later);
735+
writeFileSync(configFile, 'export default {};\n');
736+
stamp(configFile, 120_000);
665737
check(
666738
'an edited-but-unbuilt tsup.config.ts reads as stale',
667739
!inspectBundleFreshness(fresh, 'check', RERUN).fresh,
740+
mtimes(),
668741
);
669742
} finally {
670743
rmSync(fresh, { recursive: true, force: true });

0 commit comments

Comments
 (0)