diff --git a/.changeset/969-hermetic-bug969-tests-and-tsbuildinfo-relocation.md b/.changeset/969-hermetic-bug969-tests-and-tsbuildinfo-relocation.md new file mode 100644 index 000000000..318fb663a --- /dev/null +++ b/.changeset/969-hermetic-bug969-tests-and-tsbuildinfo-relocation.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1002 +--- +**Fixed a test-infrastructure regression (#996) where bug-969 hardening tests deleted the shared `gsd-core/bin/lib/core.cjs` during concurrent runs and the build tsbuildinfo lived inside the copied install tree, intermittently failing CI with MODULE_NOT_FOUND/ENOENT.** The destructive tests now run hermetically against a temp project, and the tsbuildinfo moved out of `gsd-core/bin/`. (#969) diff --git a/.gitignore b/.gitignore index f6881123f..82faf6c3c 100644 --- a/.gitignore +++ b/.gitignore @@ -66,7 +66,7 @@ build/ # ADR-457 build-at-publish: TS-generated runtime artifacts (compiled from src/*.cts # by `npm run build:lib`). Source of truth is src/; these are emitted, never edited. # Published via prepublishOnly; built before test via pretest. Grows as modules migrate. -/gsd-core/bin/tsconfig.build.tsbuildinfo +/tsconfig.build.tsbuildinfo /gsd-core/bin/lib/research-store.cjs /gsd-core/bin/lib/research-provider.cjs /gsd-core/bin/lib/package-legitimacy.cjs diff --git a/CONTEXT.md b/CONTEXT.md index bff1a6bcb..1ae3be6c4 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -640,6 +640,12 @@ Migration plan: Phase 1 (#3465) seam additions complete; Phase 2 (#3466) targets `DEFECT.WINDOWS-ARGV-OVERFLOW.fix-forward=chunk argv into batches whose total length stays under 28,000 chars (headroom under the 32,767 ceiling); run each chunk sequentially; aggregate exit codes (first non-zero wins). Expose RUN_TESTS_MAX_CMDLINE_CHARS env override so cross-platform regression tests can force chunking with short tmp paths` `DEFECT.WINDOWS-ARGV-OVERFLOW.test-anchor=tests/run-tests-harness.test.cjs "Windows argv-overflow chunking (issue #3597)" — 30 long-named fixture files + RUN_TESTS_MAX_CMDLINE_CHARS=2000 → asserts run-tests: chunk N/M marker in stderr; pattern works on every platform` +`DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.symptom=a test deletes/rewrites a SHARED REAL build artifact or fixture (e.g. gsd-core/bin/lib/*.cjs, the build tsbuildinfo) that other test files require; node --test runs files concurrently, so innocent concurrent tests intermittently fail with "Cannot find module" / ENOENT while the racy test itself passes (victim-not-culprit, leg-asymmetric red); placing mutable build state inside a copied/shipped tree (gsd-core/bin/) additionally races install-test fs.cpSync copies → copyfile ENOENT` +`DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.examples=#996/88e30d53 — bug-969 hardening tests fs.unlinkSync'd + restored the real gsd-core/bin/lib/core.cjs and set tsBuildInfoFile inside gsd-core/bin/ → next red across the full-test matrix (macOS/Windows) + ubuntu-24 coverage leg, ~40-50 MODULE_NOT_FOUND/ENOENT per leg; reproduced locally on iteration 1; fixed #1001/#1002` +`DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.detect=grep tests for fs.unlinkSync|rmSync|writeFileSync|renameSync|cpSync targeting paths resolved from the repo root (join(__dirname,'..',...)) under gsd-core/bin/lib or a shared committed fixture, instead of a mkdtempSync temp dir; any build helper (e.g. ensureBuiltArtifacts) invoked with real-tree paths during the concurrent test phase; any tsBuildInfoFile / build-cache path that lands inside a copied/shipped dir (gsd-core/bin/)` +`DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.fix-forward=tests mutate ONLY isolated mkdtempSync copies — never delete/rewrite shared real build outputs while node --test runs files concurrently; parameterize build helpers to accept {root,srcDir,outDir,tsBuildInfoPath,tsconfigPath} overrides and point the test at a throwaway temp project (precedent: #1002 ensureBuiltArtifacts(overrides)); keep mutable build state (tsbuildinfo) OUTSIDE copied/shipped trees (repo root, gitignored) + best-effort self-heal of stale bin-local copies; this is the concrete instance of the RULESET.TESTS.delete-bad-tests real-race class` +`DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.test-anchor=tests/bug-969-test-infra-flake-hardening.test.cjs (hermetic temp-project rewrite); regression gate = 10x concurrent run of that suite + tests/state.test.cjs + tests/install.test.cjs must be clean (reproduces on iter 1 when racy)` + `DEFECT.STACKED-PR-CANNOT-STAND-ALONE.symptom=patch PR was authored against scaffolding (handler files, lint scripts, generated modules) that exists only on an unmerged upstream feature branch; the PR's "base" on GitHub is the feature branch, not main; merging requires the upstream PR to land first` `DEFECT.STACKED-PR-CANNOT-STAND-ALONE.examples=#3639 + #3637 both targeted base=feat/3575-enforcement-hardening (the Phase 6 PR #3577); #3639 modifies SDK-bridge calls in 6 family-router files that on main do NOT have any SDK-bridge call yet; #3637 patches scripts/lint-shared-module-handsync.cjs which does not exist on main at all` `DEFECT.STACKED-PR-CANNOT-STAND-ALONE.detect=gh pr view --json baseRefName shows non-main base; OR git rebase --onto origin/main produces real (not whitespace) conflicts at files the patch claims to modify; OR git cat-file -e origin/main: errors with "does not exist in origin/main"` diff --git a/issue-996-regression.md b/issue-996-regression.md new file mode 100644 index 000000000..f09efb2b0 --- /dev/null +++ b/issue-996-regression.md @@ -0,0 +1,26 @@ +## Summary + +`next` is currently **red** across the full-test matrix (macOS 22/24, Windows 22) and the ubuntu-24 coverage leg. Root-caused to commit `88e30d53` (PR #996, which closed #969 "fix stale-build flake"). The commit right before it (`d617735b`) was green. + +## Root cause + +`#996` did two things that interact badly with the **concurrent** `node --test` runner: + +1. Added `tests/bug-969-test-infra-flake-hardening.test.cjs` whose two destructive tests `fs.unlinkSync` the **real shared** `gsd-core/bin/lib/core.cjs` (and the real build tsbuildinfo) mid-run, rebuilding/restoring in `finally`. Because `node --test` runs files concurrently, any other test that `require`s `core.cjs` during that window fails with `Cannot find module .../gsd-core/bin/lib/core.cjs`. This is a textbook **real-race test** (forbidden by `RULESET.TESTS.delete-bad-tests`). +2. Set `tsBuildInfoFile: "gsd-core/bin/tsconfig.build.tsbuildinfo"` — placing mutable build state **inside `gsd-core/bin/`**, a directory install tests copy recursively via `fs.cpSync`. A concurrent rebuild writing/unlinking that file races the copy → `copyfile ENOENT`. + +Symptom: ~40–50 tests fail per leg with `MODULE_NOT_FOUND`/`ENOENT`; leg-asymmetric (timing-sensitive). Reproduced locally on the first iteration of running the bug-969 test concurrently with `state.test.cjs` + `install.test.cjs`. + +## Impact + +`next` red ⇒ branch protection blocks **all** PRs from merging (Required tests fails). + +## Fix (fix-forward) + +- Make the two destructive `bug-969` tests **hermetic** — exercise a parameterized `ensureBuiltArtifacts(overrides)` against a throwaway temp TS project; never touch real `gsd-core/bin/lib`. +- Move `tsconfig.build.tsbuildinfo` **out of `gsd-core/bin/`** to the repo root (gitignored); self-heal stale bin-local copies on persistent workspaces. + +## Verification + +- 10× concurrent race check clean (was iter-1 repro before). +- Full unit suite: 4775 pass / 0 fail through the modified runner. diff --git a/pr-1001-body.md b/pr-1001-body.md new file mode 100644 index 000000000..14cc99d06 --- /dev/null +++ b/pr-1001-body.md @@ -0,0 +1,40 @@ +## Fix PR + +## Linked Issue + +Fixes #1001 + +The linked issue carries the `confirmed-bug` label. + +## What was broken + +`next` went red across the full-test matrix (macOS 22/24, Windows 22) and the ubuntu-24 coverage leg — `Required tests` failing, blocking **all** PRs from merging. Root-caused to `88e30d53` (PR #996, which closed #969). The commit immediately before it on `next` (`d617735b`) was green. + +## Root cause + +`#996` interacts badly with the concurrent `node --test` runner: + +1. `tests/bug-969-test-infra-flake-hardening.test.cjs` had two tests that `fs.unlinkSync` the **real shared** `gsd-core/bin/lib/core.cjs` (and the real build tsbuildinfo) mid-run, restoring in `finally`. Files run concurrently, so any other test requiring `core.cjs` in that window fails with `Cannot find module .../gsd-core/bin/lib/core.cjs` — a **real-race test** (`RULESET.TESTS.delete-bad-tests`). +2. `tsBuildInfoFile` was placed at `gsd-core/bin/tsconfig.build.tsbuildinfo` — mutable build state **inside** the `gsd-core/bin/` tree that install tests copy recursively (`fs.cpSync`). A concurrent rebuild writing/unlinking it races the copy → `copyfile ENOENT`. + +Reproduced locally on the first concurrent iteration of the bug-969 test + `state.test.cjs` + `install.test.cjs`. + +## The fix + +- **Hermetic tests:** `ensureBuiltArtifacts()` is now `ensureBuiltArtifacts(overrides = {})` (root/srcDir/outDir/tsBuildInfoPath/tsconfigPath overridable; production no-arg behavior unchanged). The bug-969 destructive tests (and the sentinel test) now build/delete/re-emit inside a throwaway temp TS project — they never touch real `gsd-core/bin/lib`. +- **Relocated build state:** `tsconfig.build.tsbuildinfo` moved to the repo root (gitignored), out of the copied/shipped tree. `ensureBuiltArtifacts` best-effort-purges any stale `gsd-core/bin/tsconfig.build.tsbuildinfo` so persistent workspaces/mirrors self-heal. + +## Testing + +- Regression reproduced on broken code (iter 1); **10× concurrent race check clean** after the fix. +- Full unit suite through the modified runner: **4775 pass / 0 fail**. +- `bug-969` suite 6/6; eslint + `lint-command-contract` clean. +- Independent codex adversarial review (findings — legacy-purge, sentinel hermeticity — folded in). + +## Checklist + +- [x] Linked issue carries `confirmed-bug` +- [x] Branch `fix/1001-bug969-real-race` +- [x] Conventional commits +- [x] Changeset fragment (`type: Fixed`) +- [x] Regression test made hermetic + fail-first reproduced diff --git a/scripts/run-tests.cjs b/scripts/run-tests.cjs index 50b99867c..7e056e1c4 100644 --- a/scripts/run-tests.cjs +++ b/scripts/run-tests.cjs @@ -43,14 +43,15 @@ const SUITES = ['all', 'unit', 'integration', 'install', 'security', 'slow']; // Common case: fast incremental no-op. Stale/deleted-output case: detected by // the cheap existsSync loop and force-rebuilt. Paths resolve from __dirname so // it works regardless of GSD_TEST_DIR / temp-dir cwd. -function ensureBuiltArtifacts() { +function ensureBuiltArtifacts(overrides = {}) { const { existsSync, readdirSync, statSync, unlinkSync } = require('fs'); - const root = join(__dirname, '..'); - const srcDir = join(root, 'src'); - const outDir = join(root, 'gsd-core', 'bin', 'lib'); - const tsBuildInfoPath = join(root, 'gsd-core', 'bin', 'tsconfig.build.tsbuildinfo'); + const root = overrides.root || join(__dirname, '..'); + const srcDir = overrides.srcDir || join(root, 'src'); + const outDir = overrides.outDir || join(root, 'gsd-core', 'bin', 'lib'); + const tsBuildInfoPath = overrides.tsBuildInfoPath || join(root, 'tsconfig.build.tsbuildinfo'); + const tsconfigPath = overrides.tsconfigPath || join(root, 'tsconfig.build.json'); const tscBin = require.resolve('typescript/bin/tsc'); - const tscArgs = [tscBin, '-p', join(root, 'tsconfig.build.json')]; + const tscArgs = [tscBin, '-p', tsconfigPath]; // Build the 1:1 map of expected output paths from src/*.cts sources. // Excludes *.d.cts (declaration-only files that produce no output). @@ -76,6 +77,13 @@ function ensureBuiltArtifacts() { return expectedPaths.filter(p => !existsSync(p) || statSync(p).size === 0); } + // #996 placed the tsbuildinfo inside gsd-core/bin/ (a copied/shipped tree), which + // raced install-test copies. It now lives at the repo root. Best-effort purge any + // stale bin-local copy so persistent workspaces/mirrors self-heal (no-op on a temp + // override root or a clean checkout). + const legacyTsBuildInfo = join(root, 'gsd-core', 'bin', 'tsconfig.build.tsbuildinfo'); + try { if (existsSync(legacyTsBuildInfo)) unlinkSync(legacyTsBuildInfo); } catch { /* best-effort */ } + // Step 1: incremental build (fast no-op when sources unchanged). execFileSync(process.execPath, tscArgs, { cwd: root, stdio: 'inherit' }); diff --git a/tests/bug-969-test-infra-flake-hardening.test.cjs b/tests/bug-969-test-infra-flake-hardening.test.cjs index 15d35088b..b166769b8 100644 --- a/tests/bug-969-test-infra-flake-hardening.test.cjs +++ b/tests/bug-969-test-infra-flake-hardening.test.cjs @@ -38,6 +38,47 @@ const { cleanup } = require('./helpers.cjs'); // --------------------------------------------------------------------------- describe('bug #969 A — ensureBuiltArtifacts rebuilds stale artifacts', () => { + /** + * Helper: create a self-contained temp TypeScript project with two source files + * (sentinelmod.cts and targetmod.cts) and a tsconfig that emits to /out. + * Returns { tmp, overrides, sentinelOut, targetOut, tsBuildInfoPath }. + * + * HERMETIC: all destructive tests use this helper. They NEVER touch the real + * gsd-core/bin/lib/*.cjs or the real tsbuildinfo. (Regression from #996 fixed here.) + */ + function makeTempProject() { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-bug969-')); + const srcDir = path.join(tmp, 'src'); + const outDir = path.join(tmp, 'out'); + const tsBuildInfoPath = path.join(outDir, '.tsbuildinfo'); + const tsconfigPath = path.join(tmp, 'tsconfig.build.json'); + + fs.mkdirSync(srcDir, { recursive: true }); + fs.mkdirSync(outDir, { recursive: true }); + + fs.writeFileSync(path.join(srcDir, 'sentinelmod.cts'), 'export const sentinelValue = 1;\n'); + fs.writeFileSync(path.join(srcDir, 'targetmod.cts'), 'export const targetValue = 2;\n'); + + fs.writeFileSync(tsconfigPath, JSON.stringify({ + compilerOptions: { + rootDir: 'src', + outDir: 'out', + module: 'commonjs', + target: 'es2022', + esModuleInterop: true, + noEmitOnError: true, + incremental: true, + tsBuildInfoFile: 'out/.tsbuildinfo', + }, + include: ['src/**/*.cts'], + }, null, 2)); + + const overrides = { root: tmp, srcDir, outDir, tsBuildInfoPath, tsconfigPath }; + const sentinelOut = path.join(outDir, 'sentinelmod.cjs'); + const targetOut = path.join(outDir, 'targetmod.cjs'); + return { tmp, overrides, sentinelOut, targetOut, tsBuildInfoPath }; + } + /** * FAIL-BEFORE (origin/next behavior): * The old code contained `if (existsSync(sentinel)) return;`. When the @@ -50,72 +91,56 @@ describe('bug #969 A — ensureBuiltArtifacts rebuilds stale artifacts', () => { * the artifact would remain absent. On the fix, tsc runs unconditionally * and recreates it. * - * NOTE: this case simulates the "fresh CI checkout" scenario — no tsbuildinfo - * present. With "incremental": true the tsbuildinfo had to be absent too (or - * sources modified) to force a full emit; with the non-incremental build, tsc - * always re-emits regardless, so we only need to delete the target artifact. - * We also delete the tsbuildinfo here (if present) to keep the test hermetic. - * * PASS-AFTER (fix): * The sentinel guard is removed. ensureBuiltArtifacts() always invokes tsc. * With no tsbuildinfo present (clean state), tsc performs a full emit and * recreates all .cjs outputs including the deleted non-sentinel artifact. + * + * HERMETIC: this test operates on a self-contained temp project. It NEVER + * touches gsd-core/bin/lib/core.cjs or the real tsbuildinfo. (Fixed from #996.) */ test('rebuilds a non-sentinel artifact (with no tsbuildinfo) even when sentinel exists', () => { - const root = path.join(__dirname, '..'); - const sentinelPath = path.join(root, 'gsd-core', 'bin', 'lib', 'semver-compare.cjs'); - // Pick a second built artifact that is NOT the sentinel. - const targetPath = path.join(root, 'gsd-core', 'bin', 'lib', 'core.cjs'); - // tsbuildinfo must also be absent to force a full (non-incremental) re-emit. - const tsBuildInfoPath = path.join(root, 'gsd-core', 'bin', 'tsconfig.build.tsbuildinfo'); - - // Pre-condition: both files must already exist (built). If not, skip so - // we don't break on a worktree that hasn't been built yet (CI pre-build). - if (!fs.existsSync(sentinelPath) || !fs.existsSync(targetPath)) { - // Not a test failure — just skip the behavioral assertion because the - // build hasn't run yet. The unconditional build will handle this path. - return; - } - - // Snapshot originals so we can always restore after the test. - const originalTarget = fs.readFileSync(targetPath, 'utf-8'); - const originalTsBuildInfo = fs.existsSync(tsBuildInfoPath) - ? fs.readFileSync(tsBuildInfoPath, 'utf-8') - : null; - + const { tmp, overrides, sentinelOut, targetOut, tsBuildInfoPath } = makeTempProject(); try { - // Simulate: fresh CI checkout — target artifact stale/missing, no tsbuildinfo. - fs.unlinkSync(targetPath); + // Initial build — both outputs must appear. + ensureBuiltArtifacts(overrides); + assert.ok(fs.existsSync(sentinelOut), 'initial build: sentinelmod.cjs must exist'); + assert.ok(fs.existsSync(targetOut), 'initial build: targetmod.cjs must exist'); + + // Simulate: fresh CI checkout — target artifact missing, no tsbuildinfo. + fs.unlinkSync(targetOut); if (fs.existsSync(tsBuildInfoPath)) fs.unlinkSync(tsBuildInfoPath); - assert.ok(!fs.existsSync(targetPath), 'pre-condition: target must be absent'); - assert.ok(fs.existsSync(sentinelPath), 'pre-condition: sentinel must be present'); + assert.ok(!fs.existsSync(targetOut), 'pre-condition: targetmod.cjs must be absent'); + assert.ok(fs.existsSync(sentinelOut), 'pre-condition: sentinelmod.cjs must still be present'); // Under the OLD code this returned immediately (sentinel present → return). // Under the NEW code this calls tsc unconditionally → full emit → recreated. - ensureBuiltArtifacts(); + ensureBuiltArtifacts(overrides); assert.ok( - fs.existsSync(targetPath), - `ensureBuiltArtifacts must recreate ${path.basename(targetPath)} ` + - `even when sentinel exists (sentinel-short-circuit was removed in fix #969)` + fs.existsSync(targetOut), + 'ensureBuiltArtifacts must recreate targetmod.cjs even when sentinelmod.cjs ' + + 'exists (sentinel-short-circuit was removed in fix #969)' ); } finally { - // Always restore state so other tests see a valid build. - if (!fs.existsSync(targetPath)) { - fs.writeFileSync(targetPath, originalTarget); - } - if (originalTsBuildInfo !== null && !fs.existsSync(tsBuildInfoPath)) { - fs.writeFileSync(tsBuildInfoPath, originalTsBuildInfo); - } + cleanup(tmp); } }); + /** + * PASS-AFTER: the unconditional build emits the expected output (sentinelmod.cjs). + * Uses the temp project helper so this test is fully hermetic — it never touches + * the real gsd-core/bin/lib tree. + */ test('sentinel (semver-compare.cjs) still exists after unconditional build', () => { - const root = path.join(__dirname, '..'); - const sentinelPath = path.join(root, 'gsd-core', 'bin', 'lib', 'semver-compare.cjs'); - ensureBuiltArtifacts(); - assert.ok(fs.existsSync(sentinelPath), 'sentinel must exist after ensureBuiltArtifacts'); + const { tmp, overrides, sentinelOut } = makeTempProject(); + try { + ensureBuiltArtifacts(overrides); + assert.ok(fs.existsSync(sentinelOut), 'sentinel output (sentinelmod.cjs) must exist after ensureBuiltArtifacts'); + } finally { + cleanup(tmp); + } }); /** @@ -130,31 +155,18 @@ describe('bug #969 A — ensureBuiltArtifacts rebuilds stale artifacts', () => { * 2. A stale tsbuildinfo is present (from that same branch) * 3. ensureBuiltArtifacts() calls tsc (incremental) * 4. tsc sees "sources unchanged vs tsbuildinfo" → no-ops → stale .cjs served - * With "incremental": true this test would FAIL because core.cjs remains absent. + * With "incremental": true this test would FAIL because targetmod.cjs remains absent. * - * PASS-AFTER (incremental removed — non-incremental full build): - * tsc always re-emits every output regardless of tsbuildinfo state. Even if a - * stale tsbuildinfo is present on disk, the non-incremental build overwrites all - * outputs from scratch. The deleted core.cjs is always regenerated. + * PASS-AFTER (step-3 unlink+clean-reemit logic): + * When a missing/zero-bytes output is detected after the incremental pass, + * ensureBuiltArtifacts() unlinks the tsbuildinfo and runs tsc a second time + * (clean re-emit). The stale/missing output is always regenerated. + * + * HERMETIC: this test operates on a self-contained temp project. It NEVER + * touches gsd-core/bin/lib/core.cjs or the real tsbuildinfo. (Fixed from #996.) */ test('PERSISTENT-MIRROR: rebuilds stale output even when tsbuildinfo is present (non-incremental is authoritative)', () => { - const root = path.join(__dirname, '..'); - const targetPath = path.join(root, 'gsd-core', 'bin', 'lib', 'core.cjs'); - const tsBuildInfoPath = path.join(root, 'gsd-core', 'bin', 'tsconfig.build.tsbuildinfo'); - - // Pre-condition: target must already exist from a prior build. - if (!fs.existsSync(targetPath)) { - // Worktree hasn't been built yet — skip; the unconditional build will handle it. - return; - } - - const originalTarget = fs.readFileSync(targetPath, 'utf-8'); - // Inject a synthetic stale tsbuildinfo to simulate the persistent-mirror state - // (a prior branch left a tsbuildinfo from its own incremental build on disk). - const hadRealTsBuildInfo = fs.existsSync(tsBuildInfoPath); - const originalTsBuildInfo = hadRealTsBuildInfo - ? fs.readFileSync(tsBuildInfoPath, 'utf-8') - : null; + const { tmp, overrides, targetOut, tsBuildInfoPath } = makeTempProject(); const STALE_TSBUILDINFO = JSON.stringify({ program: { fileNames: [], options: { incremental: true } }, version: '5.0.0', @@ -162,46 +174,43 @@ describe('bug #969 A — ensureBuiltArtifacts rebuilds stale artifacts', () => { }); try { + // Initial build to populate outputs. + ensureBuiltArtifacts(overrides); + assert.ok(fs.existsSync(targetOut), 'initial build: targetmod.cjs must exist'); + // Inject a stale tsbuildinfo (mirrors: old branch rsync'd state onto workspace). fs.writeFileSync(tsBuildInfoPath, STALE_TSBUILDINFO); // Delete the output .cjs (mirrors: stale/missing output on the persistent mirror). - fs.unlinkSync(targetPath); + fs.unlinkSync(targetOut); - assert.ok(!fs.existsSync(targetPath), 'pre-condition: target must be absent'); + assert.ok(!fs.existsSync(targetOut), 'pre-condition: targetmod.cjs must be absent'); assert.ok(fs.existsSync(tsBuildInfoPath), 'pre-condition: tsbuildinfo must be present'); - // FAIL-BEFORE (incremental: true): tsc would read the stale tsbuildinfo, see - // "sources unchanged", and skip re-emitting core.cjs → it would remain absent. + // FAIL-BEFORE (incremental: true, no step-3): tsc would read the stale + // tsbuildinfo, see "sources unchanged", and skip re-emitting targetmod.cjs + // → it would remain absent. // - // PASS-AFTER (non-incremental): tsc ignores the tsbuildinfo and does a full - // emit → core.cjs is regenerated unconditionally. - ensureBuiltArtifacts(); + // PASS-AFTER (step-3 unlink+clean-reemit): missing output detected after + // incremental pass → tsbuildinfo unlinked → tsc runs again → targetmod.cjs + // is regenerated unconditionally. + ensureBuiltArtifacts(overrides); assert.ok( - fs.existsSync(targetPath), - `ensureBuiltArtifacts must regenerate ${path.basename(targetPath)} ` + - `even when a stale tsbuildinfo is present on disk ` + - `(persistent-mirror scenario — incremental:true would have no-op'd here)` + fs.existsSync(targetOut), + 'ensureBuiltArtifacts must regenerate targetmod.cjs even when a stale ' + + 'tsbuildinfo is present on disk (persistent-mirror scenario — ' + + 'incremental:true alone would have no-op\'d here)' ); - // Verify the regenerated file is valid JS (non-empty, parseable require target). - const regenerated = fs.readFileSync(targetPath, 'utf-8'); - assert.ok(regenerated.length > 100, 'regenerated core.cjs must be non-trivially non-empty'); + // Verify the regenerated file is valid JS. + const regenerated = fs.readFileSync(targetOut, 'utf-8'); + assert.ok(regenerated.length > 0, 'regenerated targetmod.cjs must be non-empty'); assert.ok( - regenerated.includes('use strict') || regenerated.includes('exports.'), - 'regenerated core.cjs must look like a valid CommonJS module' + regenerated.includes('exports.') || regenerated.includes('"use strict"'), + 'regenerated targetmod.cjs must look like a valid CommonJS module' ); } finally { - // Always restore state so subsequent tests see a valid build. - if (!fs.existsSync(targetPath)) { - fs.writeFileSync(targetPath, originalTarget); - } - // Restore the real tsbuildinfo if one existed, otherwise remove the synthetic one. - if (originalTsBuildInfo !== null) { - fs.writeFileSync(tsBuildInfoPath, originalTsBuildInfo); - } else if (fs.existsSync(tsBuildInfoPath)) { - fs.unlinkSync(tsBuildInfoPath); - } + cleanup(tmp); } }); }); diff --git a/tsconfig.build.json b/tsconfig.build.json index 3a3a80c55..51f927db2 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -16,7 +16,7 @@ "noEmitOnError": true, "skipLibCheck": true, "incremental": true, - "tsBuildInfoFile": "gsd-core/bin/tsconfig.build.tsbuildinfo" + "tsBuildInfoFile": "tsconfig.build.tsbuildinfo" }, "include": ["src/**/*.cts"] }