Skip to content
Raimbek AlishSoftware engineering

Case study · Personal project · 2026

ReproSeed

Turn complex failures into minimal, reproducible evidence.

A command-line tool that replays a failing browser scenario, removes every step the failure does not need, and writes a Playwright regression test for what is left.

Duplicate submit · scenario.jsonRecorded output
  1. opengoto /
  2. previewclick #preview (removed)
  3. emailfill #email "a@example.com"
  4. notefill #note "hello" (removed)
  5. decoy-waitwait 100 ms (removed)
  6. slowdelay POST /api/records 2000 ms
  7. submit-1click #submit
  8. gapwait 800 ms
  9. submit-2click #submit
  • 9 → 6 steps, each one needed
  • 5 / 5 fresh replays reproduce the bug
  • Red → green the generated test fails on the bug and passes on the fix

Project overview

In one recorded run, ReproSeed cut the duplicate-submit scenario from 9 steps to 6, each one needed, reproduced the bug in 5 of 5 fresh replays, and wrote a Playwright test that fails on the bug and passes on the fix.

Step through the run Read the conclusionSource on GitHub

Built with
TypeScript · Node.js · Playwright
Scope
It runs only against authorized loopback test applications, not arbitrary production websites.
Status
Version 0.1.0. Runs locally from its repository; the package is private and not published to npm.
Authorship
Developed with AI-assisted coding tools, including Cursor. Implementation details and verification evidence are available in the public repository.

Problem

Problem: Timing bugs arrive as long recordings.

Some bugs need a slow response and a click at the wrong moment. They reach a developer as a long list of steps, most of them irrelevant, and they fail only some of the time. Cutting the list down by hand means replaying it again and again, and a fix shipped without a test that failed first proves little.

ReproSeed takes a browser scenario that fails and a check that defines the failure, and does five things in order:

  1. ReproduceReplay the whole scenario from a clean state; three baseline runs must fail the same way.
  2. MinimizeDelta debugging removes groups of steps, then single steps, keeping only subsets that still fail the same way.
  3. ProveTry deleting each remaining step once more; none may still reproduce the failure.
  4. VerifyFive fresh replays of the result, with no cached answers, must all fail the same way.
  5. ExportWrite a Playwright test that asserts the intended behaviour, so it fails until the bug is fixed.

Scope. It runs only against authorized loopback test applications, not arbitrary production websites. The app must run on the same machine and answer ReproSeed's identity and reset endpoints, as the two demo apps in the repository do.

Bug Minimization Explorer

Step through a verified run

A guided demonstration of one recorded run. Every outcome comes from the run's report; nothing executes in your browser, and the animation is not the minimizer.

Guided demonstration Recorded output

duplicate-submit9 of 9 steps
  1. opengoto /
  2. previewclick #preview
  3. emailfill #email "a@example.com"
  4. notefill #note "hello"
  5. decoy-waitwait 100 ms
  6. slowdelay POST /api/records 2000 ms
  7. submit-1click #submit
  8. gapwait 800 ms
  9. submit-2click #submit

The full scenario fails

A second click on Submit, while the first save is still waiting for its response, saves a second record.

Check
Records saved for a@example.com: 1 record intended
Failure
DUPLICATE · 2 records, expected 1

Baseline: the full scenario, replayed 3 times, each from a clean state Recorded output

  1. 2 records
  2. 2 records
  3. 2 records

All 3 saw the same signature, so the search could start. Had they disagreed, the run would have stopped as flaky.

Reproduction

Reproduction: Every replay starts from nothing.

A scenario is a JSON file: ordered steps (goto, click, fill, wait and delay), the steps each one depends on, and an oracle that defines the failure. The duplicate-submit scenario has nine.

The scenario file, excerpt
examples/duplicate-submit/scenario.json, excerptVerified in source
  "steps": [
    { "id": "open", "kind": "goto", "path": "/", "dependsOn": [] },
    …
    {
      "id": "slow",
      "kind": "delay",
      "urlIncludes": "/api/records",
      "method": "POST",
      "ms": 2000,
      "dependsOn": []
    },
    { "id": "submit-1", "kind": "click", "selector": "#submit", "dependsOn": ["open", "email"] },
    { "id": "gap", "kind": "wait", "ms": 800, "dependsOn": [] },
    { "id": "submit-2", "kind": "click", "selector": "#submit", "dependsOn": ["open", "email"] }
  ],
  "oracle": {
    "id": "record-count",
    "request": { "method": "GET", "path": "/api/records" },
    "match": { "field": "email", "value": "a@example.com" },
    "expect": 1
  },

One replay

  1. Check that the app on the loopback origin answers with the expected app id, then reset it with the credential from the environment. The record list must come back empty.
  2. Open a fresh browser context. Requests to any other origin are blocked.
  3. Run the steps. A delay step lets the request reach the server and holds only the response, the way a slow network would: Playwright fetches the response, waits, then delivers it.
  4. Wait for the page's requests to settle, then run the oracle.

The oracle returns a signature, not a yes or no: DUPLICATE with the email, the 2 records seen and the 1 record expected. Only an exact match counts as the bug. A timeout, a runner error or a different failure, such as 0 records, is something else. Before the search starts, three baseline replays of the full scenario must agree; otherwise the run stops as flaky or not reproducible.

Why it fails. The page locks the form only after the first response arrives, and its 400 ms double-click guard has already expired.

Conceptual illustration
Buggy page
  1. 0 msPage: Click SubmitSent to the server: POSTServer: Saves record 1. The scenario holds the response.
  2. Page: The 400 ms double-click guard runs out.Server: Response still held.
  3. 800 msPage: Click Submit again. The guard has expired and the form is not locked yet.Sent to the server: POSTServer: Saves record 2.
  4. 2,000 msPage: The form locks, too late.Arrives at the page: Response 1

Result: 2 records

Fixed page (fixed-pending)
  1. 0 msPage: Click Submit. The save is marked pending.Sent to the server: POSTServer: Saves record 1. The scenario holds the response.
  2. Page: The 400 ms double-click guard runs out.Server: Response still held.
  3. 800 msPage: Click Submit again: ignored, because a save is pending.
  4. 2,000 msPage: The form locks.Arrives at the page: Response 1

Result: 1 record

Times are the scenario's configured waits and limits in the page's source; a row without a time follows shortly after the one above. Browser latency is ignored. This is not a recorded trace.

Kept to local test apps

It runs only against authorized loopback test applications, not arbitrary production websites.

The 6 safeguards that enforce it
  • The target must be a plain http origin on localhost, 127.0.0.1 or ::1, with no path or credentials.
  • Before any reset, the app must answer GET /__reproseed/identity with the app id the run expects.
  • The reset credential comes only from an environment variable. It is never a flag, never printed, and is removed from reports.
  • Every replay starts with an authorized reset and a fresh browser context. Requests to any other origin, and redirects that leave it, are blocked.
  • A DOM check reads one element's textContent. Scenario files cannot supply scripts, and their values are never expanded.
  • The generated test refuses a non-loopback origin before it sends the credential, and runs with retries off.

Minimization

Minimization: Delete until every step is needed.

The search is delta debugging: split the steps into chunks, try each chunk and each complement, and split finer when nothing smaller fails the same way.

A subset missing a step that another step depends on is rejected before any browser starts. A new subset is replayed twice, and a disagreement counts as inconclusive; a subset tried before is answered from a cache that keeps only results whose replays agreed. A budget bounds the whole run: 60 replays and 5 minutes for this scenario. Delta debugging can stop early, so the search ends with a proof pass: each remaining step is deleted on its own, and if a deletion still reproduces the failure, the step goes and the pass starts again.

1-minimal under single deletion of this ordered scenario, given the recorded probes. This is not a proof of the globally shortest reproduction.

The command's result line, verbatim

In plain terms: no single remaining step can be deleted. That is a narrower claim than shortest. The search never reorders steps, never tries a shorter delay and never substitutes a different step, so a shorter reproduction may exist. Steps kept only because others depend on them, open and email here, have not been shown to cause the bug.

In the recorded run the search removed preview, note and decoy-wait and kept 6 steps, using 36 search replays and 11 cache hits. See every candidate in the explorer.

Regression test

Regression test: A test that fails for the right reason.

ReproSeed writes a test only for a verified run: a known baseline failure, kept steps in their original order with their dependencies, and five final replays that all reproduced it. An unstable, incomplete or over-budget run writes its report and no test.

The test asserts the intended behaviour, 1 record, not the 2 records the bug creates. It embeds the 6 kept steps with their selectors, values and delay, resets the app first, refuses a non-loopback origin before sending the credential, runs with retries off and does not import ReproSeed.

The command's terminal output
Minimizing the duplicate-submit scenarioRecorded output
$ node dist/cli.js minimize examples/duplicate-submit/scenario.json --report artifacts/report.json --spec artifacts/minimized.spec.ts --force
Checking http://127.0.0.1:4010 for app duplicate-submit.
Minimizing 9 steps. This runs in Chromium and can take several minutes.
Status: verified-one-minimal
Result: 1-minimal under single deletion of this ordered scenario, given the recorded probes. This is not a proof of the globally shortest reproduction.
Original steps: 9
Kept steps: 6
Removed steps: preview, note, decoy-wait
Failure: DUPLICATE (email=a@example.com, actual=2, expect=1)
Replay executions: 44
  baseline: 3
  search: 36
  verify: 5
Candidate evaluations: 55
Cache hits: 11
Elapsed: 80252ms
Report: …/artifacts/report.json
Test: …/artifacts/minimized.spec.ts
The regression test asserts the intended record count. This command does not run it.
The generated test, excerpt
artifacts/minimized.spec.ts, excerptRecorded output
test.describe.configure({ retries: 0, timeout: 20000 });
  const config = readRuntimeConfig(process.env);
  await confirmIdentity(config.origin, config.appId);
  await resetAuthorized(config, emptyListPath);
  const context = await browser.newContext();
    removeGuard = await installOriginGuard(context, config.origin);
      for (const step of scenario.steps) {
    const records = await readRecords(config.origin, scenario.oracle.path);
    const actual = countMatching(records, scenario.oracle.email);
    expect(actual).toBe(1);

Verified fix

Verified fix: Red on the bug, green on the fix.

The command does not run the test it writes. Run with Playwright, the same file fails against the buggy page and passes against the repository's test-only fixed page.

Buggy page
Expected: 1
Received: 2
1 failed
Fixed page (fixed-pending)
1 passed (2.6s)

Playwright output, result lines Recorded output

Playwright's full output, both runs
Generated test, buggy pageRecorded output
$ npx playwright test --config test/export/playwright.config.ts
Running 1 test using 1 worker
[1/1] artifacts/minimized.spec.ts:198:1 › minimized scenario preserves the intended record count
1) artifacts/minimized.spec.ts:198:1 › minimized scenario preserves the intended record count
Error: expect(received).toBe(expected) // Object.is equality
Expected: 1
Received: 2
1 failed
Generated test, fixed page (fixed-pending)Recorded output
$ npx playwright test --config test/export/playwright.config.ts
Running 1 test using 1 worker
[1/1] artifacts/minimized.spec.ts:198:1 › minimized scenario preserves the intended record count
1 passed (2.6s)
The fix, as a diff
The fix: Ignore a submit while the previous save is still in flight.Verified in source
let locked = false;
Added: let pending = false;
…
Removed: if (locked) {
Added: if (locked || pending) {
…
lastSubmitAt = now;
Added: pending = true;
…
Removed: });
Added: }).finally(() => {
Added:   pending = false;
Added: });

ReproSeed does not write fixes. The repository ships this page as a test-only variant, so the exported test can be checked both ways.

The same engine, a second bug

Stale search: a slow response to an earlier search arrives last and overwrites the results of the current search. It uses the same minimizer, runner, exporter and command with a different oracle, the text of one element. The minimizer has no search-specific code; it compares failure signatures. A run that never searches for beta shows “Results for alpha”, a different failure, so it does not count as the bug.

Steps
10 → 6
Removed
preview, note, decoy-wait and gap
Browser replays
52
Cache hits
14
Elapsed
30.1 s

One recorded run Recorded output · The README records a measured run of 52 replays, 14 cache hits and about 30 seconds Verified in source

Why stale search fails, and its test results

Why it fails. The page writes every response into the results, even one from an older click.

Conceptual illustration
Buggy page
  1. 0 msPage: Search for alpha.Sent to the server: GET alphaServer: Answers. The scenario holds the response.
  2. Page: Type beta and search again.Sent to the server: GET betaServer: Answers.
  3. Page: Shows “Results for beta”.Arrives at the page: Beta
  4. 800 msPage: Overwrites it: “Results for alpha (outdated)”.Arrives at the page: Alpha, late

Result: Results for alpha (outdated)

Fixed page (fixed-generation)
  1. 0 msPage: Search for alpha: request 1.Sent to the server: GET alphaServer: Answers. The scenario holds the response.
  2. Page: Type beta and search again: request 2.Sent to the server: GET betaServer: Answers.
  3. Page: Shows “Results for beta”.Arrives at the page: Beta
  4. 800 msPage: Ignored: request 1 is no longer the latest.Arrives at the page: Alpha, late

Result: Results for beta

Times are the scenario's configured waits and limits in the page's source; a row without a time follows shortly after the one above. Browser latency is ignored. This is not a recorded trace.

Buggy page
Expected: "Results for beta"
Received: "Results for alpha (outdated)"
1 failed
Fixed page (fixed-generation)
1 passed (1.4s)

Playwright output, result lines Recorded output

Conclusion

What the recorded runs show

  1. 9 → 6The duplicate-submit failure needs 6 of its 9 steps. Deleting any one of them alone stopped it, or left a step without one it depends on. How the search and the proof work
  2. 5 / 5Five fresh replays, without the cache, reproduced the same signature. See the verification
  3. Red → greenThe exported test failed on the buggy page and passed on the repository's fixed page. See both results
  4. 10 → 6The same engine reduced a second bug, a stale search result, with a different oracle and no search-specific code.

Each line is one recorded run of the repository's own command against its loopback demos. What that does not establish is listed under Limits.

Limits

What it does not claim

The result

  • Each kept step is necessary only in the sense that deleting it alone stopped the failure in the recorded replays. A shorter reproduction may exist.
  • It removes steps from an ordered scenario. It never reorders steps or tries shorter delays.
  • Reproducibility is sampled: three baseline runs, two confirmations per reduction and five final runs. A rarer flake can still exist.
  • Timeouts, runner errors and different failures never count as the bug. A step kept because removing it is invalid has not been shown to cause the bug.
  • The examples and tests cover two loopback demo apps that ship with the repository.

Not built Not built

  • A graphical interface or dashboard. ReproSeed is a command-line tool; the drawings on this page are explanations, not its interface.
  • Calls to an AI model.
  • A cloud backend, accounts or a database.
  • Publication to npm.
  • Support for arbitrary production websites.

Evidence

How this page knows what it says

Verified in source
Read in the public repository at commit dd1c61c: its source, tests and README.
Recorded output
From one local run on 2026-10-24 of the repository's own command and generated tests against its loopback demo apps. Replay counts and times vary between runs.
Conceptual illustration
Drawn to explain a mechanism in the source. Not a trace, and not the tool's interface.
Guided demonstration
Steps through the recorded data of a verified example. Nothing runs in your browser.
Not built
Excluded by the project's own phase notes. Not part of the tool.

The recorded run

Apple M4, macOS 26.6, Node 24.11, Playwright 1.64.1 with headless Chromium 156, on 2026-10-24. Both scenarios were minimized with the command, then each generated test was run against the buggy page and the test-only fixed page. Local paths in the output are shortened to “…/”.

Authorship

Developed with AI-assisted coding tools, including Cursor. Implementation details and verification evidence are available in the public repository.