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
33 changes: 33 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
name: Test (Node ${{ matrix.node-version }}, ${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest] # windows-latest
node-version: [24, 26]

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm

- run: npm ci

- name: Build
run: npm run build

- name: Run tests
run: npm test
10 changes: 7 additions & 3 deletions __tests__/mcp-initialize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ function spawnServer(cwd: string): ChildProcessWithoutNullStreams {
return spawn(process.execPath, [BIN, 'serve', '--mcp'], {
cwd,
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, CODEGRAPH_NO_RELAUNCH: '1' },
}) as ChildProcessWithoutNullStreams;
}

Expand Down Expand Up @@ -99,9 +100,12 @@ describe('MCP initialize handshake (issue #172)', () => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-mcp-init-'));
});

afterEach(() => {
if (child && !child.killed) {
child.kill('SIGKILL');
afterEach(async () => {
if (child) {
if (!child.killed) child.kill('SIGKILL');
if (child.exitCode === null) {
await new Promise<void>(resolve => child!.once('close', resolve));
}
Comment on lines +103 to +108
child = null;
}
fs.rmSync(tempDir, { recursive: true, force: true });
Expand Down
10 changes: 7 additions & 3 deletions __tests__/mcp-roots.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ function spawnServer(cwd: string): ChildProcessWithoutNullStreams {
return spawn(process.execPath, [BIN, 'serve', '--mcp', '--no-watch'], {
cwd,
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, CODEGRAPH_NO_RELAUNCH: '1' },
}) as ChildProcessWithoutNullStreams;
}

Expand Down Expand Up @@ -84,9 +85,12 @@ describe('MCP project resolution via roots/list (issue #196)', () => {
projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-mcp-proj-'));
});

afterEach(() => {
if (child && !child.killed) {
child.kill('SIGKILL');
afterEach(async () => {
if (child) {
if (!child.killed) child.kill('SIGKILL');
if (child.exitCode === null) {
await new Promise<void>(resolve => child!.once('close', resolve));
}
Comment on lines +88 to +93
child = null;
}
fs.rmSync(cwdDir, { recursive: true, force: true });
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,6 @@
"vitest": "^2.1.9"
},
"engines": {
"node": ">=20.0.0 <25.0.0"
"node": ">=20.0.0 <25.0.0 || >=26.0.0"
}
}
2 changes: 1 addition & 1 deletion src/bin/codegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const importESM = new Function('specifier', 'return import(specifier)') as
// who patched V8 themselves or want to test a future fix.
const nodeVersion = process.versions.node;
const nodeMajor = parseInt(nodeVersion.split('.')[0] ?? '0', 10);
if (nodeMajor >= 25) {
if (nodeMajor === 25) {
process.stderr.write(buildNode25BlockBanner(nodeVersion) + '\n');
if (!process.env.CODEGRAPH_ALLOW_UNSAFE_NODE) {
process.exit(1);
Expand Down
13 changes: 13 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import { defineConfig } from 'vitest/config';

const NODE_MAJOR = Number.parseInt(process.versions.node.split('.')[0] ?? '0', 10);

// The V8 turboshaft WASM Zone OOM bug that crashes tree-sitter grammar
// compilation exists in Node 22–25.x. Node 26+ fixes it; forcing
// --liftoff-only on Node 26 Windows is empirically tied to a fork-worker
// teardown crash in tinypool, so only apply the flag where it's needed.
const NEEDS_LIFTOFF_ONLY = NODE_MAJOR >= 22 && NODE_MAJOR <= 25;

export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['__tests__/**/*.test.ts'],
poolOptions: {
forks: {
execArgv: NEEDS_LIFTOFF_ONLY ? ['--liftoff-only'] : [],
},
},
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
Expand Down