Files
callstack__agent-device/scripts/mutation/report.test.ts
Michał Pierzchała 423927fdd8 chore(mutation): shrink to report-only — drop the ratchet, baseline and graduation (#1457, #1781) (#1828)
* chore(mutation): shrink the lane to report-only (#1457, #1781 wave 2)

The mutation harness's two real catches (#1474, #1475) both came from humans
reading the weekly score report. The ratchet half never operated: the baseline
was committed exactly twice (8cce0ef6b, 60400d04b), both times with
`stableRuns: 0, gating: false`, and was never updated after the very fixes it
triggered — the weekly job computed a new baseline and then `git checkout --`d
it, uploading a proposal nobody applied in 3+ weeks. A gate nobody arms is
harness weight; the report is the part that paid.

Deletes ratchet.ts + ratchet.test.ts, mutation-baselines/, and every
baseline/graduation/gating path in run.ts (`--update`, `mutation:baseline`).
run.ts now exits non-zero only on a harness failure, never on a score. The
report renders the per-kernel table (kernel, score, killed, survived, total,
timeouts) plus the surviving mutants a strengthening PR works from.

Kernel scoping stays: stryker.config.json and KERNEL_MODULES are untouched.

* fix(mutation): restore denominator coverage and publish the table before judging the shard set

Review of #1828:
- `report.test.ts` re-asserts that Ignored/CompileError/RuntimeError leave the
  denominator — the one behaviour `ratchet.test.ts` covered and nothing replaced.
  A `tally()` edit that counted tool noise would have deflated every published
  score with a green `mutation:test`.
- `assertShardsCoverModules` now runs after `emit()`, so an incomplete shard set
  still publishes the kernels that completed instead of only an error string.
  This makes the workflow comments' claim about the job summary true rather than
  re-wording them down.

* chore(mutation): trigger the affected lane on exactly the paths that can select mutants

The PR lane returns an empty matrix unless the diff touches the harness, so the
kernel-source and `**/*.test.ts` triggers only bought a 1-4 min no-op job on
~96% of PRs. `on.pull_request.paths` is now exactly `LANE_TOOLING` plus the
workflow file, asserted in both directions by workflow.test.ts against the
exported constant — a missing path would let a harness change merge unproven,
an extra one starts a job that can only answer `[]`.

Also drops the workflow header's contradictory scope paragraph: it claimed the
lane selects on kernel sources and any test reaching one, which has not been
true since the ratchet went.

* fix(mutation): score and publish a short shard set before failing on the count

The expected-count check ran inside readShardedReports, before anything was
summarized, so on the weekly's real `--expect-shards 10` one dead shard threw
away the nine that had reported — the earlier reorder only moved the
zero-mutants check. The merge now returns the shard count, and both verdicts
run after emit() with the same exit code and `score` stage.

Regression uses the weekly argument shape (`--expect-shards 10`, one shard
present) and asserts the reporting kernel's row reaches stdout while the run
still fails.
2026-08-18 17:47:29 +02:00

81 lines
2.9 KiB
TypeScript

// The report is the lane's whole product (#1457): it never gates, so a table
// that stops carrying the numbers is the lane failing silently rather than
// loudly. Both #1474 and #1475 were written from this table.
//
// Only the renderer is exercised here; every test that runs the CLI lives in
// envelope.test.ts, because a second file spawning `run.ts` would race it over
// the repo-relative lane envelope.
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { renderReport } from './report.ts';
import { summarizeReport } from './score.ts';
const PROVENANCE = { strykerVersion: '9.6.1', configHash: 'sha256:abcdef123456' };
test('the table carries score, killed, survived, total and timeouts per kernel', () => {
const scores = summarizeReport(
{
files: {
'packages/kernel/src/errors.ts': {
mutants: [
{ status: 'Killed' },
{ status: 'Timeout' },
{
status: 'Survived',
mutatorName: 'ConditionalExpression',
location: { start: { line: 7 } },
},
{
status: 'NoCoverage',
mutatorName: 'BooleanLiteral',
location: { start: { line: 9 } },
},
],
},
},
},
['kernel-errors'],
);
const markdown = renderReport(scores, PROVENANCE);
// Timeouts count as killed, so a score propped up by slow mutants rather than
// assertions is only visible if the column is rendered.
assert.match(markdown, /\| `kernel-errors` — [^|]+\| 50% \| 2 \| 2 \| 4 \| 1 \|/);
// The surviving mutants are what a test-strengthening PR works from.
assert.match(markdown, /- `packages\/kernel\/src\/errors\.ts:7` ConditionalExpression/);
assert.match(markdown, /- `packages\/kernel\/src\/errors\.ts:9` BooleanLiteral/);
});
// The denominator is the report's arithmetic: counting tool-side noise would
// silently deflate every score the lane publishes. Ported from the deleted
// ratchet.test.ts, which was where this lived.
test('statuses outside the score (Ignored, CompileError, RuntimeError) leave the denominator', () => {
const [score] = summarizeReport(
{
files: {
'packages/kernel/src/errors.ts': {
mutants: [
{ status: 'Killed' },
{ status: 'Ignored' },
{ status: 'CompileError' },
{ status: 'RuntimeError' },
],
},
},
},
['kernel-errors'],
);
assert.equal(score?.total, 1);
assert.equal(score?.score, 100);
});
test('a kernel with no surviving mutants lists no detail section', () => {
const scores = summarizeReport(
{ files: { 'packages/kernel/src/errors.ts': { mutants: [{ status: 'Killed' }] } } },
['kernel-errors'],
);
const markdown = renderReport(scores, PROVENANCE);
assert.match(markdown, /\| `kernel-errors` — [^|]+\| 100% \| 1 \| 0 \| 1 \| 0 \|/);
assert.doesNotMatch(markdown, /^### /m);
});