fix(#1001): make bug-969 hardening tests hermetic + move build tsbuildinfo out of shipped tree (regression from #996) (#1002)

* fix(#969): make bug-969 hardening tests hermetic and move build tsbuildinfo out of shipped tree (regression from #996)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(#969): self-heal legacy bin-local tsbuildinfo and make sentinel test hermetic (adversarial-review follow-ups)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(changeset): set pr number to 1002

* docs(#1001): record DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST anti-pattern in CONTEXT.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-10 13:58:56 -04:00
committed by GitHub
parent 88e30d5342
commit adaf3e17d8
8 changed files with 196 additions and 102 deletions

View File

@@ -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)

2
.gitignore vendored
View File

@@ -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

View File

@@ -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 <n> --json baseRefName shows non-main base; OR git rebase --onto origin/main <upstream-pr-branch> <patch-pr-branch> produces real (not whitespace) conflicts at files the patch claims to modify; OR git cat-file -e origin/main:<patch-target-file> errors with "does not exist in origin/main"`

26
issue-996-regression.md Normal file
View File

@@ -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.

40
pr-1001-body.md Normal file
View File

@@ -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

View File

@@ -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' });

View File

@@ -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 <tmp>/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);
}
});
});

View File

@@ -16,7 +16,7 @@
"noEmitOnError": true,
"skipLibCheck": true,
"incremental": true,
"tsBuildInfoFile": "gsd-core/bin/tsconfig.build.tsbuildinfo"
"tsBuildInfoFile": "tsconfig.build.tsbuildinfo"
},
"include": ["src/**/*.cts"]
}