Skip to content
Merged
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
7 changes: 3 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,14 @@ leaves behind, and asserts the seam: nothing outside `lib/adapter.js` may reach
package is **prepared, not published** — `ui/package.json` carries `"private": true` deliberately,
and `scripts/pack-npm.sh` only packs a tarball when `CODEGRAPH_PACK_UI=1`.

Tests run as **two vitest projects** (`vitest.workspace.mts`): `engine` (node) and `ui` (jsdom, the
Tests run as **two Vitest projects** (`test.projects` in `vitest.config.mts`): `engine` (node) and `ui` (jsdom, the
Svelte plugin, `resolve.conditions: ['browser']`) for the single `__tests__/ui-package.test.ts`.
`npm test` still runs both. The split is not cosmetic — `browser` is a package-resolution
condition, and applied globally it hands the engine's suites the browser builds of
`web-tree-sitter` and friends. The root config (`vitest.config.mts`, `.mts` because the plugin is
ESM-only and the repo is CJS) is the shared base; note that a workspace project **concatenates**
the base's `include` with its own, which is why the `ui` project does not `extends` it.
ESM-only and the repo is CJS) is the shared base; note that a project with `extends: true` **concatenates** the base's `include` with its own, which is why the `ui` project sets `extends: false`.

Node engines: `>=20.0.0 <25.0.0`. There is a hard exit on Node 25.x and below 20 (see `src/bin/node-version-check.ts`).
Node engines: `>=20.0.0 <25.0.0`. There is a hard exit on Node 25.x and below 20 (see `src/bin/node-version-check.ts`). That range is what users run. Working in this repo needs Node 22.12 or newer: `devEngines` makes `npm ci` and `npm run` stop on anything older, for Vite 7 and Vitest 4.

## Architecture

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- `codegraph serve --mcp --no-telemetry` turns telemetry off for one MCP server entry, so a shared MCP config can opt out without an environment variable; a background daemon that's already running for the project keeps its own setting until it exits. Thanks @danusha2345. (#1908)
- `codegraph implementations <name>` lists the types that implement or extend a trait, interface or class, with each one's file and line, how it's tied to the type, and whether it's test code, a Rust mock in a `#[cfg(test)]` module included; `--depth` follows subclasses of subclasses. Thanks @danusha2345. (#2833)
- Set `CODEGRAPH_SYNTH_MIN_INTERVAL_MS` to limit how often live syncs rebuild the callback and event links across the whole project, which on a large TypeScript or Vue repo otherwise happens on almost every edit; a rebuild that's put off runs when the interval is up even if no edit follows, and until then those links can be that far behind your code. A full `codegraph index` is never held. Thanks @danusha2345. (#2911)
- For contributors, the test suite now runs on Vitest 4 and working in this repository needs Node 22.12 or newer; installing and using CodeGraph doesn't change. Thanks @danusha2345.
- New `codegraph cycles` command lists the groups of files that import each other in a circle, as text or `--json`, and exits 1 when it finds any, so it works as a pre-commit or CI check (#888). Thanks @jiangyq9 and @goutham80808.
- `codegraph init --local-only` keeps `.codegraph/` out of git entirely, so nothing of CodeGraph's shows in `git status`, and it works on a project that's already set up, leaving a `.codegraph/.gitignore` you've edited or committed alone (#910). Thanks @jadchene, @jiangyq9, @Wood-Q and @walterf3.

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Thanks for helping out. Small, focused changes are the easiest to review and lan

## Build from source

You need Git, npm, and Node.js 22.5 or later, below 25. Running from source uses Node's built-in `node:sqlite`; the published installers bundle their own Node, so this only matters here.
You need Git, npm, and Node.js 22.12 or later, below 25. `npm ci` stops on anything older, because the build and test tools need it and running from source uses Node's built-in `node:sqlite`; the published installers bundle their own Node, so this only matters here.

```bash
# Fork the repository on GitHub, then:
Expand Down
39 changes: 28 additions & 11 deletions __tests__/git-index-currency.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,28 @@ vi.mock('child_process', async importOriginal => {
});
vi.mock('fs', async importOriginal => {
const actual = await importOriginal<typeof import('fs')>();
return { ...actual, readFileSync: vi.fn(actual.readFileSync) };
// openSync too: a bounded source reader opens a descriptor instead of
// calling readFileSync, and the injected read failure must reach either.
return { ...actual, readFileSync: vi.fn(actual.readFileSync), openSync: vi.fn(actual.openSync) };
});
// The real entry points, for the injecting implementations to fall through to
// and for afterEach to reinstate. Under Vitest 4 `vi.spyOn` on a mocked
// module's export returns the same `vi.fn`, so reading `cp.execFileSync` inside
// a test and calling it from the injected implementation recurses into itself,
// and `vi.restoreAllMocks()` no longer resets a `vi.fn`'s implementation.
const actualCp = await vi.importActual<typeof import('child_process')>('child_process');
const actualFs = await vi.importActual<typeof import('fs')>('fs');
const injectExecFileSync = (impl: (real: typeof cp.execFileSync, ...args: any[]) => unknown) =>
vi.mocked(cp.execFileSync).mockImplementation(((...args: any[]) => impl(actualCp.execFileSync, ...args)) as typeof cp.execFileSync);
const injectReadFileSync = (impl: (real: typeof fs.readFileSync, ...args: any[]) => unknown) =>
vi.mocked(fs.readFileSync).mockImplementation(((...args: any[]) => impl(actualFs.readFileSync, ...args)) as typeof fs.readFileSync);
const injectOpenSync = (impl: (real: typeof fs.openSync, ...args: any[]) => unknown) =>
vi.mocked(fs.openSync).mockImplementation(((...args: any[]) => impl(actualFs.openSync, ...args)) as typeof fs.openSync);
const reinstateIo = () => {
vi.mocked(cp.execFileSync).mockImplementation(actualCp.execFileSync as typeof cp.execFileSync);
vi.mocked(fs.readFileSync).mockImplementation(actualFs.readFileSync as typeof fs.readFileSync);
vi.mocked(fs.openSync).mockImplementation(actualFs.openSync as typeof fs.openSync);
};

describe('git index currency across commits and restores (#1829)', () => {
let root: string;
Expand All @@ -34,7 +54,7 @@ describe('git index currency across commits and restores (#1829)', () => {
cg = CodeGraph.initSync(root);
expect((await cg.indexAll()).success).toBe(true);
});
afterEach(() => { vi.restoreAllMocks(); cg?.close(); fs.rmSync(root, { recursive: true, force: true }); });
afterEach(() => { reinstateIo(); vi.restoreAllMocks(); cg?.close(); fs.rmSync(root, { recursive: true, force: true }); });

it.each(['index', 'sync', 'scoped'] as const)('sees a restored indexed dirty edit after %s', async mode => {
write('source.ts', 'dirtyVersion');
Expand Down Expand Up @@ -80,12 +100,11 @@ describe('git index currency across commits and restores (#1829)', () => {

it('falls back when git diff fails instead of claiming a clean index', () => {
write('new.ts', 'newSymbol'); commit();
const real = cp.execFileSync;
let injected = 0;
vi.spyOn(cp, 'execFileSync').mockImplementation(((file: string, args: string[], options: any) => {
injectExecFileSync((real, file: string, args: string[], options: any) => {
if (file === 'git' && args[0] === 'diff') { injected++; throw new Error('Injected git diff timeout'); }
return real(file, args, options);
}) as typeof cp.execFileSync);
});
expect(cg.getChangedFiles().added).toEqual(['new.ts']);
expect(injected).toBeGreaterThan(0);
});
Expand Down Expand Up @@ -135,19 +154,17 @@ describe('git index currency across commits and restores (#1829)', () => {
// Sync reads a source file through a bounded reader that opens a
// descriptor (#1910), so the failure is injected at openSync as well as
// readFileSync: it must reach whichever one the read goes through.
const realRead = fs.readFileSync;
const realOpen = fs.openSync;
let injected = 0;
const failNewTs = (real: (...args: any[]) => unknown) => (file: any, ...args: any[]) => {
const failNewTs = (real: (...args: any[]) => unknown, file: any, ...args: any[]) => {
if (String(file) === path.join(root, 'new.ts')) { injected++; throw new Error('Injected transient read error'); }
return real(file, ...args);
};
vi.spyOn(fs, 'readFileSync').mockImplementation(failNewTs(realRead as any) as typeof fs.readFileSync);
vi.spyOn(fs, 'openSync').mockImplementation(failNewTs(realOpen as any) as typeof fs.openSync);
injectReadFileSync(failNewTs);
injectOpenSync(failNewTs);
await cg.sync();
expect(injected).toBeGreaterThan(0);
expect(symbols('newSymbol')).not.toContain('newSymbol');
vi.restoreAllMocks();
reinstateIo();
expect(cg.getChangedFiles().added).toEqual(['new.ts']);
await cg.sync(); clean();
});
Expand Down
4 changes: 2 additions & 2 deletions __tests__/global-setup-home-sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import type { GlobalSetupContext } from 'vitest/node';
import type { TestProject } from 'vitest/node';

declare module 'vitest' {
export interface ProvidedContext {
Expand All @@ -20,7 +20,7 @@ declare module 'vitest' {
}
}

export default function setup({ provide }: GlobalSetupContext): () => void {
export default function setup({ provide }: TestProject): () => void {
const registry = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-test-homes-'));
provide('homeSandboxRegistry', registry);
return () => {
Expand Down
4 changes: 2 additions & 2 deletions __tests__/test-home-sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* home, Claude profile, daemon registry or git identity.
*/
import { describe, it, expect, inject } from 'vitest';
import type { GlobalSetupContext } from 'vitest/node';
import type { TestProject } from 'vitest/node';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
Expand Down Expand Up @@ -77,7 +77,7 @@ describe('test home sandbox', () => {
let registry = '';
const teardown = setupHomeSandboxRun({
provide(_key: string, value: string) { registry = value; },
} as unknown as GlobalSetupContext);
} as unknown as TestProject);
const homes = ['skipped', 'held'].map((label) => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), `codegraph-test-home-${label}-`));
fs.writeFileSync(path.join(home, '.gitconfig'), '');
Expand Down
21 changes: 21 additions & 0 deletions __tests__/vitest-worker-flags.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* The test workers run with the WASM runtime flags.
*
* Without them a pool worker compiles the tree-sitter grammars on V8's
* optimizing tier, and once enough parses have warmed a grammar up the worker
* dies with `Fatal process out of memory: Zone`, which vitest reports only as
* "Worker exited unexpectedly", with the rest of the file unrun (#1779).
*
* The flags reach the workers through a vitest option, and vitest ignores an
* option it no longer reads without a word: `poolOptions.forks.execArgv` went
* that way in the move to Vitest 4. So this reads the worker's own command
* line instead of trusting the config.
*/
import { describe, it, expect } from 'vitest';
import { WASM_RUNTIME_FLAGS } from '../src/extraction/wasm-runtime-flags';

describe('test pool workers', () => {
it('run with the WASM runtime flags', () => {
for (const flag of WASM_RUNTIME_FLAGS) expect(process.execArgv).toContain(flag);
});
});
Loading