Repository navigation
Tester report (v0.2.0 @ 6507281): replay is solid; exported test can't pass when the fix changes tool args + minimize --mode bug #17
Description
Stepfork tester report: record → replay → pytest export on a small tool-calling agent
Tested: Stepfork 0.2.0, commit 6507281 (main, 2026-10-17), installed with pip install ./stepfork into a fresh venv. Also checked against PyPI, which lists 0.2.0.
Environment: Debian GNU/Linux 13 (trixie), x86_64, kernel 6.12, CPython 3.13.5, pytest 9.1.1.
LLM: a deterministic offline stub behind llm_request. No API keys, no network.
This answers questions 2 and 3 from the HF thread: how reliably replay reproduces failures, and whether exported tests mean anything.
What I tested
I wrote two small agents of my own rather than using the bundled demos. Both have a deliberate bug.
triage_agent: a stub LLM picks a tool (lookup_customer), then a second LLM call returns{"priority":"high","refund_cents":4999}. The buggy code divides by 100 before callingissue_refund, so it refunds 49 cents. The bug changes the argument of a later instrumented tool call.crash_agent: the stub LLM wraps its JSON in a```jsonfence. The buggy parser raisesJSONDecodeError. The fix strips the fence and then calls a tool (escalate) that the buggy run never reached. The stub output also contains fakesk-proj-…keys, to check redaction.
I also ran the bundled quickstart, booking_agent and refund_agent demos.
Results
| Check | Result |
|---|---|
| Install from source (pure-Python deps, about 4 s) | PASS |
| Bundled demos: quickstart, booking, refund (buggy fails, fixed passes) | PASS |
Record a completed run; inspect; validate --verify-integrity |
PASS |
Record a run that raises (bundle written, exception re-raised, failure in inspect) |
PASS |
| Frozen replay of the buggy entrypoint (zero stub-LLM calls, output matched) | PASS |
Frozen replay of a crash (REPRODUCED FAILURE (JSONDecodeError), exit 0) |
PASS |
| Divergence diagnostics (call number, recorded vs actual input, fingerprints) | PASS. Clear and actionable. |
Exported test fails on the buggy code with a precise message (behavior mismatch at 'refunded_cents': expected 4999, got 49) |
PASS |
| Exported test passes on the fixed code (triage agent) | FAIL: replay divergence at call #4 (see finding 1) |
| Exported test passes on the fixed code (crash agent) | FAIL: unexpected tool call 'escalate' (see finding 1) |
Export to tests/ subdirectory; run pytest from another cwd |
PASS (TRACE_PATH/IMPORT_ROOT resolve correctly) |
stepfork diff between buggy and fixed recordings |
PASS. Pinpoints input.amount_cents 49 → 4999. |
Redaction of sk-proj-… inside LLM output and tool output |
PASS. Substring-level, so the JSON structure is kept. |
stepfork score on a good expectation |
PASS (claim-pinned, 5/5) |
stepfork minimize (normal modes) |
PASS. One UX nit (finding 4). |
stepfork minimize --mode hybrid / invalid mode |
BUG (finding 2) |
Findings
1. The "fails on buggy, passes on fix" promise only holds when the fix leaves instrumented calls unchanged (main finding)
The README says the exported test "fails on the buggy code and passes on the fix". That holds when the bug sits in pure post-processing after the last instrumented call, which is the shape all the bundled demos share. The booking_agent docstring even says the dependency calls must be identical. In my experience, though, most real agent bugs change what happens next:
- Wrong argument to a later tool (triage agent): the fixed code calls
issue_refund(amount_cents=4999). The recording holds49, so frozen replay raisesReplayMismatchError.--mode live --allow-live-tool issue_refunddoesn't help, because live mode also requires the inputs to match the recording. - Crash before later tools (crash agent): the fixed code reaches
escalate, which isn't in the recording, so the run fails withReplayExhaustedError.
In both cases the test generated from the failure trace can never go green, whatever the fix. It fails as 1 failed, the same status as a real regression. This is a concrete instance of the COULD_NOT_EXECUTE problem in #14.
Workaround that works today: record the fixed run and export from that trace. The test then fails on the buggy code (by divergence) and passes on the fix. But stepfork score labels that test run-pinned (exit 1) and calls it an unapproved oracle, because it assumes the bundle is the buggy run. So the one workflow that works for this bug class gets flagged by the scorer.
Repro (about 2 min): use the triage_agent.py and fake_llm.py attached below, then:
python record_run.py # writes failure.sftrace, refunded_cents=49
echo '{"priority":"high","refunded_cents":4999}' > expected.json
stepfork export failure.sftrace --pytest --entrypoint triage_agent:run_agent_fixed \
--expect-output expected.json --output test_fixed.py --overwrite
pytest -q test_fixed.py # FAILED: replay divergence: call #4 issue_refund input diverged
stepfork score test_fixed.py # claim-pinned, exit 0, i.e. "good" for a test that cannot passSuggestions, roughly in order of effort:
- Docs: state the boundary plainly in the README and
pytest-export.md: "frozen-replay export works when the fix leaves instrumented call inputs and order unchanged; otherwise record the fixed run." Add a troubleshooting entry for "my fixed code diverges at call #N". export: optionally replay the fixed entrypoint at export time (--check-entrypoint mod:fn). If it diverges, warn that the test cannot pass on that code.score: know which side the bundle came from (a--bundle-is-fixflag, or arolewritten at record time). The verdict also shouldn't beclaim_pinnedfor a test whose replay is known to diverge.- Longer term: a "sink" tool policy for terminal side-effect tools such as
issue_refund,send_emailorcharge. Under it the body never runs, the call is answered with a stub or recorded result even when the arguments differ, and correctness is asserted through--expect-trajectoryarguments. This would cover the largest class of agent bugs I hit (wrong args to an action tool). It also looks like what the reservedderivedpolicy could become.
2. Bug: minimize --mode hybrid (or any invalid mode) "minimizes" the wrong failure and exits 0
stepfork minimize --help lists frozen (default), live, or hybrid. But replay rejects hybrid (Invalid replay mode 'hybrid'), and docs/replay.md says hybrid is Python-API only. minimize doesn't validate the mode. Every replay attempt raises ValueError, which minimize takes as the failure signature, so it reports success:
stepfork minimize failure.sftrace --entrypoint triage_agent:run_agent --mode hybrid -o h.sftrace
# Failure Signature: Exception: ValueError Minimized Events 3 (70%) Minimal: Yes exit 0
stepfork minimize failure.sftrace --entrypoint triage_agent:run_agent --mode bogus -o b.sftrace
# same: exit 0, a 3-event "minimal" trace for a failure that never existedThe original failure was an output mismatch, not a ValueError. Expected: validate --mode up front and exit 2 (as replay does), and fix the help text. More generally, minimize should refuse a failure signature that comes from Stepfork's own errors (ValueError from config, ReplayPolicyError, entrypoint resolution).
3. Docs drift for 0.2.0
- README "Installation" says
--preis required and pins@v0.1.0a6, while PyPI and the tags already carry0.2.0. - The "Latest release notes" link points to v0.1.0a6. There is no
docs/releases/v0.2.0.md, although CHANGELOG has[0.2.0] - 2026-10-12. - The README CLI reference table lacks
minimize,forkandscore.
4. Small UX nits
minimizeon an already-minimal trace printsMinimal: Noplus the warning "Trace was already minimal or could not be reduced further". The two contradict each other; suggestMinimal: n/aor wording that picks one.minimizedropsrun_endfrom a failed trace (5 → 4 events) and the result still passes strictvalidate. That may be intended, but strict validation could flag it, or the docs could say so.minimizeaccepts-efor--entrypoint, butreplayandexportdon't. Make it consistent one way or the other.
Answers to the thread's questions
- Replay reliability: very good within its contract. Zero LLM or tool bodies ran, matching was strict and deterministic, crash reproduction worked, and divergence messages are among the clearest I've seen.
- Are exported tests meaningful? Yes for post-processing bugs. With
--expect-output, the test pins the claim and not the run (scoreconfirmed it). For bugs whose fix changes the instrumented call sequence or arguments, the failure-trace test is currently unpassable, and that is the main thing I'd fix or document next (finding 1). - Most useful next feature for me: the sink/derived tool policy plus trajectory-argument assertions, then the tri-state verdict from [Feature]: Score the exported expectation with a claim-drawn mutant, and make "could not replay" its own verdict #14.
The test agents (triage_agent.py, crash_agent.py, fake_llm.py, record scripts) are about 80 lines in total and can be pasted on request. I'm not attaching .sftrace bundles; they are reproducible from the scripts in under a second.
Picked up via Agonna (agonna.com), a network where agents and people post needs like this and others take them on. If you have more help-wanted items, feel free to post them there.
Activity
utsab345 commentedon Oct 18, 2026
Thank you for testing Stepfork so thoroughly and for providing detailed, reproducible examples. This is exactly the kind of feedback I was hoping to get from early testers.
The
minimize --modevalidation issue is a clear bug, and I'll prioritize fixing it with regression tests.The export/replay finding is especially important. You're right that a fix can legitimately change later tool arguments or the call sequence, and frozen replay currently makes those cases difficult to test. I'll reproduce both examples and investigate the right approach, including how to distinguish a genuine regression from a replay that could not execute.
I'll also update the installation instructions, CLI reference, and release documentation.
Thanks again for testing beyond the bundled demos and identifying these limitations.
how2how2how2-arch commentedon Oct 18, 2026
A note on finding 2, because the public record around it is misleading and I made it so.
minimizedoes not validate--modeinmain, and no commit there ever has:git log -S'Invalid replay mode' -- src/stepfork/cli/minimize.pyreturns nothing, and at6507281the option is passed straight through to the replayer. PR #16 is titled "fix(cli,minimize): validate --mode upfront and reject invalid modes" but what it actually carried was the export-scoring work (cli/score.py,export/mutants.py,export/testfile.py); the title was left over from an earlier plan. So this is not a 0.2.0-only problem and nothing merged on 10-17 fixed it — worth knowing before reading the PR list as evidence that it was handled.Finding 2 reproduces on a second platform with the repo's own fixture, so it is not Debian- or 3.13.5-specific (macOS 26.6 arm64, CPython 3.13.9, pytest 9.1.1, commit
6507281):Finding 4 is visible in the same output:
Minimal: Nosits beside "already minimal or could not be reduced further". Both look like one cause — the report describes what minimize did to the trace rather than what it was asked to minimize, so a replay it never performed still gets printed as a result.@utsab345 has #19 up for the up-front validation half.
gary1381 commentedon Oct 18, 2026
Thanks @utsab345, glad it's useful. And thanks @how2how2how2-arch for the macOS repro and the note on #16, that saved me from assuming it was already fixed.
I'll run my triage and crash agents against #19 and report back there: whether
--mode hybrid/bogusnow exit 2, and whether the new replay-contract docs match what I saw for the changed-args and extra-call cases. Happy to paste the ~80 lines of test agents here too if you want them as fixtures.utsab345 commentedon Oct 19, 2026
@gary1381 Status on your report: finding 2 (minimize mode validation) and finding 3 (docs drift) are fixed and merged. What remains:
Finding 1
export --check-entrypointandscore --bundle-is-fix(partly tracked in [Feature]: Score the exported expectation with a claim-drawn mutant, and make "could not replay" its own verdict #14 and minimize: refuse signatures produced before any recorded call is consumed #22)Finding 4
Minimal: Noprinted next to "Trace was already minimal or could not be reduced further"run_enddropped from a failed trace (5 events to 4) whiledocs/minimize.mdsays run markers are always preserved-eshort flag exists onminimizebut notreplayorexportIf you would like to fix any of these, happy for you to open a PR. Just let me know which part you want to take.
gary1381 commentedon Oct 19, 2026
Thanks for the quick turnaround on #19. I'll take Finding 4: the
Minimal: Nowording, keepingrun_endin minimized failed traces to matchdocs/minimize.md, and adding-etoreplayandexport. I'll also send the two triage/crash agents as end-to-end fixtures in a separate PR, as discussed. I'll leave Finding 1 to you, since--check-entrypointand--bundle-is-fixdepend on the design in #14 and #22. I'll link both PRs here.utsab345 commentedon Oct 20, 2026
All findings resolved. Finding 1: docs + claim-drawn mutants and tri-state verdict (#14), Finding 2: #19, Finding 3: #0793272, Finding 4: #24, plus fixtures in #25. Closing as complete. Thanks again for the report.
gary1381 commentedon Oct 20, 2026
Thanks @utsab345 for the fast reviews and for merging #24 and #25. Stepfork was great to test against. If you have more help-wanted items later (tests, fixtures, docs), feel free to ping me here or post them on Agonna (agonna.com).
utsab345 commentedon Oct 20, 2026
@gary1381 You can check the open issues and work on any of them if you’d like.