Skip to content

test: add fast-check property suite#51

Open
voxpelli wants to merge 4 commits into
claude/merge-branches-to-master-nixCBfrom
claude/fast-check-properties-nixCB
Open

test: add fast-check property suite#51
voxpelli wants to merge 4 commits into
claude/merge-branches-to-master-nixCBfrom
claude/fast-check-properties-nixCB

Conversation

@voxpelli

@voxpelli voxpelli commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Stacked on #50 — targets the 2.0.0 branch so the diff shows only the property suite; retarget to main (or let GitHub auto-retarget) once #50 merges.

What

Adds fast-check (dev-only — the zero-runtime-deps invariant is untouched) and test/properties.spec.js with three properties, plus a shared isCleanDone helper in test/utils.js (also tightening expectSingleRejectionThenDone's tail check):

  1. Differential vs native for await — any values × bufferSize × errorPos × errors-mode × lenient/defensive source: full multiset equality whenever no error surfaced (fail-fast included), multiset-aware subset on the fail-fast error path, identity-preserved terminal errors, and two model tripwires (oracle over-pull / oracle error identity) so a broken model can't masquerade as a library verdict.
  2. Committed-rejection delivery under generated abort geometry — the same commit-point oracle as the deterministic drain-race sweep in test/abort.spec.js (cross-referenced both ways: the sweep is the regression pin, this property is the accumulating search): the per-callback signal's immutable reason discriminates abort-won / fail-fast-won / error-obligated / clean runs, and each arm demands exactly the committed rejection — at most once and at least once — plus strict 2-key done shapes, a guaranteed terminal, and zero unhandled rejections. Scheduled-anchor aborts are folded into the scheduler's waitFor promise so they always fire.
  3. Protocol op-sequences vs native AsyncGenerator as the model — arbitrary next/return(v)/throw sequences settle identically, with same-microtask teeth for the await-idempotence contract (a terminal may not settle before the source's finally ran) and the two documented eager-construction divergences encoded one-directionally.

Mutation-validated (the acceptance gate)

The committed spec was run against mutant copies of index.js built from real bug classes (each mutant fails 2–17 example specs, proving non-equivalence):

  • 12 of 12 non-equivalent mutants killed within 2 000 runs — including all five that survived 20 000 runs of the suite's first iteration (swallowed aborts, swallowed callback errors, fail-fast silent value loss, cleanup-ordering violations, wrong-identity delivery).
  • The drain-race guard revert is caught at 20k-run scale (previously structurally invisible); the deterministic sweep remains the pin for that class.
  • One further mutant proved equivalent (144/144 example specs pass it; the removed line is unreachable bookkeeping) — correctly unkillable.
  • Zero false positives: the correct tree passes 20 000 runs per property (~5 s), plus the full check chain.

Notable side-discovery: the fail-fast silent-value-loss mutant passes the entire example suite — the property suite is currently the only automated coverage for that contract class.

Conventions (documented in CLAUDE.md and the file header)

  • Seeds stay random in CI — a failure is a discovery; it prints seed+path for deterministic replay. Replay must be scoped to the failing property (FC_SEED=… FC_PATH=… npx mocha test/properties.spec.js -g "<test name>" — enforced, since a shrink path is only meaningful for the property that produced it). Every discovery gets pinned as a plain example spec.
  • Hang safety: per-run fast-check timeout (a hanging library bug becomes a reported, shrunk failure instead of a bare mocha timeout losing the report) + interruptAfterTimeLimit/markInterruptAsFailure bounding the whole assert under the mocha cap, which itself scales with FC_NUM_RUNS.
  • Loud env validation: fast-check silently runs zero cases on numRuns: 0/NaN, so bad FC_NUM_RUNS/FC_SEED values throw.
  • Vacuity counters: every conditional-oracle arm asserts it actually fired across the run set — a guard that never triggers certifies nothing.
  • Property sources are timer-free; no sinon fake timers; the source/abort models are the review surface; hostile-shape testing stays in test/hostile-results.spec.js.

Verification

Full check chain (lint, tsc strict, knip, type-coverage, installed-check) + all 144 specs green; properties additionally validated by the mutation matrix above and a 20 000-run clean sweep. Design grounded in the WHATWG single-assignment abort algorithm (the commit-point oracle), fast-check's documented timeout semantics, and property-based mutation-testing methodology (Hughes' "How to Specify It!"; ICST 2023).

🤖 Generated with Claude Code

https://claude.ai/code/session_01FSyTJjbP823pjYfueuSsdu

claude added 3 commits July 7, 2026 23:58
Three properties in test/properties.spec.js (100 runs each by default,
~50ms; FC_NUM_RUNS for deep sweeps, FC_SEED/FC_PATH replay a printed
counterexample):

- Differential vs native for-await: any values × bufferSize × error
  position × errors mode × lenient/defensive source must yield the same
  value multiset (fail-fast may cut early, never invent) and the same
  identity-preserved terminal error. In the adoption spike this property
  caught the post-done over-pull bug in 2-10 runs against the pre-fix
  tree.
- Exactly-once delivery under generated abort geometry: fc.scheduler
  orders callback resolutions while a microtask-hop-count arbitrary
  (anchored at construction or at scheduler release — both matter)
  positions an external abort; invariants are at most one rejection,
  reason identity ∈ {callback error, abort reason}, done-forever after,
  zero unhandled rejections. The generalized form of the drain-race
  sweep in test/abort.spec.js; in the spike it reproduced that bug and
  shrank it to the same minimal counterexample the hand-written sweep
  pins.
- Protocol op-sequences vs a native AsyncGenerator as the model:
  arbitrary next/return(v)/throw sequences must settle with identical
  outcome shapes and rejection identities, with the two documented
  eager-construction divergences (prefetch may start/finish the source
  early) encoded as one-directional finally-parity.

Seeds stay random in CI: a property failure is a discovery (it prints
seed+path for deterministic replay), and every discovery gets pinned as
a plain example spec — regression protection never depends on run
count. CLAUDE.md documents the conventions: timer-free sources, no
fake timers, models are the review surface, hostile shapes stay in
hostile-results.spec.js. fast-check is a devDependency; the
zero-runtime-deps invariant is untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSyTJjbP823pjYfueuSsdu
A mutation-tested review found the three properties structurally unable
to catch five real bug classes (each mutant survived 4k-20k runs while
failing 2-17 example specs). The strengthening, validated by research
against the WHATWG single-assignment abort algorithm, fast-check's
documented timeout semantics, and the property-based-mutation-testing
literature (Hughes' "How to Specify It!"; ICST 2023):

- The abort property now uses the same commit-point oracle as the
  pinned drain-race sweep in test/abort.spec.js (cross-referenced both
  ways): the per-callback signal's immutable reason discriminates
  abort-won / fail-fast-won / no-commit, and each arm demands exactly
  the committed rejection — at most once AND at least once. A
  dispatched callback error with no out-racing shutdown must surface;
  a clean run must not reject; a pre-pull abort must reject the first
  pull. Every done result must be the exact 2-key shape (isCleanDone,
  extracted to test/utils.js and shared with
  expectSingleRejectionThenDone); a terminal must be reached; the
  scheduled abort anchor is now folded into the waitFor promise so it
  always fires (previously ~15% of those runs); abort geometry is one
  optional record (no dead shrink dimensions); empty sources are
  generated.
- The differential property compares full multisets whenever no error
  surfaced (fail-fast included — silent value loss on a clean stream
  previously passed the property AND the whole example suite), uses a
  multiset-aware subset on the fail-fast error path (duplication was
  invisible to includes()), and restores the two model tripwires from
  the spike (oracle over-pull, oracle error identity).
- The protocol property gains same-tick teeth: a terminal that settles
  before the library source's finally ran (the await-idempotence
  contract) now fails at the settling microtask, not via post-hoc flag
  comparison.
- Infrastructure: per-run fast-check timeout plus
  interruptAfterTimeLimit/markInterruptAsFailure so a hang-class bug
  is reported with seed/path instead of dying as a bare mocha timeout;
  FC_NUM_RUNS/FC_SEED validated loudly (fast-check silently runs zero
  cases on numRuns 0/NaN); FC_PATH replay enforced to a single
  property (scope with mocha -g); mocha timeout scales with
  FC_NUM_RUNS; vacuity counters assert every conditional-oracle arm
  actually fired.

Mutation validation with the committed spec run against mutant copies
of index.js: 12 of 12 non-equivalent mutants killed within 2 000 runs
(the five previous survivors included); the drain-race guard revert is
now caught at 20k-run scale where it survived before (the sweep
remains the deterministic pin); one mutant proved equivalent (144/144
example specs pass it — the removed line is unreachable bookkeeping).
Correct code passes 20 000 runs per property with zero false
positives, and the full check chain stays green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSyTJjbP823pjYfueuSsdu
Records, at landing time, what the suite did and did not deliver: no
new bugs found (all 2.0.0 findings came from hand-rolled probes first),
prospective value against the proven-recurring post-done bug class, the
vacuous first iteration that mutation testing caught, and the standing
policy that mutation testing gates every future property — so future
maintainers can judge the file on its record instead of its optics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSyTJjbP823pjYfueuSsdu
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSyTJjbP823pjYfueuSsdu
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants