feat(FN-2606): merge fusion/fn-2606 (auto-resolved)
- fix(FN-2606): complete Step 4 — clean up mkdtemp test directories - test(FN-2606): complete Step 3 — add isolation guard coverage - feat(FN-2606): complete Step 2 — add engine/dashboard HOME test isolation setup
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const TEMP_HOME_PREFIX = "fn-test-home-";
|
||||
|
||||
@@ -26,4 +28,20 @@ describe("test isolation setup", () => {
|
||||
expect(dir).toContain(tmpdir());
|
||||
expect(dir).toMatch(/fn-test-home-.*[\\/]\.fusion$/);
|
||||
});
|
||||
|
||||
it("GlobalSettingsStore() without explicit dir throws under VITEST guard", async () => {
|
||||
const { GlobalSettingsStore } = await import("../global-settings.js");
|
||||
|
||||
expect(() => new GlobalSettingsStore()).toThrow(
|
||||
"resolveGlobalDir() called without explicit dir during test execution. Pass a temp directory to avoid writing to real ~/.fusion/",
|
||||
);
|
||||
});
|
||||
|
||||
it("cwd is not inside the repository .fusion directory", () => {
|
||||
const thisFile = fileURLToPath(import.meta.url);
|
||||
const repoRoot = resolve(dirname(thisFile), "../../../../");
|
||||
const repoFusionDir = join(repoRoot, ".fusion");
|
||||
|
||||
expect(process.cwd().startsWith(repoFusionDir)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
|
||||
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
PRIORITY_SCRIPTS,
|
||||
detectDevServerCommands,
|
||||
@@ -23,8 +23,21 @@ async function writePackageJson(
|
||||
}
|
||||
|
||||
describe("devserver-detect", () => {
|
||||
const tempRoots = new Set<string>();
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(Array.from(tempRoots).map((root) => rm(root, { recursive: true, force: true })));
|
||||
tempRoots.clear();
|
||||
});
|
||||
|
||||
const createTempRoot = async (prefix: string) => {
|
||||
const root = await mkdtemp(join(tmpdir(), prefix));
|
||||
tempRoots.add(root);
|
||||
return root;
|
||||
};
|
||||
|
||||
it("detects priority scripts from root package.json", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "devserver-detect-"));
|
||||
const root = await createTempRoot("devserver-detect-");
|
||||
await writePackageJson(root, {
|
||||
scripts: {
|
||||
dev: "vite",
|
||||
@@ -46,7 +59,7 @@ describe("devserver-detect", () => {
|
||||
});
|
||||
|
||||
it("only returns priority scripts", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "devserver-detect-"));
|
||||
const root = await createTempRoot("devserver-detect-");
|
||||
await writePackageJson(root, {
|
||||
scripts: {
|
||||
dev: "vite",
|
||||
@@ -62,7 +75,7 @@ describe("devserver-detect", () => {
|
||||
});
|
||||
|
||||
it("scans nested package.json files in apps/* and packages/*", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "devserver-detect-"));
|
||||
const root = await createTempRoot("devserver-detect-");
|
||||
await writePackageJson(root, { scripts: {} });
|
||||
await writePackageJson(
|
||||
root,
|
||||
@@ -82,7 +95,7 @@ describe("devserver-detect", () => {
|
||||
});
|
||||
|
||||
it("sorts results by PRIORITY_SCRIPTS order", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "devserver-detect-"));
|
||||
const root = await createTempRoot("devserver-detect-");
|
||||
expect(PRIORITY_SCRIPTS.indexOf("dev")).toBeLessThan(PRIORITY_SCRIPTS.indexOf("serve"));
|
||||
await writePackageJson(root, {
|
||||
scripts: {
|
||||
@@ -99,7 +112,7 @@ describe("devserver-detect", () => {
|
||||
});
|
||||
|
||||
it("returns empty array when scripts is empty", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "devserver-detect-"));
|
||||
const root = await createTempRoot("devserver-detect-");
|
||||
await writePackageJson(root, { scripts: {} });
|
||||
|
||||
const detected = await detectDevServerCommands(root);
|
||||
@@ -107,14 +120,14 @@ describe("devserver-detect", () => {
|
||||
});
|
||||
|
||||
it("returns empty array when root package.json is missing", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "devserver-detect-"));
|
||||
const root = await createTempRoot("devserver-detect-");
|
||||
|
||||
const detected = await detectDevServerCommands(root);
|
||||
expect(detected).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns empty array for malformed package.json without throwing", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "devserver-detect-"));
|
||||
const root = await createTempRoot("devserver-detect-");
|
||||
const filePath = join(root, "package.json");
|
||||
await mkdir(dirname(filePath), { recursive: true });
|
||||
await writeFile(filePath, '{ "invalid json', "utf-8");
|
||||
@@ -124,7 +137,7 @@ describe("devserver-detect", () => {
|
||||
});
|
||||
|
||||
it("deeply nested package.json (2+ levels) is NOT scanned", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "devserver-detect-"));
|
||||
const root = await createTempRoot("devserver-detect-");
|
||||
await writePackageJson(root, { scripts: {} });
|
||||
// Create a deeply nested package.json (2 levels deep)
|
||||
await writePackageJson(
|
||||
@@ -144,7 +157,7 @@ describe("devserver-detect", () => {
|
||||
});
|
||||
|
||||
it("persists and reloads devserver configs", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "devserver-config-"));
|
||||
const root = await createTempRoot("devserver-config-");
|
||||
const configs: DevServerConfig[] = [
|
||||
{
|
||||
id: createDevServerId("cfg-1"),
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { loadDevServerConfigs, saveDevServerConfigs } from "../devserver-persistence.js";
|
||||
import { createDevServerId, type DevServerConfig } from "../devserver-types.js";
|
||||
|
||||
describe("devserver-persistence", () => {
|
||||
const tempRoots = new Set<string>();
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(Array.from(tempRoots).map((root) => rm(root, { recursive: true, force: true })));
|
||||
tempRoots.clear();
|
||||
});
|
||||
|
||||
const createTempRoot = async (prefix: string) => {
|
||||
const root = await mkdtemp(join(tmpdir(), prefix));
|
||||
tempRoots.add(root);
|
||||
return root;
|
||||
};
|
||||
|
||||
it("saves and reloads multiple dev server configs", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "devserver-persist-"));
|
||||
const root = await createTempRoot("devserver-persist-");
|
||||
const configs: DevServerConfig[] = [
|
||||
{
|
||||
id: createDevServerId("server-1"),
|
||||
@@ -46,7 +58,7 @@ describe("devserver-persistence", () => {
|
||||
});
|
||||
|
||||
it("preserves exact config properties on reload", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "devserver-persist-exact-"));
|
||||
const root = await createTempRoot("devserver-persist-exact-");
|
||||
const config: DevServerConfig = {
|
||||
id: createDevServerId("exact-test"),
|
||||
name: "Exact Config",
|
||||
@@ -66,14 +78,14 @@ describe("devserver-persistence", () => {
|
||||
});
|
||||
|
||||
it("returns empty array when no configs have been saved", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "devserver-persist-empty-"));
|
||||
const root = await createTempRoot("devserver-persist-empty-");
|
||||
|
||||
const loaded = await loadDevServerConfigs(root);
|
||||
expect(loaded).toEqual([]);
|
||||
});
|
||||
|
||||
it("handles corrupted devserver.json gracefully", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "devserver-persist-corrupt-"));
|
||||
const root = await createTempRoot("devserver-persist-corrupt-");
|
||||
const configPath = join(root, ".fusion", "devserver.json");
|
||||
await mkdir(dirname(configPath), { recursive: true });
|
||||
await writeFile(configPath, '{ "invalid json', "utf-8");
|
||||
@@ -83,7 +95,7 @@ describe("devserver-persistence", () => {
|
||||
});
|
||||
|
||||
it("handles empty devserver.json", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "devserver-persist-empty-file-"));
|
||||
const root = await createTempRoot("devserver-persist-empty-file-");
|
||||
const configPath = join(root, ".fusion", "devserver.json");
|
||||
await mkdir(dirname(configPath), { recursive: true });
|
||||
await writeFile(configPath, '', "utf-8");
|
||||
@@ -93,7 +105,7 @@ describe("devserver-persistence", () => {
|
||||
});
|
||||
|
||||
it("handles devserver.json with missing configs array", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "devserver-persist-missing-"));
|
||||
const root = await createTempRoot("devserver-persist-missing-");
|
||||
const configPath = join(root, ".fusion", "devserver.json");
|
||||
await mkdir(dirname(configPath), { recursive: true });
|
||||
await writeFile(configPath, '{"other": "data"}', "utf-8");
|
||||
@@ -103,7 +115,7 @@ describe("devserver-persistence", () => {
|
||||
});
|
||||
|
||||
it("persists three or more servers correctly", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "devserver-persist-multi-"));
|
||||
const root = await createTempRoot("devserver-persist-multi-");
|
||||
const configs: DevServerConfig[] = [
|
||||
{ id: createDevServerId("multi-1"), name: "Server 1", command: "npm run dev", cwd: root },
|
||||
{ id: createDevServerId("multi-2"), name: "Server 2", command: "npm run storybook", cwd: root },
|
||||
@@ -119,7 +131,7 @@ describe("devserver-persistence", () => {
|
||||
});
|
||||
|
||||
it("filters out invalid configs during load", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "devserver-persist-filter-"));
|
||||
const root = await createTempRoot("devserver-persist-filter-");
|
||||
const configPath = join(root, ".fusion", "devserver.json");
|
||||
await mkdir(dirname(configPath), { recursive: true });
|
||||
// Write manually with an invalid config (missing required fields)
|
||||
@@ -144,7 +156,7 @@ describe("devserver-persistence", () => {
|
||||
});
|
||||
|
||||
it("overwrites existing configs when saving", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "devserver-persist-overwrite-"));
|
||||
const root = await createTempRoot("devserver-persist-overwrite-");
|
||||
|
||||
const initialConfigs: DevServerConfig[] = [
|
||||
{ id: createDevServerId("overwrite-1"), name: "Initial", command: "npm run dev", cwd: root },
|
||||
|
||||
12
packages/dashboard/src/__tests__/setup-test-isolation.ts
Normal file
12
packages/dashboard/src/__tests__/setup-test-isolation.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Global test isolation: prevents dashboard tests from writing to the real ~/.fusion/ directory.
|
||||
*
|
||||
* This runs in every Vitest worker before shared setup. By forcing process.env.HOME
|
||||
* to a fresh temp directory, homedir()-derived paths resolve to isolated locations.
|
||||
*/
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const tempHome = mkdtempSync(join(tmpdir(), "fn-test-home-"));
|
||||
process.env.HOME = tempHome;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
describe("test isolation guard", () => {
|
||||
it("overrides HOME to a temp fn-test-home directory", () => {
|
||||
const home = process.env.HOME;
|
||||
|
||||
expect(home).toBeDefined();
|
||||
expect(home).toContain(tmpdir());
|
||||
expect(home).toContain("fn-test-home-");
|
||||
});
|
||||
|
||||
it("does not run from the real project root cwd", () => {
|
||||
const thisFile = fileURLToPath(import.meta.url);
|
||||
const projectRoot = resolve(dirname(thisFile), "../../../../");
|
||||
const projectFusionDir = join(projectRoot, ".fusion");
|
||||
|
||||
expect(process.cwd()).not.toBe(projectRoot);
|
||||
expect(process.cwd().startsWith(projectFusionDir)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,7 @@ export default defineConfig({
|
||||
globals: true,
|
||||
include: ["app/**/*.test.{ts,tsx}", "src/**/*.test.{ts,tsx}"],
|
||||
setupFiles: [
|
||||
"./src/__tests__/setup-test-isolation.ts",
|
||||
resolve(__dirname, "../core/src/__test-utils__/vitest-setup.ts"),
|
||||
"./vitest.setup.ts",
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { mkdirSync, writeFileSync, existsSync } from "node:fs";
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { createFusionAuthStorage, getFusionAuthPath } from "../auth-storage.js";
|
||||
@@ -15,7 +15,11 @@ describe("createFusionAuthStorage", () => {
|
||||
process.env.HOME = homeDir;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
if (homeDir) {
|
||||
await rm(homeDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
if (originalHome === undefined) {
|
||||
delete process.env.HOME;
|
||||
} else {
|
||||
|
||||
@@ -191,7 +191,7 @@ import { TaskExecutor, buildExecutionPrompt } from "../executor.js";
|
||||
import { createFnAgent } from "../pi.js";
|
||||
import { reviewStep as mockedReviewStepFn } from "../reviewer.js";
|
||||
import { execSync } from "node:child_process";
|
||||
import { mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { findWorktreeUser, aiMergeTask } from "../merger.js";
|
||||
@@ -8242,6 +8242,7 @@ describe("Workflow Steps Execution", () => {
|
||||
expect(promptContent).toContain("Quality gate hard failure");
|
||||
|
||||
vi.useRealTimers();
|
||||
await rm(tempRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("skips script-mode step when scriptName is missing", async () => {
|
||||
|
||||
12
packages/engine/src/__tests__/setup-test-isolation.ts
Normal file
12
packages/engine/src/__tests__/setup-test-isolation.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Global test isolation: prevents engine tests from writing to the real ~/.fusion/ directory.
|
||||
*
|
||||
* This runs in every Vitest worker before shared setup. By forcing process.env.HOME
|
||||
* to a fresh temp directory, homedir()-derived paths resolve to isolated locations.
|
||||
*/
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const tempHome = mkdtempSync(join(tmpdir(), "fn-test-home-"));
|
||||
process.env.HOME = tempHome;
|
||||
23
packages/engine/src/__tests__/test-isolation-guard.test.ts
Normal file
23
packages/engine/src/__tests__/test-isolation-guard.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { tmpdir } from "node:os";
|
||||
import { getFusionAuthPath } from "../auth-storage.js";
|
||||
|
||||
describe("test isolation guard", () => {
|
||||
it("overrides HOME to a temp fn-test-home directory", () => {
|
||||
const home = process.env.HOME;
|
||||
|
||||
expect(home).toBeDefined();
|
||||
expect(home).toContain(tmpdir());
|
||||
expect(home).toContain("fn-test-home-");
|
||||
});
|
||||
|
||||
it("resolves Fusion auth path under temp HOME", () => {
|
||||
const home = process.env.HOME;
|
||||
const authPath = getFusionAuthPath();
|
||||
|
||||
expect(home).toBeDefined();
|
||||
expect(authPath).toContain("fn-test-home-");
|
||||
expect(authPath.startsWith(home!)).toBe(true);
|
||||
expect(authPath).toContain(".fusion");
|
||||
});
|
||||
});
|
||||
@@ -16,7 +16,10 @@ export default defineConfig({
|
||||
},
|
||||
test: {
|
||||
include: ["src/**/*.test.ts"],
|
||||
setupFiles: [resolve(__dirname, "../core/src/__test-utils__/vitest-setup.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")],
|
||||
maxWorkers,
|
||||
poolOptions: { threads: { minThreads: 1, maxThreads: maxWorkers }, forks: { minForks: 1, maxForks: maxWorkers } },
|
||||
|
||||
Reference in New Issue
Block a user