test(FN-4126): complete Step 5 — extend shard planner coverage

Fusion-Task-Id: FN-4126
Fusion-Task-Lineage: ed1e7e10-43d3-4347-933b-0dc9e8cd345a
This commit is contained in:
Fusion
2026-05-13 03:31:23 -07:00
committed by gsxdsm
parent 1f58bebec3
commit 173e7d9e76

View File

@@ -5,7 +5,7 @@ import {
resolveAffectedPackages,
shouldForceFullSuite,
} from "../../../../scripts/test-changed.mjs";
import { parseShardArgs, planShardAssignments, selectShardPackages, expandVirtualPackages } from "../../../../scripts/ci-test-shard.mjs";
import { computeSplitPlan, parseShardArgs, planShardAssignments, selectShardPackages } from "../../../../scripts/ci-test-shard.mjs";
describe("root test command changed-only planning", () => {
it("uses changed mode when package-only changes are detected", () => {
@@ -78,115 +78,102 @@ describe("CI shard test planner", () => {
);
});
it("deterministically balances weighted packages across shards", () => {
it("deterministically balances weighted packages across shards with virtual dashboard slices", () => {
const weightedPackages = [
{ name: "@fusion/dashboard", testFileCount: 140 },
{ name: "@fusion/engine", testFileCount: 120 },
{ name: "@fusion/core", testFileCount: 60 },
{ name: "@runfusion/fusion", testFileCount: 40 },
{ name: "@fusion/plugin-sdk", testFileCount: 18 },
{ name: "@fusion/mobile", testFileCount: 12 },
{ name: "@fusion/desktop", testFileCount: 8 },
{ name: "@fusion/dashboard-utils", testFileCount: 4 },
{ name: "@fusion/no-tests-yet", testFileCount: 0 },
{ name: "@fusion/dashboard", testFileCount: 505 },
{ name: "@fusion/engine", testFileCount: 90 },
{ name: "@fusion/core", testFileCount: 80 },
{ name: "@runfusion/fusion", testFileCount: 50 },
{ name: "@fusion/plugin-sdk", testFileCount: 30 },
];
// Dashboard (140) exceeds avg threshold (ceil(402/3)=134) so it gets split
// into 2 virtual entries of 70 each, dispatched with vitest --shard.
const shardAssignments = planShardAssignments(weightedPackages, 3);
// Verify selectShardPackages returns matching slices
expect(selectShardPackages(weightedPackages, 1, 3)).toEqual(shardAssignments[0]);
expect(selectShardPackages(weightedPackages, 2, 3)).toEqual(shardAssignments[1]);
expect(selectShardPackages(weightedPackages, 3, 3)).toEqual(shardAssignments[2]);
// Verify shard weights are balanced within 15% of mean
const shardWeights = shardAssignments.map((shardEntries) =>
shardEntries.reduce((sum, entry) => sum + (entry as { weight: number }).weight, 0),
const dashboardSlices = shardAssignments
.flat()
.filter((entry) => entry.name === "@fusion/dashboard" && entry.shardCount === 3);
expect(dashboardSlices).toHaveLength(3);
expect(dashboardSlices.map((entry) => entry.shardIndex).sort()).toEqual([1, 2, 3]);
const shardsContainingDashboard = shardAssignments
.map((entries, index) => ({ entries, index }))
.filter(({ entries }) => entries.some((entry) => entry.name === "@fusion/dashboard"))
.map(({ index }) => index);
expect(shardsContainingDashboard).toEqual([0, 1, 2]);
const computedSplitPlan = computeSplitPlan(weightedPackages, 3);
const byWeight = new Map(computedSplitPlan.map((entry) => [
`${entry.name}:${entry.shardIndex ?? 0}/${entry.shardCount ?? 0}`,
entry.weight,
]));
const shardWeights = shardAssignments.map((entries) =>
entries.reduce(
(sum, entry) =>
sum +
(byWeight.get(`${entry.name}:${entry.shardIndex ?? 0}/${entry.shardCount ?? 0}`) ?? 0),
0,
),
);
const totalWeight = weightedPackages.reduce((sum, pkg) => sum + pkg.testFileCount, 0);
const mean = totalWeight / 3;
expect(Math.max(...shardWeights)).toBeLessThanOrEqual(mean * 1.15);
expect(Math.max(...shardWeights)).toBeLessThanOrEqual(mean * 1.1);
expect(Math.min(...shardWeights)).toBeGreaterThanOrEqual(mean * 0.85);
});
// Verify dashboard was split across 2 shards and engine is on a different shard
const dashboardShards = shardAssignments.filter((shard) =>
shard.some((e) => (e as { name: string }).name === "@fusion/dashboard"),
);
const engineShard = shardAssignments.findIndex((shard) =>
shard.some((e) => (e as { name: string }).name === "@fusion/engine"),
);
expect(dashboardShards.length).toBe(2); // split into 2 virtual entries
expect(engineShard).toBeGreaterThanOrEqual(0);
it("leaves packages whole when no single package exceeds the split threshold", () => {
const weightedPackages = [
{ name: "@fusion/engine", testFileCount: 40 },
{ name: "@fusion/core", testFileCount: 40 },
{ name: "@runfusion/fusion", testFileCount: 40 },
];
// Verify virtual entries carry vitest shard metadata
const virtualEntries = shardAssignments
.flat()
.filter((e) => (e as { vitestShardCount?: number }).vitestShardCount);
expect(virtualEntries.length).toBe(2);
for (const entry of virtualEntries) {
const e = entry as { name: string; vitestShardIndex: number; vitestShardCount: number };
expect(e.name).toBe("@fusion/dashboard");
expect(e.vitestShardCount).toBe(2);
expect(e.vitestShardIndex).toBeGreaterThanOrEqual(1);
expect(e.vitestShardIndex).toBeLessThanOrEqual(2);
const shardAssignments = planShardAssignments(weightedPackages, 3, { threshold: 2 });
expect(shardAssignments.flat().every((entry) => entry.shardCount === undefined)).toBe(true);
});
it("splits never co-locate two slices of the same package on the same shard", () => {
const weightedPackages = [
{ name: "@fusion/dashboard", testFileCount: 505 },
{ name: "@fusion/engine", testFileCount: 10 },
{ name: "@fusion/core", testFileCount: 10 },
];
const shardAssignments = planShardAssignments(weightedPackages, 3);
for (const entries of shardAssignments) {
const dashboardEntries = entries.filter((entry) => entry.name === "@fusion/dashboard");
expect(dashboardEntries).toHaveLength(1);
}
});
});
describe("expandVirtualPackages", () => {
it("passes through packages below threshold as plain entries", () => {
const pkgs = [
{ name: "small", testFileCount: 10 },
{ name: "tiny", testFileCount: 3 },
];
const result = expandVirtualPackages(pkgs, 50);
describe("computeSplitPlan", () => {
it("splits oversized package into k slices where k is capped by total", () => {
const result = computeSplitPlan([{ name: "big", testFileCount: 100 }], 3);
expect(result).toEqual([
{ name: "small", weight: 10 },
{ name: "tiny", weight: 3 },
{ name: "big", weight: 34, shardIndex: 1, shardCount: 3 },
{ name: "big", weight: 34, shardIndex: 2, shardCount: 3 },
{ name: "big", weight: 34, shardIndex: 3, shardCount: 3 },
]);
});
it("splits oversized package into evenly-weighted virtual entries", () => {
const pkgs = [{ name: "big", testFileCount: 100 }];
const result = expandVirtualPackages(pkgs, 30);
// ceil(100/30) = 4 entries, floor(100/4)=25, remainder=0
expect(result).toHaveLength(4);
for (const entry of result) {
expect(entry.name).toBe("big");
expect(entry.weight).toBe(25);
expect(entry.vitestShardCount).toBe(4);
}
expect(result.map((e) => e.vitestShardIndex)).toEqual([1, 2, 3, 4]);
});
it("returns rewritten list with whole and virtual entries", () => {
const result = computeSplitPlan(
[
{ name: "@fusion/dashboard", testFileCount: 505 },
{ name: "@fusion/core", testFileCount: 60 },
],
3,
);
it("distributes remainder to first entries when weight is not evenly divisible", () => {
const pkgs = [{ name: "odd", testFileCount: 10 }];
const result = expandVirtualPackages(pkgs, 4);
// ceil(10/4) = 3 entries, floor(10/3)=3, remainder=1
expect(result).toHaveLength(3);
expect(result.map((e) => e.weight)).toEqual([4, 3, 3]);
expect(result.map((e) => e.vitestShardIndex)).toEqual([1, 2, 3]);
expect(result.every((e) => e.vitestShardCount === 3)).toBe(true);
});
it("returns plain entry when testFileCount equals threshold exactly", () => {
const pkgs = [{ name: "exact", testFileCount: 50 }];
const result = expandVirtualPackages(pkgs, 50);
expect(result).toEqual([{ name: "exact", weight: 50 }]);
});
it("handles zero testFileCount without splitting", () => {
const pkgs = [{ name: "empty", testFileCount: 0 }];
const result = expandVirtualPackages(pkgs, 10);
expect(result).toEqual([{ name: "empty", weight: 0 }]);
});
it("defaults to no splitting when threshold is Infinity", () => {
const pkgs = [{ name: "huge", testFileCount: 9999 }];
const result = expandVirtualPackages(pkgs);
expect(result).toEqual([{ name: "huge", weight: 9999 }]);
expect(result).toEqual([
{ name: "@fusion/dashboard", weight: 169, shardIndex: 1, shardCount: 3 },
{ name: "@fusion/dashboard", weight: 169, shardIndex: 2, shardCount: 3 },
{ name: "@fusion/dashboard", weight: 169, shardIndex: 3, shardCount: 3 },
{ name: "@fusion/core", weight: 60 },
]);
});
});