feat(FN-4222): complete Step 1 — finalize plan model
Fusion-Task-Id: FN-4222 Fusion-Task-Lineage: 7de6b18c-36d5-49e8-bbd7-084707e88d6a
This commit is contained in:
163
packages/engine/src/__tests__/experiment-finalize-plan.test.ts
Normal file
163
packages/engine/src/__tests__/experiment-finalize-plan.test.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ExperimentSession, ExperimentSessionRecord } from "@fusion/core";
|
||||
import { buildDefaultPlan, 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("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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user