Files
callstack__agent-device/scripts/vitest-tmpdir-global-setup.ts
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

67 lines
2.9 KiB
TypeScript

import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
TEST_RUN_TMP_PREFIX,
TEST_RUN_TMP_ROOT,
pruneAbandonedRunDirectories,
reportPrunedRunDirectories,
} from './check-tmpdir-leaks-model.ts';
// os.tmpdir() reads TMPDIR on every call, so redirecting it here covers every
// mkdtemp call site — test and production — without touching any of them.
// globalSetup/globalTeardown run once per `vitest run` invocation, in the
// same process that spawns every worker, so this env mutation is inherited
// by all of them (confirmed: forked workers see it via normal env
// inheritance) and the removal below only ever runs once, after every worker
// across every project has finished. A per-file afterAll hook was tried
// first; it proved unreliable (some workers were torn down before running
// it), which is why this is a single run-level hook instead.
//
// The root/prefix live in check-tmpdir-leaks-model.ts, next to the liveness
// classification this setup and the post-run leak check both rely on.
let testRunTmpDir: string;
let previousTmpDir: string | undefined;
let previousSwiftCacheDir: string | undefined;
// setup/teardown are vitest's globalSetup contract: it imports this file by
// the path string in vitest.config.ts and calls these by name, so nothing in
// the source graph references them directly.
// fallow-ignore-next-line unused-export
export function setup(): void {
previousTmpDir = process.env.TMPDIR;
previousSwiftCacheDir = process.env.AGENT_DEVICE_SWIFT_CACHE_DIR;
const originalTmpDir = os.tmpdir();
if (!previousSwiftCacheDir?.trim()) {
// The Swift compiler cache is intentionally durable across test runs. If it
// followed the disposable TMPDIR, AVFoundation helpers would recompile on
// every invocation and push integration scenarios past their 5s budgets.
process.env.AGENT_DEVICE_SWIFT_CACHE_DIR = path.join(
originalTmpDir,
'agent-device-swift-cache',
);
}
reportPrunedRunDirectories(pruneAbandonedRunDirectories(TEST_RUN_TMP_ROOT));
// The pid is embedded so check-tmpdir-leaks.ts can tell a directory that's
// still in active use (its vitest process is alive — a concurrent run in
// another worktree, say) apart from one actually abandoned by a killed
// process; the trailing mkdtemp suffix still guards against same-pid reuse.
testRunTmpDir = fs.mkdtempSync(
path.join(TEST_RUN_TMP_ROOT, `${TEST_RUN_TMP_PREFIX}${process.pid}-`),
);
process.env.TMPDIR = testRunTmpDir;
}
// fallow-ignore-next-line unused-export
export function teardown(): void {
fs.rmSync(testRunTmpDir, { recursive: true, force: true });
restoreEnv('TMPDIR', previousTmpDir);
restoreEnv('AGENT_DEVICE_SWIFT_CACHE_DIR', previousSwiftCacheDir);
}
function restoreEnv(key: string, value: string | undefined): void {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}