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>
This commit is contained in:
99
scripts/check-test-isolation.mjs
Executable file
99
scripts/check-test-isolation.mjs
Executable file
@@ -0,0 +1,99 @@
|
||||
#!/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();
|
||||
}
|
||||
Reference in New Issue
Block a user