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:
@@ -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
2
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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
26
issue-996-regression.md
Normal 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
40
pr-1001-body.md
Normal 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
|
||||
@@ -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' });
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"noEmitOnError": true,
|
||||
"skipLibCheck": true,
|
||||
"incremental": true,
|
||||
"tsBuildInfoFile": "gsd-core/bin/tsconfig.build.tsbuildinfo"
|
||||
"tsBuildInfoFile": "tsconfig.build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*.cts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user