fix(FN-5633): fail loudly when an executed, never-merged task has no branch
The AI merge path treated a missing task branch as a benign no-op. That's correct when the task was never executed or already merged (branch cleaned up on a re-process), but if the task WAS executed (a baseCommitSha was recorded) and has no recorded merge, the branch should still exist — its work appears lost. Now that case throws instead of silently marking the task done; the benign cases still finalize as a no-op (reason "already-merged" / "no-branch"). Tests: executed+never-merged → throws; already-merged → no-op done; never-executed → no-op done. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -185,6 +185,39 @@ describe("runAiMerge", () => {
|
||||
expect(git(dir, "rev-parse main")).toBe(mainBefore);
|
||||
});
|
||||
|
||||
it("fails loudly when an executed, never-merged task has no branch (possible lost work)", async () => {
|
||||
const { dir } = initRepoWithBranch({ branch: "fusion/fn-1" });
|
||||
// branch points at a ref that doesn't exist; task was executed (baseCommitSha) and never merged.
|
||||
const { store } = makeStore(dir, { branch: "fusion/ghost", baseCommitSha: "0123456789abcdef" });
|
||||
|
||||
await expect(runAiMerge(store, dir, "FN-1", { manual: true }, {
|
||||
mergeAgent: vi.fn(), reviewAgent: vi.fn(async () => "REVIEW_VERDICT: approve"),
|
||||
})).rejects.toThrow(/work appears lost/);
|
||||
expect(store.moveTask).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("finalizes as a no-op when an already-merged task's branch is gone (re-process)", async () => {
|
||||
const { dir } = initRepoWithBranch({ branch: "fusion/fn-1" });
|
||||
const { store } = makeStore(dir, { branch: "fusion/ghost", baseCommitSha: "0123456789abcdef", mergeDetails: { mergeConfirmed: true } });
|
||||
|
||||
const result = await runAiMerge(store, dir, "FN-1", { manual: true }, {
|
||||
mergeAgent: vi.fn(), reviewAgent: vi.fn(),
|
||||
});
|
||||
expect(result.noOp).toBe(true);
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-1", "done");
|
||||
});
|
||||
|
||||
it("finalizes as a no-op when a never-executed task has no branch", async () => {
|
||||
const { dir } = initRepoWithBranch({ branch: "fusion/fn-1" });
|
||||
const { store } = makeStore(dir, { branch: "fusion/ghost" }); // no baseCommitSha → never executed
|
||||
|
||||
const result = await runAiMerge(store, dir, "FN-1", { manual: true }, {
|
||||
mergeAgent: vi.fn(), reviewAgent: vi.fn(),
|
||||
});
|
||||
expect(result.noOp).toBe(true);
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-1", "done");
|
||||
});
|
||||
|
||||
it("throws a clear error when the task's target branch has no local ref", async () => {
|
||||
const { dir } = initRepoWithBranch({ branch: "fusion/fn-1" });
|
||||
const { store } = makeStore(dir, { baseBranch: "release/9.9" }); // never created locally
|
||||
|
||||
@@ -599,9 +599,32 @@ export async function runAiMerge(
|
||||
|
||||
// Branch must exist to merge it.
|
||||
if (!(await gitOk(["rev-parse", "--verify", `refs/heads/${branch}`], projectRootDir))) {
|
||||
await audit.git({ type: "merge:ai-no-branch", target: branch, metadata: { taskId } });
|
||||
const done = await finalizeTask(store, taskId, noOpResult(task, branch, "no-branch"));
|
||||
return done;
|
||||
// A missing branch is benign in two cases — the task was never executed
|
||||
// (nothing to merge), or it already merged and the branch was cleaned up
|
||||
// (a re-processed task). But if the task WAS executed (a baseCommitSha was
|
||||
// recorded when it got a worktree) and was NEVER merged (no recorded
|
||||
// landing), the branch should still exist — its work appears lost. Fail
|
||||
// loudly rather than silently marking the task done.
|
||||
const wasExecuted = !!task.baseCommitSha;
|
||||
const alreadyMerged = task.mergeDetails?.mergeConfirmed === true || !!task.mergeDetails?.commitSha;
|
||||
if (wasExecuted && !alreadyMerged) {
|
||||
await audit.git({
|
||||
type: "merge:ai-no-branch",
|
||||
target: branch,
|
||||
metadata: { taskId, kind: "executed-branch-missing", baseCommitSha: task.baseCommitSha },
|
||||
});
|
||||
throw new Error(
|
||||
`AI merge for ${taskId}: branch "${branch}" is missing, but the task was executed `
|
||||
+ `(baseCommitSha ${String(task.baseCommitSha).slice(0, 8)}) and has no recorded merge — its work appears lost. `
|
||||
+ `Not finalizing; investigate.`,
|
||||
);
|
||||
}
|
||||
await audit.git({
|
||||
type: "merge:ai-no-branch",
|
||||
target: branch,
|
||||
metadata: { taskId, kind: alreadyMerged ? "already-merged" : "never-executed" },
|
||||
});
|
||||
return await finalizeTask(store, taskId, noOpResult(task, branch, alreadyMerged ? "already-merged" : "no-branch"));
|
||||
}
|
||||
|
||||
// The target branch must exist as a LOCAL ref to merge into it — surface a
|
||||
|
||||
Reference in New Issue
Block a user