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:
gsxdsm
2026-05-07 12:25:45 -07:00
parent c92eebc5c3
commit ae97fca340
3 changed files with 59 additions and 5 deletions

View File

@@ -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 });

View File

@@ -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]));

View File

@@ -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,