Files
buzz/beehive/test/disconnect.test.ts
2026-09-10 03:03:45 -04:00

46 lines
2.9 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, realpathSync, rmSync, existsSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { setTimeout as delay } from 'node:timers/promises';
import { spawnOwned } from '../src/owned.ts';
test('parent IPC loss retains anchor until resistant descendants are forcibly stopped', async t => {
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'beehive-disconnect-')));
const owned = spawnOwned(process.execPath, [resolve('test/orphan-fixture.ts'), 'resist'], dir, { PATH: '/usr/bin:/bin' });
owned.child.stdout.resume(); owned.child.stderr.resume();
let disconnectedAt: number | undefined;
let descendant: number | undefined;
try {
await owned.ready;
for (let i = 0; i < 100 && !existsSync(join(dir, 'resistant.ready')); i++) await delay(10);
assert.ok(existsSync(join(dir, 'resistant.ready')));
descendant = Number(readFileSync(join(dir, 'descendant.pid'), 'utf8'));
assert.ok(Number.isSafeInteger(descendant) && descendant > 0, 'fixture must publish a positive descendant PID');
disconnectedAt = Date.now();
owned.child.disconnect(); // Simulates host loss, not a recovered-PID cleanup operation.
let absent = false;
for (let i = 0; i < 150; i++) {
try { process.kill(-owned.child.pid!, 0); }
catch (error) { if ((error as NodeJS.ErrnoException).code === 'ESRCH') { absent = true; break; } }
await delay(20);
}
assert.ok(absent, 'IPC loss must not destroy the anchor before resistant group teardown');
assert.equal(readFileSync(join(dir, 'term-seen'), 'utf8'), '1');
assert.throws(() => process.kill(descendant!, 0), (e: unknown) => (e as NodeJS.ErrnoException).code === 'ESRCH');
const trace = readFileSync(join(dir, 'resistant.trace'), 'utf8').trim().split('\n').map(line => JSON.parse(line));
assert.deepEqual(trace.map(e => e.event), ['handler-installed', 'term-handler-entered', 'term-written']);
assert.ok(trace.every(e => e.pid === descendant && e.cwd === dir));
t.diagnostic(JSON.stringify({ disconnectedAt, observedAbsentAt: Date.now(), trace }));
} catch (error) {
// Capture before finally removes the fresh fixture. A pass cannot classify a
// prior failure; absence of a callback does not mean its handler was absent.
const tracePath = join(dir, 'resistant.trace');
throw Error(`Disconnect fixture evidence ${JSON.stringify({ directory: dir, anchor: owned.child.pid, descendant, disconnectedAt, observedAt: Date.now(), exitCode: owned.child.exitCode, signalCode: owned.child.signalCode, ready: existsSync(join(dir, 'resistant.ready')), term: existsSync(join(dir, 'term-seen')), trace: existsSync(tracePath) ? readFileSync(tracePath, 'utf8').slice(-8192) : null })}`, { cause: error });
} finally {
if (owned.child.connected) await owned.stop();
rmSync(dir, { recursive: true, force: true });
}
});