Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions scripts/lib/cli-invocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Resolve `node` argv for spawning the codegraph CLI as a subprocess.
*
* Benchmark scripts (`token-benchmark.ts`) need to run the actual
* `codegraph` CLI (`build`, `mcp`) against a target directory, not just
* import its programmatic API. This repo ships TypeScript source with no
* compiled `dist/` required for local development, so `node <root>/src/cli.js`
* fails outright — only `cli.ts` exists (#1907). Even after fixing the
* extension, a spawned child process doesn't inherit the parent's
* `--experimental-strip-types` / `--import <loader>` flags the way
* statically- or dynamically-imported modules in the parent process do, so
* those flags must be passed explicitly. Mirrors the pattern already used by
* `tests/integration/cli.test.ts`'s `NODE_TS_FLAGS`.
*/
import path from 'node:path';

/**
* @param root Absolute path to the codegraph repo root (the directory
* containing `src/`, not `src/` itself).
* @returns argv (excluding `node` itself) to spawn `src/cli.ts` with the
* flags required to run TypeScript source directly.
*/
export function resolveCliNodeArgs(root: string): string[] {
const loaderUrl = new URL('../ts-resolve-loader.ts', import.meta.url).href;
return ['--experimental-strip-types', '--import', loaderUrl, path.join(root, 'src', 'cli.ts')];
}
30 changes: 15 additions & 15 deletions scripts/token-benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,23 @@ import { parseArgs } from 'node:util';

import { ISSUES, extractAgentOutput, validateResult } from './token-benchmark-issues.js';
import { getBenchmarkVersion } from './bench-version.js';
import { srcImport } from './lib/bench-config.js';
import { median, round1, timeMedian } from './lib/bench-timing.js';
import { resolveCliNodeArgs } from './lib/cli-invocation.js';
import { extractUsageMetrics, tallyToolCalls } from './lib/session-metrics.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, '..');
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
const benchVersion = getBenchmarkVersion(pkg.version, root);

// Codegraph's own module tree lives nested under src/ (mirrors dist/'s
// layout), not flat — see scripts/lib/bench-config.ts's srcImport() for the
// same .ts-preferring resolution used by the other benchmark scripts.
const SRC_DIR = path.join(root, 'src');

const CLI_NODE_ARGS = resolveCliNodeArgs(root);

// Redirect console.log to stderr so only JSON goes to stdout
const origLog = console.log;
console.log = (...args) => console.error(...args);
Expand Down Expand Up @@ -153,10 +162,9 @@ function checkoutCommit(nextjsDir, sha) {
// ── Graph building ────────────────────────────────────────────────────────

async function buildCodegraph(nextjsDir) {
const cliPath = path.join(root, 'src', 'cli.js');
console.error('Building codegraph graph for Next.js...');
const start = performance.now();
execFileSync('node', [cliPath, 'build', nextjsDir], {
execFileSync('node', [...CLI_NODE_ARGS, 'build', nextjsDir], {
cwd: nextjsDir,
stdio: 'pipe',
timeout: 600_000, // 10 min
Expand Down Expand Up @@ -189,12 +197,11 @@ function buildSessionOptions(mode, nextjsDir) {

if (mode === 'codegraph') {
const dbPath = path.join(nextjsDir, '.codegraph', 'graph.db');
const cliPath = path.join(root, 'src', 'cli.js');
options.mcpServers = {
codegraph: {
type: 'stdio',
command: 'node',
args: [cliPath, 'mcp', '-d', dbPath],
args: [...CLI_NODE_ARGS, 'mcp', '-d', dbPath],
},
};
}
Expand Down Expand Up @@ -310,19 +317,12 @@ async function runQueryBenchmarks(hubName, dbPath, fnDepsData, fnImpactData) {
* Reuses the same codegraph APIs as the existing benchmark scripts.
*/
async function runPerfBenchmarks(nextjsDir) {
const { pathToFileURL } = await import('node:url');
const { buildGraph } = await import(
pathToFileURL(path.join(root, 'src', 'builder.js')).href
);
const { buildGraph } = await import(srcImport(SRC_DIR, 'domain/graph/builder.js'));
const { fnDepsData, fnImpactData, statsData } = await import(
pathToFileURL(path.join(root, 'src', 'queries.js')).href
);
const { isNativeAvailable } = await import(
pathToFileURL(path.join(root, 'src', 'native.js')).href
);
const { isWasmAvailable } = await import(
pathToFileURL(path.join(root, 'src', 'parser.js')).href
srcImport(SRC_DIR, 'domain/queries.js')
);
const { isNativeAvailable } = await import(srcImport(SRC_DIR, 'infrastructure/native.js'));
const { isWasmAvailable } = await import(srcImport(SRC_DIR, 'domain/parser.js'));

const dbPath = path.join(nextjsDir, '.codegraph', 'graph.db');

Expand Down
44 changes: 44 additions & 0 deletions tests/unit/bench-config-src-import.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Unit tests for scripts/lib/bench-config.ts's srcImport().
*
* Regression coverage for #1907: scripts/token-benchmark.ts's `--perf`
* dynamic imports used flat paths (e.g. `src/builder.js`, `src/queries.js`,
* `src/native.js`, `src/parser.js`) that never existed — the real modules
* live nested (`src/domain/graph/builder.ts`, `src/domain/queries.ts`,
* `src/infrastructure/native.ts`, `src/domain/parser.ts`), mirroring dist/'s
* layout. This locks in that the exact nested subpaths token-benchmark.ts
* now imports via srcImport() resolve to real, importable modules exporting
* the symbols runPerfBenchmarks() needs.
*/

import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { srcImport } from '../../scripts/lib/bench-config.js';

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
const srcDir = path.join(repoRoot, 'src');

describe('srcImport() against the module paths used by token-benchmark.ts --perf', () => {
it('resolves domain/graph/builder.js to a module exporting buildGraph', async () => {
const mod = await import(srcImport(srcDir, 'domain/graph/builder.js'));
expect(typeof mod.buildGraph).toBe('function');
});

it('resolves domain/queries.js to a module exporting fnDepsData, fnImpactData, statsData', async () => {
const mod = await import(srcImport(srcDir, 'domain/queries.js'));
expect(typeof mod.fnDepsData).toBe('function');
expect(typeof mod.fnImpactData).toBe('function');
expect(typeof mod.statsData).toBe('function');
});

it('resolves infrastructure/native.js to a module exporting isNativeAvailable', async () => {
const mod = await import(srcImport(srcDir, 'infrastructure/native.js'));
expect(typeof mod.isNativeAvailable).toBe('function');
});

it('resolves domain/parser.js to a module exporting isWasmAvailable', async () => {
const mod = await import(srcImport(srcDir, 'domain/parser.js'));
expect(typeof mod.isWasmAvailable).toBe('function');
});
});
46 changes: 46 additions & 0 deletions tests/unit/cli-invocation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Unit tests for scripts/lib/cli-invocation.ts
*
* Regression coverage for #1907: scripts/token-benchmark.ts spawned the
* codegraph CLI via `path.join(root, 'src', 'cli.js')` — a file that never
* existed (src/ has no compiled .js output; only src/cli.ts) — and, even
* after fixing the extension, a spawned child `node` process doesn't
* inherit the --experimental-strip-types / --import <loader> flags the
* parent script itself needs to run TypeScript source. resolveCliNodeArgs()
* centralizes the correct invocation so it can be exercised directly.
*/

import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { resolveCliNodeArgs } from '../../scripts/lib/cli-invocation.js';

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');

describe('resolveCliNodeArgs', () => {
it('resolves the strip-types flag, the loader, and an existing cli.ts entry point', () => {
const args = resolveCliNodeArgs(repoRoot);

expect(args[0]).toBe('--experimental-strip-types');
expect(args[1]).toBe('--import');
expect(args[2]).toMatch(/^file:.*ts-resolve-loader\.ts$/);
expect(args[3]).toBe(path.join(repoRoot, 'src', 'cli.ts'));

// Both the loader and the CLI entry point must exist on disk — this is
// exactly the class of bug #1907 fixed (paths pointing at files that
// were never there).
expect(fs.existsSync(new URL(args[2]))).toBe(true);
expect(fs.existsSync(args[3])).toBe(true);
});

it('spawns the real CLI successfully via the resolved argv (mirrors tests/integration/cli.test.ts)', () => {
const args = resolveCliNodeArgs(repoRoot);
const out = execFileSync('node', [...args, '--version'], {
encoding: 'utf-8',
timeout: 30_000,
});
expect(out.trim()).toMatch(/^\d+\.\d+\.\d+/);
});
});
Loading