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