Skip to content

test: replay a saved full run as a guard against code changes - #297

Merged
luca-belli merged 6 commits into
release/2.1from
test/full-run-replay
Oct 27, 2026
Merged

luca-belli merged 6 commits into
release/2.1from
test/full-run-replay

Conversation

@luca-belli

Copy link
Copy Markdown
Collaborator

Summary

A guard against code changes at real scale, ahead of the refactors planned in #294 and #296. Tests only.

tests/safe_changes/full_run.py works from a saved run of the recommended SI profile: 2 user models (gpt-5.2, claude-opus-4-5) × 100 conversations, chatbot gpt-5.4-mini, judged by gpt-5.4, pooled.

  • seed <saved run> <baseline> turns the saved run into a baseline without calling any model. vera pipeline runs with every user and chatbot turn answered from the saved transcripts and every judge question from the saved judge logs. Seeding is strict:

    • every history the code sends must equal the saved transcript up to that turn
    • every judge question must be the next one the log asked
    • every saved turn and answer must be used
    • each transcript is read back and must reformat to its file byte for byte, so a reply containing a chatbot: line can't be mis-split

    It records every call in full (as a sha256 key, so the file stays small) plus the outputs the current code produces.

  • check <baseline> replays it with replay.py's matching: a call must match on everything sent (model, system prompt, full history or judge question), and every recorded reply must be used. Transcripts, judge TSVs, answers.tsv, results.csv, scores and run records must be identical. Paths, timestamps, the random filename prefix, and the run record's provenance and .sha256 are normalized away.

Why the baseline comes from the current code, not the saved run: the saved run came from July's code, so its own outputs may legitimately differ. The saved run supplies the model answers; the current code supplies the expected outputs. Seeding confirms the two agree on every turn and question.

What's committed:

  • tests/fixtures/full_run/: a 10-conversation slice (--sample 5, 880 KB), checked by tests/unit/test_full_run_replay.py in CI
  • .gitignore: an exception for that fixture, because the repo ignores every conversations/ and evaluations/ folder
  • docs/safe-changes.md and CHANGELOG

The full 200-conversation baseline (~17 MB) stays local in output/full_run_baseline/ (gitignored).

Testing

  • Full run: seeding all 200 conversations (9,559 calls) passes in 6 s, so today's code runs those conversations and walks the rubric exactly as the saved run did. The full check passes in 5 s.
  • The 10-conversation slice seeds and checks clean.
  • Deliberate breaks, both caught and then reverted:
    • sending only the last 20 turns of history: caught at the first chatbot call
    • adding 0.01 to the score formula: caught in every scores.json and scores_by_risk.json

I didn't run the pytest suite locally; CI runs it.

For review: the fixture puts 10 synthetic SI conversations (generated personas discussing suicide risk), plus the judge's answers and reasoning, into this public repo. publication_data/ is the precedent.

🤖 Generated with Claude Code

luca-belli and others added 3 commits October 27, 2026 13:17
tests/safe_changes/full_run.py seeds a baseline from a saved run of the
recommended SI profile without calling any model: vera pipeline runs
with every user and chatbot turn answered from the saved transcripts and
every judge question from the saved judge logs. Seeding is strict (each
history sent must equal the saved transcript so far, each judge question
must be the next one logged, every saved turn and answer must be used)
and records every call plus the outputs the current code produces.

The check replays that baseline with replay.py's matching (everything
sent, every reply used) and requires identical transcripts, judge TSVs,
answers.tsv, results.csv, scores, and run records. CI checks a
10-conversation slice; the full 200 check locally in about 5 seconds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The committed slice is now self-contained: tests/fixtures/full_run/saved
holds the 10 saved conversations and judge logs it is seeded from, with
absolute paths replaced by <REPO>, and baseline/ what they seed into.
Anyone can re-seed it; the full run and its baseline stay local under
output/. Seeding writes calls.jsonl sorted, so re-seeding gives a
readable diff. .gitignore lets the slice's conversations/, evaluations/,
and logs/ folders through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The saved run's per-question judge answers lived only in its .log files.
A one-time `full_run answers-from-logs` writes them as the judge's own
answers/by_question/ and answers.tsv files (with the judge's writers, so
they match what a current run writes; none of the 4,114 answers changes
when written to a TSV cell). Seeding now reads those files, and the
committed slice drops its .log files.

The judge check stays as strict: the k-th question asked gets the k-th
saved answer, and its prompt must ask that rubric question. Recorded
calls are unchanged for the slice and the full run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
saved run of the recommended SI profile (two user models x 100 conversations,
judged and pooled), so a change is checked against real conversations at scale.

It works in two steps.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a command one can run that does both under the hood and skips the seeding if unnecessary?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, as of faebbc2:

uv run python -m tests.safe_changes.full_run check output/full_run_baseline --saved output

With --saved, it seeds first only when the baseline's inputs changed: the baseline is missing, or the saved run is different from the one it was seeded from. To tell, each baseline now records a fingerprint (sha256) of the saved transcripts and judge answers in its settings.json. When it seeds, it prints why, then runs the check.

It never seeds because a check failed. Otherwise every code change would quietly become the new expected output, and the guard would never fire. When a change is meant to alter the outputs, re-seeding stays an explicit seed, so the diff shows up in the PR.

Tested all three cases: an existing baseline without a fingerprint gets seeded once, then just checks; a deliberate +0.01 to the score formula fails with 6 differences and does not re-seed; a missing baseline gets seeded from scratch.

luca-belli and others added 3 commits October 27, 2026 15:42
Review on #297: one command that seeds when needed, then checks. A
baseline now records a fingerprint of the saved run it came from
(transcripts and judge answers). `check --saved <saved run>` seeds first
when the baseline is missing or that fingerprint no longer matches, and
says why. It never seeds because a check failed: that would turn every
code change into the new expected output. Re-seeding after a deliberate
change stays an explicit `seed`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On macOS the temporary folder is a symlink (/var -> /private/var) and the
run writes the resolved path, so replacing only the unresolved one left
"/private<WORK>" in the baseline's run records and pooled scores. CI on
Linux writes plain "<WORK>" and failed. Replace the resolved path first,
and re-seed the slice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failing check named the differing file and nothing else. It now shows a
short unified diff for each of the first five differing outputs (20 lines
each, long lines cut), and prints the result after the pipeline's own
output. The pytest tests assert with that text as the message, so a CI
failure shows the changed lines instead of a truncated list.

Both sides of the comparison also skip hidden files: macOS Finder writes a
binary .DS_Store into every folder it opens, and the check crashed reading
it as text.

New tests: a .DS_Store in a copy of the baseline is ignored, and an edited
baseline transcript fails with the changed line in the message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@luca-belli
luca-belli merged commit 01daec0 into release/2.1 Oct 27, 2026
3 checks passed
@luca-belli
luca-belli deleted the test/full-run-replay branch October 27, 2026 23:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant