FN-5800: fix shared planning branch-group setup context binding

Prevent unbound this crashes when shared-branch planning subtasks initialize branch groups.

- call ensureBranchGroupForSource through the store object so method context is preserved
- update the planning routes in both shared-branch creation flows to use the bound store call
- harden the planning routes test mock to rely on this.getBranchGroupBySource and keep branch-group creation behavior realistic
- add regression coverage for creating shared branch groups from subtask breakdown task creation

Files changed:
 packages/dashboard/src/__tests__/routes-planning.test.ts          | 61 ++++++++++++++++++++--
 packages/dashboard/src/routes/register-planning-subtask-routes.ts |  8 +--
 2 files changed, 61 insertions(+), 8 deletions(-)

Fusion-Task-Id: FN-5800
Fusion-Task-Lineage: c4ce9257-0bd2-4b46-b88a-672992ecd969
This commit is contained in:
gsxdsm
2026-05-31 22:02:38 -07:00
parent 4ea43c0c87
commit 392a6918b2
2 changed files with 61 additions and 8 deletions

View File

@@ -217,9 +217,8 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
updatePrInfo: vi.fn().mockResolvedValue(undefined),
updateIssueInfo: vi.fn().mockResolvedValue(undefined),
getRootDir: vi.fn().mockReturnValue("/fake/root"),
ensureBranchGroupForSource: vi.fn((sourceType: "planning" | "mission", sourceId: string, init: { branchName: string; autoMerge?: boolean }) => {
const key = `${sourceType}:${sourceId}`;
const existing = branchGroups.get(key);
ensureBranchGroupForSource: vi.fn(function (this: TaskStore, sourceType: "planning" | "mission", sourceId: string, init: { branchName: string; autoMerge?: boolean }) {
const existing = this.getBranchGroupBySource(sourceType, sourceId);
if (existing) {
return existing;
}
@@ -234,7 +233,7 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
createdAt: Date.now(),
updatedAt: Date.now(),
};
branchGroups.set(key, created);
branchGroups.set(`${sourceType}:${sourceId}`, created);
return created;
}),
getBranchGroupBySource: vi.fn((sourceType: "planning" | "mission", sourceId: string) =>
@@ -2126,6 +2125,60 @@ describe("Planning Mode Routes", () => {
);
});
it("ensures shared branch groups when creating subtask breakdown tasks", async () => {
(store.createTask as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce({
id: "FN-281",
description: "First",
column: "triage",
dependencies: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
})
.mockResolvedValueOnce({
id: "FN-282",
description: "Second",
column: "triage",
dependencies: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
});
const subtaskRes = await REQUEST(
buildApp(),
"POST",
"/api/subtasks/start-streaming",
JSON.stringify({ description: "Break down auth scope" }),
{ "Content-Type": "application/json" },
);
expect(subtaskRes.status).toBe(201);
const sessionId = subtaskRes.body.sessionId as string;
const createRes = await REQUEST(
buildApp(),
"POST",
"/api/subtasks/create-tasks",
JSON.stringify({
sessionId,
branchSelection: { mode: "custom-new", branchName: "feature/auth-breakdown", baseBranch: "main" },
branchAssignment: { mode: "shared" },
subtasks: [
{ tempId: "temp-1", title: "Auth backend", description: "Implement backend" },
{ tempId: "temp-2", title: "Auth UI", description: "Implement UI", dependsOn: ["temp-1"] },
],
}),
{ "Content-Type": "application/json" },
);
expect(createRes.status).toBe(201);
expect(store.ensureBranchGroupForSource).toHaveBeenCalledWith(
"planning",
sessionId,
expect.objectContaining({ branchName: "feature/auth-breakdown", autoMerge: false }),
);
expect(store.getBranchGroupBySource).toHaveBeenCalledWith("planning", sessionId);
});
it("prefers session autoMerge override when creating shared planning subtasks", async () => {
(store.createTask as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce({

View File

@@ -224,8 +224,8 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
? settings.defaultBranch
: "main";
const settingsAutoMerge = typeof settings.autoMerge === "boolean" ? settings.autoMerge : false;
const ensureBranchGroupForSource = (scopedStore as { ensureBranchGroupForSource?: TaskStore["ensureBranchGroupForSource"] }).ensureBranchGroupForSource;
ensureBranchGroupForSource?.("planning", sessionId, {
const branchGroupStore = scopedStore as { ensureBranchGroupForSource?: TaskStore["ensureBranchGroupForSource"] };
branchGroupStore.ensureBranchGroupForSource?.("planning", sessionId, {
branchName: resolvedBranch ?? resolvedBaseBranch ?? settingsDefaultBranch,
autoMerge: session.autoMerge ?? settingsAutoMerge,
});
@@ -1279,8 +1279,8 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
? settings.defaultBranch
: "main";
const settingsAutoMerge = typeof settings.autoMerge === "boolean" ? settings.autoMerge : false;
const ensureBranchGroupForSource = (scopedStore as { ensureBranchGroupForSource?: TaskStore["ensureBranchGroupForSource"] }).ensureBranchGroupForSource;
ensureBranchGroupForSource?.("planning", planningSessionId, {
const branchGroupStore = scopedStore as { ensureBranchGroupForSource?: TaskStore["ensureBranchGroupForSource"] };
branchGroupStore.ensureBranchGroupForSource?.("planning", planningSessionId, {
branchName: resolvedBranch ?? resolvedBaseBranch ?? settingsDefaultBranch,
autoMerge: session.autoMerge ?? settingsAutoMerge,
});