Skip to content

Commit 0d9a9bd

Browse files
Kill the shim's process tree so Windows teardown can remove the temp dir
With shell:true the direct child is cmd.exe, so SIGKILLing it orphans the real node process, which keeps a handle on the temp directory and made t.after() fail with EBUSY -- after the handshake had already passed. The assertion was green and the cleanup was red. Kill the whole tree via taskkill /T /F on Windows, and give the rmSync maxRetries for the handles Windows releases lazily. POSIX still SIGKILLs the child directly.
1 parent bb36742 commit 0d9a9bd

1 file changed

Lines changed: 17 additions & 3 deletions

File tree

tests/bin_entrypoint.test.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
*/
1818
import { test } from 'node:test';
1919
import assert from 'node:assert/strict';
20-
import { spawn, execFileSync } from 'node:child_process';
20+
import { spawn, spawnSync, execFileSync } from 'node:child_process';
2121
import fs from 'node:fs';
2222
import os from 'node:os';
2323
import path from 'node:path';
@@ -27,6 +27,18 @@ const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
2727
const pkg = JSON.parse(fs.readFileSync(path.join(REPO, 'package.json'), 'utf8'));
2828

2929
/** Complete a real MCP stdio handshake against `cmd argv`. Resolves a report, never throws. */
30+
// Under `shell: true` the direct child is cmd.exe, so killing it orphans the
31+
// real node process, which keeps a handle on the temp dir and makes the
32+
// teardown rmdir fail with EBUSY. Kill the whole tree on Windows.
33+
function killTree(child) {
34+
if (!child.pid) return;
35+
if (IS_WIN) {
36+
try { spawnSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' }); } catch { /* already gone */ }
37+
return;
38+
}
39+
try { child.kill('SIGKILL'); } catch { /* already gone */ }
40+
}
41+
3042
function probe(cmd, argv, cwd, opts = {}) {
3143
return new Promise((resolve) => {
3244
const child = spawn(cmd, argv, { cwd, stdio: ['pipe', 'pipe', 'pipe'], ...opts });
@@ -46,7 +58,7 @@ function probe(cmd, argv, cwd, opts = {}) {
4658
clearTimeout(timer);
4759
result.nonJson = nonJson.trim();
4860
result.stderr = err.trim().split('\n').slice(0, 4).join('\n');
49-
try { child.kill('SIGKILL'); } catch { /* already gone */ }
61+
killTree(child);
5062
resolve(result);
5163
}
5264
child.on('error', (e) => { result.reason = `PROBE_ERROR spawn ${e.message}`; finish(); });
@@ -105,7 +117,9 @@ const npmOpts = (opts) => (IS_WIN ? { ...opts, shell: true } : opts);
105117

106118
test('the INSTALLED bin shim completes a real MCP handshake', { timeout: 300000 }, async (t) => {
107119
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'tvbin-'));
108-
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
120+
// Windows releases file handles lazily even after the tree is dead, so the
121+
// first rmdir can still hit EBUSY. maxRetries is Node's documented remedy.
122+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }));
109123

110124
// Production path: pack the working tree exactly as `npm publish` would, then install it.
111125
const tarName = execFileSync(NPM, npmArgs(['pack', '--pack-destination', tmp]), npmOpts({ cwd: REPO, encoding: 'utf8' })).trim().split('\n').pop();

0 commit comments

Comments
 (0)