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:
@@ -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];
|
||||
|
||||
@@ -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" }])
|
||||
|
||||
Reference in New Issue
Block a user