From 2ce0c1329ae249857895c5e51917f542c7b09c65 Mon Sep 17 00:00:00 2001 From: oratis Date: Thu, 28 May 2026 00:48:51 +0800 Subject: [PATCH] fix(ci): Node 22 + EPIPE in hooks dispatcher + bash cwd test regex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI on Ubuntu was failing on commits b70c0e1 and febfa30 with three real (non-flaky) issues: 1. GlobTool tests failed because fs.promises.glob was added in Node 22 but CI used Node 20. Bumped engines.node to >=22 across all packages + .nvmrc + workflow setup-node version. Node 22 has been LTS since Oct 2024, so this is reasonable. 2. HookDispatcher tests intermittently failed with EPIPE because child processes that don't read stdin close the pipe before our write completes. Added stdin error listener + try/catch around write/end to swallow EPIPE/EBADF cleanly. 3. BashTool 'runs in the given cwd' test had a regex bug: escaping step ran AFTER inserting `(/private)?` so the parens/question-mark got literally escaped instead of staying as regex syntax. Replaced with a simpler suffix-substring check that works on both platforms. Verified locally (Node 24 → behaviour equivalent to Node 22): pnpm test → 258 passed / 4 skipped / 0 failed pnpm typecheck → green pnpm format:check → conformant Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 4 +++- .nvmrc | 2 +- apps/cli/package.json | 2 +- apps/desktop/package.json | 2 +- package.json | 2 +- packages/core/src/hooks/dispatcher.ts | 15 ++++++++++++++- packages/core/src/tools/bash.test.ts | 7 +++---- 7 files changed, 24 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84c5b3e..09a9498 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,9 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + # Need Node 22+ for fs.promises.glob (used by GlobTool). + # @deepcode/core's `package.json` engines field requires >=22 too. + node-version: '22' cache: 'pnpm' - name: Install dependencies diff --git a/.nvmrc b/.nvmrc index 209e3ef..2bd5a0a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 +22 diff --git a/apps/cli/package.json b/apps/cli/package.json index 1a61894..96d1056 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -30,6 +30,6 @@ "vitest": "^2.1.0" }, "engines": { - "node": ">=20" + "node": ">=22" } } diff --git a/apps/desktop/package.json b/apps/desktop/package.json index a912a6f..5bebc4b 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -21,6 +21,6 @@ "typescript": "^5.7.0" }, "engines": { - "node": ">=20" + "node": ">=22" } } diff --git a/package.json b/package.json index 09b053a..43ce69d 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "description": "DeepCode — Claude Code 的 DeepSeek 版(monorepo root)", "license": "MIT", "engines": { - "node": ">=20", + "node": ">=22", "pnpm": ">=9" }, "packageManager": "pnpm@9.12.0", diff --git a/packages/core/src/hooks/dispatcher.ts b/packages/core/src/hooks/dispatcher.ts index a11b6f2..3b290c0 100644 --- a/packages/core/src/hooks/dispatcher.ts +++ b/packages/core/src/hooks/dispatcher.ts @@ -148,9 +148,22 @@ export function runCommand( resolveResult({ stdout, stderr, exitCode }); }); + // Suppress EPIPE / EBADF when the child closes stdin before our write + // completes (handlers that don't read stdin are common and harmless). + child.stdin.on('error', () => { + // swallow + }); if (opts.stdin) { - child.stdin.write(opts.stdin); + try { + child.stdin.write(opts.stdin); + } catch { + // pipe already closed — fine + } + } + try { child.stdin.end(); + } catch { + // already ended — fine } }); } diff --git a/packages/core/src/tools/bash.test.ts b/packages/core/src/tools/bash.test.ts index da247f9..fed3183 100644 --- a/packages/core/src/tools/bash.test.ts +++ b/packages/core/src/tools/bash.test.ts @@ -42,9 +42,8 @@ describe('BashTool', () => { it('runs in the given cwd', async () => { const r = await BashTool.execute({ command: 'pwd' }, { cwd: tmp }); - // macOS resolves /private/var symlinks for /tmp paths; tolerate that - expect(r.content).toMatch( - new RegExp(tmp.replace(/^\/tmp/, '(/private)?/tmp').replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')), - ); + // macOS resolves /tmp → /private/tmp via symlink. Check just the suffix to be portable. + const suffix = tmp.replace(/^\/tmp\//, '').replace(/^\/private\/tmp\//, ''); + expect(r.content).toContain(suffix); }); });