FN-6255: redirect tmpdir mkdtemp calls in tests

Keep test-created temp directories under the Fusion worker root.

- Redirect fs.mkdtemp and fs.promises.mkdtemp prefixes rooted at the OS temp dir into per-process worker sinks.
- Sweep stale redirect sinks and clean current-process sinks on exit.
- Add regression coverage for sync, async, realpath, nested, and Buffer prefix behavior.
- Remove the restored merger file-scope invariant test from quarantine.

Files changed:
 packages/core/src/__test-utils__/vitest-setup.ts   | 119 +++++++++++++++++++--
 .../__tests__/vitest-setup-tmp-redirect.test.ts    |  68 ++++++++++++
 scripts/lib/test-quarantine.json                   |   5 -
 3 files changed, 181 insertions(+), 11 deletions(-)

Fusion-Task-Id: FN-6255

Fusion-Task-Lineage: 4cc855c6-37dd-4dfb-a545-1fd1885779c4
This commit is contained in:
gsxdsm
2026-06-11 12:11:37 -07:00
parent c285f3fb88
commit 2add48c8b6
3 changed files with 181 additions and 11 deletions

View File

@@ -15,7 +15,7 @@
import { afterEach, expect } from "vitest";
import { createRequire, syncBuiltinESMExports } from "node:module";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { basename, dirname, join, resolve } from "node:path";
import { promisify } from "node:util";
import { isMainThread } from "node:worker_threads";
import { assertOutsideRealFusionPath } from "../test-safety.js";
@@ -40,7 +40,16 @@ const requireFromHere = createRequire(import.meta.url);
const fs = requireFromHere("node:fs") as FsModule;
const fsPromises = requireFromHere("node:fs/promises") as FsPromisesModule;
const childProcess = requireFromHere("node:child_process") as ChildProcessModule;
const { mkdtempSync, mkdirSync, rmSync, realpathSync, existsSync } = fs;
const {
appendFileSync,
mkdtempSync,
mkdirSync,
readFileSync,
rmSync,
realpathSync,
existsSync,
writeFileSync,
} = fs;
type EmitWarningArgs = Parameters<typeof process.emitWarning>;
type EmitWarningRestArgs = EmitWarningArgs extends [string | Error, ...infer Rest] ? Rest : never;
@@ -160,6 +169,102 @@ const WORKER_ROOT = join(tmpdir(), "fusion-test-workers");
try { mkdirSync(WORKER_ROOT, { recursive: true }); } catch { /* ignore */ }
process.env.FUSION_TEST_WORKER_ROOT = WORKER_ROOT;
const REAL_TMPDIR = (() => {
try {
return realpathSync(tmpdir());
} catch {
return resolve(tmpdir());
}
})();
const TMPDIR_REDIRECT_REGISTRY = join(WORKER_ROOT, ".redir-pids");
let tmpdirRedirectSink: string | null = null;
let tmpdirRedirectExitCleanupInstalled = false;
let tmpdirRedirectSweepComplete = false;
function isProcessAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
return code === "EPERM";
}
}
function sweepDeadTmpdirRedirectSinks(): void {
if (tmpdirRedirectSweepComplete) return;
tmpdirRedirectSweepComplete = true;
let ownerPids: number[];
try {
ownerPids = Array.from(new Set(
readFileSync(TMPDIR_REDIRECT_REGISTRY, "utf8")
.split(/\r?\n/)
.map((line) => Number.parseInt(line, 10))
.filter((pid) => Number.isInteger(pid) && pid > 0),
));
} catch {
return;
}
const liveOwnerPids: number[] = [];
for (const ownerPid of ownerPids) {
if (ownerPid === process.pid || isProcessAlive(ownerPid)) {
liveOwnerPids.push(ownerPid);
continue;
}
try {
rmSync(join(WORKER_ROOT, `redir-${ownerPid}`), { recursive: true, force: true });
} catch {
// Ignore stale-sink cleanup failures; global teardown still owns WORKER_ROOT.
}
}
try {
writeFileSync(TMPDIR_REDIRECT_REGISTRY, liveOwnerPids.length > 0 ? `${liveOwnerPids.join("\n")}\n` : "");
} catch {
// Best-effort only; stale entries are harmless and swept by future workers.
}
}
function ensureTmpdirRedirectSink(): string {
if (tmpdirRedirectSink) return tmpdirRedirectSink;
sweepDeadTmpdirRedirectSinks();
const sink = join(WORKER_ROOT, `redir-${process.pid}`);
mkdirSync(sink, { recursive: true });
try {
appendFileSync(TMPDIR_REDIRECT_REGISTRY, `${process.pid}\n`);
} catch {
// Best-effort only; the process exit hook and global teardown still clean up.
}
tmpdirRedirectSink = sink;
if (!tmpdirRedirectExitCleanupInstalled) {
tmpdirRedirectExitCleanupInstalled = true;
process.once("exit", () => {
try {
rmSync(sink, { recursive: true, force: true });
} catch {
// Best-effort only. vitest globalTeardown also sweeps WORKER_ROOT.
}
});
}
return sink;
}
function redirectTmpdirPrefix<T>(prefix: T): T {
if (typeof prefix !== "string") return prefix;
const parent = dirname(prefix);
if (parent !== tmpdir() && parent !== REAL_TMPDIR) return prefix;
return join(ensureTmpdirRedirectSink(), basename(prefix)) as T;
}
function ensureIsolatedHome(): void {
const existingHome = process.env.HOME ?? process.env.USERPROFILE;
if (existingHome && existingHome.includes(tmpdir()) && existingHome.includes(TEST_HOME_PREFIX)) {
@@ -290,8 +395,9 @@ function installFsGuards(): void {
return originalFs.cpSync(src, dest, options as Parameters<typeof fs.cpSync>[2]);
}) as typeof fs.cpSync;
mutableFs.mkdtempSync = ((prefix, options) => {
guardOne(prefix, "fs.mkdtempSync");
return originalFs.mkdtempSync(prefix, options as Parameters<typeof fs.mkdtempSync>[1]);
const redirectedPrefix = redirectTmpdirPrefix(prefix);
guardOne(redirectedPrefix, "fs.mkdtempSync");
return originalFs.mkdtempSync(redirectedPrefix, options as Parameters<typeof fs.mkdtempSync>[1]);
}) as typeof fs.mkdtempSync;
mutableFs.openSync = ((path, flags, mode) => {
guardOne(path, "fs.openSync");
@@ -408,8 +514,9 @@ function installFsGuards(): void {
return originalFsPromises.open(...args);
}) as typeof fsPromises.open;
mutableFsPromises.mkdtemp = (async (...args: Parameters<typeof fsPromises.mkdtemp>) => {
guardOne(args[0], "fs.promises.mkdtemp");
return originalFsPromises.mkdtemp(...args);
const redirectedPrefix = redirectTmpdirPrefix(args[0]);
guardOne(redirectedPrefix, "fs.promises.mkdtemp");
return originalFsPromises.mkdtemp(redirectedPrefix, args[1]);
}) as typeof fsPromises.mkdtemp;
mutableFsPromises.truncate = (async (...args: Parameters<typeof fsPromises.truncate>) => {
guardOne(args[0], "fs.promises.truncate");

View File

@@ -0,0 +1,68 @@
import { mkdtempSync, mkdirSync, rmSync, realpathSync } from "node:fs";
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, sep } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
const createdPaths: string[] = [];
function remember(path: string): string {
createdPaths.push(path);
return path;
}
function expectUnderWorkerRoot(path: string): void {
const workerRoot = process.env.FUSION_TEST_WORKER_ROOT;
expect(workerRoot).toBeTruthy();
expect(path.startsWith(`${workerRoot}${sep}`)).toBe(true);
expect(dirname(path)).toBe(join(workerRoot!, `redir-${process.pid}`));
}
afterEach(() => {
for (const path of createdPaths.splice(0).reverse()) {
rmSync(path, { recursive: true, force: true });
}
});
describe("vitest setup tmpdir mkdtemp redirect", () => {
it("redirects sync mkdtemp prefixes rooted directly at the OS temp dir", () => {
const path = remember(mkdtempSync(join(tmpdir(), "fn-redirect-sync-")));
expectUnderWorkerRoot(path);
});
it("redirects async mkdtemp prefixes rooted directly at the OS temp dir", async () => {
const path = remember(await mkdtemp(join(tmpdir(), "fn-redirect-async-")));
expectUnderWorkerRoot(path);
});
it("redirects the realpath spelling of the OS temp dir when it differs", () => {
const realTmpdir = realpathSync(tmpdir());
if (realTmpdir === tmpdir()) {
expect(realTmpdir).toBe(tmpdir());
return;
}
const path = remember(mkdtempSync(join(realTmpdir, "fn-redirect-realpath-")));
expectUnderWorkerRoot(path);
});
it("leaves nested temp-root prefixes unchanged", () => {
const parent = remember(join(tmpdir(), `fn-redirect-parent-${process.pid}-${Date.now()}`));
mkdirSync(parent, { recursive: true });
const path = remember(mkdtempSync(join(parent, "nested-")));
expect(path.startsWith(`${parent}${sep}`)).toBe(true);
});
it("leaves non-string prefixes untouched", () => {
const prefix = Buffer.from(join(tmpdir(), "fn-redirect-buffer-"));
const path = remember(mkdtempSync(prefix));
expect(path.startsWith(`${tmpdir()}${sep}`)).toBe(true);
});
});

View File

@@ -11,11 +11,6 @@
"reason": "Flake: vi.mock('node:child_process') occasionally doesn't take under workspace-concurrent runs, letting real git binary leak and report staged files unrelated to test scope (trips FileScopeViolationError). Same logic covered by real-git fixture tests in reliability-interactions/workflow-and-file-scope. FN-6206.",
"quarantinedAt": "2026-06-10"
},
{
"file": "packages/engine/src/__tests__/merger-file-scope-invariant.test.ts",
"reason": "Flake: same mock-contention mode as the sibling changeset-file test above (vi.mock('node:child_process') not taking under concurrent load). FN-6206.",
"quarantinedAt": "2026-06-10"
},
{
"file": "packages/engine/src/__tests__/merger-ai-cleanup.test.ts",
"reason": "Flake observed during FN-6206 verification: `pruneExistingAiMergeWorktrees skips active-session paths` failed in full `pnpm --filter @fusion/engine test` runs while the file passed standalone, indicating suite-order/concurrency sensitivity. Follow-up FN-6207.",