diff --git a/packages/loopover-engine/test/issue-quality-report.test.ts b/packages/loopover-engine/test/issue-quality-report.test.ts index 164222b20..329097d00 100644 --- a/packages/loopover-engine/test/issue-quality-report.test.ts +++ b/packages/loopover-engine/test/issue-quality-report.test.ts @@ -16,8 +16,10 @@ function now(): string { return new Date().toISOString(); } +const FIXTURE_NOW_MS = Date.now(); + function daysAgoIso(days: number): string { - return new Date(Date.now() - days * 86_400_000).toISOString(); + return new Date(FIXTURE_NOW_MS - days * 86_400_000).toISOString(); } function registryConfig(overrides: Json = {}): Json { diff --git a/scripts/check-fixture-clock-races.ts b/scripts/check-fixture-clock-races.ts index 5347923dd..f8ad79433 100644 --- a/scripts/check-fixture-clock-races.ts +++ b/scripts/check-fixture-clock-races.ts @@ -32,6 +32,11 @@ import { fileURLToPath, URL } from "node:url"; export type FixtureClockRace = { file: string; helper: string; calls: number }; +/** Every directory whose fixtures the checker walks. `packages/loopover-engine/test` is a second, + * independently-run required suite (`npm run test --workspace @loopover/engine`) with its own convention + * for this bug shape, and is scanned alongside the root suite so a violation there fails the same way. */ +export const FIXTURE_TEST_ROOTS = ["test", "packages/loopover-engine/test"] as const; + /** Helper declarations of the racy shape: takes an offset parameter AND computes from a fresh `Date.now()`. */ const OFFSET_HELPER_RE = /(?:^|\n)\s*(?:export\s+)?(?:function\s+(\w+)\s*\(([^)]*)\)|const\s+(\w+)\s*=\s*\(([^)]*)\)\s*(?::[^=]+)?=>)/g; @@ -80,7 +85,10 @@ export function findFixtureClockRaces(file: string, source: string): FixtureCloc return races; } -function walk(dir: string, out: string[]): void { +/** Recursively collects `*.test.ts` files under `dir` into `out`, tolerating a missing directory (a checkout + * without the engine package must still run rather than crash). Exported so the roots loop's tolerance and + * reach can be asserted directly rather than only through `main()`'s unmockable filesystem paths. */ +export function walk(dir: string, out: string[]): void { let entries: ReadonlyArray<{ name: string; isDirectory(): boolean }>; try { entries = readdirSync(dir, { withFileTypes: true }); @@ -101,7 +109,7 @@ function walk(dir: string, out: string[]): void { function main(): void { const root = join(fileURLToPath(new URL(".", import.meta.url)), ".."); const files: string[] = []; - walk(join(root, "test"), files); + for (const testRoot of FIXTURE_TEST_ROOTS) walk(join(root, testRoot), files); const races = files.flatMap((file) => findFixtureClockRaces(file.slice(root.length + 1), readFileSync(file, "utf8"))); if (races.length > 0) { diff --git a/test/unit/check-fixture-clock-races-script.test.ts b/test/unit/check-fixture-clock-races-script.test.ts index cdcce8f79..a4a324545 100644 --- a/test/unit/check-fixture-clock-races-script.test.ts +++ b/test/unit/check-fixture-clock-races-script.test.ts @@ -4,9 +4,13 @@ // here matter as much as the positive one. Reading the clock live is CORRECT wherever the passage of time is // itself under test; the distinguishing property is whether the helper projects a fixture timestamp from an // offset its caller varies. +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + import { describe, expect, it } from "vitest"; -import { findFixtureClockRaces } from "../../scripts/check-fixture-clock-races"; +import { FIXTURE_TEST_ROOTS, findFixtureClockRaces, walk } from "../../scripts/check-fixture-clock-races"; describe("findFixtureClockRaces (#9955)", () => { it("REGRESSION: catches the exact queue-trends shape that reached CI", () => { @@ -105,4 +109,52 @@ seed(dayAgo(3)); `; expect(findFixtureClockRaces("f.test.ts", source)).toHaveLength(1); }); + + it("REGRESSION (#10043): reports the daysAgoIso shape that lived in packages/loopover-engine/test unscanned", () => { + // Verbatim the pre-fix helper from packages/loopover-engine/test/issue-quality-report.test.ts, plus its + // two call sites -- the exact shape the checker never saw because main() only walked test/. + const source = ` +function daysAgoIso(days: number): string { + return new Date(Date.now() - days * 86_400_000).toISOString(); +} +const stale = build(daysAgoIso(60)); +const ancient = build(daysAgoIso(100)); +`; + expect(findFixtureClockRaces("packages/loopover-engine/test/x.test.ts", source)).toEqual([ + { file: "packages/loopover-engine/test/x.test.ts", helper: "daysAgoIso", calls: 2 }, + ]); + }); +}); + +describe("FIXTURE_TEST_ROOTS and walk (#10043)", () => { + it("scans both the root suite and the engine suite", () => { + expect(FIXTURE_TEST_ROOTS).toContain("test"); + expect(FIXTURE_TEST_ROOTS).toContain("packages/loopover-engine/test"); + }); + + it("walk tolerates a missing directory instead of throwing", () => { + const out: string[] = []; + expect(() => walk(join(tmpdir(), "check-fixture-clock-races-missing-dir-fixture"), out)).not.toThrow(); + expect(out).toEqual([]); + }); + + it("walk reaches a fixture under EITHER root the same way, mirroring main()'s per-root loop", () => { + const workspace = mkdtempSync(join(tmpdir(), "fixture-clock-races-")); + try { + const rootTestDir = join(workspace, "test", "unit"); + const engineTestDir = join(workspace, "packages", "loopover-engine", "test"); + mkdirSync(rootTestDir, { recursive: true }); + mkdirSync(engineTestDir, { recursive: true }); + writeFileSync(join(rootTestDir, "root-fixture.test.ts"), "export const rootFixture = 1;\n"); + writeFileSync(join(engineTestDir, "engine-fixture.test.ts"), "export const engineFixture = 1;\n"); + + const found: string[] = []; + for (const testRoot of FIXTURE_TEST_ROOTS) walk(join(workspace, testRoot), found); + + expect(found).toContain(join(rootTestDir, "root-fixture.test.ts")); + expect(found).toContain(join(engineTestDir, "engine-fixture.test.ts")); + } finally { + rmSync(workspace, { recursive: true, force: true }); + } + }); });