fix(FN-branch-group): review fixes — fast-path mergeTargetSource + open-PR reuse only

Code review (Tier 2) found two P1s: (1) the early no-op fast-path persisted
mergeConfirmed/mergeTargetBranch without mergeTargetSource, so a shared-group
member landing via it could never satisfy the strict completion predicate —
promotion permanently blocked; thread mergeTarget.source through like the
standard landing sites. (2) createGroupPrCallback's findPrForBranch used
state:'all' and could reuse a closed/merged PR from a prior group, persisting
a terminal prState onto a fresh promotion; create path now matches open PRs
only.
This commit is contained in:
gsxdsm
2026-06-03 11:23:14 -07:00
parent 928b14ae1b
commit bde7bdf766
4 changed files with 163 additions and 5 deletions

View File

@@ -34,6 +34,7 @@ vi.mock("@fusion/core", async () => {
import { activeSessionRegistry } from "@fusion/engine";
import {
cleanupMergedTaskArtifacts,
createGroupPrCallback,
processPullRequestMergeTask,
getTaskBranchName,
syncGroupPrCallback,
@@ -1373,3 +1374,76 @@ describe("syncGroupPrCallback (U6)", () => {
});
});
describe("createGroupPrCallback", () => {
beforeEach(() => {
execMock.mockReset();
execMock.mockImplementation(() => "");
});
const group = {
id: "BG-1",
sourceType: "planning" as const,
sourceId: "P-1",
branchName: "fusion/groups/p-1",
autoMerge: false,
prState: "none" as const,
status: "open" as const,
createdAt: Date.now(),
updatedAt: Date.now(),
};
const members = [{ id: "FN-A", title: "Alpha", description: "a", column: "in-review" } as never];
it("queries only OPEN PRs for the head branch (does not reuse terminal PRs)", async () => {
const github = {
findPrForBranch: vi.fn(async () => null),
createPr: vi.fn(async () => ({
number: 99,
url: "https://github.com/owner/repo/pull/99",
status: "open" as const,
})),
};
const callback = createGroupPrCallback(github as never);
await callback({
cwd: "/repo",
group: group as never,
members,
headBranch: group.branchName,
baseBranch: "main",
});
expect(github.findPrForBranch).toHaveBeenCalledWith({ head: group.branchName, state: "open" });
});
it("does not reuse a closed PR from a prior group — creates a fresh one", async () => {
// With state:"open", findPrForBranch returns null for a head whose only PR
// is closed/merged, so the create path runs instead of resurrecting the
// terminal PR (which would poison the newly promoted group's prState).
const github = {
findPrForBranch: vi.fn(async () => null),
createPr: vi.fn(async () => ({
number: 123,
url: "https://github.com/owner/repo/pull/123",
status: "open" as const,
})),
};
const callback = createGroupPrCallback(github as never);
const result = await callback({
cwd: "/repo",
group: group as never,
members,
headBranch: group.branchName,
baseBranch: "main",
});
expect(github.findPrForBranch).toHaveBeenCalledWith({ head: group.branchName, state: "open" });
expect(github.createPr).toHaveBeenCalledTimes(1);
expect(result).toEqual({
prNumber: 123,
prUrl: "https://github.com/owner/repo/pull/123",
prState: "open",
});
});
});

View File

@@ -203,7 +203,7 @@ export function createGroupPrCallback(
github: Pick<GitHubOperations, "findPrForBranch" | "createPr">,
): CreateGroupPrFn {
return async ({ cwd, group, members, headBranch, baseBranch }) => {
const existing = await github.findPrForBranch({ head: headBranch, state: "all" });
const existing = await github.findPrForBranch({ head: headBranch, state: "open" });
if (existing) {
return { prNumber: existing.number, prUrl: existing.url, prState: toBranchGroupPrState(existing) };
}