Files
fusion/packages/engine/src/__tests__/experiment-finalize-plan.test.ts
gsxdsm 0c425788cb 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
2026-05-31 13:50:23 -07:00

193 lines
7.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { BranchGroup, ExperimentSession, ExperimentSessionRecord } from "@fusion/core";
import { buildDefaultPlan, buildTaskGroupPlan, mergePlanWithUserOverrides } from "../experiment/finalize-plan.js";
import { ExperimentFinalizePlanError } from "../experiment/finalize-types.js";
function createSession(overrides: Partial<ExperimentSession> = {}): ExperimentSession {
return {
id: "EXP-1",
name: "Experiment",
status: "active",
metric: { name: "score", direction: "maximize" },
currentSegment: 2,
keptRunIds: [],
tags: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
...overrides,
};
}
function runRecord(id: string, segment: number, seq: number, status: "keep" | "discard" = "keep", commit?: string, asi?: Record<string, unknown>): ExperimentSessionRecord {
return {
id,
sessionId: "EXP-1",
segment,
seq,
type: "run",
payload: {
status,
commit,
primaryMetric: 1,
secondaryMetrics: [],
asi,
},
createdAt: "2026-01-01T00:00:00.000Z",
};
}
describe("experiment finalize plan", () => {
it("groups by segment with unique slug branch names", () => {
const records = [
runRecord("r1", 1, 1, "keep", "c1"),
runRecord("r2", 1, 2, "keep", "c2"),
runRecord("r3", 1, 3, "keep", "c3"),
runRecord("r4", 2, 4, "keep", "c4"),
runRecord("r5", 2, 5, "keep", "c5"),
runRecord("r6", 2, 6, "keep", "c6"),
];
const session = createSession({ keptRunIds: records.map((r) => r.id) });
const plan = buildDefaultPlan({ session, records, integrationBranch: "main", mergeBaseCommit: "base" });
expect(plan.groups).toHaveLength(2);
expect(plan.groups[0].runRecordIds).toEqual(["r1", "r2", "r3"]);
expect(plan.groups[1].runRecordIds).toEqual(["r4", "r5", "r6"]);
expect(plan.groups[0].suggestedBranchName).toContain("segment-1");
expect(plan.groups[1].suggestedBranchName).toContain("segment-2");
expect(plan.groups[0].suggestedBranchName).not.toBe(plan.groups[1].suggestedBranchName);
});
it("clusters by asi.group regardless of segment", () => {
const records = [
runRecord("r1", 1, 1, "keep", "c1", { group: "Latency" }),
runRecord("r2", 2, 2, "keep", "c2", { group: "Latency" }),
runRecord("r3", 1, 3, "keep", "c3"),
];
const session = createSession({ keptRunIds: records.map((r) => r.id) });
const plan = buildDefaultPlan({ session, records, integrationBranch: "main", mergeBaseCommit: "base" });
expect(plan.groups).toHaveLength(2);
expect(plan.groups[0].title).toBe("Latency");
expect(plan.groups[0].runRecordIds).toEqual(["r1", "r2"]);
});
it("captures orphaned kept runs missing commit with warning", () => {
const records = [runRecord("r1", 1, 1, "keep", undefined), runRecord("r2", 1, 2, "keep", "c2")];
const session = createSession({ keptRunIds: ["r1", "r2"] });
const plan = buildDefaultPlan({ session, records, integrationBranch: "main", mergeBaseCommit: "base" });
expect(plan.orphanedRunRecordIds).toEqual(["r1"]);
expect(plan.warnings.some((warning) => warning.includes("r1"))).toBe(true);
expect(plan.groups).toHaveLength(1);
expect(plan.groups[0].runRecordIds).toEqual(["r2"]);
});
it("throws when override causes branch-name collision", () => {
const records = [
runRecord("r1", 1, 1, "keep", "c1"),
runRecord("r2", 2, 2, "keep", "c2"),
];
const session = createSession({ id: "EXP-COLLIDE", keptRunIds: ["r1", "r2"] });
const plan = buildDefaultPlan({ session, records, integrationBranch: "main", mergeBaseCommit: "base" });
expect(() =>
mergePlanWithUserOverrides(plan, {
groups: plan.groups.map((group) => ({
id: group.id,
runRecordIds: group.runRecordIds,
suggestedBranchName: "same-branch",
})),
}),
).toThrow(ExperimentFinalizePlanError);
});
it("supports overrides moving records and preserving order", () => {
const records = [
runRecord("r1", 1, 1, "keep", "c1"),
runRecord("r2", 1, 2, "keep", "c2"),
runRecord("r3", 2, 3, "keep", "c3"),
];
const session = createSession({ keptRunIds: ["r1", "r2", "r3"] });
const plan = buildDefaultPlan({ session, records, integrationBranch: "main", mergeBaseCommit: "base" });
const merged = mergePlanWithUserOverrides(plan, {
groups: [
{
id: plan.groups[0].id,
runRecordIds: ["r2", "r1", "r3"],
suggestedBranchName: "experiment/exp-1/custom-a",
},
],
});
expect(merged.groups).toHaveLength(1);
expect(merged.groups[0].runRecordIds).toEqual(["r2", "r1", "r3"]);
expect(merged.groups[0].commits).toEqual(["c2", "c1", "c3"]);
});
it("throws when override leaves group empty", () => {
const records = [runRecord("r1", 1, 1, "keep", "c1")];
const session = createSession({ keptRunIds: ["r1"] });
const plan = buildDefaultPlan({ session, records, integrationBranch: "main", mergeBaseCommit: "base" });
expect(() => mergePlanWithUserOverrides(plan, { groups: [{ id: plan.groups[0].id, runRecordIds: [] }] })).toThrow(
ExperimentFinalizePlanError,
);
});
it("builds a task-group finalize plan keyed to existing branch name", () => {
const branchGroup: BranchGroup = {
id: "BG-1",
sourceType: "mission",
sourceId: "SL-1",
branchName: "fusion/groups/sl-1",
autoMerge: false,
prState: "none",
status: "open",
createdAt: Date.now(),
updatedAt: Date.now(),
};
const plan = buildTaskGroupPlan({
branchGroup,
memberTasks: [
{ id: "FN-2", createdAt: "2026-01-02T00:00:00.000Z", mergeDetails: { commitSha: "c2" } },
{ id: "FN-1", createdAt: "2026-01-01T00:00:00.000Z", mergeDetails: { commitSha: "c1" } },
],
integrationBranch: "main",
mergeBaseCommit: "base",
});
expect(plan.groups).toHaveLength(1);
expect(plan.groups[0].suggestedBranchName).toBe("fusion/groups/sl-1");
expect(plan.groups[0].runRecordIds).toEqual(["FN-1", "FN-2"]);
expect(plan.groups[0].commits).toEqual(["c1", "c2"]);
});
it("falls back baseline commit to baseline run then merge-base", () => {
const baseline = runRecord("base", 1, 1, "keep", "baseline-commit");
const kept = runRecord("r1", 1, 2, "keep", "c1");
const withBaselineRun = buildDefaultPlan({
session: createSession({ keptRunIds: ["r1"], baselineRunId: "base" }),
records: [baseline, kept],
integrationBranch: "main",
mergeBaseCommit: "merge-base",
});
expect(withBaselineRun.baselineCommit).toBe("baseline-commit");
const mergeBaseFallback = buildDefaultPlan({
session: createSession({ keptRunIds: ["r1"] }),
records: [kept],
integrationBranch: "main",
mergeBaseCommit: "merge-base",
});
expect(mergeBaseFallback.baselineCommit).toBe("merge-base");
expect(mergeBaseFallback.warnings).toContain("no baseline commit; using merge-base as degenerate baseline");
});
});