Files
fusion/scripts/check-test-isolation.mjs
gsxdsm bc841e9bf6 test: enforce test-directory isolation across all packages
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>
2026-04-18 23:41:47 -07:00

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