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