feat(FN-4018): isolate split merger temp workspaces with settings persisten

Merged FN-4014 to fix dual-scope GitHub tracking settings persistence and validation (mission vs task scope), scoped repo saves to the relevant section, and documented the dual defaults. Also landed FN-4018 to isolate split merger temp workspaces and add regression checks for split-suite test isolat

Fusion-Task-Id: FN-4018
This commit is contained in:
Fusion
2026-05-11 11:36:39 -07:00
committed by gsxdsm
parent e441398208
commit 1cf987208c
16 changed files with 160 additions and 25 deletions

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { join, resolve } from "node:path";
import { tmpdir } from "node:os";
import { execSync } from "node:child_process";
import { commitOrAmendMergeWithFixes } from "../merger.js";
@@ -20,6 +20,12 @@ function initRepo(dir: string): void {
git(dir, 'git commit -m "chore: initial"');
}
function assertIsolatedWorkspace(dir: string): void {
const repoRoot = process.env.FUSION_TEST_REAL_ROOT;
if (!repoRoot) return;
expect(resolve(dir).startsWith(resolve(repoRoot))).toBe(false);
}
function runFinalize(dir: string, taskId: string, branch: string, preAttemptHeadSha: string) {
return commitOrAmendMergeWithFixes(
dir,
@@ -45,7 +51,8 @@ describe("commitOrAmendMergeWithFixes ancestor/equivalent-content short-circuit"
let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "fn-ancestor-shortcircuit-"));
dir = mkdtempSync(join(tmpdir(), "fusion-test-merger-ancestor-"));
assertIsolatedWorkspace(dir);
initRepo(dir);
});

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { join, resolve } from "node:path";
import { tmpdir } from "node:os";
import { execSync } from "node:child_process";
import type { TaskStore } from "@fusion/core";
@@ -44,6 +44,12 @@ function stashList(dir: string): string {
return git(dir, 'git stash list --format="%H %gd %s"');
}
function assertIsolatedWorkspace(dir: string): void {
const repoRoot = process.env.FUSION_TEST_REAL_ROOT;
if (!repoRoot) return;
expect(resolve(dir).startsWith(resolve(repoRoot))).toBe(false);
}
function makeStore(tasks: Record<string, string>, opts?: { throwOnGetTask?: boolean }): TaskStore {
return {
getTask: async (taskId: string) => {
@@ -69,7 +75,8 @@ describe("sweepStaleAutostashes", () => {
let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "fn-autostash-stale-"));
dir = mkdtempSync(join(tmpdir(), "fusion-test-merger-autostash-stale-"));
assertIsolatedWorkspace(dir);
initRepo(dir);
});
@@ -145,7 +152,8 @@ describe("sweepAutostashOrphans", () => {
let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "fn-autostash-"));
dir = mkdtempSync(join(tmpdir(), "fusion-test-merger-autostash-"));
assertIsolatedWorkspace(dir);
initRepo(dir);
});

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { join, resolve } from "node:path";
import { tmpdir } from "node:os";
import { execSync } from "node:child_process";
import { __test__ } from "../merger.js";
@@ -38,11 +38,18 @@ function createAutostash(dir: string, label: string, content: string): string {
return sha;
}
function assertIsolatedWorkspace(dir: string): void {
const repoRoot = process.env.FUSION_TEST_REAL_ROOT;
if (!repoRoot) return;
expect(resolve(dir).startsWith(resolve(repoRoot))).toBe(false);
}
describe("autostash orphan surface", () => {
let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "fn-autostash-surface-"));
dir = mkdtempSync(join(tmpdir(), "fusion-test-merger-autostash-surface-"));
assertIsolatedWorkspace(dir);
initRepo(dir);
});

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { mkdtempSync, rmSync, writeFileSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { join, resolve } from "node:path";
import { tmpdir } from "node:os";
import { execSync } from "node:child_process";
import { commitOrAmendMergeWithFixes } from "../merger.js";
@@ -33,6 +33,12 @@ function stageSquashThenClear(dir: string, branch: string, file: string, content
return preAttemptSha;
}
function assertIsolatedWorkspace(dir: string): void {
const repoRoot = process.env.FUSION_TEST_REAL_ROOT;
if (!repoRoot) return;
expect(resolve(dir).startsWith(resolve(repoRoot))).toBe(false);
}
const STUB_SETTINGS = {
...DEFAULT_SETTINGS,
commitAuthorEnabled: false,
@@ -42,7 +48,8 @@ describe("commitOrAmendMergeWithFixes no-op finalize", () => {
let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "fn-noop-finalize-"));
dir = mkdtempSync(join(tmpdir(), "fusion-test-merger-noop-"));
assertIsolatedWorkspace(dir);
initRepo(dir);
});

View File

@@ -17,7 +17,7 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { join, resolve } from "node:path";
import { tmpdir } from "node:os";
import { execSync } from "node:child_process";
import { snapshotDirtyFiles, commitOrAmendMergeWithFixes } from "../merger.js";
@@ -74,6 +74,12 @@ function squashBranch(dir: string, branchName: string, fileName: string, content
// Minimal stub settings / args used by commitOrAmendMergeWithFixes
// ---------------------------------------------------------------------------
function assertIsolatedWorkspace(dir: string): void {
const repoRoot = process.env.FUSION_TEST_REAL_ROOT;
if (!repoRoot) return;
expect(resolve(dir).startsWith(resolve(repoRoot))).toBe(false);
}
const STUB_SETTINGS = {
...DEFAULT_SETTINGS,
commitAuthorEnabled: false, // skip --author flag to avoid user config issues
@@ -87,7 +93,8 @@ describe("snapshotDirtyFiles", () => {
let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "fn-snapshot-"));
dir = mkdtempSync(join(tmpdir(), "fusion-test-merger-snapshot-"));
assertIsolatedWorkspace(dir);
initRepo(dir);
});
@@ -135,7 +142,8 @@ describe("snapshotDirtyFiles", () => {
});
it("returns empty set when rootDir is not a git repo (error swallowed)", async () => {
const nonRepo = mkdtempSync(join(tmpdir(), "fn-non-repo-"));
const nonRepo = mkdtempSync(join(tmpdir(), "fusion-test-merger-non-repo-"));
assertIsolatedWorkspace(nonRepo);
try {
const snapshot = await snapshotDirtyFiles(nonRepo);
expect(snapshot.size).toBe(0);
@@ -150,7 +158,8 @@ describe("commitOrAmendMergeWithFixes — staging allowlist", () => {
let warnSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "fn-allowlist-"));
dir = mkdtempSync(join(tmpdir(), "fusion-test-merger-allowlist-"));
assertIsolatedWorkspace(dir);
initRepo(dir);
warnSpy = vi.spyOn(mergerLog, "warn");
});
@@ -480,7 +489,8 @@ describe("snapshotDirtyFiles — paths with embedded spaces", () => {
let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "fn-snapshot-spaces-"));
dir = mkdtempSync(join(tmpdir(), "fusion-test-merger-snapshot-spaces-"));
assertIsolatedWorkspace(dir);
initRepo(dir);
});
@@ -532,7 +542,8 @@ describe("commitOrAmendMergeWithFixes — embedded-space paths round-trip", () =
let warnSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "fn-allowlist-spaces-"));
dir = mkdtempSync(join(tmpdir(), "fusion-test-merger-allowlist-spaces-"));
assertIsolatedWorkspace(dir);
initRepo(dir);
warnSpy = vi.spyOn(mergerLog, "warn");
});

View File

@@ -1,6 +1,8 @@
import { describe, expect, it } from "vitest";
import { tmpdir } from "node:os";
import { getFusionAuthPath } from "../auth-storage.js";
import { mkdtempSync, rmSync } from "node:fs";
import { join, resolve } from "node:path";
describe("test isolation guard", () => {
it("overrides HOME to a temp fn-test-home directory", () => {
@@ -20,4 +22,15 @@ describe("test isolation guard", () => {
expect(authPath.startsWith(home!)).toBe(true);
expect(authPath).toContain(".fusion");
});
it("creates temp workspaces outside the real repo root", () => {
const workspace = mkdtempSync(join(tmpdir(), "fusion-test-guard-workspace-"));
try {
const repoRoot = process.env.FUSION_TEST_REAL_ROOT;
expect(repoRoot).toBeDefined();
expect(resolve(workspace).startsWith(resolve(repoRoot!))).toBe(false);
} finally {
rmSync(workspace, { recursive: true, force: true });
}
});
});