Skip to content

Tester report (v0.2.0 @ 6507281): replay is solid; exported test can't pass when the fix changes tool args + minimize --mode bug #17

Closed

Description

@gary1381

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.

  1. 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 calling issue_refund, so it refunds 49 cents. The bug changes the argument of a later instrumented tool call.
  2. crash_agent: the stub LLM wraps its JSON in a ```json fence. The buggy parser raises JSONDecodeError. The fix strips the fence and then calls a tool (escalate) that the buggy run never reached. The stub output also contains fake sk-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 holds 49, so frozen replay raises ReplayMismatchError. --mode live --allow-live-tool issue_refund doesn'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 with ReplayExhaustedError.

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 pass

Suggestions, 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-fix flag, or a role written at record time). The verdict also shouldn't be claim_pinned for a test whose replay is known to diverge.
  • Longer term: a "sink" tool policy for terminal side-effect tools such as issue_refund, send_email or charge. 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-trajectory arguments. This would cover the largest class of agent bugs I hit (wrong args to an action tool). It also looks like what the reserved derived policy 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 existed

The 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 --pre is required and pins @v0.1.0a6, while PyPI and the tags already carry 0.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, fork and score.

4. Small UX nits

  • minimize on an already-minimal trace prints Minimal: No plus the warning "Trace was already minimal or could not be reduced further". The two contradict each other; suggest Minimal: n/a or wording that picks one.
  • minimize drops run_end from a failed trace (5 → 4 events) and the result still passes strict validate. That may be intended, but strict validation could flag it, or the docs could say so.
  • minimize accepts -e for --entrypoint, but replay and export don'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 (score confirmed 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

  1. utsab345 commented on Oct 18, 2026

    @utsab345
    Owner

    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 --mode validation 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.

  2. self-assigned this
    on Oct 18, 2026
  3. how2how2how2-arch commented on Oct 18, 2026

    @how2how2how2-arch
    Contributor

    A note on finding 2, because the public record around it is misleading and I made it so.

    minimize does not validate --mode in main, and no commit there ever has: git log -S'Invalid replay mode' -- src/stepfork/cli/minimize.py returns nothing, and at 6507281 the 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):

    $ stepfork minimize tests/golden/failed_tool_run.sftrace \
        --entrypoint examples.booking_agent.agent:run_agent --mode bogus -o /tmp/bogus.sftrace
    │   Failure Signature    Exception: ValueError   │
    │   Minimal              No                      │
      • Trace was already minimal or could not be reduced further.
    ✓ Minimized trace saved to: /tmp/bogus.sftrace
    $ echo $?
    0

    Finding 4 is visible in the same output: Minimal: No sits 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.

  4. gary1381 commented on Oct 18, 2026

    @gary1381
    ContributorAuthor

    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/bogus now 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.

  5. utsab345 commented on Oct 19, 2026

    @utsab345
    Owner

    @gary1381 Status on your report: finding 2 (minimize mode validation) and finding 3 (docs drift) are fixed and merged. What remains:

    Finding 1

    Finding 4

    • Minimal: No printed next to "Trace was already minimal or could not be reduced further"
    • run_end dropped from a failed trace (5 events to 4) while docs/minimize.md says run markers are always preserved
    • -e short flag exists on minimize but not replay or export

    If 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.

  6. gary1381 commented on Oct 19, 2026

    @gary1381
    ContributorAuthor

    Thanks for the quick turnaround on #19. I'll take Finding 4: the Minimal: No wording, keeping run_end in minimized failed traces to match docs/minimize.md, and adding -e to replay and export. 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-entrypoint and --bundle-is-fix depend on the design in #14 and #22. I'll link both PRs here.

  7. utsab345 commented on Oct 20, 2026

    @utsab345
    Owner

    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.

  8. gary1381 commented on Oct 20, 2026

    @gary1381
    ContributorAuthor

    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).

  9. utsab345 commented on Oct 20, 2026

    @utsab345
    Owner

    @gary1381 You can check the open issues and work on any of them if you’d like.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions