Ctrl K
ComunidadGuías…

test/pty-kill-tree.test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// 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);
});