Files
codegraph/__tests__/global-setup-dist.ts
T
Colby Mchenry 0f44fdf8af fix(tests): build missing or stale test prerequisites (#1879) (#2021)
* test: build dist/ before the suite when it is missing or stale (#1879)

About thirty suites spawn dist/bin/codegraph.js, the parallel resolver loads
its worker from dist/, and the `codegraph ui` suites serve dist/viewer/. On a
fresh checkout `npm ci && npm test` failed them on spawn timeouts and a
missing-viewer error that never mentioned the build; after an edit without a
rebuild they quietly exercised the old code.

A Vitest globalSetup now rebuilds each half only when it is missing or older
than its sources: `tsc` + copy-assets when anything under src/ is newer than
dist/bin/codegraph.js, `build:ui` when ui/src/ is newer than
dist/viewer/index.html. A current dist/ costs one mtime walk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(tests): build missing or stale test prerequisites (#1879)

Dist-dependent tests launched missing or stale compiled outputs without a build prerequisite.
Retain the contributor's shared setup and track build inputs and individual outputs for reliable invalidation.
Resolve TypeScript locally, preserve asset copying and engine/UI separation, and report build failures directly.
Add regression coverage for cold builds, warm runs, configuration changes, and missing assets.

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:12:55 +00:00

92 lines
3.9 KiB
TypeScript

/**
* Build what the suite reads from `dist/` before tests spawn the CLI or load
* workers and the viewer (#1879). Keep the engine first: the viewer build
* checks the engine's copied grammars.
*/
import { execFileSync, execSync } from 'child_process';
import { createHash } from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
const ROOT = path.resolve(__dirname, '..');
type BuildPart = 'engine' | 'viewer';
type BuildRunner = (part: BuildPart, root: string) => void;
function filesUnder(root: string, relative: string): string[] {
const full = path.join(root, relative);
if (!fs.existsSync(full)) return [];
if (!fs.statSync(full).isDirectory()) return [relative];
return fs.readdirSync(full).sort()
.filter(name => !['node_modules', 'dist', '.git'].includes(name))
.flatMap(name => filesUnder(root, path.join(relative, name)));
}
function inputFingerprint(root: string, part: BuildPart): string {
const inputs = [
'package.json', 'package-lock.json', 'scripts', '__tests__/global-setup-dist.ts',
...fs.readdirSync(root).filter(name => /^tsconfig.*\.json$/.test(name)),
part === 'engine' ? 'src' : 'ui',
];
const hash = createHash('sha256');
for (const file of inputs.flatMap(input => filesUnder(root, input)).sort()) {
hash.update(file).update('\0').update(fs.readFileSync(path.join(root, file))).update('\0');
}
return hash.digest('hex');
}
function outputSnapshot(root: string, part: BuildPart): Record<string, string> {
const directory = part === 'engine' ? 'dist' : 'dist/viewer';
return Object.fromEntries(filesUnder(root, directory)
.filter(file => !path.basename(file).startsWith('.test-build-'))
.filter(file => part !== 'engine' || !file.startsWith(`dist${path.sep}viewer${path.sep}`))
.map(file => {
const stat = fs.statSync(path.join(root, file));
return [file, `${stat.size}:${stat.mtimeMs}`];
}));
}
function build(part: BuildPart, root: string): void {
if (part === 'engine') {
// Resolve the installed compiler directly: npx may download an unrelated
// package named tsc when dependencies are missing.
const compiler = require.resolve(path.join(root, 'node_modules/typescript/bin/tsc'));
execFileSync(process.execPath, [compiler], { cwd: root, stdio: 'inherit' });
execSync('npm run copy-assets', { cwd: root, stdio: 'inherit' });
} else {
execSync('npm run build:ui', { cwd: root, stdio: 'inherit' });
}
}
/** Exported for isolated filesystem fixtures; Vitest calls the default wrapper. */
export function ensureTestDist(root: string, runBuild: BuildRunner = build): void {
for (const part of ['engine', 'viewer'] as const) {
const stamp = path.join(root, 'dist', `.test-build-${part}.json`);
const inputs = inputFingerprint(root, part);
const outputs = outputSnapshot(root, part);
let current = false;
try {
const previous = JSON.parse(fs.readFileSync(stamp, 'utf8'));
current = previous.inputs === inputs && Object.keys(outputs).length > 0
&& JSON.stringify(previous.outputs) === JSON.stringify(outputs);
} catch { /* No valid successful-build record: rebuild. */ }
if (current) continue;
process.stderr.write(`[test setup] ${part} build is missing or stale; rebuilding\n`);
// Never leave a successful stamp behind after a partially failed rebuild.
fs.rmSync(stamp, { force: true });
try {
runBuild(part, root);
const built = outputSnapshot(root, part);
const sentinel = part === 'engine' ? 'dist/bin/codegraph.js' : 'dist/viewer/index.html';
if (!fs.existsSync(path.join(root, sentinel))) throw new Error(`Build did not produce ${sentinel}`);
fs.writeFileSync(stamp, JSON.stringify({ inputs, outputs: built }));
} catch (error) {
throw new Error(`[test setup] ${part} build failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
}
}
}
export default function setup(): void {
ensureTestDist(ROOT);
}