import { mkdirSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import fc, { type RunDetails } from "fast-check"; export { fc }; export interface PropertyOptions { /** Deterministic seed; recorded so a failure replays exactly. Default `0x5eed`. */ seed?: number; /** Cases to generate. Default 200. */ numRuns?: number; } function slugify(name: string): string { return ( name .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") || "property" ); } /** * Run a seeded property. On failure, store the minimal (shrunk) counterexample * under `/regressions/.json` and throw with the replay seed. * * Pass `import.meta.url` as `testFile` so regressions land beside the test. */ export function assertProperty( testFile: string, name: string, property: fc.IRawProperty, options: PropertyOptions = {}, ): void { const seed = options.seed ?? 0x5eed; const numRuns = options.numRuns ?? 200; // Properties are synchronous; narrow the sync|async return of `check`. const run = fc.check(property, { seed, numRuns }) as RunDetails; if (!run.failed) return; const dir = join(dirname(fileURLToPath(testFile)), "regressions"); mkdirSync(dir, { recursive: true }); const file = join(dir, `${slugify(name)}.json`); writeFileSync( file, `${JSON.stringify( { name, seed: run.seed, numRuns: run.numRuns, counterexamplePath: run.counterexamplePath, counterexample: run.counterexample, }, null, "\t", )}\n`, ); throw new Error( `property "${name}" failed after ${run.numRuns} run(s); replay with seed=${run.seed} path=${run.counterexamplePath}; counterexample=${JSON.stringify(run.counterexample)}; minimal case stored at ${file}`, ); }