fix(engine): short-circuit zero-commits-ahead branch before AI-merge clean-room churn

An AI-merge land of a branch with zero commits ahead of the integration
tip (the shape a coding agent that produced no commits leaves behind)
builds a clean-room worktree and runs a dependency install before
reaching the empty outcome via mergeAndReview. On a non-workspace land
the dep install throws hard, so the merge is transient-retried to
exhaustion (3/3) and the card is terminally parked failed.

Short-circuit on a CONFIDENT zero-ahead count (a git failure yields NaN
and falls through) and return the identical outcome:"empty" shape
before the throw-prone churn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
fusion-merge-train
2026-07-05 20:09:25 +02:00
parent 17c40070db
commit ca2fbbd8f8
2 changed files with 39 additions and 0 deletions

View File

@@ -434,6 +434,32 @@ describe("runAiMerge", () => {
expect(store.moveTask).toHaveBeenCalledWith("FN-1", "done", expect.objectContaining({ moveSource: "engine", preserveProgress: true }));
});
it("short-circuits a zero-commits-ahead branch before the clean-room/merge-agent churn (empty-branch wedge)", async () => {
const { dir } = initRepoWithBranch({ branch: "fusion/fn-1" });
// Move the task branch back onto main's tip → 0 commits ahead of the
// integration branch (the empty-branch shape a coding agent that produced
// no commits leaves behind).
git(dir, "branch -f fusion/fn-1 main");
const { store } = makeStore(dir);
const mainBefore = git(dir, "rev-parse main");
const mergeAgent = vi.fn(async () => { /* would build a squash if reached */ });
const result = await runAiMerge(store, dir, "FN-1", { manual: true }, {
mergeAgent,
reviewAgent: vi.fn(async () => "REVIEW_VERDICT: approve"),
});
// The zero-ahead short-circuit fires BEFORE the clean-room build + dep
// install, so the merge agent is never invoked. In prod that dep install
// throws and gets transient-retried to exhaustion, terminally parking the
// card failed — skipping it is the wedge fix.
expect(mergeAgent).not.toHaveBeenCalled();
expect(result.noOp).toBe(true);
expect(result.merged).toBe(false);
expect(git(dir, "rev-parse main")).toBe(mainBefore);
expect(store.moveTask).toHaveBeenCalledWith("FN-1", "done", expect.objectContaining({ moveSource: "engine", preserveProgress: true }));
});
it("demotes a no-commits task with skipped-out work instead of AI empty-merge finalizing done", async () => {
const { dir } = initRepoWithBranch({ branch: "fusion/fn-1" });
git(dir, "merge -q fusion/fn-1");

View File

@@ -622,6 +622,19 @@ export async function landOneRepo(
throwIfAborted(signal, taskId);
const tipSha = await git(["rev-parse", "--verify", `refs/heads/${integrationBranch}`], repoRootDir);
// Short-circuit a branch with zero commits ahead of the integration tip
// BEFORE building a clean room + installing deps. A truly-empty branch would
// reach the identical `outcome: "empty"` return below via mergeAndReview →
// no squashSha, but only after the throw-prone dep-install churn (which a
// non-workspace land hard-fails), so the merge gets transient-retried to
// exhaustion and the card is parked failed. Only short-circuit on a CONFIDENT
// 0: a git failure yields "" → parseInt → NaN (≠ 0) and falls through.
const aheadRaw = await git(["rev-list", "--count", `${integrationBranch}..${branch}`], repoRootDir).catch(() => "");
if (Number.parseInt(aheadRaw.trim(), 10) === 0) {
await audit.git({ type: "merge:ai-empty", target: integrationBranch, metadata: { taskId, tipSha } });
return { outcome: "empty", tipSha, integrationBranch };
}
// 1. Clean-room worktree at the integration tip.
let mergeRoot: string | undefined;
let worktreeAdded = false;