mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 13:53:01 +08:00
* 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>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { ensureTestDist } from './global-setup-dist';
|
||||
|
||||
let root: string;
|
||||
let builds: string[];
|
||||
function write(file: string, content = file): void {
|
||||
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, file), content);
|
||||
}
|
||||
const engineOutputs = ['dist/bin/codegraph.js', 'dist/db/schema.sql',
|
||||
'dist/extraction/wasm/typescript.wasm', 'dist/resolution/resolve-worker.js'];
|
||||
const viewerOutputs = ['dist/viewer/index.html', 'dist/viewer/assets/app.js', 'dist/viewer/assets/app.css'];
|
||||
function build(part: 'engine' | 'viewer'): void {
|
||||
builds.push(part);
|
||||
for (const file of part === 'engine' ? engineOutputs : viewerOutputs) write(file);
|
||||
}
|
||||
function setup(): void { ensureTestDist(root, build); }
|
||||
|
||||
beforeEach(() => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-test-build-'));
|
||||
builds = [];
|
||||
for (const file of ['src/bin/codegraph.ts', 'src/db/schema.sql',
|
||||
'src/extraction/wasm/typescript.wasm', 'tsconfig.json', 'package.json',
|
||||
'package-lock.json', 'scripts/check-ui-build.mjs', 'ui/src/App.svelte',
|
||||
'ui/vite.config.ts', 'ui/svelte.config.js', 'ui/tsconfig.json', 'ui/package.json', 'ui/index.html']) write(file);
|
||||
});
|
||||
afterEach(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
|
||||
describe('test dist prerequisites', () => {
|
||||
it('builds a cold checkout in dependency order and leaves a warm checkout untouched', () => {
|
||||
setup();
|
||||
expect(builds).toEqual(['engine', 'viewer']);
|
||||
builds = [];
|
||||
setup();
|
||||
expect(builds).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([...engineOutputs, ...viewerOutputs])('repairs missing output %s even with current sentinels', file => {
|
||||
setup();
|
||||
builds = [];
|
||||
fs.unlinkSync(path.join(root, file));
|
||||
setup();
|
||||
expect(builds).toEqual([file.startsWith('dist/viewer/') ? 'viewer' : 'engine']);
|
||||
expect(fs.existsSync(path.join(root, file))).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['src/bin/codegraph.ts', ['engine']],
|
||||
['src/db/schema.sql', ['engine']],
|
||||
['src/extraction/wasm/typescript.wasm', ['engine']],
|
||||
['tsconfig.json', ['engine', 'viewer']],
|
||||
['package.json', ['engine', 'viewer']],
|
||||
['package-lock.json', ['engine', 'viewer']],
|
||||
['scripts/check-ui-build.mjs', ['engine', 'viewer']],
|
||||
['ui/src/App.svelte', ['viewer']],
|
||||
['ui/vite.config.ts', ['viewer']],
|
||||
['ui/svelte.config.js', ['viewer']],
|
||||
['ui/tsconfig.json', ['viewer']],
|
||||
['ui/package.json', ['viewer']],
|
||||
['ui/index.html', ['viewer']],
|
||||
])('invalidates changed input %s even with a preserved mtime', (file, expected) => {
|
||||
setup();
|
||||
builds = [];
|
||||
const stat = fs.statSync(path.join(root, file));
|
||||
write(file, 'changed');
|
||||
fs.utimesSync(path.join(root, file), stat.atime, stat.mtime);
|
||||
setup();
|
||||
expect(builds).toEqual(expected);
|
||||
});
|
||||
|
||||
it('invalidates added and removed source files', () => {
|
||||
setup();
|
||||
builds = [];
|
||||
write('src/new-worker.ts');
|
||||
setup();
|
||||
fs.unlinkSync(path.join(root, 'src/new-worker.ts'));
|
||||
setup();
|
||||
expect(builds).toEqual(['engine', 'engine']);
|
||||
});
|
||||
|
||||
it('fails directly when the checkout has no installed compiler', () => {
|
||||
expect(() => ensureTestDist(root)).toThrow(/engine build failed: Cannot find module/);
|
||||
expect(fs.existsSync(path.join(root, 'dist/.test-build-engine.json'))).toBe(false);
|
||||
});
|
||||
|
||||
it('reports build errors directly and retries instead of recording partial success', () => {
|
||||
expect(() => ensureTestDist(root, () => {
|
||||
write('dist/bin/codegraph.js');
|
||||
throw new Error('compiler diagnostic');
|
||||
})).toThrow('[test setup] engine build failed: compiler diagnostic');
|
||||
expect(fs.existsSync(path.join(root, 'dist/.test-build-engine.json'))).toBe(false);
|
||||
setup();
|
||||
expect(builds).toEqual(['engine', 'viewer']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
@@ -11,6 +11,8 @@ export default defineConfig({
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['__tests__/**/*.test.ts'],
|
||||
// Suites that spawn the built CLI need a current dist/ (#1879).
|
||||
globalSetup: ['./__tests__/global-setup-dist.ts'],
|
||||
/**
|
||||
* Several MCP integration tests (mcp-daemon, mcp-initialize, mcp-ppid-watchdog,
|
||||
* mcp-roots) spawn `dist/bin/codegraph.js serve --mcp` with `process.execPath`
|
||||
|
||||
Reference in New Issue
Block a user