Files
Michał Pierzchała 0fb38f1da2 test: prune abandoned test-run tmp directories at run setup (#1834)
* test: prune abandoned test-run tmp directories at run setup

A run killed before its teardown (tool-timeout SIGKILL, OOM, cancelled job)
left /tmp/agent-device-test-run-<pid>-* behind, and check:tmpdir-leaks — which
runs after test:unit in check:unit — flagged every dead-pid directory it
found. It could not tell this run's leak from a historical one, so one killed
run made every later, otherwise-green gate on the host fail.

Both TMPDIR redirection entry points (the Vitest global setup and the
node --test wrapper) now prune dead-pid run directories before creating their
own, printing one [tmpdir] line when they did; the post-run check keeps its
semantics and can now only ever name the run that just finished. Live owners
(a concurrent run in another worktree) are never touched.

The root/prefix constants move into check-tmpdir-leaks-model.ts, next to the
liveness classification, so the setup can import the prune without a cycle.

* test(tmpdir): a run directory is live while any process still holds it as TMPDIR, not only while its owner runs

Review (P1): owner-pid liveness alone would prune a directory out from under
the orphaned children of a SIGKILLed run — the node --test chain, Vitest forks,
or a daemon a test spawned all keep running with that TMPDIR. The liveness
model now reads every process's TMPDIR (ps -E on macOS, /proc/<pid>/environ
on Linux) and treats a run directory as live while its owner pid is alive OR
any process's TMPDIR points into it; both the prune and the post-run leak
check use it. Regression: a wrapped probe spawns a detached long-lived child,
only the wrapper is SIGKILLed, the next prune preserves the directory; after
every consumer exits, the next prune removes it. Planted red with owner-only
liveness: the orphaned directory is pruned.
2026-08-18 17:48:12 +02:00

33 lines
1.8 KiB
TypeScript

// Fails if any agent-device-test-run-* directory under TEST_RUN_TMP_ROOT is
// abandoned — its owning process has exited without running its cleanup
// (crash, OOM, timeout kill) AND no live process still holds it as TMPDIR.
// Directories owned by a still-running process are left alone: a concurrent
// `vitest run` (or node --test lane, wrapped by scripts/node-test-tmpdir.ts) in
// another worktree keeps its own directory present until its own teardown,
// which is not a leak; so are the orphaned children of a killed run, until the
// last of them exits. See check-tmpdir-leaks-model.ts for the liveness model.
//
// Covers both redirection mechanisms sharing this root/prefix: Vitest's
// globalSetup/globalTeardown (scripts/vitest-tmpdir-global-setup.ts) and the
// node --test wrapper (scripts/node-test-tmpdir.ts, #1595) that every
// `node --test` package.json script now runs through. Both prune what an
// earlier killed run left behind before creating their own directory, so a
// failure here names the run that just finished — never a historical one.
import path from 'node:path';
import { TEST_RUN_TMP_ROOT, findLeakedRunDirectories } from './check-tmpdir-leaks-model.ts';
const leaks = findLeakedRunDirectories(TEST_RUN_TMP_ROOT);
if (leaks.length > 0) {
const details = leaks.map((name) => `- ${path.join(TEST_RUN_TMP_ROOT, name)}`).join('\n');
throw new Error(
`Found ${leaks.length} abandoned agent-device-test-run-* director${leaks.length === 1 ? 'y' : 'ies'} in ${TEST_RUN_TMP_ROOT}:\n${details}\n` +
'Their owning process has already exited, so their globalTeardown never ran (crash, OOM, timeout kill); investigate the run that produced them.',
);
}
process.stdout.write(
`No abandoned agent-device-test-run-* directories found in ${TEST_RUN_TMP_ROOT}.\n`,
);