diff --git a/packages/core/src/__tests__/mission-store.test.ts b/packages/core/src/__tests__/mission-store.test.ts index 53b719616e..5f32ad78fd 100644 --- a/packages/core/src/__tests__/mission-store.test.ts +++ b/packages/core/src/__tests__/mission-store.test.ts @@ -2018,7 +2018,12 @@ describe("MissionStore", () => { const task = await ts.createTask({ title: "Task", description: "Linked task" }); ms.linkFeatureToTask(feature.id, task.id); - db.prepare("UPDATE tasks SET missionId = NULL WHERE id = ?").run(task.id); + // Clear missionId on THIS test's in-memory TaskStore db (the outer `db` + // belongs to a different store) so the lookup genuinely exercises the + // feature-linkage fallback instead of the normal task→mission path. + (ts as unknown as { db: { prepare(sql: string): { run(...args: unknown[]): unknown } } }).db + .prepare("UPDATE tasks SET missionId = NULL WHERE id = ?") + .run(task.id); expect(ms.listGoalIdsForTask(task.id)).toEqual([goal.id]); expect(ms.listGoalsForTask(task.id)).toEqual([goal]); diff --git a/packages/dashboard/src/__tests__/routes-branch-groups.test.ts b/packages/dashboard/src/__tests__/routes-branch-groups.test.ts index b6be5df505..f9d4ed442c 100644 --- a/packages/dashboard/src/__tests__/routes-branch-groups.test.ts +++ b/packages/dashboard/src/__tests__/routes-branch-groups.test.ts @@ -341,10 +341,11 @@ describe("branch group abandon (U6, R7)", () => { const res = await REQUEST(app, "POST", "/branch-groups/BG-AB/abandon", JSON.stringify({}), { "content-type": "application/json" }); expect(res.status).toBe(200); expect(closeGroupPr).not.toHaveBeenCalled(); - expect(updateBranchGroup).toHaveBeenCalledWith( + expect(updateBranchGroup).toHaveBeenLastCalledWith( "BG-AB", expect.objectContaining({ status: "abandoned", prState: "none" }), ); + expect(res.body.group.prState).toBe("none"); }); }); diff --git a/packages/engine/src/__tests__/group-merge-coordinator.test.ts b/packages/engine/src/__tests__/group-merge-coordinator.test.ts index 5eb76293b5..e0ee11766c 100644 --- a/packages/engine/src/__tests__/group-merge-coordinator.test.ts +++ b/packages/engine/src/__tests__/group-merge-coordinator.test.ts @@ -866,6 +866,39 @@ describe("promoteBranchGroup finalized-but-PR-less repair (Fix #4 part 2)", () = expect(mainAfter).toBe(mainBefore); }); + it("repairs the legacy fallback state: finalized + prState 'open' + prNumber null still creates the PR", async () => { + // The old code flipped prState to "open" without creating a PR — re-running + // with createGroupPr wired must not be short-circuited by the open-state guard. + const rootDir = makePrRepo(); + let group = makeGroup({ status: "finalized", prState: "open", prNumber: null, prUrl: null }); + let createCalls = 0; + const store = { + getBranchGroup: () => group, + getBranchGroupByBranchName: () => null, + listTasksByBranchGroup: async () => [landedMember("FN-A", group.branchName)], + updateBranchGroup: (_id: string, patch: Record) => { + group = { ...group, ...patch }; + return group; + }, + } as any; + + const result = await promoteBranchGroup({ + rootDir, + groupId: group.id, + settings: prSettings, + store, + createGroupPr: async () => { + createCalls += 1; + return { prNumber: 91, prUrl: "https://github.com/x/y/pull/91", prState: "open" as const }; + }, + }); + + expect(result.reason).toBe("promoted"); + expect(createCalls).toBe(1); + expect(group.prNumber).toBe(91); + expect(group.prState).toBe("open"); + }); + it("a finalized group that already has a prNumber is still short-circuited (no repair, no PR re-create)", async () => { const rootDir = makePrRepo(); let group = makeGroup({ status: "finalized", prState: "open", prNumber: 5, prUrl: "https://github.com/x/y/pull/5" }); diff --git a/packages/engine/src/group-merge-coordinator.ts b/packages/engine/src/group-merge-coordinator.ts index d7a097268f..bcd00189f4 100644 --- a/packages/engine/src/group-merge-coordinator.ts +++ b/packages/engine/src/group-merge-coordinator.ts @@ -287,7 +287,10 @@ async function promoteBranchGroupInner(input: PromoteBranchGroupInput): Promise< }; } - if (group.prState === "open") { + // Legacy fallback rows are exactly `finalized + prState:"open" + prNumber:null` + // (the old code flipped prState without creating a PR) — the repair path must + // not be short-circuited by the open-state guard for them. + if (!needsPrRepair && group.prState === "open") { return { groupId: group.id, promoted: false,