test/pty-kill-tree.test.js
Rama main · réplica de lectura del repositorio original.
// Closing a session takes what its agent left running in the background
// (server/pty.js, killSessionProcesses): a plain `&` job in the shell's group,
// a detached child in a session of its own (how Claude Code runs Bash), and
// one that ignores SIGTERM. Real PTYs, so not on Windows.
import { describe, it, expect, vi } from 'vitest';
import { spawn } from 'node-pty';
import { killSessionProcesses } from '../server/pty.js';
const alive = (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } };
async function gone(pids, ms = 5000) {
const until = Date.now() + ms;
while (Date.now() < until && pids.some(alive)) await new Promise((r) => setTimeout(r, 100));
return pids.filter(alive);
}
// A shell that prints `PIDS a b c` once its children are up.
function startSession(script) {
const pty = spawn('/bin/sh', ['-c', script], { cols: 80, rows: 24, env: process.env });
const exited = new Promise((r) => pty.onExit(r));
const pids = new Promise((resolve) => {
let out = '';
pty.onData((d) => {
out += d;
const m = out.match(/PIDS([\d ]+)\r?\n/);
if (m) resolve(m[1].trim().split(/\s+/).map(Number));
});
});
return { session: { name: 'Test', pty }, exited, pids };
}
describe.skipIf(process.platform === 'win32')('killSessionProcesses', () => {
it('kills background, detached and TERM-ignoring grandchildren', async () => {
const detached = `node -e "const c=require('child_process').spawn(process.execPath,['-e','setInterval(()=>{},1e3)'],{detached:true,stdio:'ignore'});console.log(c.pid);setInterval(()=>{},1e3)"`;
const { session, exited, pids } = startSession(
`sleep 60 & a=$!; (trap '' TERM; exec sleep 61) & b=$!; ${detached} > /tmp/.pty-kill-$$ & sleep 1; echo PIDS $a $b $(cat /tmp/.pty-kill-$$); rm -f /tmp/.pty-kill-$$; wait`,
);
const kids = await pids;
expect(kids).toHaveLength(3);
expect(kids.every(alive)).toBe(true);
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
killSessionProcesses(session, { graceMs: 300 });
await exited;
expect(await gone(kids)).toEqual([]);
expect(log.mock.calls.flat().join('\n')).toMatch(/Test: stopping [3-9] process\(es\) it started/);
log.mockRestore();
}, 15000);
it('closes a plain session as before', async () => {
const { session, exited, pids } = startSession('echo PIDS $$; exec sleep 60');
const [pid] = await pids;
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
killSessionProcesses(session);
await exited;
expect(alive(pid)).toBe(false);
expect(log).toHaveBeenCalledWith('Test: no processes of its own left to stop');
log.mockRestore();
}, 10000);
});