Shrink-aware property fixtures (alpha)
This optional package targets exactly fast-check@4.10.2 and the accompanying version-matched Mimlet core. Alpha releases use the next tag. The core itself does not depend on fast-check.
Native arbitraries and ordinary builders
fromArbitrary(arbitrary) returns a builder whose factory takes an explicit GenerationSession. It samples the native arbitrary using a session-derived 32-bit seed. fromSchemaArbitrary(schema, arbitrary) also retains Standard Schema input/output typing and explicit validated builds. Ordinary builder sampling is not a new shrinker; use the mapping functions for property-based testing.
import * as fc from 'fast-check';
import { createBuilder } from '@mimlet/core';
import { mapFixtureArbitrary, assertFixtureProperty } from '@mimlet/fast-check';
const users = fc.record({ age: fc.integer({ min: 18, max: 100 }), role: fc.string() });
const admins = mapFixtureArbitrary(users, (input) =>
createBuilder(() => input)
.with({ role: 'admin' })
.build()
);
assertFixtureProperty(admins, (user) => user.age >= 18 && user.role === 'admin', {
seed: 12345,
identity: { fingerprint: 'admin-fixtures/v1', provider: 'application@1' },
});mapFixtureArbitrary retains fast-check's original shrink context. It clones supported input graphs before every mapping, protecting that context from mapper mutation. Fixed overrides and derivations are reapplied to every shrunk input. Mappings must be pure and synchronous. Supply clone for native values outside cloneFixture's support, and an unmapper to support shrinking manually supplied examples. No inverse transformation is guessed.
schemaFixtureArbitrary(schema, source) validates each candidate exactly once and returns parsed output. The source must already generate compatible input. Invalid candidates throw rather than silently dropping constraints or retrying effectful validation. Async validation belongs inside an async property over the original input arbitrary, for example through createSchemaBuilder(...).buildValidatedAsync().
Scenarios that shrink coherently
import * as fc from 'fast-check';
import { createScenario } from '@mimlet/core';
import { scenarioArbitrary } from '@mimlet/fast-check';
const cart = createScenario()
.node('lines', [], (): number[] => [])
.node('total', ['lines'], ({ lines }) => lines.reduce((sum, price) => sum + price, 0));
const carts = scenarioArbitrary(
fc.array(fc.integer({ min: 1, max: 100 }), { minLength: 1, maxLength: 10 }),
(lines) => cart.override('lines', () => lines),
{ seed: 'cart-context', fingerprint: 'cart/v1', provider: 'application@1' }
);The explicit parameter arbitrary supplies meaningful structural shrinking. Each shrink reconstructs the scenario using the same session configuration, recomputing dependents such as totals. Native arbitrary constraints, fixed overrides, and the scenario's declared relationships are retained. Opaque random factories do not acquire a semantic shrinker by having their seed reduced.
Reports, assertions, and replay
checkFixtureProperty and checkFixturePropertyAsync return native run details and, for an ordinary counterexample, a JSON-compatible replay record. Always inspect details.failed, or use assertFixtureProperty/assertFixturePropertyAsync to fail a test automatically. FixturePropertyError.report exposes details explicitly; counterexamples are not enumerable default error properties.
replayFixtureProperty and its async counterpart require the original arbitrary, predicate, replay record, and matching consumer identity. They verify the recorded fast-check version, fingerprint, provider/version, configuration, seed, and shrink path, then replay the counterexample without restarting shrink exploration. Changes to native arbitraries, mapping code, or application behavior must update the consumer identity. Replay records are not cryptographically authenticated.
These wrappers use ordinary fast-check seed/path replay. Advanced model-based commands and gen have additional replay requirements; use their native replay APIs rather than assuming that a generic seed/path record captures them. See the fast-check replay documentation.
Deterministic sampling/check/replay wrappers reject modified process-global fast-check configuration instead of silently inheriting hidden randomness or runner settings. Mapped arbitraries remain ordinary arbitraries and can be used with native fc.assert/fc.check and arbitrary custom configuration. No global fast-check settings are changed by this library.
Seeds are explicit signed 32-bit integers. Run count defaults to 100, and skips per run to 100. Counts have finite configuration limits, but arbitrary callbacks and custom arbitraries are trusted code: these wrappers are not an interruptible sandbox. No fixtures, seeds, or reports are transmitted or logged automatically.
Verification
The compatibility fixture pins fast-check and its resolved dependency in a committed npm lockfile. node scripts/test-optional.mjs fast-check builds actual package tarballs, installs them outside the repository, compiles their declarations, and checks shrinking, replay, async predicates, isolation, and generated invariants. The optional package has its own runtime coverage gates. Local offline development can supply already installed, version-checked dependencies; CI uses npm ci and registry integrity verification.