fix(core): reuse same-name branch group instead of colliding on triage

branch_groups.branchName is globally UNIQUE, but ensureBranchGroupForSource
only looked up an existing group by (sourceType, sourceId). When a second
mission's shared-branch triage resolved to a base branch (e.g. "main") that
another mission already owned a group for, createBranchGroup threw
"UNIQUE constraint failed: branch_groups.branchName". That error escaped
triageFeature and was swallowed by both callers (validation-failure
auto-triage and the reconcile sweep), leaving the mission's "defined"
features — including generated fix features — permanently un-triaged.

ensureBranchGroupForSource now reuses an existing open group for the same
branch name before attempting to create one, matching the established
getBranchGroupByBranchName(...) ?? ensureBranchGroupForSource(...) idiom.

Confirmed by reproducing against a snapshot of the affected mission DB:
triageFeature threw the UNIQUE error before, succeeds after.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-06-03 08:40:01 -07:00
parent 04a5cd196c
commit 314411c497
3 changed files with 48 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
---
"@runfusion/fusion": patch
---
Fix mission triage silently stranding features when two missions share a base branch.
`branch_groups.branchName` is globally unique, but `ensureBranchGroupForSource` only checked for an existing group by `(sourceType, sourceId)`. When a second mission's shared-branch triage resolved to a base branch (e.g. `main`) that another mission already owned a branch group for, `createBranchGroup` threw `UNIQUE constraint failed: branch_groups.branchName`. That error escaped `triageFeature` and was swallowed by both of its callers (the validation-failure auto-triage and the startup/maintenance reconcile sweep), leaving the mission's `defined` features — including auto-generated fix features — permanently un-triaged and the mission unable to progress.
`ensureBranchGroupForSource` now reuses an existing open group for the same branch name (matching the established `getBranchGroupByBranchName(...) ?? ensureBranchGroupForSource(...)` idiom) instead of colliding on the unique constraint.

View File

@@ -67,6 +67,34 @@ describe("TaskStore branch groups", () => {
expect(second.autoMerge).toBe(true); expect(second.autoMerge).toBe(true);
}); });
it("reuses an existing open group with the same branchName across sources instead of throwing", () => {
// Regression: branch_groups.branchName is globally UNIQUE. When one mission
// already owns an open group for a shared base branch, a second source whose
// triage resolves to the same branch must reuse that group rather than crash
// on the UNIQUE constraint. (Mission triage discards the result and only needs
// it not to throw; a thrown error there silently strands "defined" features.)
const owner = store.createBranchGroup({ sourceType: "mission", sourceId: "M-OWNER", branchName: "main" });
let reusedByMission!: ReturnType<typeof store.ensureBranchGroupForSource>;
expect(() => {
reusedByMission = store.ensureBranchGroupForSource("mission", "M-OTHER", {
branchName: "main",
autoMerge: true,
});
}).not.toThrow();
expect(reusedByMission.id).toBe(owner.id);
// Invariant holds across the other source types that share this helper.
const reusedByNewTask = store.ensureBranchGroupForSource("new-task", "shared/main", { branchName: "main" });
expect(reusedByNewTask.id).toBe(owner.id);
const reusedByPlanning = store.ensureBranchGroupForSource("planning", "PS-main", { branchName: "main" });
expect(reusedByPlanning.id).toBe(owner.id);
// No duplicate rows were created for the shared branch.
expect(store.listBranchGroups().filter((g) => g.branchName === "main")).toHaveLength(1);
});
it("supports new-task branch group sources and round-trips through lookups", () => { it("supports new-task branch group sources and round-trips through lookups", () => {
const group = store.ensureBranchGroupForSource("new-task", "shared/onboarding", { const group = store.ensureBranchGroupForSource("new-task", "shared/onboarding", {
branchName: "shared/onboarding", branchName: "shared/onboarding",

View File

@@ -4385,6 +4385,17 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
return existing; return existing;
} }
// `branch_groups.branchName` is globally UNIQUE — a branch is represented by
// exactly one open group. If another source already owns an open group for
// this branch, reuse it rather than calling createBranchGroup and violating
// the UNIQUE constraint. Without this, two missions whose shared base resolves
// to the same branch (e.g. "main") collide: the throw escapes triageFeature
// and is swallowed by its callers, silently stranding "defined" features.
const existingByBranch = this.getBranchGroupByBranchName(init.branchName);
if (existingByBranch) {
return existingByBranch;
}
return this.createBranchGroup({ return this.createBranchGroup({
sourceType, sourceType,
sourceId, sourceId,