test(pet): release rejected recorders and wait for startup readiness

Unreference the Windows test IPC channel so startup rejection can exit.
Exercise that path on every host and await CLI readiness before shutdown;
an archive link alone can precede replacement and signal-handler setup.
The production recorder and native bundle bytes are unchanged.

Validation: npm --prefix pet run check: 61 passed, 0 failed, 0 canceled.
npm test: 66 package + 12 SDK + 446 web passed, 0 failed.
npm run check:web: passed, 0 errors, 2 existing warnings.
The prior IPC preload hangs a rejected CLI until SIGKILL; the corrected
preload exits with code 1. Windows CI remains the platform proof.

Signed-off-by: CodeWhale Bot <[email protected]>
This commit is contained in:
CodeWhale Bot committed 2026-09-12 14:00:27 -07:00
1 parent 11c6fbfc24
commit c342329f62
3 files changed
+26 -9

No files matched your search

+13
View File
@@ -209,3 +209,16 @@ TUI and native bundle bytes are unchanged from the previously verified build.
Grokbot review and current-head Windows/macOS recorder CI are separate receipts.
Native active-session delivery, current macOS popover inspection and device
transport remain open. No release or deployment is implied.
The first restart commit `11c6fbfc` passed all 61 tests independently in
Codewhalebot on Node 22.19.0 with no remaining concrete source finding. macOS
recorder CI also passed. Windows passed 60 tests and canceled one on timeout:
the test-only IPC message listener kept a correctly rejected startup alive.
The same preload reproduced the hang locally; unreferencing its IPC channel
lets the rejected CLI exit with code 1. The process-death/restart test now uses
that IPC path on every host, while other POSIX cases still use actual SIGINT.
The fixture also waits for CLI readiness before shutdown: the archive link may
appear before replacement and before the signal handler is installed. A broad
local run exposed this early-stop race after the IPC fix. Production recorder
and native bundle bytes are unchanged in this follow-up.
+6 -6
View File
@@ -2,17 +2,17 @@ import { spawn } from 'node:child_process';
/** Windows child.kill('SIGINT') force-terminates the process. This test-only
* preload delivers the handler notification over IPC; POSIX uses a real signal.
* The IPC channel is unreferenced so startup rejection can exit naturally.
* No test control channel is added to the shipped recorder. */
export function spawnRecorder(args, env = process.env) {
const windows = process.platform === 'win32';
const control = `process.once('message', () => { process.disconnect(); process.emit('SIGINT'); });`;
const preload = windows ? ['--import', `data:text/javascript,${encodeURIComponent(control)}`] : [];
export function spawnRecorder(args, env = process.env, ipc = process.platform === 'win32') {
const control = `process.once('message', () => { process.disconnect(); process.emit('SIGINT'); }); process.channel.unref();`;
const preload = ipc ? ['--import', `data:text/javascript,${encodeURIComponent(control)}`] : [];
const child = spawn(process.execPath, [...preload, 'scripts/pet.mjs', ...args], {
cwd: new URL('../../', import.meta.url), env, stdio: windows ? ['ignore', 'pipe', 'pipe', 'ipc'] : ['ignore', 'pipe', 'pipe'],
cwd: new URL('../../', import.meta.url), env, stdio: ipc ? ['ignore', 'pipe', 'pipe', 'ipc'] : ['ignore', 'pipe', 'pipe'],
});
child.stopRecorder = () => {
if (child.exitCode !== null || child.signalCode !== null) return false;
return windows ? child.send('stop') : child.kill('SIGINT');
return ipc ? child.send('stop') : child.kill('SIGINT');
};
return child;
}
+7 -3
View File
@@ -191,7 +191,9 @@ test('the actual CLI resumes after process death at the same path without reclai
await writeFile(input, JSON.stringify({ schemaVersion: 1, id: 'old', traceId: 'fixture', name: 'bash', category: 'code', startTime: 0, endTime: 1, attributes: {} }));
const args = [`--input=${input}`, `--output=${path}`, '--watch'];
const launch = extra => {
const child = spawnRecorder([...args, ...extra]);
// Exercise the Windows IPC harness on every platform; it must not keep
// a correctly rejected startup alive after the CLI sets its exit code.
const child = spawnRecorder([...args, ...extra], process.env, true);
child.log = ''; child.stdout.on('data', b => child.log += b); child.stderr.on('data', b => child.log += b);
child.exited = once(child, 'exit');
t.after(() => { if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL'); });
@@ -209,14 +211,16 @@ test('the actual CLI resumes after process death at the same path without reclai
first.kill('SIGKILL'); await first.exited;
const previous = await readFile(path, 'utf8');
const second = launch(['--resume']);
await waitFor(async () => (await stat(part(path, 1))).size && decodePetJSONL(await readFile(path, 'utf8')).length >= 2);
// The archive link can exist before replacement and handler installation.
// Wait for CLI readiness as well as the new tape before requesting shutdown.
await waitFor(async () => second.log.includes('Recording local live pet states.') && (await stat(part(path, 1))).size && decodePetJSONL(await readFile(path, 'utf8')).length >= 2);
second.stopRecorder(); assert.equal((await second.exited)[0], 0, second.log);
assert.equal(await readFile(part(path, 1), 'utf8'), previous);
const buckets = decodePetJSONL(await readFile(path, 'utf8'));
assert.equal(buckets[0].observed, 0); assert.equal(buckets[0].waiting, false); assert.equal(buckets[0].errors, 0);
assert.ok(buckets[0].onsets.every(n => n === 0));
const third = launch(['--resume']);
await waitFor(async () => (await stat(part(path, 2))).size && decodePetJSONL(await readFile(path, 'utf8')).length >= 2);
await waitFor(async () => third.log.includes('Recording local live pet states.') && (await stat(part(path, 2))).size && decodePetJSONL(await readFile(path, 'utf8')).length >= 2);
third.stopRecorder(); assert.equal((await third.exited)[0], 0, third.log);
assert.equal(decodePetJSONL(await readFile(part(path, 2), 'utf8')).length, buckets.length);
});