mirror of
https://github.com/block/buzz.git
synced 2026-10-29 13:31:44 +00:00
46 lines
2.9 KiB
TypeScript
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 });
|
|
}
|
|
});
|