Files
OmniRoute/tests/unit/balance-e2e-shards.test.ts
Diego Rodrigues de Sa e Souza 4505e67c04 feat(ci): duration-balanced E2E shards via LPT bin-packing (WS4.1) (#7090)
Playwright --shard distributes by count (per file with fullyParallel:false),
blind to duration — measured skew on the 9-shard matrix: 24m47s worst vs 1m47s
best (14x), putting E2E on the CI critical path (~25min of the 33min gate).

- scripts/quality/balance-e2e-shards.mjs: LPT greedy (heaviest first into the
  lightest shard) over config/quality/e2e-timings.json; deterministic
  (weight desc, filename tiebreak); new specs get the median weight; the CLI
  self-verifies the shard union equals the discovered spec list and exits
  non-zero on ANY inconsistency (missing timings, lost spec) so the CI step
  falls back to plain --shard — never fewer specs than before.
- config/quality/e2e-timings.json: relative weights seeded from spec LOC
  (proxy); replace with real per-file durations from a full run when convenient
  (documented in _meta). LOC-seeded packing already lands at 742-761 per shard
  (1.03x skew) vs the alphabetical round-robin that produced 14x.
- ci.yml test-e2e: balanced list per shard with logged assignment + fallback.
TDD: 5 unit tests (LPT invariants, determinism, completeness, median fallback,
seed-vs-specs drift guard).
2026-07-14 07:07:27 -03:00

76 lines
2.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { lptAssign, weightItems } from "../../scripts/quality/balance-e2e-shards.mjs";
// WS4.1 (v3.8.49 quality plan) — the E2E matrix skew was 14× (24m47s vs 1m47s)
// because Playwright --shard distributes by count, not duration. These tests pin
// the LPT packing invariants; the hard one is COMPLETENESS (a lost spec would
// silently hollow the suite — the CLI self-checks it and falls back to --shard).
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
test("lptAssign puts the heaviest item alone before doubling up lighter shards", () => {
const shards = lptAssign(
[
{ file: "huge.spec.ts", weight: 100 },
{ file: "a.spec.ts", weight: 30 },
{ file: "b.spec.ts", weight: 30 },
{ file: "c.spec.ts", weight: 30 },
],
2
);
assert.deepEqual(shards[0].files, ["huge.spec.ts"]);
assert.deepEqual(shards[1].files, ["a.spec.ts", "b.spec.ts", "c.spec.ts"]);
assert.equal(shards[0].total, 100);
assert.equal(shards[1].total, 90);
});
test("lptAssign is deterministic on equal weights (filename tiebreak)", () => {
const items = [
{ file: "b.spec.ts", weight: 10 },
{ file: "a.spec.ts", weight: 10 },
];
const s1 = lptAssign(items, 2);
const s2 = lptAssign([...items].reverse(), 2);
assert.deepEqual(s1, s2);
});
test("completeness: every file lands in exactly one shard", () => {
const items = Array.from({ length: 37 }, (_, i) => ({
file: `f${String(i).padStart(2, "0")}.spec.ts`,
weight: (i * 7) % 40,
}));
const shards = lptAssign(items, 9);
const union = shards.flatMap((s) => s.files).sort();
assert.deepEqual(union, items.map((i) => i.file).sort());
});
test("weightItems gives unknown/new specs the median weight, not an extreme", () => {
const items = weightItems(["new.spec.ts", "big.spec.ts", "small.spec.ts"], {
_meta: "x",
"big.spec.ts": 600,
"small.spec.ts": 20,
"other.spec.ts": 100,
});
const byFile = Object.fromEntries(items.map((i) => [i.file, i.weight]));
assert.equal(byFile["big.spec.ts"], 600);
assert.equal(byFile["small.spec.ts"], 20);
assert.equal(byFile["new.spec.ts"], 100); // median of [20,100,600]
});
test("the committed timings seed covers every current e2e spec (no drift)", () => {
const timings = JSON.parse(
fs.readFileSync(path.join(ROOT, "config", "quality", "e2e-timings.json"), "utf8")
);
const specs = fs
.readdirSync(path.join(ROOT, "tests", "e2e"))
.filter((f) => f.endsWith(".spec.ts"));
const missing = specs.filter((f) => !(f in timings));
// Missing entries are tolerated at runtime (median fallback) — this assert keeps
// the seed honest so balance quality does not silently rot as specs are added.
assert.deepEqual(missing, [], `add to config/quality/e2e-timings.json: ${missing.join(", ")}`);
});