test(FN-4476): add subsumed conflict and patch preservation coverage

Fusion-Task-Id: FN-4476
Fusion-Task-Lineage: 8673c1d9-f47e-4d02-89e6-646040b9be32
This commit is contained in:
Fusion
2026-05-14 08:27:36 -07:00
committed by gsxdsm
parent d8255c2bb0
commit e5c0fbd78c
2 changed files with 110 additions and 0 deletions

View File

@@ -102,6 +102,52 @@ describe("branch-conflicts", () => {
expect(result).toEqual({ kind: "stale-resolved" });
});
it("FN-4476/FN-4471: classifies live branch at main tip as fully-subsumed even with stale-base churn", async () => {
mockedExecSync.mockImplementation((cmd: string | string[]) => {
const command = typeof cmd === "string" ? cmd : cmd[0];
if (command === "git worktree prune") return Buffer.from("");
if (command === "git worktree list --porcelain") {
return Buffer.from(["worktree /tmp/existing-wt", "HEAD 2222222", "branch refs/heads/fusion/fn-4068", ""].join("\n"));
}
if (command.includes("git rev-parse --verify 'refs/heads/fusion/fn-4068^{commit}'")) {
return Buffer.from("abc123def456\n");
}
if (command.includes("git rev-parse --verify 'fusion/fn-4068^{commit}'")) {
return Buffer.from("abc123def456\n");
}
if (command.includes("git rev-parse --verify 'main^{commit}'")) {
return Buffer.from("abc123def456\n");
}
if (command === "git merge-base 'main' 'fusion/fn-4068'") {
return Buffer.from("50ccd27a\n");
}
if (command === "git cherry 'main' 'fusion/fn-4068' '50ccd27a'") {
return Buffer.from("");
}
if (command.includes("git log --reverse --format=%H%x09%s 'main..fusion/fn-4068'")) {
throw new Error("stale-base rev-list should not run when git cherry succeeds");
}
if (command.includes("git log --format=%H%x00%s%x00%b 'main..fusion/fn-4068'")) {
return Buffer.from("");
}
throw new Error(`Unexpected command: ${command}`);
});
const result = await inspectBranchConflict({
repoDir: "/tmp/repo",
branchName: "fusion/fn-4068",
conflictingWorktreePath: "/tmp/existing-wt",
requestingTaskId: "FN-4068",
startPoint: "main",
});
expect(result).toEqual({
kind: "fully-subsumed",
livePath: "/tmp/existing-wt",
tipSha: "abc123def456",
});
});
it("returns fully-subsumed when git cherry reports no unique commits", async () => {
mockedExecSync.mockImplementation((cmd: string | string[]) => {
const command = typeof cmd === "string" ? cmd : cmd[0];

View File

@@ -75,6 +75,9 @@ import type { TaskStore, Settings, Task, AgentStore, Agent, NotificationProvider
import { EventEmitter } from "node:events";
import { execSync } from "node:child_process";
import { existsSync } from "node:fs";
import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { isUsableTaskWorktree, scanOrphanedBranches } from "../worktree-pool.js";
import * as branchConflictModule from "../branch-conflicts.js";
import { createLogger } from "../logger.js";
@@ -5819,6 +5822,35 @@ describe("SelfHealingManager reclaimSelfOwnedBranchConflicts", () => {
mockedIsUsableTaskWorktree.mockResolvedValue(true);
});
it("reclaims fully-subsumed branch conflicts and emits subsumed audit metadata", async () => {
store = createMockStore({
getSettings: vi.fn().mockResolvedValue({ globalPause: false, enginePaused: false } as any),
recordRunAuditEvent: vi.fn().mockResolvedValue(undefined),
});
manager = new SelfHealingManager(store, { rootDir: "/tmp/test-project" });
(store.listTasks as any)
.mockResolvedValueOnce([{ id: "FN-509", checkedOutBy: null, branch: "fusion/fn-509", worktree: "/tmp/fn-509", lineageId: "lin-9" }])
.mockResolvedValueOnce([]);
vi.spyOn(branchConflictModule, "inspectBranchConflict").mockResolvedValueOnce({
kind: "fully-subsumed",
livePath: "/tmp/fn-509",
tipSha: "abc123def456",
} as any);
const recovered = await manager.reclaimSelfOwnedBranchConflicts();
expect(recovered).toBe(1);
expect(store.updateTask).toHaveBeenCalledWith("FN-509", { worktree: "/tmp/fn-509", branch: "fusion/fn-509" });
expect((store as any).recordRunAuditEvent).toHaveBeenCalledWith(
expect.objectContaining({
domain: "git",
mutationType: "branch:auto-reclaim",
target: "fusion/fn-509",
metadata: expect.objectContaining({ subsumed: true, strandedCommitCount: 0 }),
}),
);
});
it("reclaims stranded same-task branch conflicts", async () => {
(store.listTasks as any)
.mockResolvedValueOnce([{ id: "FN-500", checkedOutBy: null, branch: "fusion/fn-500", worktree: "/tmp/fn-500", lineageId: "lin-1" }])
@@ -5904,6 +5936,38 @@ describe("SelfHealingManager reclaimSelfOwnedBranchConflicts", () => {
expect(store.moveTask).toHaveBeenCalledWith("FN-503", "in-review");
});
it("preserves dirty worktree as recovery patch before unrecoverable escalation", async () => {
const fixtureRoot = await mkdtemp(join(tmpdir(), "fn-4476-self-heal-"));
manager = new SelfHealingManager(store, { rootDir: fixtureRoot });
(store.listTasks as any)
.mockResolvedValueOnce([{ id: "FN-504", checkedOutBy: null, branch: "fusion/fn-504", worktree: "/tmp/fn-504" }])
.mockResolvedValueOnce([]);
vi.spyOn(branchConflictModule, "inspectBranchConflict").mockRejectedValueOnce(new Error("boom"));
mockedExecSync.mockImplementation((cmd: any) => {
const command = String(cmd);
if (command === "git status --porcelain") {
return Buffer.from(" M src/file.ts\n");
}
if (command === "git diff HEAD --binary") {
return Buffer.from("diff --git a/src/file.ts b/src/file.ts\n");
}
return Buffer.from("");
});
const recovered = await manager.reclaimSelfOwnedBranchConflicts();
expect(recovered).toBe(0);
expect(store.moveTask).toHaveBeenCalledWith("FN-504", "in-review");
const recoveryDir = join(fixtureRoot, ".fusion", "recovery");
const files = await readdir(recoveryDir);
const patchName = files.find((entry) => entry.startsWith("fn-504-") && entry.endsWith(".patch"));
expect(patchName).toBeTruthy();
const patchContent = await readFile(join(recoveryDir, patchName ?? ""), "utf-8");
expect(patchContent).toContain("diff --git");
await rm(fixtureRoot, { recursive: true, force: true });
});
it("escalates unrecoverable reclaim failures to in-review failed", async () => {
(store.listTasks as any)
.mockResolvedValueOnce([{ id: "FN-502", checkedOutBy: null, branch: "fusion/fn-502", worktree: "/tmp/fn-502" }])