Skip to content

[Feature] Replay-mutation recovery fuzzer #208

Description

@pathosDev

Size / Priority

Rationale

PersistentActor assumes journal events: arrive in-order, exactly-once, immutable. Real-world failures break these:

  • Network glitch duplicates an event.
  • Backend corruption skips events.
  • Manual ops accidentally reorders entries.

A Replay-Mutation Recovery Fuzzer automatically mutates a journal during test-replay and verifies recovery behaviour:

  • Delete a random event → does the actor surface a coherent error?
  • Duplicate an event → does it idempotently handle?
  • Reorder events → does sequence-validation catch it?
  • Corrupt an event payload → does deserialisation fail safely?

Catches silent corruption assumptions.

Design sketch

// src/testkit/ReplayFuzzer.ts (new)

export type MutationStrategy = 'delete' | 'duplicate' | 'reorder' | 'corrupt' | 'truncate';

export interface ReplayFuzzerOptions {
  readonly strategies: ReadonlyArray<MutationStrategy>;
  readonly mutationsPerRun?: number;
  readonly seed: bigint;
  readonly numRuns: number;
}

export function runReplayFuzzer<Cmd, Event, State>(
  actorClass: new () => PersistentActor<Cmd, Event, State>,
  baselineJournal: PersistentEvent<Event>[],
  options: ReplayFuzzerOptions,
): Promise<FuzzerReport>;

interface FuzzerReport {
  readonly totalRuns: number;
  readonly succeeded: number;
  readonly failedWithErrorMessage: number;
  readonly silentlySucceeded: number;        // RED FLAG — recovery passed despite mutation
  readonly examples: ReadonlyArray<{ seed: bigint; mutation: string; result: 'ok' | 'error' | 'silent' }>;
}

Integration

Out of scope / non-goals

  • Production use — testkit only.
  • Cross-actor fuzzing — single actor at a time.

Open design questions

  1. Mutation policy: random subset (sketch) vs exhaustive. Random suits long fuzz runs.
  2. Detection of "silent success": tricky — recovery passed; was the state wrong? Compare against baseline final state.

Test plan

  1. Baseline replay → succeeds.
  2. Delete event → either errors clearly OR state matches "as if event was never persisted" (legitimate).
  3. Duplicate event → idempotent handler OR error.
  4. Reorder → sequence-validation catches.
  5. Corrupt payload → deserialise fails clearly.
  6. Report flags "silent success" as needs-investigation.

Acceptance criteria

  • runReplayFuzzer function.
  • 5 mutation strategies.
  • FuzzerReport with silent-success detection.
  • Documentation: "Recovery robustness with replay fuzzer".
  • Test suite.
  • CHANGELOG entry.

Pre-implementation checklist

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestpriority: lowNice-to-have / niche / demand-driven

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions