fix(test-isolation): allow-list known isolated HOMEs to stop FN-3711-style flakes
The leak guard in scripts/check-test-isolation.mjs reported fusion-test-home-root-* dirs created by scripts/test-changed.mjs as leaks when transient EBUSY on /var/folders prevented cleanup or the baseline file got rotated. Track every HOME basename the script mints and pass it via FUSION_TEST_ISOLATION_IGNORE_NAMES so the check allow-lists them unconditionally. Surface cleanup rm failures via console.warn instead of swallowing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,34 @@ test("fails when a tracked temp leak appears after baseline", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("ignores leaked temp dirs whose basenames appear in FUSION_TEST_ISOLATION_IGNORE_NAMES", () => {
|
||||
withFixture(({ cwd, home }) => {
|
||||
const before = runScript(["--before"], { cwd, home });
|
||||
assert.equal(before.status, 0);
|
||||
|
||||
// Simulate a fusion-test-home-root-* dir that survived cleanup. Without the
|
||||
// env allow-list this would trip the leak guard; with it, the check passes.
|
||||
const leakedName = `fusion-test-home-root-flake-${process.pid}`;
|
||||
const leakedPath = path.join(tmpdir(), leakedName);
|
||||
mkdirSync(leakedPath, { recursive: true });
|
||||
try {
|
||||
const result = spawnSync(process.execPath, [scriptPath], {
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: home,
|
||||
USERPROFILE: home,
|
||||
FUSION_TEST_ISOLATION_IGNORE_NAMES: leakedName,
|
||||
},
|
||||
encoding: "utf8",
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
} finally {
|
||||
rmSync(leakedPath, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("fails when protected repo .fusion data changes after baseline", () => {
|
||||
withFixture(({ cwd, home }) => {
|
||||
const before = runScript(["--before"], { cwd, home });
|
||||
|
||||
@@ -196,6 +196,15 @@ function checkAgainstBaseline() {
|
||||
}
|
||||
|
||||
const baselineNames = new Set(baseline.tmpNames ?? []);
|
||||
// Caller (scripts/test-changed.mjs) tells us which fusion-test-home-root-*
|
||||
// basenames it minted this run. We allow-list them unconditionally so a
|
||||
// transient cleanup failure or a rotated baseline file can't masquerade as
|
||||
// a real test leak.
|
||||
const callerIgnoreNames = (process.env.FUSION_TEST_ISOLATION_IGNORE_NAMES ?? "")
|
||||
.split(",")
|
||||
.map((name) => name.trim())
|
||||
.filter(Boolean);
|
||||
for (const name of callerIgnoreNames) baselineNames.add(name);
|
||||
const leaks = snapshotTmp().filter((e) => !baselineNames.has(e.name));
|
||||
|
||||
const baselineByDir = new Map((baseline.protectedFusion ?? []).map((entry) => [entry.dir, entry]));
|
||||
|
||||
@@ -44,7 +44,16 @@ function run(command, commandArgs, options = {}) {
|
||||
function runIsolationCheck(before = false, env = process.env) {
|
||||
const args = [checkIsolationScript];
|
||||
if (before) args.push("--before");
|
||||
run(process.execPath, args, { env });
|
||||
// Inject the names of every isolated HOME this script created so the check
|
||||
// never reports them as a leak even if the rm-rf in cleanup silently failed
|
||||
// or the baseline file got rotated mid-run. Without this, a transient EBUSY
|
||||
// on /var/folders (SQLite WAL still mmap'd, orphan child holding an fd)
|
||||
// leaks a `fusion-test-home-root-*` dir and trips the guard.
|
||||
const ignoreNames = [...knownIsolatedHomeBasenames].join(",");
|
||||
const checkEnv = ignoreNames
|
||||
? { ...env, FUSION_TEST_ISOLATION_IGNORE_NAMES: ignoreNames }
|
||||
: env;
|
||||
run(process.execPath, args, { env: checkEnv });
|
||||
}
|
||||
|
||||
export function shouldRunIsolationGuard(env = process.env) {
|
||||
@@ -70,8 +79,9 @@ function pruneFusionTestHomes() {
|
||||
}
|
||||
try {
|
||||
rmSync(rawPath, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Best-effort pruning only.
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
console.warn(`[test-changed] failed to prune leftover ${rawPath}: ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -620,12 +630,17 @@ export function defaultTestWorkerBudget(env = process.env) {
|
||||
const { totalWorkers, concurrency } = defaultTestWorkerBudget(process.env);
|
||||
|
||||
const isolatedHomesToCleanup = new Set();
|
||||
// Basenames of every fusion-test-home-root-* dir this process has minted.
|
||||
// Passed to check-test-isolation.mjs via env so it allow-lists them
|
||||
// unconditionally, even if cleanup's rm silently failed.
|
||||
const knownIsolatedHomeBasenames = new Set();
|
||||
|
||||
function cleanupIsolatedHomePath(homePath) {
|
||||
try {
|
||||
rmSync(homePath, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Best-effort cleanup only.
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
console.warn(`[test-changed] failed to remove isolated HOME ${homePath}: ${message}`);
|
||||
}
|
||||
isolatedHomesToCleanup.delete(homePath);
|
||||
}
|
||||
@@ -651,6 +666,8 @@ export function createIsolatedHomeEnv(env = process.env) {
|
||||
const isolatedHome = realpathSync(rawIsolatedHome);
|
||||
isolatedHomesToCleanup.add(rawIsolatedHome);
|
||||
isolatedHomesToCleanup.add(isolatedHome);
|
||||
knownIsolatedHomeBasenames.add(path.basename(rawIsolatedHome));
|
||||
knownIsolatedHomeBasenames.add(path.basename(isolatedHome));
|
||||
|
||||
const nextEnv = {
|
||||
...env,
|
||||
|
||||
Reference in New Issue
Block a user