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.
- open
goto / - preview
click #preview(removed) - email
fill #email "a@example.com" - note
fill #note "hello"(removed) - decoy-wait
wait 100 ms(removed) - slow
delay POST /api/records 2000 ms - submit-1
click #submit - gap
wait 800 ms - submit-2
click #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.
- 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:
- ReproduceReplay the whole scenario from a clean state; three baseline runs must fail the same way.
- MinimizeDelta debugging removes groups of steps, then single steps, keeping only subsets that still fail the same way.
- ProveTry deleting each remaining step once more; none may still reproduce the failure.
- VerifyFive fresh replays of the result, with no cached answers, must all fail the same way.
- 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.
- open
goto / - preview
click #preview - email
fill #email "a@example.com" - note
fill #note "hello" - decoy-wait
wait 100 ms - slow
delay POST /api/records 2000 ms - submit-1
click #submit - gap
wait 800 ms - submit-2
click #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
- 2 records
- 2 records
- 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
"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
- 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.
- Open a fresh browser context. Requests to any other origin are blocked.
- 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.
- 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- 0 msPage: Click SubmitSent to the server: POSTServer: Saves record 1. The scenario holds the response.
- Page: The 400 ms double-click guard runs out.Server: Response still held.
- 800 msPage: Click Submit again. The guard has expired and the form is not locked yet.Sent to the server: POSTServer: Saves record 2.
- 2,000 msPage: The form locks, too late.Arrives at the page: Response 1
Result: 2 records
- 0 msPage: Click Submit. The save is marked pending.Sent to the server: POSTServer: Saves record 1. The scenario holds the response.
- Page: The 400 ms double-click guard runs out.Server: Response still held.
- 800 msPage: Click Submit again: ignored, because a save is pending.
- 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.
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
$ 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
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.
Expected: 1
Received: 2
1 failed1 passed (2.6s)Playwright output, result lines Recorded output
Playwright's full output, both runs
$ 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$ 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
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- 0 msPage: Search for alpha.Sent to the server: GET alphaServer: Answers. The scenario holds the response.
- Page: Type beta and search again.Sent to the server: GET betaServer: Answers.
- Page: Shows “Results for beta”.Arrives at the page: Beta
- 800 msPage: Overwrites it: “Results for alpha (outdated)”.Arrives at the page: Alpha, late
Result: Results for alpha (outdated)
- 0 msPage: Search for alpha: request 1.Sent to the server: GET alphaServer: Answers. The scenario holds the response.
- Page: Type beta and search again: request 2.Sent to the server: GET betaServer: Answers.
- Page: Shows “Results for beta”.Arrives at the page: Beta
- 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.
Expected: "Results for beta"
Received: "Results for alpha (outdated)"
1 failed1 passed (1.4s)Playwright output, result lines Recorded output
Conclusion
What the recorded runs show
- 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
- 5 / 5Five fresh replays, without the cache, reproduced the same signature. See the verification
- Red → greenThe exported test failed on the buggy page and passed on the repository's fixed page. See both results
- 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.