Introduce a shared test-utils module and global vitest setup that guarantee tests never write to the real .fusion directory or leak temp directories under /tmp. Infrastructure: - packages/core/src/__test-utils__/workspace.ts — tempWorkspace(), useIsolatedCwd(), trackForCleanup(), assertOutsideRealFusion() with auto-cleanup in afterEach. - packages/core/src/__test-utils__/vitest-setup.ts — per-worker guard: chdirs each worker into an isolated tmp dir, wraps process.chdir to refuse the real .fusion, scopes tmp dirs under fusion-test-workers/ (skips cwd change in thread-pool workers where chdir isn't supported). - packages/core/src/__test-utils__/vitest-teardown.ts — globalSetup hook that wipes the shared parent even when workers are SIGKILLed. - scripts/check-test-isolation.mjs + `test:isolated` / `test:check- isolation` scripts for CI. - @fusion/test-utils alias + setupFiles + globalSetup wired into core, cli, engine, dashboard, tui vitest configs; matching tsconfig paths. Test refactors (no behavior change): - cli provider-settings, auth-paths, provider-auth — switch leaking mkdtempSync calls to tempWorkspace(). - core migration, first-run, store-backward-compat — replace manual process.chdir save/restore with useIsolatedCwd(). - tui fusion-context — replace 9 hardcoded tmp paths (collision-prone under parallelism) with tempWorkspace(). - dashboard useTheme, FileBrowser, TaskCard — resolve source-file reads against a PACKAGE_ROOT computed from import.meta.url instead of cwd, so tests don't depend on the process working directory. Verified: full suite (~15,500 tests across 8 packages + plugins) passes and the orphan-detector reports zero leaked temp directories after a complete run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
100 lines
3.0 KiB
JavaScript
Executable File
100 lines
3.0 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* Verifies that the test suite doesn't leak temp directories or touch the
|
|
* real .fusion directory.
|
|
*
|
|
* Usage:
|
|
* node scripts/check-test-isolation.mjs [--before]
|
|
*
|
|
* --before Record baseline state before running tests (writes /tmp/.fusion-isolation-baseline).
|
|
* (default) Compare current state to baseline and fail on leaks.
|
|
*
|
|
* Integration:
|
|
* node scripts/check-test-isolation.mjs --before
|
|
* pnpm test
|
|
* node scripts/check-test-isolation.mjs
|
|
*/
|
|
|
|
import { readdirSync, statSync, existsSync, writeFileSync, readFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
const BASELINE_FILE = join(tmpdir(), ".fusion-isolation-baseline");
|
|
|
|
// Prefixes the test suite is allowed to create under /tmp. Any dir matching
|
|
// one of these must be cleaned up by the end of the test run.
|
|
const TRACKED_PREFIXES = [
|
|
"fusion-worker-",
|
|
"fusion-test-",
|
|
"fusion-test-cwd-",
|
|
"fusion-provider-settings-",
|
|
"fusion-provider-auth-",
|
|
"fusion-provider-auth-oauth-",
|
|
"fusion-agent-dir-",
|
|
"kb-db-test-",
|
|
"kb-backup-test-",
|
|
"kb-migration-test-",
|
|
"kb-fresh-",
|
|
"kb-needs-migration-",
|
|
"kb-compat-test-",
|
|
"kb-first-run-test-",
|
|
];
|
|
|
|
function snapshotTmp() {
|
|
const entries = readdirSync(tmpdir());
|
|
const matching = [];
|
|
for (const name of entries) {
|
|
if (!TRACKED_PREFIXES.some((p) => name.startsWith(p))) continue;
|
|
const full = join(tmpdir(), name);
|
|
try {
|
|
const stat = statSync(full);
|
|
if (stat.isDirectory()) {
|
|
matching.push({ name, mtime: stat.mtimeMs });
|
|
}
|
|
} catch {
|
|
// Ignore — could be gone already.
|
|
}
|
|
}
|
|
return matching;
|
|
}
|
|
|
|
function recordBaseline() {
|
|
const baseline = snapshotTmp();
|
|
writeFileSync(BASELINE_FILE, JSON.stringify(baseline.map((e) => e.name)));
|
|
console.log(`[test-isolation] Baseline recorded: ${baseline.length} existing dir(s) matched patterns.`);
|
|
}
|
|
|
|
function checkAgainstBaseline() {
|
|
let baselineNames = new Set();
|
|
if (existsSync(BASELINE_FILE)) {
|
|
try {
|
|
baselineNames = new Set(JSON.parse(readFileSync(BASELINE_FILE, "utf-8")));
|
|
} catch {
|
|
// Ignore malformed baseline.
|
|
}
|
|
}
|
|
const current = snapshotTmp();
|
|
const leaks = current.filter((e) => !baselineNames.has(e.name));
|
|
if (leaks.length === 0) {
|
|
console.log("[test-isolation] No leaked temp directories detected.");
|
|
process.exit(0);
|
|
}
|
|
console.error(`[test-isolation] FAIL: ${leaks.length} leaked temp director${leaks.length === 1 ? "y" : "ies"}:`);
|
|
for (const leak of leaks) {
|
|
console.error(` ${join(tmpdir(), leak.name)}`);
|
|
}
|
|
console.error("");
|
|
console.error("Tests must clean up their temp directories. Use helpers from");
|
|
console.error(" packages/core/src/__test-utils__/workspace.ts (@fusion/test-utils)");
|
|
console.error(" - tempWorkspace(prefix) — auto-cleaned in afterEach");
|
|
console.error(" - useIsolatedCwd(prefix) — auto-cleaned + cwd restored");
|
|
process.exit(1);
|
|
}
|
|
|
|
const args = process.argv.slice(2);
|
|
if (args.includes("--before")) {
|
|
recordBaseline();
|
|
} else {
|
|
checkAgainstBaseline();
|
|
}
|