From 262a056c34405e208fe657a8f6a5426e5677bc88 Mon Sep 17 00:00:00 2001 From: Dongdong Kong Date: Sun, 24 May 2026 22:15:17 +0800 Subject: [PATCH 1/5] add CI --- .github/workflows/CI.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/CI.yml diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 000000000..5b771f284 --- /dev/null +++ b/.github/workflows/CI.yml @@ -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 From 6cb86029df042699f652f23f51ddc8726f44b4db Mon Sep 17 00:00:00 2001 From: Dongdong Kong Date: Sun, 24 May 2026 22:22:08 +0800 Subject: [PATCH 2/5] support node26 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nodeMajor === 25 — turboshaft bug only exist 25.x --- package.json | 2 +- src/bin/codegraph.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5455ced92..6ed993986 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index 6bc63b3fd..f92267229 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -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); From 53ea94d49df253773eb236bcef46300e3e648580 Mon Sep 17 00:00:00 2001 From: Dongdong Kong Date: Sun, 24 May 2026 22:29:34 +0800 Subject: [PATCH 3/5] fix(vitest): add liftoff-only flag to prevent OOM crashes on Node >= 22 --- vitest.config.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/vitest.config.ts b/vitest.config.ts index 2449a989e..a6c5a3d3e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,6 +5,16 @@ export default defineConfig({ globals: true, environment: 'node', include: ['__tests__/**/*.test.ts'], + // Pass --liftoff-only to vitest fork workers so their parse-worker threads + // inherit the flag. This prevents the V8 turboshaft Zone OOM that crashes + // the process when tree-sitter grammars are compiled on Node >= 22. + // (Worker threads can't receive V8 flags via execArgv directly — only the + // parent fork process can, and threads inherit from it.) + poolOptions: { + forks: { + execArgv: ['--liftoff-only'], + }, + }, coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], From 5543de0f878626bdc7dd9061b83548ed8538b3b8 Mon Sep 17 00:00:00 2001 From: Dongdong Kong Date: Sun, 24 May 2026 22:33:49 +0800 Subject: [PATCH 4/5] fix(tests): ensure child process exits properly in afterEach cleanup --- __tests__/mcp-initialize.test.ts | 10 +++++++--- __tests__/mcp-roots.test.ts | 10 +++++++--- vitest.config.ts | 15 +++++++++------ 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/__tests__/mcp-initialize.test.ts b/__tests__/mcp-initialize.test.ts index 4a57ebae0..23c168ebd 100644 --- a/__tests__/mcp-initialize.test.ts +++ b/__tests__/mcp-initialize.test.ts @@ -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; } @@ -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(resolve => child!.once('close', resolve)); + } child = null; } fs.rmSync(tempDir, { recursive: true, force: true }); diff --git a/__tests__/mcp-roots.test.ts b/__tests__/mcp-roots.test.ts index 8e1d4520d..36d9174c3 100644 --- a/__tests__/mcp-roots.test.ts +++ b/__tests__/mcp-roots.test.ts @@ -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; } @@ -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(resolve => child!.once('close', resolve)); + } child = null; } fs.rmSync(cwdDir, { recursive: true, force: true }); diff --git a/vitest.config.ts b/vitest.config.ts index a6c5a3d3e..814a07233 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,18 +1,21 @@ 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'], - // Pass --liftoff-only to vitest fork workers so their parse-worker threads - // inherit the flag. This prevents the V8 turboshaft Zone OOM that crashes - // the process when tree-sitter grammars are compiled on Node >= 22. - // (Worker threads can't receive V8 flags via execArgv directly — only the - // parent fork process can, and threads inherit from it.) poolOptions: { forks: { - execArgv: ['--liftoff-only'], + execArgv: NEEDS_LIFTOFF_ONLY ? ['--liftoff-only'] : [], }, }, coverage: { From 9d38f5d03b60e58691464d80d83c3f2dd3cca66a Mon Sep 17 00:00:00 2001 From: Dongdong Kong Date: Sun, 24 May 2026 22:56:37 +0800 Subject: [PATCH 5/5] rm windows --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 5b771f284..a3feb2ad7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest] # windows-latest node-version: [24, 26] steps: