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:
@@ -1,10 +1,15 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor, cleanup, act } from "@testing-library/react";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { FileBrowser } from "../FileBrowser";
|
||||
import type { FileNode } from "../../api";
|
||||
|
||||
// Resolve paths relative to this test file so tests pass regardless of cwd
|
||||
// (a global test safety guard may change cwd to a per-worker temp dir).
|
||||
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
|
||||
// ── Mocks ───────────────────────────────────────────────────────────────
|
||||
|
||||
vi.mock("lucide-react", async () => {
|
||||
@@ -370,7 +375,7 @@ describe("FileBrowser", () => {
|
||||
});
|
||||
|
||||
it("defines mobile-friendly touch targets for context menu items", () => {
|
||||
const cssPath = resolve(process.cwd(), "app/styles.css");
|
||||
const cssPath = resolve(PACKAGE_ROOT, "app/styles.css");
|
||||
const css = readFileSync(cssPath, "utf8");
|
||||
expect(css).toMatch(/\.file-browser-context-menu__item\s*\{[^}]*min-height:\s*36px;/);
|
||||
});
|
||||
|
||||
@@ -5,6 +5,12 @@ import type { Column, Task, TaskDetail } from "@fusion/core";
|
||||
import { TaskCard } from "../TaskCard";
|
||||
import React, { useState } from "react";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
// Resolve paths relative to this test file so tests pass regardless of cwd
|
||||
// (a global test safety guard may change cwd to a per-worker temp dir).
|
||||
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
|
||||
vi.mock("../../api", () => ({
|
||||
fetchTaskDetail: vi.fn(),
|
||||
@@ -3599,7 +3605,7 @@ describe("TaskCard agent badge", () => {
|
||||
expect(text).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const styles = readFileSync("app/styles.css", "utf-8");
|
||||
const styles = readFileSync(resolve(PACKAGE_ROOT, "app/styles.css"), "utf-8");
|
||||
expect(styles).toMatch(/\.card-agent-badge\s*\{[^}]*flex-shrink:\s*0;/);
|
||||
expect(styles).toMatch(/\.card-agent-badge\s*\{[^}]*max-width:\s*120px;/);
|
||||
expect(styles).toMatch(/\.card-agent-badge-text\s*\{[^}]*text-overflow:\s*ellipsis;/);
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { renderHook, act, waitFor } from "@testing-library/react";
|
||||
import { COLOR_THEMES, type Settings } from "@fusion/core";
|
||||
import { useTheme, getThemeInitScript } from "../useTheme";
|
||||
import { fetchGlobalSettings, updateGlobalSettings } from "../../api";
|
||||
|
||||
// Resolve paths relative to this test file so tests pass regardless of cwd
|
||||
// (a global test safety guard may change cwd to a per-worker temp dir).
|
||||
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
|
||||
vi.mock("../../api", () => ({
|
||||
fetchGlobalSettings: vi.fn(),
|
||||
updateGlobalSettings: vi.fn(),
|
||||
@@ -433,8 +439,8 @@ describe("useTheme", () => {
|
||||
it("applies factory-specific design tokens from the stylesheet", () => {
|
||||
// Load both base styles and theme data (theme blocks are in a separate file)
|
||||
const style = document.createElement("style");
|
||||
const baseCss = readFileSync("app/styles.css", "utf8");
|
||||
const themeDataCss = readFileSync("app/public/theme-data.css", "utf8");
|
||||
const baseCss = readFileSync(resolve(PACKAGE_ROOT, "app/styles.css"), "utf8");
|
||||
const themeDataCss = readFileSync(resolve(PACKAGE_ROOT, "app/public/theme-data.css"), "utf8");
|
||||
style.textContent = baseCss + "\n" + themeDataCss;
|
||||
document.head.appendChild(style);
|
||||
|
||||
@@ -1145,7 +1151,7 @@ describe("getThemeInitScript", () => {
|
||||
});
|
||||
|
||||
it("keeps index.html inline theme validation in sync with supported themes", () => {
|
||||
const indexHtml = readFileSync("app/index.html", "utf8");
|
||||
const indexHtml = readFileSync(resolve(PACKAGE_ROOT, "app/index.html"), "utf8");
|
||||
|
||||
COLOR_THEMES.forEach((theme) => {
|
||||
expect(indexHtml).toContain(`'${theme}'`);
|
||||
@@ -1164,7 +1170,7 @@ describe("getThemeInitScript", () => {
|
||||
it("index.html uses correct URL replacement pattern", () => {
|
||||
// Verify that the inline script in index.html uses the correct URL replacement
|
||||
// pattern (handle both directory paths and filename paths) rather than buggy concatenation
|
||||
const indexHtml = readFileSync("app/index.html", "utf8");
|
||||
const indexHtml = readFileSync(resolve(PACKAGE_ROOT, "app/index.html"), "utf8");
|
||||
|
||||
// The correct pattern: check if base ends with '/' and use slice or replace accordingly
|
||||
// The buggy pattern: base.substring(0, 7) + dirPath + 'theme-data.css'
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"jsx": "react-jsx",
|
||||
"types": ["node", "vitest/globals", "@testing-library/jest-dom"]
|
||||
"types": ["node", "vitest/globals", "@testing-library/jest-dom"],
|
||||
"paths": {
|
||||
"@fusion/test-utils": ["../core/src/__test-utils__/workspace.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/**/__tests__/**/*"]
|
||||
|
||||
@@ -13,13 +13,18 @@ export default defineConfig({
|
||||
alias: {
|
||||
"@fusion/core": resolve(__dirname, "../core/src/index.ts"),
|
||||
"@fusion/engine": resolve(__dirname, "../engine/src/index.ts"),
|
||||
"@fusion/test-utils": resolve(__dirname, "../core/src/__test-utils__/workspace.ts"),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
globals: true,
|
||||
include: ["app/**/*.test.{ts,tsx}", "src/**/*.test.{ts,tsx}"],
|
||||
setupFiles: ["./vitest.setup.ts"],
|
||||
setupFiles: [
|
||||
resolve(__dirname, "../core/src/__test-utils__/vitest-setup.ts"),
|
||||
"./vitest.setup.ts",
|
||||
],
|
||||
globalSetup: [resolve(__dirname, "../core/src/__test-utils__/vitest-teardown.ts")],
|
||||
maxWorkers,
|
||||
poolOptions: { threads: { minThreads: 1, maxThreads: maxWorkers }, forks: { minForks: 1, maxForks: maxWorkers } },
|
||||
fileParallelism: true,
|
||||
|
||||
Reference in New Issue
Block a user