Repository navigation
test: replay a saved full run as a guard against code changes - #297
Conversation
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. |
There was a problem hiding this comment.
is there a command one can run that does both under the hood and skips the seeding if unnecessary?
There was a problem hiding this comment.
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.
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>
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.pyworks from a saved run of the recommended SI profile: 2 user models (gpt-5.2,claude-opus-4-5) × 100 conversations, chatbotgpt-5.4-mini, judged bygpt-5.4, pooled.seed <saved run> <baseline>turns the saved run into a baseline without calling any model.vera pipelineruns with every user and chatbot turn answered from the saved transcripts and every judge question from the saved judge logs. Seeding is strict:chatbot:line can't be mis-splitIt 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 withreplay.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'sprovenanceand.sha256are 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 bytests/unit/test_full_run_replay.pyin CI.gitignore: an exception for that fixture, because the repo ignores everyconversations/andevaluations/folderdocs/safe-changes.mdand CHANGELOGThe full 200-conversation baseline (~17 MB) stays local in
output/full_run_baseline/(gitignored).Testing
scores.jsonandscores_by_risk.jsonI 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