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:
9
.changeset/fix-branch-group-name-collision-triage.md
Normal file
9
.changeset/fix-branch-group-name-collision-triage.md
Normal 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.
|
||||
@@ -67,6 +67,34 @@ describe("TaskStore branch groups", () => {
|
||||
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", () => {
|
||||
const group = store.ensureBranchGroupForSource("new-task", "shared/onboarding", {
|
||||
branchName: "shared/onboarding",
|
||||
|
||||
@@ -4385,6 +4385,17 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
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({
|
||||
sourceType,
|
||||
sourceId,
|
||||
|
||||
Reference in New Issue
Block a user