Files
fusion/scripts/__tests__/ci-test-shard-timings.test.mjs
gsxdsm f1a2d9ae1f FN-8626: validate committed test timing snapshot
Add an automated guard that keeps CI test-sharding timings usable.

- Validate snapshot structure, freshness, and recorded test-file paths.
- Confirm planning loads the snapshot and shard dry-runs use it without stale warnings.

Files changed:
 scripts/__tests__/ci-test-shard-timings.test.mjs | 50 +++++++++++++++++++++++-
 1 file changed, 49 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-8626

Fusion-Task-Lineage: ada6525f-8dcc-4494-8586-3aa2e41618f3

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 20:42:30 -07:00

284 lines
11 KiB
JavaScript

/**
* Unit tests for the U1 timing-telemetry aggregation built into
* scripts/ci-test-shard.mjs.
*
* Runner: node --test scripts/__tests__/ci-test-shard-timings.test.mjs
*/
import test from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, existsSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import {
bucketDuration,
attributeTestFile,
extractFileDurations,
buildTimingsSnapshot,
writeTimings,
TIMINGS_SNAPSHOT_RELATIVE,
TIMINGS_STALENESS_DAYS,
discoverWorkspaceTimingFiles,
loadPlanningTimings,
} from "../ci-test-shard.mjs";
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const PACKAGES = [
{ name: "@fusion/core", dir: "packages/core" },
{ name: "@fusion/engine", dir: "packages/engine" },
];
function makeReport(projectRoot, files) {
// files: Array<{ rel: string, durationMs: number }>
return {
testResults: files.map(({ rel, durationMs }) => ({
name: path.join(projectRoot, rel),
startTime: 1000,
endTime: 1000 + durationMs,
assertionResults: [],
})),
};
}
function tmpRoot() {
return mkdtempSync(path.join(tmpdir(), "fusion-timings-test-"));
}
/*
* FNXC:CITestSharding 2026-07-27-03:20:
* The committed timing snapshot must remain current and refer only to live test
* files. Stale or phantom-path entries silently degrade CI shard balancing and
* can hide merge-gate regressions, so this guard fails before that drift lands.
*/
test("committed timing snapshot is fresh, parseable, and references live test files", () => {
const snapshotPath = path.join(REPO_ROOT, TIMINGS_SNAPSHOT_RELATIVE);
const snapshot = JSON.parse(readFileSync(snapshotPath, "utf8"));
assert.ok(snapshot.packages && typeof snapshot.packages === "object");
assert.ok(Object.keys(snapshot.packages).length > 0, "snapshot must contain package timings");
const capturedAtMs = new Date(snapshot.capturedAt).getTime();
assert.ok(Number.isFinite(capturedAtMs), `capturedAt must be a valid ISO timestamp: ${snapshot.capturedAt}`);
assert.ok(capturedAtMs <= Date.now(), `capturedAt must not be future-dated: ${snapshot.capturedAt}`);
assert.ok(
(Date.now() - capturedAtMs) / 86_400_000 <= TIMINGS_STALENESS_DAYS,
`capturedAt exceeds the ${TIMINGS_STALENESS_DAYS}-day staleness budget: ${snapshot.capturedAt}`,
);
const recordedFiles = Object.values(snapshot.packages).flatMap((pkg) => Object.keys(pkg.files ?? {}));
const missingFiles = recordedFiles.filter((file) => !existsSync(path.join(REPO_ROOT, file)));
assert.deepEqual(missingFiles, [], `timing snapshot references missing test files:\n${missingFiles.join("\n")}`);
const timings = loadPlanningTimings({ projectRoot: REPO_ROOT });
assert.equal(timings.present, true);
assert.equal(timings.stale, false);
assert.ok(timings.fileDurations.size > 0, "committed snapshot must expose duration-weighted files");
const dryRun = spawnSync(
process.execPath,
[path.join(REPO_ROOT, "scripts/ci-test-shard.mjs"), "--dry-run", "--total", "4"],
{ cwd: REPO_ROOT, encoding: "utf8" },
);
assert.equal(dryRun.status, 0, dryRun.stderr);
assert.doesNotMatch(
`${dryRun.stdout}\n${dryRun.stderr}`,
/timing snapshot is stale|no timing snapshot found/i,
"dry-run must use the committed duration snapshot without missing or stale warnings",
);
});
test("bucketDuration rounds to nearest 100ms, floors non-zero to one bucket", () => {
assert.equal(bucketDuration(0), 0);
assert.equal(bucketDuration(40), 100); // sub-bucket non-zero floors up
assert.equal(bucketDuration(149), 100);
assert.equal(bucketDuration(150), 200);
assert.equal(bucketDuration(1234), 1200);
assert.equal(bucketDuration(-5), 0);
});
test("attributeTestFile maps absolute paths to owning package, repo-relative", () => {
const root = "/repo";
const got = attributeTestFile("/repo/packages/core/src/__tests__/a.test.ts", PACKAGES, root);
assert.deepEqual(got, { pkg: "@fusion/core", file: "packages/core/src/__tests__/a.test.ts" });
assert.equal(attributeTestFile("/repo/tools/x.test.ts", PACKAGES, root), null);
});
test("attributeTestFile accepts CI absolute paths when their checkout differs", () => {
const got = attributeTestFile(
"/home/runner/work/Fusion/Fusion/packages/core/src/__tests__/a.test.ts",
PACKAGES,
"/local/worktree/Fusion",
);
assert.deepEqual(got, { pkg: "@fusion/core", file: "packages/core/src/__tests__/a.test.ts" });
});
test("extractFileDurations sums per-file durations and tolerates bad rows", () => {
const root = "/repo";
const report = {
testResults: [
{ name: "/repo/packages/core/a.test.ts", startTime: 0, endTime: 250 },
{ name: "/repo/packages/core/a.test.ts", startTime: 250, endTime: 500 }, // same file, summed
{ name: "/repo/packages/engine/b.test.ts", startTime: 0, endTime: 700 },
{ name: 42, startTime: 0, endTime: 1 }, // bad name
{ name: "/repo/packages/core/c.test.ts", startTime: 500, endTime: 100 }, // end<start ignored
{ name: "/repo/outside/d.test.ts", startTime: 0, endTime: 5 }, // unattributable
],
};
const byPkg = extractFileDurations(report, PACKAGES, root);
assert.equal(byPkg.get("@fusion/core").get("packages/core/a.test.ts"), 500);
assert.equal(byPkg.get("@fusion/engine").get("packages/engine/b.test.ts"), 700);
assert.ok(!byPkg.get("@fusion/core").has("packages/core/c.test.ts"));
});
test("buildTimingsSnapshot merges two shard JSON fixtures, sums per file, buckets", () => {
const root = tmpRoot();
try {
const f1 = path.join(root, "s1.json");
const f2 = path.join(root, "s2.json");
writeFileSync(f1, JSON.stringify(makeReport(root, [
{ rel: "packages/core/src/__tests__/a.test.ts", durationMs: 240 },
{ rel: "packages/engine/src/__tests__/b.test.ts", durationMs: 1010 },
])));
writeFileSync(f2, JSON.stringify(makeReport(root, [
// same file as f1 → durations sum across shards before bucketing
{ rel: "packages/core/src/__tests__/a.test.ts", durationMs: 60 },
])));
const snap = buildTimingsSnapshot([f1, f2], { projectRoot: root, packages: PACKAGES, capturedAt: "2026-06-03T00:00:00.000Z" });
assert.equal(snap.capturedAt, "2026-06-03T00:00:00.000Z");
// 240 + 60 = 300 → bucketed to 300
assert.equal(snap.packages["@fusion/core"].files["packages/core/src/__tests__/a.test.ts"], 300);
// 1010 → 1000
assert.equal(snap.packages["@fusion/engine"].files["packages/engine/src/__tests__/b.test.ts"], 1000);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("buildTimingsSnapshot tolerates a corrupt shard file: skips it, keeps others", () => {
const root = tmpRoot();
try {
const good = path.join(root, "good.json");
const bad = path.join(root, "bad.json");
writeFileSync(good, JSON.stringify(makeReport(root, [
{ rel: "packages/core/x.test.ts", durationMs: 300 },
])));
writeFileSync(bad, "{not valid json");
const snap = buildTimingsSnapshot([bad, good, path.join(root, "missing.json")], {
projectRoot: root,
packages: PACKAGES,
capturedAt: "2026-06-03T00:00:00.000Z",
});
assert.equal(snap.packages["@fusion/core"].files["packages/core/x.test.ts"], 300);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("buildTimingsSnapshot omits a zero-test package entirely (no zero entry)", () => {
const root = tmpRoot();
try {
const f = path.join(root, "s.json");
writeFileSync(f, JSON.stringify(makeReport(root, [
{ rel: "packages/core/y.test.ts", durationMs: 200 },
])));
const snap = buildTimingsSnapshot([f], { projectRoot: root, packages: PACKAGES, capturedAt: "2026-06-03T00:00:00.000Z" });
assert.ok(snap.packages["@fusion/core"]);
assert.ok(!("@fusion/engine" in snap.packages));
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("writeTimings writes snapshot to scripts/test-timings.json under a project root", () => {
const root = tmpRoot();
try {
const inputDir = path.join(root, ".timings");
mkdirSync(inputDir, { recursive: true });
writeFileSync(path.join(inputDir, "timings-shard1-0.json"), JSON.stringify(makeReport(root, [
{ rel: "packages/core/z.test.ts", durationMs: 500 },
])));
const snapshotPath = path.join(root, TIMINGS_SNAPSHOT_RELATIVE);
const result = writeTimings({
projectRoot: root,
inputDir,
snapshotPath,
packages: PACKAGES,
capturedAt: "2026-06-03T00:00:00.000Z",
});
assert.equal(result.written, true);
const written = JSON.parse(readFileSync(snapshotPath, "utf8"));
assert.equal(written.packages["@fusion/core"].files["packages/core/z.test.ts"], 500);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("writeTimings refuses to overwrite a newer snapshot", () => {
const root = tmpRoot();
try {
const inputDir = path.join(root, ".timings");
mkdirSync(inputDir, { recursive: true });
writeFileSync(path.join(inputDir, "timings-shard1-0.json"), JSON.stringify(makeReport(root, [
{ rel: "packages/core/z.test.ts", durationMs: 500 },
])));
const snapshotPath = path.join(root, "snap.json");
// Existing snapshot dated in the future.
writeFileSync(snapshotPath, JSON.stringify({ capturedAt: "2999-01-01T00:00:00.000Z", packages: { keep: { files: {} } } }));
const result = writeTimings({
projectRoot: root,
inputDir,
snapshotPath,
packages: PACKAGES,
capturedAt: "2026-06-03T00:00:00.000Z",
});
assert.equal(result.written, false);
assert.equal(result.reason, "newer-snapshot");
// Original untouched.
const after = JSON.parse(readFileSync(snapshotPath, "utf8"));
assert.equal(after.capturedAt, "2999-01-01T00:00:00.000Z");
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("writeTimings warns and does not write when there are no input files", () => {
const root = tmpRoot();
try {
const result = writeTimings({
projectRoot: root,
inputDir: path.join(root, ".timings-empty"),
snapshotPath: path.join(root, "snap.json"),
});
assert.equal(result.written, false);
assert.equal(result.reason, "no-inputs");
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("discoverWorkspaceTimingFiles finds root and per-package .timings files", (t) => {
const root = mkdtempSync(path.join(tmpdir(), "wts-discover-"));
t.after(() => rmSync(root, { recursive: true, force: true }));
mkdirSync(path.join(root, ".timings"), { recursive: true });
mkdirSync(path.join(root, "packages/aaa/.timings"), { recursive: true });
mkdirSync(path.join(root, "plugins/bbb/.timings"), { recursive: true });
mkdirSync(path.join(root, "packages/no-timings-here"), { recursive: true });
writeFileSync(path.join(root, ".timings/timings-shard1-0.json"), "{}");
writeFileSync(path.join(root, "packages/aaa/.timings/timings-shard1-0.json"), "{}");
writeFileSync(path.join(root, "plugins/bbb/.timings/timings-shard2-0.json"), "{}");
writeFileSync(path.join(root, "packages/aaa/.timings/not-a-match.txt"), "");
const found = discoverWorkspaceTimingFiles(root).map((f) => path.relative(root, f));
assert.deepEqual(found.sort(), [
".timings/timings-shard1-0.json",
"packages/aaa/.timings/timings-shard1-0.json",
"plugins/bbb/.timings/timings-shard2-0.json",
]);
});