free-ci-health: a split shard's overrun compares wall time with its prediction over jobs (GST-1653) (#4085)

shardOverruns compared a shard's wall-clock elapsedMs with predictedMs, the
shard's serial seed sum. free-tests.yml plans every shard at --shard-jobs 4,
so the 1.5x warning only fired after about a 6x slowdown. The prediction is
now divided by the plan shard's jobs, ciHealthSummary carries jobs through,
and the warning names the parallel job count. k=1 output is unchanged.

Rewritten on main from #4051, which fixes #3851.

Co-authored-by: Indra Prasetya <indraoverflow@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-11-06 16:31:18 +00:00
1 parent cf4d847e01
commit 115e245ace
4 files changed
+43 -8

No files matched your search

+26
View File
@@ -134,6 +134,15 @@ describe('free lane CI warnings', () => {
expect(overrunWarning([])).toBeNull();
});
test('a shard split across parallel jobs is measured against its prediction divided by its job count', () => {
const timing = (shard: number, jobs: number, elapsedMs: number) => ({ shard, predictedMs: 400_000, elapsedMs, jobs });
expect(shardOverruns([timing(1, 1, 610_000), timing(2, 1, 590_000)]).map(overrun => overrun.shard)).toEqual([1]);
const overruns = shardOverruns([timing(1, 4, 160_000), timing(2, 4, 140_000)]);
expect(overruns.map(overrun => overrun.shard)).toEqual([1]);
expect(overrunWarning(overruns)).toContain('shard 1 took 160s vs 100s predicted across 4 parallel jobs');
expect(overrunWarning([timing(1, 1, 610_000)])).toContain('shard 1 took 610s vs 400s predicted)');
});
test('the --ci-verify summary lists shards that outran the seed, counting the first attempt only', () => {
const outcome = (shard: number, files: string[], failingFiles: string[], elapsedMs: number): FreeShardOutcome =>
({ shard, files, failingFiles, elapsedMs, status: failingFiles.length ? 'failed' : 'passed' }) as FreeShardOutcome;
@@ -151,4 +160,21 @@ describe('free lane CI warnings', () => {
expect(summary[0]).not.toContain('shard 1');
expect(ciHealthSummary(plan, [results[0]].map(r => ({ ...r, outcome: { ...r.outcome, elapsedMs: 100_000 } })))).toEqual([]);
});
test('the --ci-verify summary divides each shard\'s prediction by the plan shard\'s jobs', () => {
const outcome = (shard: number, elapsedMs: number): FreeShardOutcome =>
({ shard, files: [], status: 'passed', exitCode: 0, elapsedMs, groupPid: null, failingFiles: [], unattributedFailures: 0 });
const plan = { version: 1, revision: 'r', id: 'p', shards: [
{ shard: 1, files: ['test/a.test.ts'], predictedMs: 400_000, jobs: 4 },
{ shard: 2, files: ['test/b.test.ts'], predictedMs: 400_000, jobs: 4 },
] } satisfies FreeCiPlan;
const results: FreeCiResult[] = [
{ planId: 'p', revision: 'r', outcome: outcome(1, 160_000), retry: null },
{ planId: 'p', revision: 'r', outcome: outcome(2, 120_000), retry: null },
];
const summary = ciHealthSummary(plan, results);
expect(summary).toHaveLength(1);
expect(summary[0]).toContain('shard 1 took 160s vs 100s predicted across 4 parallel jobs');
expect(summary[0]).not.toContain('shard 2');
});
});