test(fuzz): structured CLI/Maestro generators that reach command validation and assert error codes (#1781 B2) - #1866
test(fuzz): structured CLI/Maestro generators that reach command validation and assert error codes (#1781 B2)#1866thymikee wants to merge 7 commits into
Conversation
…dation and assert error codes (#1781 B2)
…p numeric flag samples inside their range
…ness paths keep their startup
Size Report
Startup median (7 runs, lower is better):
Top changed chunks: no changes in the largest emitted chunks. |
…eplay keeps its small module graph
|
CI note (first push): Coverage failed with Cause: Fix (f421b6a): the validation generators are resolved in the run path ( Two related mitigations already in the PR: the schema-derived CLI surface is built lazily and memoized (an eager build at import timed out the coverage-instrumented promotion test), and the harness self-check assertions were merged into one run rather than two so the serialized |
…finite classes as seeds, guard lazy surface derivation
…d classes, and PR-time gates
|
Thanks — the two evidence-integrity findings were both right, and one of them was worse than reported. Every measurement below is re-run against the current generator; the PR body and the #1781 table are restated, and the original B3 comment is marked superseded rather than edited away. Fixed
Declined / changed differently
Nits taken: CI job is 47s (I had read step times, not job times); "shrunk to a minimal case" replaced with "deterministic and replayable", with a note that fast-check shrinks the record pre-image rather than the rendered payload; the numeric-sampling narrowing from the float-phantom fix is now stated as a deliberate reach trade in the Cost line; the reach-not-yield caveat is in the PR body, not only on #1781. |
Summary
Closes the B2 half of #1781. CLI-arg and Maestro mutations used to die in the tokenizer, so nothing behind it was exercised, and every case was judged only by "did it fail well" — which cannot see a parser that silently accepts input it should refuse (the #1433 class).
Two targets,
cli-validationandmaestro-validation, build cases from the real command surface (the CLI schema registry, the Maestro command shapes) so they tokenize cleanly, and each case carries the outcome its generator planted (scripts/fuzz/validation-case.ts). The judge therefore reports two kinds the old invariant could not:silent-accept— an input built to be invalid parsed cleanly (fix(cli): reject excess positionals #1433);wrong-code— rejected, but not with theAppError.codethe generator planted, so no "some error" passes.Each CLI class declares the layer that refuses it.
command-validationclasses (excess positionals, unsupported-for-command flags, unknown commands) survive the argv scan intofinalizeParsedArgs— the reach this PR adds — and hold 80% of the mutated budget.token-scanclasses (bad enum, out-of-range int, missing value, valued boolean) are refused insideparseFlagValueduring the scan, which the classiccli-argstarget already reaches; they are weighted down to under 25% and kept only for the error-code assertioncli-argscannot make. Both layers are asserted per class invalidation-arbitraries.test.ts, so the reach is disclosed rather than implied.Rules whose entire input space is a few strings (
batch's step-source rule; the--in-app/--systemconflict) are pinned seed cases, not generated classes — generating them re-executed ~15 literals thousands of times a night for no added reach.15 files.
Validation
Rediscovery from a clean corpus (planted red)
Seeded by reverting the fixing hunk onto current
main: the pre-fix trees predate the fuzz harness (#1438 landed a day after #1433) and the.adparser has since moved intopackages/ad-script, so a literal checkout cannot run these generators. Each fix and its pre-fix parent are named; the seeded behaviour is the parent's.#1433 excess positionals — fix
4c02b6ad2, pre-fix parentfcaa6c995.assertCommandPositionalArityneutralized,--target cli-validation --iterations 38000, three seeds:The A/B that makes this new reach rather than a restatement: against the same seeded tree, the untouched
cli-argstarget ran a full 38,000 cases and found nothing — a silent acceptance is invisible to a rejection-only invariant. Case pinned inscripts/fuzz/corpus/regressions.jsonvia the real--append-corpuspath, and green again once the fix was restored.Replay parse leak — fix
006c4cadc(#1438), pre-fix parente545544df. ThereadQuotedReplayTokenJSON guard reverted so the quoted literal reachesJSON.parseraw:This one is found by the pre-existing
replay-scripttarget: it is evidence the harness still works, not evidence for the new generators.What the calibration actually shows (#1781 B3, corrected)
Eight seeded defects, all rediscovered, but only two by generation the classic targets cannot match:
cli-validation), silently-accepted Maestro field (maestro-validation; classicmaestroblind at 38,000 cases)batchstep-source,--in-app/--systemconflict (pinned seeds, so "time-to-find" is 0 by construction)Errorat the enum seam (classiccli-args: 114/179/114 cases), Maestro unsupported-command plainError(classicmaestro: 27/36/49), rawYAMLParseErrorescapeAn earlier version of this PR credited two of the fourth column to
cli-validation/maestro-validation; that was wrong, and an independent review caught it. Full table and method on #1781.This measures reach, not yield. The seeded set is drawn from the bug shapes these generators aim at, so it shows the lane can see this class — it does not predict how many unknown defects it will find. #1869 is the ledger that tests yield.
Nightly runtime
38,000 cases/target, chosen by measurement rather than rounding. Paired repeats on a quiet host, before (5 classic x 50k) vs after (7 x 38k): 17.07s / 16.48s before, 17.04s / 16.88s after — parity. A flat 33k would have been ~2s cheaper while costing the five untouched targets a third of their depth, so it was not worth taking. Current nightly for reference (run 32211761708):
Fuzz parsersstep 21s, job 47s, against a 20-minute timeout.Local gates:
pnpm check:affected --rungreen.Catches / Evidence / Cost / Kill-criterion
AppError.code, and Maestro command/field rules that stop rejecting or lose their typed boundary.--seed+ path; note fast-check shrinks the record pre-image here, not the rendered payload). 8/8 seeded defects rediscovered overall, of which 2 are new generated reach, 2 are new detection class via pinned seeds, and 4 were already reachable — measured, not asserted.subprocess-stubmember); ~530 lines of harness/generator plus ~120 of test. Reach traded away deliberately: numeric flag values are sampled at {min, max, midpoint} rather than swept (float modulo drifted past a fractionalmaxand produced a phantom), and the four token-scan classes are capped under 25% of the mutated budget.Related: #1414 (the original lane), #1823 (sibling tracker; this PR adds no
subprocess-stubmember), #1792 (plain-Error residue; PR #1853 owns that fix, untouched here).