FN-5828: route shared mission triage to per-task working branches

Ensure mission shared-branch triage creates unique task branches while preserving a single shared merge target.

- Compute branch assignment during mission feature triage with resolveEntryPointBranchAssignment and persist per-task working branches in shared mode.
- Keep shared branch-group initialization tied to the mission merge-target branch for shared assignment flows.
- Update core and dashboard mission triage tests to assert distinct per-task branches under shared strategy and verify branch-group behavior.
- Refresh mission docs to clarify shared vs per-task-derived branch strategy semantics.

Files changed:
 docs/missions.md                                   |  8 +--
 packages/core/src/__tests__/mission-store.test.ts  | 42 +++++++++++++--
 packages/core/src/mission-store.ts                 | 19 ++++---
 packages/dashboard/src/__tests__/mission-e2e.test.ts    | 63 ++++++++++++++++++++--
 4 files changed, 115 insertions(+), 17 deletions(-)

Fusion-Task-Id: FN-5828

Fusion-Task-Lineage: 9854ea25-6926-4883-92ae-36a5c0a817c5
This commit is contained in:
gsxdsm
2026-06-01 03:09:50 -07:00
parent 35ce9bd244
commit 2546065674
4 changed files with 115 additions and 17 deletions

View File

@@ -2155,7 +2155,9 @@ describe("MissionStore", () => {
const triaged = await msWithTs.triageFeature(feature.id);
const task = await ts.getTask(triaged.taskId!);
expect(task?.branch).toBe("release/shared");
expect(task?.branch).toMatch(/^release\/shared\//);
expect(task?.branch).not.toBe("release/shared");
expect(task?.branchContext?.groupId).toBe(`mission:${mission.id}`);
expect(task?.branchContext?.assignmentMode).toBe("shared");
});
@@ -2175,7 +2177,9 @@ describe("MissionStore", () => {
});
const task = await ts.getTask(triaged.taskId!);
expect(task?.branch).toBe("hotfix/shared");
expect(task?.branch).toMatch(/^hotfix\/shared\//);
expect(task?.branch).not.toBe("hotfix/shared");
expect(task?.branchContext?.groupId).toBe(`mission:${mission.id}`);
expect(task?.branchContext?.assignmentMode).toBe("shared");
});
@@ -2399,11 +2403,43 @@ describe("MissionStore", () => {
});
const task = await ts.getTask(triaged[0].taskId!);
expect(task?.branch).toBe("feature/manual");
expect(task?.branch).toMatch(/^feature\/manual\//);
expect(task?.branch).not.toBe("feature/manual");
expect(task?.baseBranch).toBe("release");
expect(task?.branchContext?.groupId).toBe(`mission:${mission.id}`);
expect(task?.branchContext?.assignmentMode).toBe("shared");
});
it("triageSlice shared mode creates distinct per-task branches with one shared merge target", async () => {
const { TaskStore } = await import("../store.js");
const ts = new TaskStore(tmpDir, join(tmpDir, ".fusion-global-settings"), { inMemoryDb: true });
const msWithTs = ts.getMissionStore();
const mission = msWithTs.createMission({
title: "Mission",
branchStrategy: { mode: "existing", branchName: "feature/shared" },
});
const milestone = msWithTs.addMilestone(mission.id, { title: "Milestone" });
const slice = msWithTs.addSlice(milestone.id, { title: "Slice" });
msWithTs.addFeature(slice.id, { title: "Feature 1" });
msWithTs.addFeature(slice.id, { title: "Feature 2" });
const triaged = await msWithTs.triageSlice(slice.id);
const firstTask = await ts.getTask(triaged[0].taskId!);
const secondTask = await ts.getTask(triaged[1].taskId!);
expect(firstTask?.branch).toMatch(/^feature\/shared\//);
expect(secondTask?.branch).toMatch(/^feature\/shared\//);
expect(firstTask?.branch).not.toBe("feature/shared");
expect(secondTask?.branch).not.toBe("feature/shared");
expect(firstTask?.branch).not.toBe(secondTask?.branch);
expect(firstTask?.branchContext?.groupId).toBe(`mission:${mission.id}`);
expect(secondTask?.branchContext?.groupId).toBe(`mission:${mission.id}`);
const branchGroup = ts.getBranchGroupBySource("mission", mission.id);
expect(branchGroup?.branchName).toBe("feature/shared");
});
it("triageSlice does not inject baseBranch when mission has none", async () => {
const { TaskStore } = await import("../store.js");
const ts = new TaskStore(tmpDir, join(tmpDir, ".fusion-global-settings"), { inMemoryDb: true });

View File

@@ -55,6 +55,7 @@ import {
type MissionHierarchySnapshot,
} from "./shared-mesh-state.js";
import { reconcileDeterministicDuplicate, runDeterministicDuplicateGuard } from "./duplicate-guard.js";
import { resolveEntryPointBranchAssignment } from "./branch-assignment.js";
// ── Constants ────────────────────────────────────────────────────────
/**
@@ -3526,6 +3527,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
if (guard.action === "duplicate" && guard.existing) {
linkedTaskId = guard.existing.id;
} else {
let sharedBranchBaseForMission: string | undefined;
if (missionId && resolvedAssignmentMode === "shared") {
const settings = await this.taskStore.getSettings();
const settingsDefaultBranch =
@@ -3533,16 +3535,24 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
? settings.defaultBranch
: "main";
const settingsAutoMerge = typeof settings.autoMerge === "boolean" ? settings.autoMerge : false;
sharedBranchBaseForMission = resolvedBranch ?? resolvedBaseBranch ?? settingsDefaultBranch;
this.taskStore.ensureBranchGroupForSource("mission", missionId, {
branchName: resolvedBranch ?? resolvedBaseBranch ?? settingsDefaultBranch,
branchName: sharedBranchBaseForMission,
autoMerge: mission?.autoMerge ?? settingsAutoMerge,
});
}
const taskSegment = feature.id;
const branchAssignment = resolveEntryPointBranchAssignment({
assignmentMode: resolvedAssignmentMode,
resolvedBranch: resolvedAssignmentMode === "shared" ? sharedBranchBaseForMission ?? resolvedBranch : resolvedBranch,
taskSegment,
});
const createdTask = await this.taskStore.createTask({
title: taskTitle || feature.title,
description,
branch: resolvedBranch,
branch: branchAssignment.workingBranch,
baseBranch: resolvedBaseBranch,
...(missionId
? {
@@ -3617,14 +3627,11 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
const triaged: MissionFeature[] = [];
for (const feature of definedFeatures) {
const strategyBranch = resolvedAssignmentMode === "per-task-derived"
? (resolvedBranch ? `${resolvedBranch}/${feature.id.toLowerCase()}` : undefined)
: resolvedBranch;
const strategyBranch = resolvedBranch;
const updated = await this.triageFeature(feature.id, undefined, undefined, {
branch: strategyBranch,
baseBranch: resolvedBaseBranch,
assignmentMode: resolvedAssignmentMode,
...branchOptions,
});
triaged.push(updated);
}