feat(FN-5348): remove cwd-main integration mode fallback
Removes the `cwd-main` integration fallback mode (FN-5348), eliminating the legacy shortcut path where the merger would operate directly on the project root instead of a dedicated worktree. Steps normalize the `reuse-task-worktree` integration mode as the sole path, wire stricter mode invariants in Fusion-Task-Id: FN-5348
This commit is contained in:
committed by
gsxdsm
parent
79850b233f
commit
b3f995c857
@@ -0,0 +1,131 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("../pi.js", () => ({
|
||||
createFnAgent: vi.fn(async () => ({
|
||||
prompt: vi.fn(async () => undefined),
|
||||
dispose: vi.fn(async () => undefined),
|
||||
})),
|
||||
describeModel: vi.fn(() => "mock-provider/mock-model"),
|
||||
promptWithFallback: vi.fn(async (session: { prompt: (prompt: string) => Promise<unknown> }, prompt: string) => {
|
||||
await session.prompt(prompt);
|
||||
}),
|
||||
compactSessionContext: vi.fn(),
|
||||
}));
|
||||
|
||||
import { aiMergeTask } from "../merger.js";
|
||||
import { mergerLog } from "../logger.js";
|
||||
import { resolveMergeIntegrationRoot } from "../merger-integration-worktree.js";
|
||||
import { git, hasGit, makeReliabilityFixture } from "./reliability-interactions/_helpers.js";
|
||||
|
||||
describe("FN-5348 cwd integration fallback removed", () => {
|
||||
it.skipIf(!hasGit)("Scenario A/B: dirty refusal keeps integration ref unchanged and emits refusal audit on master", async () => {
|
||||
const fixture = await makeReliabilityFixture({
|
||||
taskId: "FN-5348-DIRTY-REFUSAL",
|
||||
settings: {
|
||||
baseBranch: "master",
|
||||
mergeIntegrationWorktree: "reuse-task-worktree",
|
||||
} as any,
|
||||
});
|
||||
|
||||
try {
|
||||
const { rootDir, store, task } = fixture;
|
||||
const actualTask = await store.getTask(task.id);
|
||||
const branch = `fusion/${actualTask!.id.toLowerCase()}`;
|
||||
const worktreeRoot = `${rootDir}-worktrees`;
|
||||
const worktreePath = join(worktreeRoot, actualTask!.id.toLowerCase());
|
||||
|
||||
git(rootDir, "git branch -m main master");
|
||||
const completedSteps = (actualTask?.steps ?? []).map((step) => ({ ...step, status: "done" as const }));
|
||||
await store.updateTask(task.id, {
|
||||
baseBranch: "master",
|
||||
branch,
|
||||
steps: completedSteps,
|
||||
currentStep: completedSteps.length,
|
||||
} as any);
|
||||
await fixture.createBranch(branch);
|
||||
await fixture.writeAndCommit("packages/engine/src/fn-5348-dirty.ts", "export const dirty = true;\n", "feat: add dirty refusal content");
|
||||
await fixture.checkout("master");
|
||||
git(rootDir, `git worktree add ${JSON.stringify(worktreePath)} ${JSON.stringify(branch)}`);
|
||||
await store.updateTask(task.id, { worktree: worktreePath, branch } as any);
|
||||
store.enqueueMergeQueue(task.id);
|
||||
git(worktreePath, "sh -c 'printf dirty > DIRTY.txt'");
|
||||
|
||||
const integrationBefore = git(rootDir, "git rev-parse refs/heads/master");
|
||||
await expect(aiMergeTask(store, rootDir, task.id)).rejects.toMatchObject({
|
||||
name: "MergeHandoffRefusedError",
|
||||
gate: "working-tree-dirty",
|
||||
reason: "dirty-worktree",
|
||||
});
|
||||
const integrationAfter = git(rootDir, "git rev-parse refs/heads/master");
|
||||
expect(integrationAfter).toBe(integrationBefore);
|
||||
|
||||
const refused = store.getRunAuditEvents({ taskId: task.id }).filter((event) => event.mutationType === "merge:reuse-handoff-refused");
|
||||
expect(refused).toHaveLength(1);
|
||||
expect(refused[0]?.metadata).toMatchObject({ gate: "working-tree-dirty", reason: "dirty-worktree" });
|
||||
expect(refused[0]?.metadata?.integrationBranch).toBeUndefined();
|
||||
expect(JSON.stringify(refused[0]?.metadata ?? {})).not.toContain("\"main\"");
|
||||
const latest = await store.getTask(task.id);
|
||||
expect(latest?.column).toBe("in-review");
|
||||
// aiMergeTask rethrows refusal; upstream project-engine catch maps this to status=failed.
|
||||
expect(JSON.stringify({ gate: "working-tree-dirty", reason: "dirty-worktree" })).toContain("dirty-worktree");
|
||||
} finally {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
it("Scenario C: worktrunk no longer forces cwd mode", () => {
|
||||
const root = resolveMergeIntegrationRoot({
|
||||
task: { id: "FN-5348", worktree: "/tmp/task-worktree" } as any,
|
||||
settings: { mergeIntegrationWorktree: "reuse-task-worktree", worktrunk: { enabled: true } } as any,
|
||||
projectRoot: "/tmp/project-root",
|
||||
});
|
||||
expect(root.mode).toBe("reuse-task-worktree");
|
||||
});
|
||||
|
||||
it.skipIf(!hasGit)("Scenario D: explicit opt-in (legacy alias) emits warning", async () => {
|
||||
const fixture = await makeReliabilityFixture({
|
||||
taskId: "FN-5348-CWD-OPTIN",
|
||||
settings: {
|
||||
baseBranch: "master",
|
||||
mergeIntegrationWorktree: "cwd-main",
|
||||
} as any,
|
||||
});
|
||||
|
||||
const warnSpy = vi.spyOn(mergerLog, "warn").mockImplementation(() => undefined as any);
|
||||
|
||||
try {
|
||||
const { rootDir, store, task } = fixture;
|
||||
const actualTask = await store.getTask(task.id);
|
||||
const branch = `fusion/${actualTask!.id.toLowerCase()}`;
|
||||
|
||||
git(rootDir, "git branch -m main master");
|
||||
const completedSteps = (actualTask?.steps ?? []).map((step) => ({ ...step, status: "done" as const }));
|
||||
await store.updateTask(task.id, {
|
||||
baseBranch: "master",
|
||||
branch,
|
||||
steps: completedSteps,
|
||||
currentStep: completedSteps.length,
|
||||
} as any);
|
||||
await fixture.createBranch(branch);
|
||||
await fixture.writeAndCommit("packages/engine/src/fn-5348-optin.ts", "export const optin = true;\n", "feat: add cwd opt-in merge content");
|
||||
await fixture.checkout("master");
|
||||
|
||||
const result = await aiMergeTask(store, rootDir, task.id);
|
||||
expect(result.merged).toBe(true);
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("mergeIntegrationWorktree=cwd-integration-branch is explicit opt-in"));
|
||||
const auditTypes = store.getRunAuditEvents({ taskId: task.id }).map((event) => event.mutationType);
|
||||
expect(auditTypes).not.toContain("merge:cwd-integration-fallback-removed");
|
||||
} finally {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
it.todo("Scenario E: reserved tripwire — no production emit site after Step 3; future regression would add one");
|
||||
|
||||
it("Scenario E: no production code path assigns integrationRoot.mode = cwd-main", () => {
|
||||
const merger = readFileSync(new URL("../merger.ts", import.meta.url), "utf-8");
|
||||
expect(merger).not.toMatch(/^\s*integrationRoot\.mode\s*=\s*\"cwd-main\"/m);
|
||||
});
|
||||
});
|
||||
@@ -34,7 +34,21 @@ describe("resolveMergeIntegrationRoot", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves the legacy cwd-main mode when explicitly selected", () => {
|
||||
it("maps explicit opt-in to canonical cwd-integration-branch mode", () => {
|
||||
expect(
|
||||
resolveMergeIntegrationRoot({
|
||||
task: { id: "FN-5279", worktree: "/tmp/task-worktree" } as any,
|
||||
settings: { mergeIntegrationWorktree: "cwd-integration-branch" as const, worktrunk: { enabled: false } } as any,
|
||||
projectRoot: "/tmp/project-root",
|
||||
}),
|
||||
).toEqual({
|
||||
mode: "cwd-integration-branch",
|
||||
rootDir: "/tmp/project-root",
|
||||
branchName: "fusion/fn-5279",
|
||||
});
|
||||
});
|
||||
|
||||
it("maps legacy cwd-main input to canonical cwd-integration-branch mode", () => {
|
||||
expect(
|
||||
resolveMergeIntegrationRoot({
|
||||
task: { id: "FN-5279", worktree: "/tmp/task-worktree" } as any,
|
||||
@@ -42,7 +56,7 @@ describe("resolveMergeIntegrationRoot", () => {
|
||||
projectRoot: "/tmp/project-root",
|
||||
}),
|
||||
).toEqual({
|
||||
mode: "cwd-main",
|
||||
mode: "cwd-integration-branch",
|
||||
rootDir: "/tmp/project-root",
|
||||
branchName: "fusion/fn-5279",
|
||||
});
|
||||
@@ -62,7 +76,7 @@ describe("resolveMergeIntegrationRoot", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("defers to the project root when worktrunk owns merge orchestration", () => {
|
||||
it("keeps reuse-task-worktree mode when worktrunk is enabled", () => {
|
||||
expect(
|
||||
resolveMergeIntegrationRoot({
|
||||
task: { id: "FN-5279", worktree: "/tmp/task-worktree" } as any,
|
||||
@@ -70,8 +84,8 @@ describe("resolveMergeIntegrationRoot", () => {
|
||||
projectRoot: "/tmp/project-root",
|
||||
}),
|
||||
).toEqual({
|
||||
mode: "cwd-main",
|
||||
rootDir: "/tmp/project-root",
|
||||
mode: "reuse-task-worktree",
|
||||
rootDir: "/tmp/task-worktree",
|
||||
branchName: "fusion/fn-5279",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { aiMergeTask } from "../../merger.js";
|
||||
import { git, hasGit, makeReliabilityFixture } from "./_helpers.js";
|
||||
|
||||
describe("FN-5348 reliability interactions: cwd fallback removal", () => {
|
||||
it.skipIf(!hasGit)("autoMerge=false + reuse refusal stays in-review and emits no cwd fallback events", async () => {
|
||||
const fixture = await makeReliabilityFixture({
|
||||
taskId: "FN-5348-RI-AUTO-OFF-REFUSAL",
|
||||
settings: {
|
||||
autoMerge: false,
|
||||
baseBranch: "master",
|
||||
mergeIntegrationWorktree: "reuse-task-worktree",
|
||||
} as any,
|
||||
});
|
||||
|
||||
try {
|
||||
const { rootDir, store, task } = fixture;
|
||||
const actualTask = await store.getTask(task.id);
|
||||
const branch = `fusion/${actualTask!.id.toLowerCase()}`;
|
||||
const worktreeRoot = `${rootDir}-worktrees`;
|
||||
const worktreePath = join(worktreeRoot, actualTask!.id.toLowerCase());
|
||||
|
||||
git(rootDir, "git branch -m main master");
|
||||
const completedSteps = (actualTask?.steps ?? []).map((step) => ({ ...step, status: "done" as const }));
|
||||
await store.updateTask(task.id, {
|
||||
baseBranch: "master",
|
||||
branch,
|
||||
steps: completedSteps,
|
||||
currentStep: completedSteps.length,
|
||||
} as any);
|
||||
await fixture.createBranch(branch);
|
||||
await fixture.writeAndCommit("packages/engine/src/fn-5348-ri-refusal.ts", "export const refusal = true;\n", "feat: add refusal merge content");
|
||||
await fixture.checkout("master");
|
||||
await mkdir(worktreeRoot, { recursive: true });
|
||||
git(rootDir, `git worktree add ${JSON.stringify(worktreePath)} ${JSON.stringify(branch)}`);
|
||||
await store.updateTask(task.id, { worktree: worktreePath, branch } as any);
|
||||
store.enqueueMergeQueue(task.id);
|
||||
git(worktreePath, "sh -c 'printf dirty > DIRTY.txt'");
|
||||
|
||||
await expect(aiMergeTask(store, rootDir, task.id)).rejects.toMatchObject({
|
||||
name: "MergeHandoffRefusedError",
|
||||
gate: "working-tree-dirty",
|
||||
});
|
||||
|
||||
const latest = await store.getTask(task.id);
|
||||
expect(latest?.column).toBe("in-review");
|
||||
const auditTypes = store.getRunAuditEvents({ taskId: task.id }).map((event) => event.mutationType);
|
||||
expect(auditTypes).toContain("merge:reuse-handoff-refused");
|
||||
expect(auditTypes).not.toContain("merge:cwd-integration-fallback-removed");
|
||||
} finally {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
}, 30_000);
|
||||
});
|
||||
@@ -438,7 +438,7 @@ describe("FN-5279 reliability interactions: merge reuse task worktree", () => {
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
it.skipIf(!hasGit)("worktrunk override records deferred-to-worktrunk without acquiring reuse handoff", async () => {
|
||||
it.skipIf(!hasGit)("worktrunk-enabled reuse mode still acquires reuse handoff", async () => {
|
||||
const fixture = await makeReliabilityFixture({
|
||||
taskId: "FN-5279-RI-WORKTRUNK",
|
||||
settings: {
|
||||
@@ -452,6 +452,8 @@ describe("FN-5279 reliability interactions: merge reuse task worktree", () => {
|
||||
const { rootDir, store, task } = fixture;
|
||||
const actualTask = await store.getTask(task.id);
|
||||
const branch = `fusion/${actualTask!.id.toLowerCase()}`;
|
||||
const worktreeRoot = `${rootDir}-worktrees`;
|
||||
const worktreePath = join(worktreeRoot, actualTask!.id.toLowerCase());
|
||||
|
||||
git(rootDir, "git branch -m main master");
|
||||
const completedSteps = (actualTask?.steps ?? []).map((step) => ({ ...step, status: "done" as const }));
|
||||
@@ -464,12 +466,16 @@ describe("FN-5279 reliability interactions: merge reuse task worktree", () => {
|
||||
await fixture.createBranch(branch);
|
||||
await fixture.writeAndCommit("packages/engine/src/fn-5279-ri-worktrunk.ts", "export const deferred = true;\n", "feat: add worktrunk merge content");
|
||||
await fixture.checkout("master");
|
||||
await mkdir(worktreeRoot, { recursive: true });
|
||||
git(rootDir, `git worktree add ${JSON.stringify(worktreePath)} ${JSON.stringify(branch)}`);
|
||||
await store.updateTask(task.id, { worktree: worktreePath, branch } as any);
|
||||
store.enqueueMergeQueue(task.id);
|
||||
|
||||
const result = await aiMergeTask(store, rootDir, task.id);
|
||||
expect(result.merged).toBe(true);
|
||||
const auditTypes = store.getRunAuditEvents({ taskId: task.id }).map((event) => event.mutationType);
|
||||
expect(auditTypes).toContain("merge:reuse-handoff-deferred-to-worktrunk");
|
||||
expect(auditTypes).not.toContain("merge:reuse-handoff-acquired");
|
||||
expect(auditTypes).toContain("merge:reuse-handoff-acquired");
|
||||
} finally {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user