test(rex): wait for a timed-out run's orphaned child to be reaped instead of checking once (#1954)

## What this fixes

`rex-transport.test.ts` › "ends a run that does not answer in time, with its process group" failed intermittently on macOS CI runners (for example run 40141827784, attempt 1). The failing assertion was the last one: an instant check that the script's backgrounded `sleep` was gone (`expect(() => process.kill(child, 0)).toThrow()`). The deadline and timing assertions before it passed.

`stopGroup` returns once the run's process group is empty and the direct child is reaped. The orphaned `sleep` is reaped by the system on its own schedule, so on a loaded runner it can still be listed for a moment after the run settles. An empty pid file would also have read as pid 0, and `process.kill(0, 0)` signals the test's own process group.

## The change (test only)

- **The orphan check:** wait up to five seconds for it to go (`expect.poll`), instead of reading the process table at one instant.
- **The pid:** assert the pid read from the file is a real one (an integer above 1).
- **The deadline:** one second instead of 300 ms, so the script's start fits well inside it on a loaded runner.
- **The elapsed bound:** under two seconds, which is the deadline plus `stopGroup`'s one-second grace. It still fails if the deadline stops ending the group at once.

No product code changes.

## Proof (local, macOS)

- **The test alone:** 8 of 8 repeated runs pass, and the whole file passes 24 of 24.
- **Without the immediate group kill at the deadline:** the test fails (2,050 ms against the 2,000 ms bound).
- **Without the deadline:** the test fails (it times out at 20 s).
This commit is contained in:
Mike Schwarz authored and GitHub committed 2026-10-24 18:30:23 -07:00
1 parent c3c514f2a0
commit 54864d331f
1 file changed
+11 -4
+11 -4
View File
@@ -197,12 +197,19 @@ describe("spawnRex", () => {
try {
const failure = vi.fn();
const started = performance.now();
await spawnRex([file], "{}", 300).catch(failure);
expect(performance.now() - started).toBeLessThan(1_000);
// A deadline the script's start fits well inside, even on a loaded runner.
await spawnRex([file], "{}", 1_000).catch(failure);
// Ended at the deadline: not after stopGroup's one-second grace, and not when the sleep ends 30 s later.
expect(performance.now() - started).toBeLessThan(2_000);
expect(failure).toHaveBeenCalledOnce();
expect(failure.mock.calls[0]![0]).toMatchObject({ exitCode: null, message: "did not answer within 300 ms" });
expect(failure.mock.calls[0]![0]).toMatchObject({ exitCode: null, message: "did not answer within 1000 ms" });
// An empty file would read as pid 0, which names this test's own process group.
const child = Number(fs.readFileSync(path.join(path.dirname(file), "child.pid"), "utf8"));
expect(() => process.kill(child, 0)).toThrow();
expect(Number.isInteger(child) && child > 1, `backgrounded pid ${child}`).toBe(true);
// The backgrounded sleep was ended with the run's group. Its parent is gone, so the system reaps it on its own
// schedule: wait a bounded moment for that instead of reading the process table at one instant.
await expect.poll(() => { try { process.kill(child, 0); return true; } catch { return false; } }, { timeout: 5_000, interval: 20 })
.toBe(false);
} finally {
cleanup();
}