export interface MutationCase { name: string; value: T; } export interface MutationReport { baseline: O; killed: string[]; survived: string[]; } export interface DetectMutationsOptions { baseline: T; mutations: Array>; exercise(value: T): O | Promise; equivalent?(left: O, right: O): boolean; } /** * Run the same behavioural probe against a baseline and explicit mutations. * A mutation survives when the observable result is unchanged. The helper * throws with every survivor by default, making it suitable for an ordinary * Bun test and CI without a second runner or source-rewriting process. */ export async function detectMutations( options: DetectMutationsOptions, ): Promise> { if (!options.mutations.length) { throw new Error("WRN-MUTATION-EMPTY: declare at least one meaningful mutation"); } const equivalent = options.equivalent ?? ((left, right) => Object.is(left, right)); const baseline = await options.exercise(options.baseline); const killed: string[] = []; const survived: string[] = []; for (const mutation of options.mutations) { if (!mutation.name.trim()) throw new Error("WRN-MUTATION-NAME: every mutation needs a name"); let same = false; try { same = equivalent(baseline, await options.exercise(mutation.value)); } catch { // A behavioural failure is a killed mutation. } (same ? survived : killed).push(mutation.name); } if (survived.length) { throw Object.assign(new Error(`WRN-MUTATION-SURVIVED: ${survived.join(", ")}`), { report: { baseline, killed, survived }, }); } return { baseline, killed, survived }; }