FN-5782: wire branch groups into merge routing

Connect branch_group metadata through planning and merge execution for grouped integration branches.

- propagate `branchGroup` and `branchGroupName` through core store types, merge metadata, and CLI task lifecycle APIs
- add merge coordination that computes branch-group plans and routes grouped tasks via `group-merge-coordinator`
- extend finalize-plan, merger, and reliability tests to cover grouped merge routing and integration behavior
- document the architecture update and add a patch changeset for `@runfusion/fusion`

Files changed:
 .changeset/fn-5782-branch-group-merge.md           |   7 +
 docs/architecture.md                               |   2 +
 packages/cli/src/commands/__tests__/task-lifecycle.test.ts  |  64 ++++++++-
 packages/cli/src/commands/task-lifecycle.ts        |   4 +
 packages/core/src/__tests__/branch-group-store.test.ts  |  20 +++
 packages/core/src/__tests__/task-merge.test.ts     |  64 +++++++++
 packages/core/src/store.ts                         |  22 ++-
 packages/core/src/task-merge.ts                    |  26 +++-
 packages/core/src/types.ts                         |   2 +-
 packages/engine/src/__tests__/experiment-finalize-plan.test.ts |  33 ++++-
 packages/engine/src/__tests__/group-merge-coordinator.test.ts  |  62 +++++++++
 packages/engine/src/__tests__/reliability-interactions/branch-group-merge-routing.test.ts | 153 +++++++++++++++++++++
 packages/engine/src/experiment/finalize-plan.ts    |  62 +++++++--
 packages/engine/src/group-merge-coordinator.ts     |  51 +++++++
 packages/engine/src/index.ts                       |   4 +
 packages/engine/src/merger.ts                      |  47 ++++++-
 16 files changed, 601 insertions(+), 22 deletions(-)

Fusion-Task-Id: FN-5782
Fusion-Task-Lineage: 0a70cfaa-2379-4955-b295-64de1f3093c8
This commit is contained in:
gsxdsm
2026-05-31 12:45:35 -07:00
parent f1aba5b534
commit 0c425788cb
16 changed files with 601 additions and 22 deletions

View File

@@ -67,6 +67,7 @@ function makeStore(task: MockTask, settings: Record<string, unknown> = {}) {
moveTask: vi.fn(async (_id: string, column: string) => ({ ...task, column })),
logEntry: vi.fn().mockResolvedValue(undefined),
getActiveMergingTask: vi.fn().mockReturnValue(null),
getBranchGroup: vi.fn().mockReturnValue(null),
_updates: updates,
});
}
@@ -90,6 +91,7 @@ function makeStatefulStore(task: MockTask, settings: Record<string, unknown> = {
}),
logEntry: vi.fn().mockResolvedValue(undefined),
getActiveMergingTask: vi.fn().mockReturnValue(null),
getBranchGroup: vi.fn().mockReturnValue(null),
_getState: () => state,
});
}
@@ -175,6 +177,17 @@ describe("processPullRequestMergeTask", () => {
};
const branch = getTaskBranchName(task.id);
const store = makeStore(task, { baseBranch: "main" });
(store.getBranchGroup as ReturnType<typeof vi.fn>).mockReturnValue({
id: "BG-1",
sourceType: "planning",
sourceId: "planning:abc",
branchName: "fusion/groups/planning-abc",
autoMerge: false,
prState: "none",
status: "open",
createdAt: Date.now(),
updatedAt: Date.now(),
});
execMock.mockImplementation(() => "");
const github = {
@@ -184,7 +197,7 @@ describe("processPullRequestMergeTask", () => {
url: "https://github.com/x/y/pull/7",
status: "open" as const,
headBranch: branch,
baseBranch: "develop",
baseBranch: "fusion/groups/planning-abc",
})),
getPrMergeStatus: vi.fn(async () => ({
prInfo: { number: 7, status: "open" as const, url: "https://github.com/x/y/pull/7" },
@@ -205,7 +218,54 @@ describe("processPullRequestMergeTask", () => {
);
expect(github.createPr).toHaveBeenCalledWith(expect.objectContaining({
base: "develop",
base: "fusion/groups/planning-abc",
}));
});
it("keeps per-task-derived members on the project default PR base", async () => {
const task: MockTask = {
id: "FN-9002",
title: "test",
description: "desc",
column: "in-review",
branchContext: {
groupId: "planning:abc",
source: "planning",
assignmentMode: "per-task-derived",
},
};
const store = makeStore(task, { baseBranch: "main" });
execMock.mockImplementation(() => "");
const github = {
findPrForBranch: vi.fn(async () => null),
createPr: vi.fn(async () => ({
number: 7,
url: "https://github.com/x/y/pull/7",
status: "open" as const,
headBranch: getTaskBranchName(task.id),
baseBranch: "main",
})),
getPrMergeStatus: vi.fn(async () => ({
prInfo: { number: 7, status: "open" as const, url: "https://github.com/x/y/pull/7" },
reviewDecision: null,
checks: [],
mergeReady: false,
blockingReasons: [],
})),
mergePr: vi.fn(),
};
await processPullRequestMergeTask(
store as never,
"/repo",
task.id,
github as never,
() => undefined,
);
expect(github.createPr).toHaveBeenCalledWith(expect.objectContaining({
base: "main",
}));
});