test(FN-2360): harden test isolation around repo fusion state

This commit is contained in:
gsxdsm
2026-04-29 12:02:56 -07:00
parent cecf14a4f1
commit 822bde7829
10 changed files with 370 additions and 51 deletions

View File

@@ -1,22 +1,26 @@
import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { existsSync, renameSync, rmSync } from "node:fs"; import { existsSync, mkdirSync, renameSync, rmSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import vitestConfig from "../../vitest.config"; import vitestConfig from "../../vitest.config";
const cliRoot = join(__dirname, "..", ".."); const cliRoot = join(__dirname, "..", "..");
const workspaceRoot = join(cliRoot, "..", ".."); const workspaceRoot = join(cliRoot, "..", "..");
const hiddenDistRoot = join(workspaceRoot, `.tmp-fn-vitest-workspace-resolution-${process.pid}`);
const internalPackages = ["core", "engine", "dashboard"] as const; const internalPackages = ["core", "engine", "dashboard"] as const;
const movedDistDirs: Array<{ from: string; to: string }> = []; const movedDistDirs: Array<{ from: string; to: string }> = [];
function hideInternalPackageDistDirs() { function hideInternalPackageDistDirs() {
rmSync(hiddenDistRoot, { recursive: true, force: true });
mkdirSync(hiddenDistRoot, { recursive: true });
for (const pkg of internalPackages) { for (const pkg of internalPackages) {
const distPath = join(workspaceRoot, "packages", pkg, "dist"); const distPath = join(workspaceRoot, "packages", pkg, "dist");
if (!existsSync(distPath)) { if (!existsSync(distPath)) {
continue; continue;
} }
const hiddenPath = `${distPath}.__fn2360-hidden-${process.pid}`; const hiddenPath = join(hiddenDistRoot, `${pkg}-dist`);
if (existsSync(hiddenPath)) { if (existsSync(hiddenPath)) {
rmSync(hiddenPath, { recursive: true, force: true }); rmSync(hiddenPath, { recursive: true, force: true });
} }
@@ -39,6 +43,7 @@ function restoreInternalPackageDistDirs() {
renameSync(to, from); renameSync(to, from);
} }
movedDistDirs.length = 0; movedDistDirs.length = 0;
rmSync(hiddenDistRoot, { recursive: true, force: true });
} }
describe("CLI Vitest workspace resolution", () => { describe("CLI Vitest workspace resolution", () => {

View File

@@ -5,16 +5,26 @@
* 2. Changes process.cwd() to a per-worker temp dir (main thread only) so any * 2. Changes process.cwd() to a per-worker temp dir (main thread only) so any
* accidental `process.cwd()` call resolves to a disposable path. * accidental `process.cwd()` call resolves to a disposable path.
* 3. Wraps `process.chdir` to reject attempts to chdir into the real .fusion. * 3. Wraps `process.chdir` to reject attempts to chdir into the real .fusion.
* 4. Wraps write-capable fs APIs so tests cannot mutate the repo's live .fusion.
* *
* Worker temp dirs live under a single parent (FUSION_WORKER_ROOT) that is * Worker temp dirs live under a single parent (FUSION_WORKER_ROOT) that is
* wiped by the vitest globalTeardown in vitest-teardown.ts — this handles the * wiped by the vitest globalTeardown in vitest-teardown.ts — this handles the
* case where workers are killed (SIGKILL) and never run their exit handlers. * case where workers are killed (SIGKILL) and never run their exit handlers.
*/ */
import { mkdtempSync, mkdirSync, rmSync, realpathSync, existsSync } from "node:fs"; import { createRequire, syncBuiltinESMExports } from "node:module";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { dirname, join, resolve, sep } from "node:path"; import { dirname, join, resolve } from "node:path";
import { isMainThread } from "node:worker_threads"; import { isMainThread } from "node:worker_threads";
import { assertOutsideRealFusionPath } from "../test-safety.js";
type FsModule = typeof import("node:fs");
type FsPromisesModule = typeof import("node:fs/promises");
const requireFromHere = createRequire(import.meta.url);
const fs = requireFromHere("node:fs") as FsModule;
const fsPromises = requireFromHere("node:fs/promises") as FsPromisesModule;
const { mkdtempSync, mkdirSync, rmSync, realpathSync, existsSync } = fs;
const originalCwd = process.cwd.bind(process); const originalCwd = process.cwd.bind(process);
@@ -77,22 +87,250 @@ if (isMainThread) {
process.chdir(workerTempDir); process.chdir(workerTempDir);
} }
function installFsGuards(): void {
const guardState = globalThis as typeof globalThis & { __fusionTestFsGuardInstalled?: boolean };
if (guardState.__fusionTestFsGuardInstalled) return;
guardState.__fusionTestFsGuardInstalled = true;
const mutableFs = fs as unknown as Record<string, unknown>;
const mutableFsPromises = fsPromises as unknown as Record<string, unknown>;
const originalFs = {
mkdirSync: fs.mkdirSync.bind(fs),
writeFileSync: fs.writeFileSync.bind(fs),
appendFileSync: fs.appendFileSync.bind(fs),
rmSync: fs.rmSync.bind(fs),
unlinkSync: fs.unlinkSync.bind(fs),
rmdirSync: fs.rmdirSync.bind(fs),
renameSync: fs.renameSync.bind(fs),
copyFileSync: fs.copyFileSync.bind(fs),
cpSync: fs.cpSync.bind(fs),
mkdtempSync: fs.mkdtempSync.bind(fs),
openSync: fs.openSync.bind(fs),
createWriteStream: fs.createWriteStream.bind(fs),
truncateSync: fs.truncateSync.bind(fs),
linkSync: fs.linkSync.bind(fs),
symlinkSync: fs.symlinkSync.bind(fs),
mkdir: fs.mkdir.bind(fs),
writeFile: fs.writeFile.bind(fs),
appendFile: fs.appendFile.bind(fs),
rm: fs.rm.bind(fs),
unlink: fs.unlink.bind(fs),
rmdir: fs.rmdir.bind(fs),
rename: fs.rename.bind(fs),
copyFile: fs.copyFile.bind(fs),
cp: fs.cp.bind(fs),
open: fs.open.bind(fs),
truncate: fs.truncate.bind(fs),
link: fs.link.bind(fs),
symlink: fs.symlink.bind(fs),
};
const originalFsPromises = {
mkdir: fsPromises.mkdir.bind(fsPromises),
writeFile: fsPromises.writeFile.bind(fsPromises),
appendFile: fsPromises.appendFile.bind(fsPromises),
rm: fsPromises.rm.bind(fsPromises),
unlink: fsPromises.unlink.bind(fsPromises),
rmdir: fsPromises.rmdir.bind(fsPromises),
rename: fsPromises.rename.bind(fsPromises),
copyFile: fsPromises.copyFile.bind(fsPromises),
cp: fsPromises.cp.bind(fsPromises),
open: fsPromises.open.bind(fsPromises),
mkdtemp: fsPromises.mkdtemp.bind(fsPromises),
truncate: fsPromises.truncate.bind(fsPromises),
link: fsPromises.link.bind(fsPromises),
symlink: fsPromises.symlink.bind(fsPromises),
};
const guardOne = (pathValue: unknown, context: string) => {
if (pathValue === undefined || pathValue === null) return;
assertOutsideRealFusionPath(pathValue as Parameters<typeof assertOutsideRealFusionPath>[0], context);
};
const guardBoth = (source: unknown, target: unknown, context: string) => {
guardOne(source, `${context} source`);
guardOne(target, `${context} target`);
};
mutableFs.mkdirSync = ((path, options) => {
guardOne(path, "fs.mkdirSync");
return originalFs.mkdirSync(path, options as Parameters<typeof fs.mkdirSync>[1]);
}) as typeof fs.mkdirSync;
mutableFs.writeFileSync = ((path, data, options) => {
guardOne(path, "fs.writeFileSync");
return originalFs.writeFileSync(path, data, options as Parameters<typeof fs.writeFileSync>[2]);
}) as typeof fs.writeFileSync;
mutableFs.appendFileSync = ((path, data, options) => {
guardOne(path, "fs.appendFileSync");
return originalFs.appendFileSync(path, data, options as Parameters<typeof fs.appendFileSync>[2]);
}) as typeof fs.appendFileSync;
mutableFs.rmSync = ((path, options) => {
guardOne(path, "fs.rmSync");
return originalFs.rmSync(path, options as Parameters<typeof fs.rmSync>[1]);
}) as typeof fs.rmSync;
mutableFs.unlinkSync = ((path) => {
guardOne(path, "fs.unlinkSync");
return originalFs.unlinkSync(path);
}) as typeof fs.unlinkSync;
mutableFs.rmdirSync = ((path, options) => {
guardOne(path, "fs.rmdirSync");
return originalFs.rmdirSync(path, options as Parameters<typeof fs.rmdirSync>[1]);
}) as typeof fs.rmdirSync;
mutableFs.renameSync = ((oldPath, newPath) => {
guardBoth(oldPath, newPath, "fs.renameSync");
return originalFs.renameSync(oldPath, newPath);
}) as typeof fs.renameSync;
mutableFs.copyFileSync = ((src, dest, mode) => {
guardBoth(src, dest, "fs.copyFileSync");
return originalFs.copyFileSync(src, dest, mode as Parameters<typeof fs.copyFileSync>[2]);
}) as typeof fs.copyFileSync;
mutableFs.cpSync = ((src, dest, options) => {
guardBoth(src, dest, "fs.cpSync");
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]);
}) as typeof fs.mkdtempSync;
mutableFs.openSync = ((path, flags, mode) => {
guardOne(path, "fs.openSync");
return originalFs.openSync(path, flags, mode as Parameters<typeof fs.openSync>[2]);
}) as typeof fs.openSync;
mutableFs.createWriteStream = ((path, options) => {
guardOne(path, "fs.createWriteStream");
return originalFs.createWriteStream(path, options as Parameters<typeof fs.createWriteStream>[1]);
}) as typeof fs.createWriteStream;
mutableFs.truncateSync = ((path, len) => {
guardOne(path, "fs.truncateSync");
return originalFs.truncateSync(path, len as Parameters<typeof fs.truncateSync>[1]);
}) as typeof fs.truncateSync;
mutableFs.linkSync = ((existingPath, newPath) => {
guardBoth(existingPath, newPath, "fs.linkSync");
return originalFs.linkSync(existingPath, newPath);
}) as typeof fs.linkSync;
mutableFs.symlinkSync = ((target, path, type) => {
guardBoth(target, path, "fs.symlinkSync");
return originalFs.symlinkSync(target, path, type as Parameters<typeof fs.symlinkSync>[2]);
}) as typeof fs.symlinkSync;
mutableFs.mkdir = ((...args: Parameters<typeof fs.mkdir>) => {
guardOne(args[0], "fs.mkdir");
return originalFs.mkdir(...args);
}) as typeof fs.mkdir;
mutableFs.writeFile = ((...args: Parameters<typeof fs.writeFile>) => {
guardOne(args[0], "fs.writeFile");
return originalFs.writeFile(...args);
}) as typeof fs.writeFile;
mutableFs.appendFile = ((...args: Parameters<typeof fs.appendFile>) => {
guardOne(args[0], "fs.appendFile");
return originalFs.appendFile(...args);
}) as typeof fs.appendFile;
mutableFs.rm = ((...args: Parameters<typeof fs.rm>) => {
guardOne(args[0], "fs.rm");
return originalFs.rm(...args);
}) as typeof fs.rm;
mutableFs.unlink = ((...args: Parameters<typeof fs.unlink>) => {
guardOne(args[0], "fs.unlink");
return originalFs.unlink(...args);
}) as typeof fs.unlink;
mutableFs.rmdir = ((...args: Parameters<typeof fs.rmdir>) => {
guardOne(args[0], "fs.rmdir");
return originalFs.rmdir(...args);
}) as typeof fs.rmdir;
mutableFs.rename = ((...args: Parameters<typeof fs.rename>) => {
guardBoth(args[0], args[1], "fs.rename");
return originalFs.rename(...args);
}) as typeof fs.rename;
mutableFs.copyFile = ((...args: Parameters<typeof fs.copyFile>) => {
guardBoth(args[0], args[1], "fs.copyFile");
return originalFs.copyFile(...args);
}) as typeof fs.copyFile;
mutableFs.cp = ((...args: Parameters<typeof fs.cp>) => {
guardBoth(args[0], args[1], "fs.cp");
return originalFs.cp(...args);
}) as typeof fs.cp;
mutableFs.open = ((...args: Parameters<typeof fs.open>) => {
guardOne(args[0], "fs.open");
return originalFs.open(...args);
}) as typeof fs.open;
mutableFs.truncate = ((...args: Parameters<typeof fs.truncate>) => {
guardOne(args[0], "fs.truncate");
return originalFs.truncate(...args);
}) as typeof fs.truncate;
mutableFs.link = ((...args: Parameters<typeof fs.link>) => {
guardBoth(args[0], args[1], "fs.link");
return originalFs.link(...args);
}) as typeof fs.link;
mutableFs.symlink = ((...args: Parameters<typeof fs.symlink>) => {
guardBoth(args[0], args[1], "fs.symlink");
return originalFs.symlink(...args);
}) as typeof fs.symlink;
mutableFsPromises.mkdir = (async (...args: Parameters<typeof fsPromises.mkdir>) => {
guardOne(args[0], "fs.promises.mkdir");
return originalFsPromises.mkdir(...args);
}) as typeof fsPromises.mkdir;
mutableFsPromises.writeFile = (async (...args: Parameters<typeof fsPromises.writeFile>) => {
guardOne(args[0], "fs.promises.writeFile");
return originalFsPromises.writeFile(...args);
}) as typeof fsPromises.writeFile;
mutableFsPromises.appendFile = (async (...args: Parameters<typeof fsPromises.appendFile>) => {
guardOne(args[0], "fs.promises.appendFile");
return originalFsPromises.appendFile(...args);
}) as typeof fsPromises.appendFile;
mutableFsPromises.rm = (async (...args: Parameters<typeof fsPromises.rm>) => {
guardOne(args[0], "fs.promises.rm");
return originalFsPromises.rm(...args);
}) as typeof fsPromises.rm;
mutableFsPromises.unlink = (async (...args: Parameters<typeof fsPromises.unlink>) => {
guardOne(args[0], "fs.promises.unlink");
return originalFsPromises.unlink(...args);
}) as typeof fsPromises.unlink;
mutableFsPromises.rmdir = (async (...args: Parameters<typeof fsPromises.rmdir>) => {
guardOne(args[0], "fs.promises.rmdir");
return originalFsPromises.rmdir(...args);
}) as typeof fsPromises.rmdir;
mutableFsPromises.rename = (async (...args: Parameters<typeof fsPromises.rename>) => {
guardBoth(args[0], args[1], "fs.promises.rename");
return originalFsPromises.rename(...args);
}) as typeof fsPromises.rename;
mutableFsPromises.copyFile = (async (...args: Parameters<typeof fsPromises.copyFile>) => {
guardBoth(args[0], args[1], "fs.promises.copyFile");
return originalFsPromises.copyFile(...args);
}) as typeof fsPromises.copyFile;
mutableFsPromises.cp = (async (...args: Parameters<typeof fsPromises.cp>) => {
guardBoth(args[0], args[1], "fs.promises.cp");
return originalFsPromises.cp(...args);
}) as typeof fsPromises.cp;
mutableFsPromises.open = (async (...args: Parameters<typeof fsPromises.open>) => {
guardOne(args[0], "fs.promises.open");
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);
}) as typeof fsPromises.mkdtemp;
mutableFsPromises.truncate = (async (...args: Parameters<typeof fsPromises.truncate>) => {
guardOne(args[0], "fs.promises.truncate");
return originalFsPromises.truncate(...args);
}) as typeof fsPromises.truncate;
mutableFsPromises.link = (async (...args: Parameters<typeof fsPromises.link>) => {
guardBoth(args[0], args[1], "fs.promises.link");
return originalFsPromises.link(...args);
}) as typeof fsPromises.link;
mutableFsPromises.symlink = (async (...args: Parameters<typeof fsPromises.symlink>) => {
guardBoth(args[0], args[1], "fs.promises.symlink");
return originalFsPromises.symlink(...args);
}) as typeof fsPromises.symlink;
syncBuiltinESMExports();
}
installFsGuards();
const originalChdir = process.chdir.bind(process); const originalChdir = process.chdir.bind(process);
process.chdir = (target: string) => { process.chdir = (target: string) => {
const resolvedTarget = (() => { assertOutsideRealFusionPath(target, "process.chdir");
try {
return realpathSync(target);
} catch {
return resolve(target);
}
})();
const realFusion = join(repoRoot, ".fusion");
if (resolvedTarget === realFusion || resolvedTarget.startsWith(realFusion + sep)) {
throw new Error(
`[test-safety] Test attempted process.chdir into real .fusion directory: ${resolvedTarget}\n` +
`Use useIsolatedCwd() from __test-utils__/workspace.ts instead.`
);
}
originalChdir(target); originalChdir(target);
}; };

View File

@@ -3,7 +3,7 @@
* *
* All tests that touch the filesystem or resolve paths from process.cwd() * All tests that touch the filesystem or resolve paths from process.cwd()
* should use these helpers so they never touch the real user's ~/Projects or * should use these helpers so they never touch the real user's ~/Projects or
* the repo's real .fusion/ directory. * the repo's protected .fusion/ directory.
* *
* - `tempWorkspace()` returns a tracked temp dir that is auto-removed in afterEach. * - `tempWorkspace()` returns a tracked temp dir that is auto-removed in afterEach.
* - `useIsolatedCwd()` chdirs into a tracked temp dir for the test and restores after. * - `useIsolatedCwd()` chdirs into a tracked temp dir for the test and restores after.
@@ -12,40 +12,12 @@
import { mkdtempSync, rmSync, existsSync, realpathSync } from "node:fs"; import { mkdtempSync, rmSync, existsSync, realpathSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join, resolve, sep } from "node:path"; import { join, resolve } from "node:path";
import { afterEach } from "vitest"; import { afterEach } from "vitest";
import { assertOutsideRealFusionPath } from "../test-safety.js";
let realFusionRootCache: string | null = null;
function getRealFusionRoot(): string | null {
if (realFusionRootCache !== null) return realFusionRootCache;
const fromEnv = process.env.FUSION_TEST_REAL_ROOT;
if (fromEnv) {
try {
realFusionRootCache = realpathSync(fromEnv);
} catch {
realFusionRootCache = resolve(fromEnv);
}
return realFusionRootCache;
}
return null;
}
export function assertOutsideRealFusion(path: string, context = "operation"): void { export function assertOutsideRealFusion(path: string, context = "operation"): void {
const realRoot = getRealFusionRoot(); assertOutsideRealFusionPath(path, context);
if (!realRoot) return;
let candidate: string;
try {
candidate = realpathSync(path);
} catch {
candidate = resolve(path);
}
const realFusionDir = join(realRoot, ".fusion");
if (candidate === realFusionDir || candidate.startsWith(realFusionDir + sep)) {
throw new Error(
`[test-safety] ${context} targeted real user .fusion directory: ${candidate}\n` +
`Tests must operate inside a temp directory. Use tempWorkspace() or useIsolatedCwd().`
);
}
} }
const activeTempDirs = new Set<string>(); const activeTempDirs = new Set<string>();

View File

@@ -1,7 +1,10 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { mkdirSync, writeFileSync } from "node:fs";
import * as fsPromises from "node:fs/promises";
import { homedir, tmpdir } from "node:os"; import { homedir, tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path"; import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { Database } from "../db.js";
const TEMP_HOME_PREFIX = "fn-test-home-"; const TEMP_HOME_PREFIX = "fn-test-home-";
@@ -46,4 +49,38 @@ describe("test isolation setup", () => {
expect(process.cwd().startsWith(repoFusionDir)).toBe(false); expect(process.cwd().startsWith(repoFusionDir)).toBe(false);
}); });
it("blocks sync filesystem writes into the repository .fusion directory", () => {
const thisFile = fileURLToPath(import.meta.url);
const repoRoot = resolve(dirname(thisFile), "../../../../");
const blockedPath = join(repoRoot, ".fusion", "__vitest-guard-sync__");
expect(() => mkdirSync(blockedPath, { recursive: true })).toThrow(
"targeted protected repo .fusion directory",
);
expect(() => writeFileSync(join(repoRoot, ".fusion", "__vitest-guard-sync__.txt"), "x")).toThrow(
"targeted protected repo .fusion directory",
);
});
it("blocks async filesystem writes into the repository .fusion directory", async () => {
const thisFile = fileURLToPath(import.meta.url);
const repoRoot = resolve(dirname(thisFile), "../../../../");
await expect(
fsPromises.mkdir(join(repoRoot, ".fusion", "__vitest-guard-async__"), { recursive: true }),
).rejects.toThrow("targeted protected repo .fusion directory");
await expect(
fsPromises.writeFile(join(repoRoot, ".fusion", "__vitest-guard-async__.txt"), "x"),
).rejects.toThrow("targeted protected repo .fusion directory");
});
it("blocks SQLite opens against the repository fusion database", () => {
const thisFile = fileURLToPath(import.meta.url);
const repoRoot = resolve(dirname(thisFile), "../../../../");
expect(() => new Database(join(repoRoot, ".fusion"))).toThrow(
"targeted protected repo .fusion directory",
);
});
}); });

View File

@@ -13,6 +13,7 @@
*/ */
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import { assertOutsideRealFusionPath } from "./test-safety.js";
const isBun = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined"; const isBun = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
@@ -70,6 +71,7 @@ export class DatabaseSync {
private impl: RawDatabase; private impl: RawDatabase;
constructor(path: string) { constructor(path: string) {
assertOutsideRealFusionPath(path, "SQLite database open");
const Ctor = loadDatabaseCtor(); const Ctor = loadDatabaseCtor();
this.impl = new Ctor(path); this.impl = new Ctor(path);
} }

View File

@@ -0,0 +1,51 @@
import { realpathSync, type PathLike } from "node:fs";
import { join, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
function pathLikeToString(pathValue: PathLike): string {
if (typeof pathValue === "string") return pathValue;
if (pathValue instanceof URL) return fileURLToPath(pathValue);
return pathValue.toString();
}
function resolveGuardPath(pathValue: PathLike): string {
const raw = pathLikeToString(pathValue);
if (!raw || raw === ":memory:") return raw;
try {
return realpathSync(raw);
} catch {
return resolve(raw);
}
}
export function getProtectedFusionDir(): string | null {
const root = process.env.FUSION_TEST_REAL_ROOT;
if (!root) return null;
const resolvedRoot = resolveGuardPath(root);
if (!resolvedRoot) return null;
return join(resolvedRoot, ".fusion");
}
export function isWithinProtectedFusionDir(pathValue: PathLike): boolean {
const protectedFusionDir = getProtectedFusionDir();
if (!protectedFusionDir) return false;
const candidate = resolveGuardPath(pathValue);
if (!candidate || candidate === ":memory:") return false;
return candidate === protectedFusionDir || candidate.startsWith(protectedFusionDir + sep);
}
export function assertOutsideRealFusionPath(pathValue: PathLike, context = "operation"): void {
const protectedFusionDir = getProtectedFusionDir();
if (!protectedFusionDir) return;
const candidate = resolveGuardPath(pathValue);
if (!candidate || candidate === ":memory:") return;
if (!isWithinProtectedFusionDir(candidate)) return;
throw new Error(
`[test-safety] ${context} targeted protected repo .fusion directory: ${candidate}\n` +
"Tests must operate inside a temp directory. Use tempWorkspace() or useIsolatedCwd().",
);
}

View File

@@ -1,5 +1,6 @@
import { defineConfig } from "vitest/config"; import { defineConfig } from "vitest/config";
import { cpus } from "node:os"; import { cpus } from "node:os";
import { resolve } from "node:path";
const defaultMaxWorkers = Math.max(1, cpus().length - 1); const defaultMaxWorkers = Math.max(1, cpus().length - 1);
const requestedMaxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS ?? String(defaultMaxWorkers), 10); const requestedMaxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS ?? String(defaultMaxWorkers), 10);
@@ -8,6 +9,8 @@ process.env.VITEST_MAX_WORKERS = String(maxWorkers);
export default defineConfig({ export default defineConfig({
test: { test: {
setupFiles: [resolve(__dirname, "../core/src/__test-utils__/vitest-setup.ts")],
globalSetup: [resolve(__dirname, "../core/src/__test-utils__/vitest-teardown.ts")],
testTimeout: 30_000, testTimeout: 30_000,
hookTimeout: 30_000, hookTimeout: 30_000,
maxWorkers, maxWorkers,

View File

@@ -1,5 +1,6 @@
import { defineConfig } from "vitest/config"; import { defineConfig } from "vitest/config";
import { cpus } from "node:os"; import { cpus } from "node:os";
import { resolve } from "node:path";
const defaultMaxWorkers = Math.max(1, cpus().length - 1); const defaultMaxWorkers = Math.max(1, cpus().length - 1);
const requestedMaxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS ?? String(defaultMaxWorkers), 10); const requestedMaxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS ?? String(defaultMaxWorkers), 10);
@@ -9,6 +10,8 @@ process.env.VITEST_MAX_WORKERS = String(maxWorkers);
export default defineConfig({ export default defineConfig({
test: { test: {
include: ["src/**/*.test.ts"], include: ["src/**/*.test.ts"],
setupFiles: [resolve(__dirname, "../core/src/__test-utils__/vitest-setup.ts")],
globalSetup: [resolve(__dirname, "../core/src/__test-utils__/vitest-teardown.ts")],
maxWorkers, maxWorkers,
poolOptions: { threads: { minThreads: 1, maxThreads: maxWorkers }, forks: { minForks: 1, maxForks: maxWorkers } }, poolOptions: { threads: { minThreads: 1, maxThreads: maxWorkers }, forks: { minForks: 1, maxForks: maxWorkers } },
fileParallelism: true, fileParallelism: true,

View File

@@ -1,9 +1,14 @@
import { defineConfig } from "vitest/config"; import { defineConfig } from "vitest/config";
import { resolve } from "node:path";
export default defineConfig({ export default defineConfig({
test: { test: {
globals: true, globals: true,
setupFiles: ["./src/__tests__/setup-test-isolation.ts"], setupFiles: [
"./src/__tests__/setup-test-isolation.ts",
resolve(__dirname, "../core/src/__test-utils__/vitest-setup.ts"),
],
globalSetup: [resolve(__dirname, "../core/src/__test-utils__/vitest-teardown.ts")],
coverage: { coverage: {
provider: "v8", provider: "v8",
reporter: ["text", "json-summary"], reporter: ["text", "json-summary"],

View File

@@ -1,5 +1,6 @@
import { defineConfig } from "vitest/config"; import { defineConfig } from "vitest/config";
import { cpus } from "node:os"; import { cpus } from "node:os";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
const defaultMaxWorkers = Math.max(1, cpus().length - 1); const defaultMaxWorkers = Math.max(1, cpus().length - 1);
@@ -15,6 +16,8 @@ export default defineConfig({
}, },
test: { test: {
include: ["src/**/*.test.ts"], include: ["src/**/*.test.ts"],
setupFiles: [resolve(__dirname, "../core/src/__test-utils__/vitest-setup.ts")],
globalSetup: [resolve(__dirname, "../core/src/__test-utils__/vitest-teardown.ts")],
pool: "threads", pool: "threads",
maxWorkers, maxWorkers,
poolOptions: { threads: { minThreads: 1, maxThreads: maxWorkers }, forks: { minForks: 1, maxForks: maxWorkers } }, poolOptions: { threads: { minThreads: 1, maxThreads: maxWorkers }, forks: { minForks: 1, maxForks: maxWorkers } },