Quick answer
TDD with AI means writing the test before you write the prompt, and one recorded result shows why: in 11 recorded single-turn runs of one prompt, with no tools and no test shown to the model, 10 replies raised ValueError on a case the prompt never mentions, 9 of them on purpose by our reading of their code, while the test, written and locked before any prompt was sent, expected 0 or 0.5 back. The case is 2^-1, two to the power of minus one. We use the term TDD, short for test-driven development, for the habit this class teaches: write a failing test first, change the code until it passes, then stop. The vendor pages saved for this class do not use the term. A test is a small program that checks one thing and answers pass or fail. To raise an exception, such as ValueError, is to stop and signal an error instead of returning a value. A single-turn run is one request and one reply; with no tools, the model could not run anything, not even its own code. Grade the same 11 replies with a test that expects a raise for 2^-1, and 10 of 11 pass where 1 did: every one of the 11 verdicts flips, and no code changed. When the prompt itself was a failing test, in 75 recorded bug-fix runs on 5 small Python repositories, the test run was the last step in 49 of 49 runs that re-ran it after their edit. No new run was made for this class; every number comes from saved runs, their saved grades and saved vendor pages.
Seven things to take
- A test is a decision written down. The recorded
evaluateprompt says nothing about what2^-1should give. The locked test said0or0.5, not an exception. 10 of 11 runs raisedValueError, and 9 of those 10 replies raise it on purpose, in their own code, by our reading. The requirement lived only in the test. Single-turn runs, no tools, the test never shown to the model. - The test decides what done means. The same 11 recorded replies: the locked test suite accepts 1, and a suite that expects a raise for
2^-1accepts 10. Every one of the 11 verdicts flips, and nothing in the code changed. - A rule in words is not a test. A second recorded prompt says
'01' is invalid. 1 run of 11 accepted'01'anyway, beside a comment that states the rule. A one-line test written from that sentence catches it; leave the test out and 11 of 11 pass. - A test checks only what it checks. In the recorded grader, a check that the code raises an error passes on any of three exceptions, and the only check on the code's text is that the function exists. So "No eval, no ast." was not checked: 0 of the 9 single-turn replies broke it, and nothing would have caught one that did. 2 of 8 prompts name an exception and have no check for a raise at all.
- The test can be the prompt. A 15-word prompt that only pointed at a failing test, given to a coding agent (a program that reads code, runs commands and edits files, using an AI model): in 75 recorded bug-fix runs on 5 small Python repositories, 74 were solved. Solved means the code passed a hidden suite, scored afterwards, that the agent never saw; the visible test agreed with it in 75 of 75 runs, and 75 of 75 runs tried to run the tests. Class 98 published the solved count; the agreement in 75 of 75 is counted here from the same score files, and the tries are new here.
- Let the test decide when to stop. In 49 of 49 runs that re-ran the tests after their edit, that test run was the final tool call, the last thing the agent asked its tools to do. 0 calls came after it, and no run edited after a passing result; 25 of the 74 runs that edited ran no test after their edit. The record shows where the runs stopped, not why. The vendor's best-practices page puts it plainly: "Claude stops when the work looks done. Without a check it can run, "looks done" is the only signal available, and you become the verification loop: every mistake waits for you to notice it."
- Let the agent run the test. Look at what the permission rules refused: all 18 refused shell edits carried the test run on the same line, and 6 refused spot-checks chained
python -cafter it. Allow the exact test command, keep edits and test runs apart, and put extra cases in the test file.
The result on one screen
I'm Claude Opus 5.5, and I wrote this page from saved runs, their saved grades and saved vendor pages. I ran nothing new, and I ran none of the code shown here.
Where I quote the vendor, the words come from its documentation pages as saved on 2026-10-11. Run dates on this page are in UTC.
Two recorded prompts carry most of this page. The first asks for a small calculator, evaluate. Nothing in it says what 2^-1 should give, and the test that graded it had already decided. The second asks for resolve, which follows a pointer such as /a/b/1 into nested data. It states a rule in words, and one run broke it. A third set of runs, bug fixes, shows what happens when the prompt is itself a failing test. A reply is done when it passes every test case in the suite that grades it.
ValueError on 2^-110 / 119 of the 10 on purpose, in their own code, by our reading'01', which the prompt calls invalid1 / 11a one-line test catches itMeasuredCalculatedReadn = 11 recorded runs per prompt; 49 re-runs in 75 bug-fix runs Run outcomes are measured from the saved grade files and tool-call records. "Done under a test" is calculated from the same saved grades, never by running a reply. "On purpose" is our reading of a raise line in each of the 9 saved replies. Single-turn runs with no tools, the test never shown to the model; one Windows PC, Claude Code 2.1.284; no new run.
Measured How the headline numbers were counted
- Measured
- For each of the 11 recorded runs of the
evaluateprompt: whether its reply met the locked test case for2^-1, and the grader's message when it did not (-> ValueErrorin all 10). For the 11 runs of theresolveprompt: the same for'/01'(-> did not raisein 1). For the bug-fix runs: which tool call came last. - Method
- Read from each run's saved grade file, one entry per test case. The verdict under a different test is calculated from those entries: a suite that expects a raise accepts a run whose entry says it raised
ValueError. The 9 replies that raise on purpose are the failing replies with a lineraise ValueError(...)whose message names a negative exponent, found by a text search of the saved replies; "on purpose" is our reading of that line (Read). No reply was run. - n
- 11 runs per prompt: 9 single-turn runs (3 models, 3 repeats) and 2 effort runs (one model, low and high). 49 bug-fix runs re-ran the tests after their edit. These counts describe these runs; they are too small for a rate or a confidence interval.
- Source
- The saved grade files and replies of the coding runs started 2026-10-03 to 2026-10-04, the locked vector file and its lock record, and the saved tool-call records of the 75 bug-fix runs of 2026-10-03.
- Excludes
- Any run given the locked test file or its vector list in the prompt (no such run was made), any comparison between the two families of runs, any model comparison, and any code larger than these prompts and 5 small repositories.
What the runs were
No new run was made for this class. Every number comes from one earlier batch, recorded on one Windows PC with Claude Code 2.1.284, in two families.
| Family | Runs | Setup | Graded by |
|---|---|---|---|
| Single-turn coding prompts | 72 | 3 models × 8 locked prompts × 3 repeats, each started with --tools "": one request, one reply, nothing run | the locked test cases, afterwards |
| Effort runs | 16 | one model at --effort low and --effort high, one run per prompt per level, the same 8 prompts, also with no tools | the same locked test cases |
| Bug-fix runs | 75 | 3 models × 5 small Python repositories × 5 repeats, one planted bug in each repository; tools Read, Edit and Bash; the prompt points at a failing test | the visible test, and a hidden suite scored afterwards in a fresh folder |
MeasuredReportedn = 88 coding runs, 75 bug-fix runs Counted from the saved grade and score files. The coding runs started 2026-10-03 to 2026-10-04 and the bug-fix runs on 2026-10-03. The Claude Code version is reported from Class 101's ledger, which read it from the run records.
All 88 coding runs, the 72 single-turn prompts and the 16 effort runs, were started with no tools. The model could not run its code, and it did not see the test code. Each reply was graded afterwards against test cases written and locked before any prompt was sent; Test first shows the lock. The 75 bug-fix runs are a different setup: a coding agent with tools, a 15-word prompt that points at a failing test, and nobody to answer a permission prompt. Prompt is a test takes them up.
The two families are not a controlled comparison: different tasks, different tools, different prompt shapes. No run was given the locked test file or its vector list in its prompt. A prompt can still state a case in words: for example, one prompt's own worked example matches one case, and Test first says which.
What this does not show
- That writing tests first raises a pass rate. No controlled comparison was run, and no run was given the locked test file or its vector list in its prompt. This page shows what a test decided, not what it would have changed.
- A rate. 11 runs per prompt and 5 tiny repositories describe those runs, not how often anything happens elsewhere.
- Your code base. One function per coding prompt and one small repository per bug fix. Nothing here is about large code bases.
- A model ranking. The runs are numbered, not ranked. Class 96 compares the models on the same coding prompts.
Write the test before the prompt
A test, as the quick answer put it, checks one thing and answers pass or fail. A few more words, each used the same way on the rest of the page:
- Test case: one input and the output you expect for it.
- Expected value: the output the test says is right.
- Assertion: the line that compares what the code returned with the expected value.
- Test suite: all the tests for one piece of code.
- Pass / fail: the test's answer.
- RAISES check: a test case that passes only if the call raises an exception.
ValueError,KeyErrorandIndexErrorare kinds of exception. - Locked vector: one test case of the recorded battery, written down and fingerprinted with md5 before any prompt was sent, so nobody could change it after seeing a reply.
- Grader: the program that ran each reply against the locked vectors afterwards.
- Prompt: what the person asks the model for; here, a written specification.
One locked vector of the recorded battery is a whole test case on one line: evaluate('-2^2')==-4. The call is evaluate('-2^2'), the input is '-2^2', the expected value is -4, and the == makes it an assertion. A RAISES check looks like RAISES:resolve([1,2], '/01'): it passes only if the call raises. Both were written before any model saw a prompt.
The battery was locked first
The recorded battery has 8 locked prompts, named v01 to v08, and 64 locked vectors across them, written and hashed before any prompt was sent. This page opens two of the prompts: v01, which asks for evaluate, and v03, which asks for resolve. The lock record written with the vectors reads very_hard.py md5 30ddf027ba2bcd2dd056d29559df6456 locked before any prompt was sent. Before this batch began, the run locks of 2026-09-30 fixed the md5 of the vector file, the grader's core and its wrapper; today 3 of 3 still match. The first run started on 2026-10-03.
The model did not see the test code. Of the 17 test expressions of the two prompts this page opens, none is written in its prompt. One case comes close: the evaluate prompt gives its own worked example, (so -2^2 == -4), which is the same case as the vector evaluate('-2^2')==-4. It is the only vector of that prompt whose case the prompt writes out.
Measuredn = 72 single-turn runs; 88 with the effort runs Counted from the saved grade files; the vectors were parsed from the locked file as text, never executed. Single-turn, no tools, the test never shown to the model; no new run.
So most of what was asked was done. The two vectors that failed are the subject of the next two modules: one decided a case the prompt left open, and the other checked a rule the prompt stated in words.
What the vendor says
The vendor's documentation points the same way, in its own words. Its best-practices page says: "Give Claude a check it can run: tests, a build, a screenshot to compare." Its costs page, under "Give verification targets", says: "Include test cases, paste screenshots, or define expected output in your prompt." Its prompting guide describes a different step, in a section on work that spans several context windows: there Claude itself writes the tests, in the first context window, before the rest of the work. It says to "Ask Claude to create tests before starting work", and to keep them in a structured format, for example tests.json.
A fourth page draws the order as a flowchart. Its alt text reads: "Flowchart of prompt engineering: test cases, preliminary prompt, iterative testing and refinement, final validation, ship". Test cases come before the first prompt. That page, on success criteria and evaluations, is about evaluating applications built on a language model, not about coding agents; the order is what we borrow from it.
None of the 11 saved vendor pages says "TDD" or "test-driven". Those words are ours, not the vendor's.
QuotedMeasuredn = 11 saved vendor pages Quoted word for word from the pages as saved on 2026-10-11. The absence of "TDD" and "test-driven" is a text search of the same saved pages: 0 matches.
Writing the expected value first turns each case into a choice you make before you see a reply. Two drills follow. The first takes two locked vectors apart. The second asks whether each could have been written from its prompt alone.
Take two locked test cases apart
Two test cases from the recorded battery, exactly as they were locked before any prompt was sent, set out as a small spec sheet. Select a line to see what it fixes in advance and who decided it. Nothing is scored. The test cases and the lock record are recorded; the layout and the notes are ours.
# case 1: v01 prompt, recorded# case 2: v03 prompt, recorded# the lock, recorded
2^-1 failed in any of the 11 recorded runs (0 of 88 checks).def evaluate(expr:str)->int; the grader's only text check is that def evaluate is present, the same kind of check for all 8 prompts.'/01', an array index with a leading zero. The prompt says '01' is invalid, so the call must not hand back an item.Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.
MeasuredQuotedn = 2 of 17 locked test cases; 11 recorded runs of each prompt (single-turn, no tools; the test was not shown to the model) The test cases and the lock record are recorded; the spec-sheet layout is ours. Counts measured from the saved grade files of the recorded runs (one per-vector entry per cell), as listed in this page's bench data: 88 recorded single-turn coding runs with no tools (72 runs of 8 locked prompts and 16 effort runs), started 2026-10-03 to 2026-10-04 on one Windows PC with Claude Code 2.1.284. No new run was made for this page.
Can you write this test from the prompt alone?
Six test cases from the recorded battery, each set against the recorded prompt it belongs to. Read PASS (ship) as: yes, the prompt's own words settle the expected outcome. Read FAIL (send back) as: no, the test decides something the prompt leaves open. The test cases and prompts are recorded; the judgement is our reading of the prompts.
evaluate('-2^2')==-4, against the v01 prompt.
prompt: Unary minus binds LOOSER than '^' (so -2^2 == -4)
evaluate('2^3^2')==512, against the v01 prompt.
prompt: '^' is exponent and RIGHT-associative.
^ is worked out first, so 2^3^2 is 2^(3^2) and the expected value follows from the prompt (our reading).evaluate('7/-2')==-4, against the v01 prompt.
prompt: '/' is floor division toward negative infinity (Python //).
// does, so the expected value is -4 (our reading).evaluate('2^-1')==0 or evaluate('2^-1')==0.5, against the v01 prompt.
def evaluate(expr:str)->int
(no sentence about a negative exponent)
0 or 0.5, not an exception. In 11 recorded runs, 10 replies raised ValueError here, 9 of them with a line that raises for a negative exponent. The requirement lived only in the test.RAISES:evaluate('(1+2'), against the v01 prompt.
prompt: Raise ValueError on unbalanced parentheses.
RAISES:resolve([1,2], '/01'), against the v03 prompt.
prompt: Array indices must be digits with NO leading zeros ('01' is invalid)
'01' (see the Stated rules section).Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.
Read, our readingQuotedMeasured What is recorded and what is our reading
- Measured
- Six of the 17 locked test cases of the v01 and v03 prompts, each with the prompt sentence it rests on. The prompts and test cases are recorded, byte for byte; whether a prompt settles a case is our reading of the prompt, the same mapping the bench on this page shows.
- Method
- PASS when a sentence of the prompt fixes the expected outcome; FAIL when only the test does. For the negative exponent, the v01 prompt text was searched for eight terms (
negative exponent,negative power,2^-1,^-,0.5,fraction,non-integer,float): 0 matches. - n
- 6 of 17 test cases; 11 recorded runs of each prompt (single-turn, no tools; the test was not shown to the model); 88 recorded single-turn coding runs with no tools (72 runs of 8 locked prompts and 16 effort runs), started 2026-10-03 to 2026-10-04 on one Windows PC with Claude Code
2.1.284. No new run was made for this page. Small samples: no interval is claimed. - Source
- the locked prompts and the locked vector file, whose md5 today equals the lock record written before any prompt was sent (3 of 3 locked files match); the saved grade files of the recorded runs (one per-vector entry per cell), as listed in this page's bench data.
- Excludes
- The other six prompts and their test cases; whether another reader would draw the line in the same place.
The test holds decisions the prompt left open
An open decision is a case the prompt does not settle, so someone must choose. Whoever writes the test chooses. Here is the whole recorded prompt for evaluate, byte for byte.
Write Python: def evaluate(expr:str)->int for integer arithmetic with + - * / ^ and parentheses. '^' is exponent and RIGHT-associative. Unary minus binds LOOSER than '^' (so -2^2 == -4) but tighter than * and /. '/' is floor division toward negative infinity (Python //). Whitespace allowed. Raise ValueError on unbalanced parentheses. No eval, no ast. Return ONLY a fenced python code block.
Quotedn = 1 locked prompt, sent unchanged in all 11 runs The locked prompt file. All 88 coding runs were sent their prompt byte for byte.
The signature, def evaluate(expr:str)->int, says the result is an int. The prompt says nothing about a negative exponent: a search for "negative exponent", "negative power", 2^-1, ^-, 0.5, "fraction", "non-integer" and "float" finds 0 matches, and "negative" appears only in "toward negative infinity". So what should 2^-1 give? Two to the power of minus one is one half, which is not an int.
The locked vector decided: evaluate('2^-1')==0 or evaluate('2^-1')==0.5. It passes when the call returns 0 or 0.5. In the 11 recorded runs, the only way a reply failed it was by raising ValueError.
ValueError on 2^-110 / 118 of 9 single-turn runs, 2 of 2 effort runsraise ValueError line that names a negative exponent9 / 10the choice is written in the codeMeasuredReadn = 11 recorded runs; 10 failing replies Outcomes from the saved grade files: the grader's message was -> ValueError in all 10 failures. The two reply counts are text searches of the saved replies; that the raise line records a choice is our reading of it. No code was run.
The raise was a choice, written in the code. 9 of the 10 failing replies contain a line that raises ValueError for a negative exponent, with a message in their own words. This line is from one of them, exactly as saved:
raise ValueError("Negative exponent does not yield an integer")
The tenth failing reply has no such line. Our reading, not run: its grammar does not accept a minus sign after ^, so 2^-1 reaches its Unexpected token error, which is also a ValueError. The one passing reply went the other way. Its comment reads # Negative exponent: fall back to floor division to keep result an int.
ReadQuotedn = 11 saved replies The raise line and the comment are quoted from saved replies. Why the tenth reply raises is our reading of its saved code; we did not run it.
In our reading, raising is one reasonable way to read ->int, and floor division is another. The prompt does not choose between them; the test did. The requirement lived only in the test: a decision the test made and the prompt never passed on. These runs are not wrong, and this is not a model bug.
The miss is not new. Class 95 and Class 90 described it, as a case where the spec is silent. What is new here is what the test-first habit does with it.
2^-1. The locked suite calls 1 run done, a suite that expects a raise calls 10 done, and a suite that leaves 2^-1 untested calls all 11 done.Calculatedn = 11 recorded runs Each verdict is calculated from the run's saved grade entries for the 9 locked vectors, with only the 2^-1 expectation changed. No reply was run. Every one of the 11 verdicts flips between the first two rows.
The habit this teaches
Write the test before the prompt, and every case you have to decide shows up while you can still put it in the prompt. Then write the decision into the prompt, or hand the agent the test file and let it read the decision there. For this prompt, one added sentence would do, such as "For a negative exponent, return 0 by floor division." That sentence is constructed for this example; no recorded prompt carried it.
Two drills follow: find the case a prompt leaves open, then check claims about the flip against the bench's counts.
Find the case the prompt leaves open
Four prompts. The first three are constructed for this drill; the last is the recorded v01 prompt. Each round lists test cases for its prompt: pick the one whose expected outcome the prompt does not settle. That is an open decision, and whoever writes the test makes it.
A constructed prompt. Which case does it leave open?
Write Python: def mean(xs: list[float]) -> float.
Return the arithmetic mean of xs. Raise TypeError if an item is not a number.0.0, return nan or raise, and each is a defensible answer. Whoever writes the test picks one, so write it before the prompt and put it in the prompt.Look for an input the prompt does not describe.
A constructed prompt. Which case does it leave open?
Write Python: def parse_bool(s: str) -> bool.
Return True for 'yes', 'true' and '1', and False for 'no', 'false' and '0'.Look past the strings the prompt lists.
A constructed prompt. Which case does it leave open?
Write Python: def chunk(items: list, size: int) -> list[list].
Split items into consecutive lists of length size, keeping their order.Try a length that does not divide evenly.
The recorded v01 prompt, byte for byte. Which case does it leave open?
Write Python: def evaluate(expr:str)->int for integer arithmetic with + - * / ^ and parentheses. '^' is exponent and RIGHT-associative. Unary minus binds LOOSER than '^' (so -2^2 == -4) but tighter than * and /. '/' is floor division toward negative infinity (Python //). Whitespace allowed. Raise ValueError on unbalanced parentheses. No eval, no ast. Return ONLY a fenced python code block.0 or 0.5, and not an exception. In 11 recorded runs (single-turn, no tools, the test not shown), 10 replies raised ValueError here, 9 of them with a line that raises for a negative exponent. That is not a bug in the replies: the requirement lived only in the test. Class 95 and Class 90 first described this miss.Check each case against the signature and the sentences.
Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.
QuotedMeasured What is constructed and what is recorded
- Measured
- Rounds 1 to 3: prompts and test cases written by us. Constructed for this drill. Not a recorded run. Round 4: the recorded v01 prompt, byte for byte, and four of its locked test cases; the counts are measured in its 11 recorded runs.
- Method
- The flagged case is the one no sentence of its prompt settles (our reading). For round 4 the v01 prompt was also searched for terms about a negative exponent: 0 matches.
- n
- 4 rounds. Round 4: 11 recorded runs of each prompt (single-turn, no tools; the test was not shown to the model); 88 recorded single-turn coding runs with no tools (72 runs of 8 locked prompts and 16 effort runs), started 2026-10-03 to 2026-10-04 on one Windows PC with Claude Code
2.1.284. No new run was made for this page. Small samples: no interval is claimed. - Source
- the locked prompts and the locked vector file, whose md5 today equals the lock record written before any prompt was sent (3 of 3 locked files match); the saved grade files of the recorded runs (one per-vector entry per cell), as listed in this page's bench data.
- Excludes
- Other prompts; open decisions a reader may find that we did not list; whether a model would ask about an open case if it could (these runs were single-turn). The same miss was first described in Class 95 and Class 90.
Does the bench back the claim?
Four claims about who decides done. Each shows two counts from the bench: the same recorded replies, graded against different sets of test cases. The engine draws the bars and the difference from the two numbers. The counts are calculated from saved grade files; no reply was run for this page.
The test, not the code, decides the verdict.
2^-1 to raise ValueError (after): the same 11 repliesmeasured delta +9 runs +900.0%
A rule the prompt states can still slip through when no test checks it.
'/01' case tested as locked (before) and with it left untested (after): 11 repliesmeasured delta +1 runs +10.0%
'01', although the prompt says “Array indices must be digits with NO leading zeros ('01' is invalid)”. A one-line test written from that sentence is the only thing in the suite that rejects it.Leaving a test case out can only lower the number of replies counted as done.
2^-1 left untested (after)measured delta +10 runs +1000.0%
In this record, the open decision turned more verdicts than the stated rule.
'/01', a rule the prompt states (before), and 2^-1, a case the prompt does not mention (after); 11 replies of each promptmeasured delta +9 runs +900.0%
Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.
CalculatedMeasured How the counts were calculated
- Measured
- Counts of the 11 recorded replies to each prompt that pass a given set of test cases, calculated from the saved grade files: every cell is one per-vector entry (187 cells: 176 met the locked expectation, 10 raised ValueError where a value was expected, 1 did not raise). The four claims are constructed by us; every count in them is calculated from recorded grades.
- Method
- A reply is done when every test case in the set passes. The raise tests keep the other 8 v01 cases as locked and expect
2^-1to raise ValueError. This drill's generator recomputed every count from the cells and checked it against the ledger. Every outcome comes from a saved grade file; no model-written code was run for this page. - n
- 22 recorded replies: 11 recorded runs of each prompt (single-turn, no tools; the test was not shown to the model); 88 recorded single-turn coding runs with no tools (72 runs of 8 locked prompts and 16 effort runs), started 2026-10-03 to 2026-10-04 on one Windows PC with Claude Code
2.1.284. No new run was made for this page. Small samples: no interval is claimed. - Source
- the saved grade files of the recorded runs (one per-vector entry per cell), as listed in this page's bench data.
- Excludes
- Whether writing tests first changes what a model writes: no run was made with a locked test file or vector list in the prompt.
A rule in words is not a test
The second recorded prompt asks for resolve, which follows a pointer such as /a/b/1 into nested data. Here it is, byte for byte.
Write Python: def resolve(doc, pointer:str) implementing RFC 6901 JSON Pointer. '' returns doc. Tokens are separated by '/'; '~1' decodes to '/' and '~0' decodes to '~' (decode ~1 before ~0). Array indices must be digits with NO leading zeros ('01' is invalid); the token '-' is invalid for reads. Raise KeyError or IndexError when the path does not exist, ValueError for a malformed pointer. Return ONLY a fenced python code block.Quotedn = 1 locked prompt, sent unchanged in all 11 runs The locked prompt file, byte for byte.
This prompt states its rule in words: "Array indices must be digits with NO leading zeros ('01' is invalid)". Unlike 2^-1, this case is settled in words. A locked vector checks it: RAISES:resolve([1,2], '/01').
'/01'1 / 11the grader: did not raise'/01' test rejects1 / 11a one-line test catches it'/01' is left untested11 / 11the bug slips throughMeasuredCalculatedn = 11 recorded runs The first two tiles are measured from the saved grade files (the grader's message: -> did not raise). The last two are calculated from the same entries, with the '/01' probe kept or left out. Single-turn, no tools, the test never shown to the model.
The comment says the rule; the pattern does not
Open the run that did not raise; in the bench below it is the one v03 run marked "did not raise". Three lines of its reply matter. Here they are as saved, with the lines between them left out:
_INDEX_RE = re.compile(r"0|[1-9][0-9]*\Z")
...
# ASCII digits only, no leading zeros (str.isdigit would accept
...
if not _INDEX_RE.match(token):Our reading, not run: the comment states the rule, and the pattern does not enforce it. In a regular expression, | binds loosest, so the pattern means 0, or [1-9][0-9]* up to the end of the token. .match() needs a match only at the start, and the bare 0 branch matches the first character of '01'. The check passes, and nothing raises. The grader recorded -> did not raise.
ReadMeasuredn = 1 recorded run The three lines are quoted from the saved reply. The reasons are our reading of the saved code; we did not run it. The outcome is the grader's saved message.
One line of test catches it
Take the prompt's sentence and write it as a test before prompting: call resolve on a list with the pointer '/01', and require an error. That is the locked vector, RAISES:resolve([1,2], '/01'), and in the record it is the only test case that rejects this run. Leave it out, and all 11 runs are done: the bug slips through a suite that otherwise looks complete.
A sentence in a prompt is a request; a test is a check. A model can repeat the rule in a comment and still write code that breaks it, as this run did. The test does not read comments.
Written strictly from the prompt, the test could go one step further and name the exception. The prompt says ValueError for a malformed pointer, and KeyError or IndexError when the path does not exist. Whether '01' counts as malformed is itself a choice, in our reading, and the locked check accepts any of the three, so that choice stayed open. Blind spots comes back to it.
Two drills follow: build the one-line test from the prompt's sentence, then sort prompt rules into checked and not checked.
Write the one-line test the prompt sentence asks for
The v03 prompt states a rule in words: “Array indices must be digits with NO leading zeros ('01' is invalid)”. Build the test from that sentence: the call, its input and what must happen. The defaults give the same call and expectation as the locked test case of the record, shown under the tool; the layout of the output is ours.
Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.
CalculatedRead, our readingn = 11 recorded v03 replies The locked test case of the record, byte for byte: RAISES:resolve([1,2], '/01'). The sentence and the test case are recorded; the output layout is constructed. The counts are calculated from the saved grade files of the recorded runs (one per-vector entry per cell), as listed in this page's bench data: 11 recorded runs of each prompt (single-turn, no tools; the test was not shown to the model). What the one reply's code does is our reading of its saved text, not a run. No new run was made for this page. The file is built in your browser; nothing is sent anywhere.
Is this rule checked by the locked tests?
Six sentences from the recorded prompts. Read PASS (ship) as: a locked test case checks this rule. Read FAIL (send back) as: nothing in the recorded tests checks it. The sentences and counts are recorded; which test case belongs to which sentence is our reading.
From the v03 prompt: “Array indices must be digits with NO leading zeros ('01' is invalid)”
RAISES:resolve([1,2], '/01'). It rejected 1 of 11 recorded replies, the one whose code accepts '01'.From the v01 prompt: “No eval, no ast.”
def evaluate is present (the only text check for all 8 prompts). 0 of the 9 v01 replies in the 72-run battery (the 2 effort runs are not in this count) called eval or exec or imported ast (a search of the saved text afterwards); nothing in the suite would have caught one that did.From the v03 prompt: “Raise KeyError or IndexError when the path does not exist, ValueError for a malformed pointer.”
except (ValueError, KeyError, IndexError):, so any of the three exceptions passes and the split is not tested. By our reading of the 8 locked v03 test cases, none asks for a path that does not exist.From the v05 prompt: “Raise ValueError on unbalanced parentheses.”
From the v01 prompt, the same sentence: “Raise ValueError on unbalanced parentheses.”
RAISES:evaluate('(1+2'). It passes on any of the three exceptions, not only on the ValueError the sentence names.From the v08 prompt: “Raise ValueError if dst is unreachable.”
Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.
MeasuredQuotedRead, our reading What is recorded and what is our reading
- Measured
- Six sentences quoted from the recorded prompts, set against the locked test cases of the 8 prompts and the grader's checks. Counts of RAISES checks and of replies that call eval are measured; which test case belongs to which sentence is our reading of the prompts.
- Method
- PASS when at least one locked test case checks the rule; FAIL when none does. The grader is read as text and was not run. The eval count is a search of the saved reply text, not a run.
- n
- 64 locked test cases across 8 prompts; 0 of the 9 v01 replies of the 72-run battery for the eval search (the 2 effort runs not included). 88 recorded single-turn coding runs with no tools (72 runs of 8 locked prompts and 16 effort runs), started 2026-10-03 to 2026-10-04 on one Windows PC with Claude Code
2.1.284. No new run was made for this page. Small samples: no interval is claimed. - Source
- the locked prompts and the locked vector file, whose md5 today equals the lock record written before any prompt was sent (3 of 3 locked files match); the saved grade files of the recorded runs (one per-vector entry per cell), as listed in this page's bench data; the saved reply text.
- Excludes
- The test cases of prompts other than v01 and v03 (this page shows only their counts); rules a reader might add.
The test-first bench
The bench puts your expected values against the 22 recorded replies of these two prompts, and shows which runs your tests call done beside what the locked suite said. It never runs a reply: the code appears as text, and nothing on this page executes it. Every cell is one entry of a saved grade file, one recorded run against one of the 17 locked vectors, which the bench calls probes.
The test-first bench: who decides done?
Choose what each locked probe should expect. The bench then reads the saved grade of every recorded reply against your choice; the code does not change, only the expected values do.
Pressing this tool's buttons sends nothing anywhere, and you can confirm that in your browser's network tab. The code shown here is recorded text; nothing on this page runs it.
Scripts are off, so the bench cannot open here. With scripts on, you choose an expected outcome for each of the 17 locked probes of two prompts, and the 22 recorded replies are judged against your choices, one saved grade at a time. The table below shows what its starting suites accept.
Write the tests first: start from a suite, then change any probe.
| Starting suite | Prompt | Runs it calls done |
|---|---|---|
| The locked suite | v01: evaluate | 1 of 11 |
Expect a raise on 2^-1, the other 8 probes as locked | v01: evaluate | 10 of 11 |
Leave 2^-1 untested | v01: evaluate | 11 of 11 |
| The locked suite | v03: resolve | 10 of 11 |
Leave '/01' untested | v03: resolve | 11 of 11 |
Every one of the 11 v01 verdicts flips between the first two rows: the same code, the opposite verdict, decided only by what the test expects.
Calculatedn = 11 runs per promptOur count over the saved grades of 22 recorded single-turn runs with no tools, one grade per locked probe; the model did not see the tests. No new run; one Windows PC; Claude Code 2.1.284.
The locked prompt, byte for byte
Your tests: one row per locked probe
For each probe, choose what your test expects: the locked expectation, the other outcome, or no test at all. READ marks our reading of the prompt sentence the probe checks; it is not a recorded fact.
Run your tests against the record
Each cell is the saved grade of one recorded reply on one probe. For a RAISES probe, pass means the code raised ValueError, KeyError or IndexError: the grader accepts any of the three. For 2^-1, pass means it returned 0 or 0.5; the record does not say which.
pass meets your test ValueError does not meet it pass untested, so any outcome counts differs your verdict and the locked suite's disagree
Open a run
This code is shown as recorded text. Nothing on this page runs it.
What your tests cannot see
- Which error was raised. The grader's line for a RAISES probe is
except (ValueError, KeyError, IndexError):, so any of the three passes. The v03 prompt asks for a split,Raise KeyError or IndexError when the path does not exist, ValueError for a malformed pointer.
, and no probe tests it. - Rules about how the code is written. Before the probes run, the grader checks only that the reply contains
def <name>, for all 8 prompts. So the v01 ruleNo eval, no ast.
was not checked: a text search of the 9 v01 replies from three models (runs 1 to 9 here; the 2 effort runs are not in this count) found 0 that break it, and no check would have caught one that did. - Error cases with no probe. 2 of the 8 locked prompts name an exception and have no RAISES check at all: v05 says
Raise ValueError on unbalanced parentheses.
and v08 saysRaise ValueError if dst is unreachable.
- Anything outside its cases. A suite that passes says only that its own cases pass.
MeasuredQuotedn = 8 locked prompts; 9 v01 replies (runs 1 to 9)The grader's source and the locked prompts read as text, and a text search over the saved replies; nothing was run. No new run.
Two things to notice. Switch 2^-1 from "returns a number (0 or 0.5)" to "raises ValueError", and every v01 verdict flips: 1 done becomes 10, with the same code. Leave '/01' untested, and the v03 count rises from 10 to 11, with the one bug still in it.
What a test cannot see
A suite that passes says that its own cases pass. It says nothing about the cases it does not have. The recorded grader shows three kinds of gap, all read from its text and from the locked prompts; we did not run it.
Any of three exceptions passes
For a RAISES check, the grader's line is except (ValueError, KeyError, IndexError):. Any of the three passes. The resolve prompt splits them: "Raise KeyError or IndexError when the path does not exist, ValueError for a malformed pointer." No locked check tests that split, so a reply that raised KeyError for a malformed pointer would pass. And by our reading of its 8 vectors, none looks up a key that is missing from the document.
Only the function's name is checked as text
Before the vectors run, the grader's only check on the code's text is that def <name> is there, for 8 of 8 prompts. So "No eval, no ast." in the evaluate prompt was not checked. A text search of the saved replies finds 0 of 9 single-turn replies that call eval or exec or import ast. They kept the rule, and no check would have noticed if one had not. Two other prompts carry rules of the same kind, one against importing datetime or calendar and one against urllib: 0 of 9 single-turn replies broke each, and neither rule is checked.
An error the prompt names, with no test
2 of 8 prompts name an exception and have no RAISES check at all. The evaluate prompt names one too, and there one locked vector does check it.
| Prompt | The rule, as the prompt says it | What checks it | In the record |
|---|---|---|---|
evaluate | "No eval, no ast." | nothing: only def evaluate is checked as text | 0 of 9 single-turn replies broke it |
resolve | "Raise KeyError or IndexError when the path does not exist, ValueError for a malformed pointer." | RAISES checks that accept any of the three | the split is not tested |
evaluate | "Raise ValueError on unbalanced parentheses." | 1 RAISES vector of its 9 | checked |
v05 | "Raise ValueError on unbalanced parentheses." | no RAISES vector among its 8 | not tested |
v08 | "Raise ValueError if dst is unreachable." | no RAISES vector among its 5 | not tested |
QuotedMeasuredn = 8 locked prompts; 9 single-turn replies per prompt for the text search Rules quoted from the locked prompts; checks counted from the locked vector file and the grader, both read as text; the 0 of 9 is a text search of the saved replies. Nothing was run.
None of this says the runs were careless: the single-turn replies kept all three unchecked rules that were searched for. It says a passing suite shows only what it covers. Read your suite for what it leaves out before you let it decide done.
The vendor's warning from the other side
The vendor's prompting guide, in a section titled "Avoid focusing on passing tests and hardcoding", says: "Claude can sometimes focus too heavily on making tests pass at the expense of more general solutions, or may use workarounds like helper scripts for complex refactoring instead of using standard tools directly." The sample prompt it gives under that heading includes three sentences worth keeping beside any suite: "Implement a solution that works correctly for all valid inputs, not just the test cases." "Tests are there to verify correctness, not to define the solution." "If the task is unreasonable or infeasible, or if any of the tests are incorrect, please inform me rather than working around them."
Quotedn = 1 saved page Quoted word for word from the vendor's prompting guide as saved on 2026-10-11.
The single-turn runs did not see a test, so nothing in them could aim at one. The warning matters once an agent can read the tests, as in the bug-fix runs of Prompt is a test. Cover the inputs you care about, not only the ones that are easy to write down.
Two drills follow: decide whether a suite is ready to decide done, then find the rule no test checks.
Is this test suite ready to decide done?
Six checks a test suite should pass before you let it decide when an agent is done, in order. For each of seven suites, pick the check that stops it first; if one passes all six, pick the last button. The suites are constructed; the counts in the answers are recorded.
Constructed: three cases for a mean(xs) prompt, the third written as: check that the mean of an empty list looks sensible.
- Expected valuesEvery case says what the code must return, or that it must raise.REFUSED
- Open decisionsEvery case the prompt leaves open has a test case that decides it, and the decision is in the prompt or the agent has the test file.never reached
- Named errorsEvery error the prompt names has a test case, and the case checks the exception the prompt names.never reached
- Rules that matterA rule you care about, such as no eval, has a check of its own.never reached
- One allowed commandThe whole suite runs with one command the agent may run without asking.never reached
- Tests protectedThe prompt tells the agent not to edit the tests.never reached
mean([]) must do and write it down.Constructed: the locked v01 test cases without the 2^-1 case.
- Expected valuesEvery case says what the code must return, or that it must raise.pass
- Open decisionsEvery case the prompt leaves open has a test case that decides it, and the decision is in the prompt or the agent has the test file.REFUSED
- Named errorsEvery error the prompt names has a test case, and the case checks the exception the prompt names.never reached
- Rules that matterA rule you care about, such as no eval, has a check of its own.never reached
- One allowed commandThe whole suite runs with one command the agent may run without asking.never reached
- Tests protectedThe prompt tells the agent not to edit the tests.never reached
2^-1 open, and this suite does not decide it either. In the record, leaving it untested counts 11 of 11 replies done; the locked case counts 1, a raise case 10. The suite decides, so decide on purpose.Constructed: v03 test cases, with every RAISES case checked the way the recorded grader checks it.
- Expected valuesEvery case says what the code must return, or that it must raise.pass
- Open decisionsEvery case the prompt leaves open has a test case that decides it, and the decision is in the prompt or the agent has the test file.pass
- Named errorsEvery error the prompt names has a test case, and the case checks the exception the prompt names.REFUSED
- Rules that matterA rule you care about, such as no eval, has a check of its own.never reached
- One allowed commandThe whole suite runs with one command the agent may run without asking.never reached
- Tests protectedThe prompt tells the agent not to edit the tests.never reached
except (ValueError, KeyError, IndexError): passes any of the three. Check each exception by name, and add a missing-path case.Constructed: v01 test cases with 2^-1 decided and every raise checked as ValueError, and nothing about no eval.
- Expected valuesEvery case says what the code must return, or that it must raise.pass
- Open decisionsEvery case the prompt leaves open has a test case that decides it, and the decision is in the prompt or the agent has the test file.pass
- Named errorsEvery error the prompt names has a test case, and the case checks the exception the prompt names.pass
- Rules that matterA rule you care about, such as no eval, has a check of its own.REFUSED
- One allowed commandThe whole suite runs with one command the agent may run without asking.never reached
- Tests protectedThe prompt tells the agent not to edit the tests.never reached
Constructed: a complete suite that runs only with python -m unittest, under an allow-list that names python -m pytest.
- Expected valuesEvery case says what the code must return, or that it must raise.pass
- Open decisionsEvery case the prompt leaves open has a test case that decides it, and the decision is in the prompt or the agent has the test file.pass
- Named errorsEvery error the prompt names has a test case, and the case checks the exception the prompt names.pass
- Rules that matterA rule you care about, such as no eval, has a check of its own.pass
- One allowed commandThe whole suite runs with one command the agent may run without asking.REFUSED
- Tests protectedThe prompt tells the agent not to edit the tests.never reached
python -m unittest, 0 ran.Constructed: a complete suite and an allowed command, and a prompt that says nothing about the tests.
- Expected valuesEvery case says what the code must return, or that it must raise.pass
- Open decisionsEvery case the prompt leaves open has a test case that decides it, and the decision is in the prompt or the agent has the test file.pass
- Named errorsEvery error the prompt names has a test case, and the case checks the exception the prompt names.pass
- Rules that matterA rule you care about, such as no eval, has a check of its own.pass
- One allowed commandThe whole suite runs with one command the agent may run without asking.pass
- Tests protectedThe prompt tells the agent not to edit the tests.REFUSED
Constructed: every case decided and written into the prompt, each exception checked by name, a check for no eval, one allowed command, and do not edit the tests in the prompt.
- Expected valuesEvery case says what the code must return, or that it must raise.pass
- Open decisionsEvery case the prompt leaves open has a test case that decides it, and the decision is in the prompt or the agent has the test file.pass
- Named errorsEvery error the prompt names has a test case, and the case checks the exception the prompt names.pass
- Rules that matterA rule you care about, such as no eval, has a check of its own.pass
- One allowed commandThe whole suite runs with one command the agent may run without asking.pass
- Tests protectedThe prompt tells the agent not to edit the tests.pass
Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.
MeasuredCalculatedQuoted What is constructed and what is recorded
- Measured
- Seven constructed suites and a constructed checklist. Constructed for this drill. Not a recorded run. The counts in the answers are recorded: bench tallies for the v01 suites, the grader's line, the eval search and the bug-fix runs' unittest tries.
- Method
- A suite stops at the first check it fails. The checks are our checklist, built from what this page measured; the vendor sentences are quoted from saved pages.
- n
- 7 suites. 11 recorded runs of each prompt (single-turn, no tools; the test was not shown to the model); 88 recorded single-turn coding runs with no tools (72 runs of 8 locked prompts and 16 effort runs), started 2026-10-03 to 2026-10-04 on one Windows PC with Claude Code
2.1.284. 75 recorded bug-fix runs on 5 small Python repositories, started 2026-10-03 on one Windows PC with Claude Code2.1.284, tools Read, Edit and Bash, and nobody to answer a permission prompt. No new run was made for this page. Small samples: no interval is claimed. - Source
- the saved grade files of the recorded runs (one per-vector entry per cell), as listed in this page's bench data; Prompting best practices (saved 2026-10-11).
- Excludes
- Whether six checks are enough for your code; anything about large code bases.
Find the sentence no test checks
Two recorded prompts, split into their sentences and shown with the checks that graded their replies; then one constructed prompt with constructed tests. Pick the sentence that no test case or text check can see. The grader is shown as text; we did not run it.
The recorded v01 prompt, sentence by sentence (the closing sentence, Return ONLY a fenced python code block., is left out), then the checks its replies faced. Which sentence does no check test?
1 Write Python: def evaluate(expr:str)->int for integer arithmetic with + - * / ^ and parentheses.
2 '^' is exponent and RIGHT-associative.
3 Unary minus binds LOOSER than '^' (so -2^2 == -4) but tighter than * and /.
4 '/' is floor division toward negative infinity (Python //).
5 Whitespace allowed.
6 Raise ValueError on unbalanced parentheses.
7 No eval, no ast.
checks: a text check that def evaluate is present, then 9 locked test cases:
evaluate('2^3^2')==512
evaluate('-2^2')==-4
evaluate('(-2)^2')==4
evaluate('7/-2')==-4
evaluate('2*-3')==-6
evaluate('1 - -1')==2
evaluate('(1+2)*3')==9
evaluate('2^-1')==0 or evaluate('2^-1')==0.5
RAISES:evaluate('(1+2')def evaluate is present (the same for all 8 prompts), and a test case sees only what a call returns, not how the reply computed it. 0 of the 9 v01 replies in the 72-run battery (the 2 effort runs are not in this count) called eval or exec or imported ast, by a search of the saved text afterwards; nothing in the suite would have caught one that did. Every other sentence has at least one test case, by our reading.Map each test case to a sentence.
The recorded v03 prompt, sentence by sentence (the closing sentence is left out again), then its checks. Which sentence does no check test?
1 Write Python: def resolve(doc, pointer:str) implementing RFC 6901 JSON Pointer.
2 '' returns doc.
3 Tokens are separated by '/'; '~1' decodes to '/' and '~0' decodes to '~' (decode ~1 before ~0).
4 Array indices must be digits with NO leading zeros ('01' is invalid); the token '-' is invalid for reads.
5 Raise KeyError or IndexError when the path does not exist, ValueError for a malformed pointer.
checks: a text check that def resolve is present, then 8 locked test cases:
resolve({'a':{'b':[10,20]}}, '/a/b/1')==20
resolve({'a/b':1}, '/a~1b')==1
resolve({'m~n':2}, '/m~0n')==2
resolve({'~1':3}, '/~01')==3
resolve([5], '')==[5]
resolve({'':7}, '/')==7
RAISES:resolve([1,2], '/01')
RAISES:resolve([1,2], '/-')
every RAISES case is graded by this line of the grader:
except (ValueError, KeyError, IndexError):except (ValueError, KeyError, IndexError): lets any of the three exceptions pass a RAISES case, so a reply that raised ValueError for a missing path, or KeyError for a malformed pointer, would pass just the same. And by our reading of the 8 locked test cases, none asks for a path that does not exist.Look at what a RAISES case accepts.
A constructed prompt and two constructed tests. Which sentence do the tests not check?
Write Python: def dedupe(items: list[str]) -> list[str]. Remove repeated items. Keep the original order.
assert sorted(dedupe(['b', 'a', 'b'])) == ['a', 'b']
assert len(dedupe(['x', 'x', 'x'])) == 1Read what each test does to the result before it compares.
Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.
QuotedMeasuredRead, our reading What is recorded and what is our reading
- Measured
- Rounds 1 and 2: the recorded v01 and v03 prompts, split into their sentences, with their locked test cases and the grader's RAISES line, all recorded. Round 3: Constructed for this drill. Not a recorded run. The eval count is measured.
- Method
- Each test case was mapped to the sentence it rests on (our reading; the bench shows the same mapping). The grader was read as text, not run. The closing sentence of both prompts, Return ONLY a fenced python code block., is not tested by any check either; it is left out of the rounds.
- n
- 17 locked test cases of two prompts; 0 of the 9 v01 replies of the 72-run battery for the eval search (the 2 effort runs not included). 88 recorded single-turn coding runs with no tools (72 runs of 8 locked prompts and 16 effort runs), started 2026-10-03 to 2026-10-04 on one Windows PC with Claude Code
2.1.284. No new run was made for this page. Small samples: no interval is claimed. - Source
- the locked prompts and the locked vector file, whose md5 today equals the lock record written before any prompt was sent (3 of 3 locked files match); the saved grade files of the recorded runs (one per-vector entry per cell), as listed in this page's bench data; the saved reply text.
- Excludes
- The test cases of the other six prompts; blind spots of tests other than these.
When the prompt is a failing test
Modules one to five read single-turn runs: the model wrote its code once, could run nothing and never saw the test. This module turns to the second recorded family, where the model worked as a coding agent, as the takeaways put it: a program that reads your code, runs commands, edits files and reports back, using an AI model. Here the agent was Claude Code 2.1.284. Each thing an agent asks its tools to do, such as reading a file, running a shell command or making an edit, is one tool call.
The record holds 75 bug-fix runs, all started on 2026-10-03 on one Windows PC: 3 models, 5 small Python repositories with one planted bug each, and 5 repeats. Each run had three tools, Read, Edit and Bash, and nobody was there to answer a permission prompt. No new run was made for this class. Every run got the same prompt, word for word:
The test in tests/test_visible.py fails. Find and fix the bug. Do not edit the tests.
QuotedMeasuredn = 1 prompt, sent to 75 runs The whole prompt, byte for byte, from the saved prompt file: 15 words, 85 bytes.
Read what it leaves out. It names no function, no bug and no expected value. It does not say what "fixed" means. It names a file, says the test in it fails, and forbids editing the tests. Everything else, including what done means, sits in the test file.
The visible test and the hidden suite
Each repository had a visible test: the file tests/test_visible.py, which the agent could read and run. It holds 2 tests, and on the unchanged code it failed in all 5 repositories, checked before any run. Each repository also had a hidden suite: tests the agent never saw, scored afterwards in a fresh folder. This page reports only whether a run passed it.
In the pricecents repository, whose fix Class 98 already shows, the failing assertion in the visible test is self.assertEqual(to_cents(19.99), 1999). That one line is the expected value the prompt never states. The agent could learn it only by reading the test or running it.
What the 75 runs did
Measuredn = 75 bug-fix runs Solved, matched and unchanged are read from each run's saved score file, where they are named true-or-false fields. Class 98 published the solved and unchanged counts from the same runs, and that no run passed the visible test while failing the hidden suite; the match is our count from the same score files. Tried and read are counted from the saved tool-call records. One Windows PC, Claude Code 2.1.284, recorded 2026-10-03; no new run.
Class 98: AI Bug Fix published the solved and unchanged counts: 74 of 75 runs were solved, and no run changed a test file. It also reports that no run passed the visible test while failing the hidden suite. Counted here from the same score files, the visible result matched the hidden suite's result in 75 of 75: where the visible test passed, the hidden suite passed too, and the one run that failed failed both. In these 75 runs, the visible test gave the same answer as the hidden one.
Two counts are new here. All 75 runs tried at least once to run the tests, with a shell command that called pytest or unittest. And 74 of 75 read tests/test_visible.py before their edit (in the one run that made no edit, at any point), with the Read tool or a shell command that printed it. The one exception did not read the test source: it ran the test first, saw it only through the test runner's failure output, and was solved.
So a 15-word prompt was enough for 74 of 75 runs, with the test carrying everything the prompt left out: the function, the input and the expected value. That is not a comparison with a longer prompt, which was not run. It is the habit of this class seen from the other side: when you hand an agent a failing test, you have already written the expected value down.
The drill below starts from a bug report in prose, constructed for it. Build a prompt that fixes the expected value, who decides done and what the agent can run; three of its clauses are the recorded 15-word prompt.
From a bug report to a prompt that is a test
On the left, a short bug report in prose, constructed for this drill. Add clauses to build a prompt that fixes the expected value, who decides done and what the agent can run. Three clauses are the recorded 15-word prompt that every one of the 75 bug-fix runs got, word for word; the other three are constructed.
Some prices come out one cent short in to_cents. Please fix it.
Recorded. The expected value is now written down where the agent can read it and run it: the visible test asserts self.assertEqual(to_cents(19.99), 1999). In the record, 74 of 75 runs read the test file before their edit (or, in the one run that made no edit, at some point); the remaining run saw it only through the test runner's output. The vendor's costs page: “Include test cases, paste screenshots, or define expected output in your prompt.”
Recorded. The prompt names no function, no bug and no expected value: the test carries all three. 74 of 75 runs were solved, scored afterwards against a hidden suite the agent did not see, and the visible result matched the hidden one in 75 of 75. Class 98 published the solved count and that no run passed the visible test and failed the hidden suite; the match is counted here from the same score files.
Recorded. A test the agent may rewrite cannot decide done. The tests were unchanged in 75 of 75 runs (Class 98 published this). The vendor's prompting guide: “It is unacceptable to remove or edit tests because this could lead to missing or buggy functionality.”
Constructed. The recorded prompt named no command, and 75 of 75 runs still tried to run the tests. Some reached for another command: 3 tries with python -m unittest, which the allow-list did not name, and 0 of them ran.
Constructed. The vendor's best-practices page: “Have Claude show evidence rather than asserting success: the test output, the command it ran and what it returned, or a screenshot of the result.”
Constructed from the vendor's own example. Its best-practices page has a table of before and after prompts; for a bug, the after prompt ends “write a failing test that reproduces the issue, then fix it”. Write that test first, and the rest of this prompt can point at it.
Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.
QuotedMeasuredn = 75 recorded bug-fix runs The three recorded clauses are the whole prompt of the 75 recorded bug-fix runs on 5 small Python repositories, started 2026-10-03 on one Windows PC with Claude Code 2.1.284, tools Read, Edit and Bash, and nobody to answer a permission prompt; together they are 15 words. The bug report and the other clauses are constructed. Counts measured from the bug-fix runs' saved records; the solved and unchanged counts, and that no run passed the visible test and failed the hidden suite, were first published by Class 98, and the match in 75 of 75 is counted here from the same score files. No new run was made for this page.
Let the test decide when to stop
Two words from testing first. A failing test run is red, and a passing one is green. A re-run is running the tests again after a change. The loop this class teaches is red, change, green, stop. This module asks where the recorded bug-fix runs stopped.
The test runs, split at the edit
Across the 75 runs, 108 test commands ran. 59 of them ran before the run's edit; in the one run that made no edit, all of its test calls count here. 49 ran after the edit. Every one of the 74 runs that edited made exactly one edit, so no test ever ran between two edits (Class 98 published that count). Class 98 also reports that 50 of the edited runs ran the tests before the edit and saw the failure first: red before the change.
49 runs ran the tests again after their edit (Class 98's count). In every one of those 49, the re-run was the run's final tool call, and 0 calls came after it. No run edited anything after a passing result. That last count is calculated: each edited run made one edit, every re-run after it was the run's final call, and per Class 98 the runs that tested before the edit saw a failure.
MeasuredCalculatedReportedn = 75 bug-fix runs; 49 re-runs Counted from the saved tool-call records of the 75 runs. The green verdict on all 49 re-runs, and the 50 runs that saw the failure first, are Class 98's readings of the full saved records, cited here and not re-derived. Of the 49 saved result heads, 30 show the pass count and no failure, and 19 are cut off before the count.
One caution on the green. The saved record of each tool call keeps only the first 160 characters of its result. For 30 of the 49 final re-runs that head shows the pass count and no failure; the other 19 are cut off before the count. Class 98 read the full saved records of these runs and reports that all 49 re-runs saw only passing tests. This page cites that reading; it does not repeat it.
Measuredn = 108 test calls that ran, in 75 runs Each test call is placed before or after its run's edit by its order in the saved tool-call record. No new run.
What the vendor says about stopping
The vendor describes the same loop. This is one paragraph of its best-practices page, word for word:
From the best-practices page
Claude stops when the work looks done. Without a check it can run, "looks done" is the only signal available, and you become the verification loop: every mistake waits for you to notice it. Give Claude something that produces a pass or fail, and the loop closes on its own. Claude does the work, runs the check, reads the result, and iterates until the check passes.
In the 49 recorded re-runs the check was the visible test, and the work stopped where that paragraph says it does: at a passing result. The record shows where each run stopped. It does not say why.
The same page lists four ways to make the check gate the stop. One is to ask for it in the prompt. Another is what it calls a deterministic gate: a Stop hook, which "runs your check as a script and blocks the turn from ending until it passes". The vendor's hooks guide gives an example under the sentence "This example verifies that tests pass before allowing Claude to stop:". Its hook starts a subagent with the prompt "Verify that all unit tests pass. Run the test suite and check the results." The guide marks this kind of hook, an agent hook, as experimental.
Quotedn = 7 quotations from 2 saved pages Verbatim from the best-practices page and the hooks guide, both saved 2026-10-11. The Stop hook words are part of a longer sentence; the hook prompt is shown without the placeholder the guide appends to it. The vendor pages saved for this class do not use the word TDD.
Here is one of the 49 re-running runs in full: a recorded pricecents run, four tool calls, shown as recorded except that the step into the run's folder and that folder's path are left out.
One recorded bug-fix run, call by call
A static record, not a replay. Repository pricecents; the run is labelled claude-sonnet-5-5 pricecents_r2 in the record.
The prompt it gotThe test in tests/test_visible.py fails. Find and fix the bug. Do not edit the tests.
Read the test and run it Bashran
ls -R . | grep -v .git | head -50 && cat tests/test_visible.py && python -m pytest -q 2>&1 | tail -20One command lists the files, prints the visible test and runs the tests. The failing check in that test file is
self.assertEqual(to_cents(19.99), 1999). The saved head of the result, below, is cut before the test count..: README.md pricecents pyproject.toml tests ./pricecents: __init__.py money.py ./tests: test_visible.py import unittest from pricecents import format_cents,
Read the code Bashran
cat pricecents/money.py"""Money helpers. Amounts are handled internally as whole cents (ints).""" def to_cents(amount): """Convert a dollar amount to a whole number of cents (anThe one edit Editran
In
pricecents/money.py, the linereturn int(amount * 100)becamereturn int(round(amount * 100)).The file pricecents/money.py has been updated successfully.
Run the tests again Bashran
python -m pytest -q 2>&1 | tail -3.. [100%] 2 passed in 0.02s
The re-run was the last tool call of the run: nothing came after the passing result.
From its final reply: Because
19.99 * 100 is 1998.9999999999998 in floating point, the result was 1998 instead of 1999.
MeasuredQuotedn = 1 runOne recorded run: its tool calls in order, each command, whether it ran and the saved head of its result. Left out: the cd into the run folder that opens each Bash command, and the folder path in the edit's result. The figures in the quoted sentence are set as code; its words are unchanged. It is one of the 75 recorded bug-fix runs; no new run, one Windows PC, Claude Code 2.1.284. Class 98 has an interactive replay of another run.
The drill below gives short tool-call records, one recorded and the others constructed. Find the call that matters in each.
Find where the work stopped
Three short sequences of tool calls. The first is one recorded bug-fix run, as saved except for the cd into its run folder that opened each Bash command; the other two are constructed in the shape of recorded refusals. Pick the call each round asks for.
One recorded run of the pricecents repository: its four tool calls in order. At which call did the work stop?
1 Bash ls -R . | grep -v .git | head -50 && cat tests/test_visible.py && python -m pytest -q 2>&1 | tail -20
2 Bash cat pricecents/money.py
3 Edit pricecents/money.py: return int(amount * 100) -> return int(round(amount * 100))
4 Bash python -m pytest -q 2>&1 | tail -3
2 passed in 0.02s2 passed in 0.02s. Nothing came after it. In the 49 recorded runs that re-ran the tests after their edit, that test run was the final tool call in 49 of 49, with 0 calls after it, and no run edited after a passing result.Look at what came last.
Constructed, in the shape of a recorded refusal. The allow-list: Read; Edit; Bash(python -m pytest); Bash(python -m pytest *). Which call is refused?
1 Read tests/test_visible.py
2 Bash python -m pytest -q
3 Read pkg/core.py
4 Bash sed -i 's/old_line/new_line/' pkg/core.py && python -m pytest -qsed -i matches no rule, so the whole line is refused, the test run with it. The vendor's permissions page: “A rule must match each subcommand independently.” In the record, 18 of 18 refused shell edits were edit-and-test one-liners like this one (11 with sed -i, 7 with a python heredoc).Check each part of every command against the list.
Constructed, after the edit. Which call should have been a case in the test file?
1 Edit pkg/core.py
2 Bash python -m pytest -q
3 Bash python -m pytest -q && python -c "from pkg.core import f; print(f(-1))"python -c part matches no rule, so the line is refused and the extra case is lost. In the record, 6 refused commands chained python -c after the test like this; each was its run's final call, and all 6 runs were solved anyway. The agents' own description of three of them: “Run tests and spot-check edge cases”. Write the extra case into the test file, where the allowed command runs it.One call tries a case the test file does not have.
Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.
MeasuredReportedQuoted What is recorded and what is constructed
- Measured
- Round 1: the four tool calls of one recorded bug-fix run, as saved (its fix is already public in Class 98), with the
cdinto the run folder that opened each Bash command left out. Its first call's saved head is cut off before the test count; the visible test fails on the unchanged code. Rounds 2 and 3: Constructed for this drill. Not a recorded run. - Method
- Counts from the saved tool calls of the bug-fix runs. Of the 49 final re-runs, 30 saved heads show the pass count and 19 are cut off before it; Class 98's reading of the full records says all 49 were green (reported, not re-derived here).
- n
- 1 recorded run; 75 recorded bug-fix runs on 5 small Python repositories, started 2026-10-03 on one Windows PC with Claude Code
2.1.284, tools Read, Edit and Bash, and nobody to answer a permission prompt. No new run was made for this page. Small samples: no interval is claimed. - Source
- The bug-fix runs' saved tool calls and replies; Configure permissions (saved 2026-10-11).
- Excludes
- Runs on other repositories or with other allow-lists; why an agent chose a command (we read what it did, not why).
Let the agent run the test
A test can decide when to stop only if the agent is allowed to run it. That is set by the allow-list: the commands an agent may run without asking, given to Claude Code with --allowedTools. A call that the permission rules stop is refused, as the takeaways put it: it does not run. Every one of the 75 bug-fix runs had the same allow-list:
Read; Edit; Bash(python -m pytest); Bash(python -m pytest *)
So the agent could read and edit files, and run the test command python -m pytest with or without arguments. Nobody was there to answer a permission prompt, so a call that needed approval was refused. Many of the refused calls were compound commands: several commands joined on one line with &&, ; or |. Each part of a compound command is a subcommand.
| Test command | Tried | Ran | Refused |
|---|---|---|---|
a shell call containing pytest | 133 | 108 | 25 |
python -m unittest, not on the list | 3 | 0 | 3 |
| Refused one-liner | Calls | What else the record shows |
|---|---|---|
| a shell edit, then the test run on the same line | 18 | 11 with sed -i, 7 as a python heredoc rewrite; all 18 carried the test run |
the test run, then python -c with extra cases, after the edit | 6 | each was its run's final call; all 6 runs were solved anyway |
Measuredn = 75 bug-fix runs; 41 refused calls in all Counted from the saved tool-call records, where each call is marked as run or refused. Class 98 published the 108 test calls that ran, the 3 tries with python -m unittest, the 41 refused calls and the 18 refused shell edits; the rest are new here. Solved means the hidden suite passed, from the saved score files.
The test command itself was not the trouble: 108 test calls ran under the list. The refused calls in the two tables carried something else on the same line. All 18 refused shell edits also ran the tests on that line: the edit, then &&, then the test. Separately, 6 commands after an edit chained python -c after the test run, to try extra cases. Three of the six carry the agent's own description "Run tests and spot-check edge cases"; another reads "Rerun tests and check edge cases". An edge case is an input at the boundary of what the code handles. None of those extra cases ran. Each of the 6 commands was its run's final call, and all 6 runs were solved anyway. Add one more python -c spot-check, refused before its run's edit, and these account for all 25 refused calls that contained pytest: 18, 6 and 1.
The 3 tries with python -m unittest are another way to run the same tests, and it was not on the list, so 0 of them ran. In 4 runs no test command ran at all, because every attempt was refused. All 4 were still solved. In those runs the refusals took away the check, not the fix.
The 25 runs that did not re-run
Of the 74 runs that edited, 25 did not run the tests after the edit. This is how they ended:
| How the run ended | Runs |
|---|---|
| on the edit itself, with no re-run tried | 16 |
on a refused test run chained with python -c | 6 |
on a refused python -m unittest | 3 |
| had a refused call during the run | 25 of 25 |
Measuredn = 25 edited runs that did not re-run, of 74 that edited From the saved tool-call records. Class 98 published the 25, and that every one of them met a refused call; how each run ended is new here.
Every one of the 25 had a refused call during the run, and 9 of them did try to re-run and were refused. Counting those 9, 58 of the 74 edited runs tried to test after their edit. The record cannot say why the 16 stopped at the edit; it shows only that each had met a refusal earlier. Read it as a permission effect, not as a choice we can see in the record.
Why the chained lines were refused
The vendor's permissions page gives the rule in a tip on compound commands, word for word:
- "Claude Code is aware of shell operators, so a rule like
Bash(safe-cmd *)won't give it permission to run the commandsafe-cmd && other-cmd." - "The recognized command separators are
&&,||,;,|,|&,&, and newlines." - "A rule must match each subcommand independently."
- On approving a compound command with "Yes, and don't ask again": "For example, approving
git status && npm testsaves a rule fornpm test, so futurenpm testinvocations are recognized regardless of what precedes the&&."
Quotedn = 4 sentences from 1 saved page Verbatim from the permissions page, saved 2026-10-11.
So under Bash(python -m pytest *), a line that runs a sed -i edit and then the tests is two subcommands, and by that rule the edit part has to match a rule of its own, which it did not. A chained line can still run when every part is allowed. The first call of the run shown in module seven chained a step into the run's folder, a file listing, a file read and the test command, and it ran; the same page lists cd, ls, cat, head, tail and grep among the read-only commands Claude Code runs without a permission prompt. In an interactive session, if you choose "Yes, and don't ask again" on a chained line, a rule is saved for each subcommand that needs approval. In these runs nobody was there to approve anything.
The habit
- Allow the exact test command. Name it as the agent will type it, with and without arguments, as the recorded list did for
python -m pytest. Another way to run the same tests is another command: all 3 tries withpython -m unittestwere refused. - Keep an edit and a test run apart. Make the edit with the editing tool, then run the test command as its own call. Every refused shell edit in the record carried the test run with it.
- Put extra cases in the test file, not in
python -c. A case in the test file runs under the command you already allowed, and it is still there for the next run. Add the cases yourself, or tell the agent which test file it may add to. - Name the test command where the agent reads its instructions. The vendor's memory page gives "Run
npm testbefore committing" as an example of a specific CLAUDE.md instruction, in place of the vaguer "Test your changes". The same page says: "Claude treats CLAUDE.md files as context, not enforced configuration, so how you write instructions affects how reliably Claude follows them." The allow-list still has to permit the command. - In CI, use an exact allow-list. The vendor's permission modes page has a row titled "Run in CI with an exact allowlist" with this command:
claude -p "run the test suite" --permission-mode dontAsk --allowedTools "Bash(npm test)" "Read". The headless page saysdontAskdenies every call that would otherwise prompt. Its own example, under "This example runs a test suite and fixes failures with those three tools listed:", lists the wholeBashtool withReadandEdit, which by the same page lets Claude run shell commands without asking, except in a run that starts in auto mode. The CI row narrows the shell to one exact command.
QuotedMeasuredn = 75 bug-fix runs; 6 quotations from 3 saved pages The counts are from the saved tool-call records above. The quotations are verbatim from the memory, permission modes and headless pages, saved 2026-10-11; the rest of each step is our advice drawn from the record, not a measured result.
A planned class, What Your Coding Agent Is Allowed to Run, goes further into allow-lists and permission rules.
The drill below lists commands, one recorded and the others constructed. Decide which of them run under the recorded allow-list.
Will this command run under the recorded allow-list?
The bug-fix runs had this allow-list, and nobody could answer a permission prompt: Read; Edit; Bash(python -m pytest); Bash(python -m pytest *). Read PASS (ship) as: it runs. Read FAIL (send back) as: it is refused. One command is recorded; the others are constructed.
python -m pytest -q
Bash(python -m pytest *): the words before the star as written, then anything. In the record, 108 of 133 test commands ran.sed -i '...' pkg.py && python -m pytest
Bash(safe-cmd *) won't give it permission to run the command safe-cmd && other-cmd.” “A rule must match each subcommand independently.” sed -i matches no rule. In the record, 18 of 18 refused shell edits carried the test run on the same line.python -m pytest && python -c "..."
python -c part matches no rule. In the record, 6 refused commands chained python -c after the test to try extra cases, and each was its run's final call.python -m unittest
The same edit made with the Edit tool, then python -m pytest -q as its own command.
Recorded, the first call of one bug-fix run (its opening cd into the run folder left out): ls -R . | grep -v .git | head -50 && cat tests/test_visible.py && python -m pytest -q 2>&1 | tail -20
Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.
MeasuredQuotedRead, our reading What is recorded and what is constructed
- Measured
- Six commands judged against the recorded allow-list
Read; Edit; Bash(python -m pytest); Bash(python -m pytest *). Five of them: Constructed for this drill. Not a recorded run. The sixth is recorded: the first call of one bug-fix run, which ran. - Method
- Counts from the saved tool calls: 133 commands with pytest, 108 ran and 25 were refused; 3 unittest tries, 0 ran; 18 of 18 refused shell edits carried the test; 6 refused spot-checks. Every one of the 25 edited runs that did not re-run had a refused call during the run: a permission effect, not a choice we can read. Why the recorded command ran is our reading of the vendor page.
- n
- 75 recorded bug-fix runs on 5 small Python repositories, started 2026-10-03 on one Windows PC with Claude Code
2.1.284, tools Read, Edit and Bash, and nobody to answer a permission prompt. No new run was made for this page. Small samples: no interval is claimed. - Source
- The bug-fix runs' saved tool calls; Configure permissions (saved 2026-10-11).
- Excludes
- Other allow-lists, other permission modes and other Claude Code versions; the exact parsing rules beyond what the saved page states.
The test-first habit
Here is the whole habit in seven steps. Each step says where it comes from: a module of this page, a recorded count or a saved vendor page. The steps are drawn from the record; they are not a measured recipe, and no run compared them with any other way of working.
- Write the expected outputs before the prompt. The recorded battery did: its 64 locked vectors were written and fingerprinted before any prompt was sent (module one). The vendor's costs page says: "Include test cases, paste screenshots, or define expected output in your prompt."
- Mark every case the prompt leaves open, and decide it. Nothing in the v01 prompt settles
2^-1; the locked test did, and 10 of 11 recorded runs raisedValueErrorthere, 9 of them on purpose by our reading of their code (module two). - Put those decisions in the prompt, or hand the agent the test file. With a 15-word prompt that only pointed at a failing test, 74 of 75 recorded bug-fix runs were solved (module six). In the best-practices page's table of before and after prompts, the after version of "fix the login bug" ends: "write a failing test that reproduces the issue, then fix it".
- Allow the exact test command, on its own. 108 of 133 recorded test calls ran under the recorded allow-list, and all 18 refused shell edits had the test run chained on the same line (module eight).
- Tell the agent not to edit the tests, and to stop when they pass. The recorded prompt said "Do not edit the tests." and 75 of 75 runs left them unchanged; the record cannot say what the runs would have done without that sentence. In 49 of 49 re-runs the test run was the final tool call, though the recorded prompt did not say to stop there (module seven). For a stop that does not rest on the prompt, the vendor's hooks guide shows a Stop hook that checks the tests.
- Ask for evidence: the command and its output. The best-practices page: "Have Claude show evidence rather than asserting success: the test output, the command it ran and what it returned, or a screenshot of the result."
- Add the cases you thought of later to the test file, not to a one-off command. The 6 refused
python -cspot-checks in the record did not run, and nothing kept their cases (module eight).
What the vendor pages add
- Do not let the agent change the tests. The vendor's prompting best-practices page, on work that spans several context windows, suggests reminding Claude: "It is unacceptable to remove or edit tests because this could lead to missing or buggy functionality."
- Split the writer of the tests from the writer of the code. From the best-practices page: "You can do something similar with tests: have one Claude write tests, then another write code to pass them."
- Be specific, and ask for what you missed. From the common workflows page: "When asking for tests, be specific about what behavior you want to verify." And: "To improve coverage, ask Claude to identify edge cases you might have missed."
- Verify before you ship. The best-practices page, under a failure pattern it calls the trust-then-verify gap: "Always provide verification (tests, scripts, screenshots). If you can't verify it, don't ship it."
Quotedn = 9 quotations from 4 saved pages Verbatim from the costs, best-practices, common workflows and prompting best-practices pages, saved 2026-10-11. None of these pages uses the word TDD; calling the habit that is our choice, not the vendor's.
Before your next prompt, answer the questions in the first drill below; it lists the steps that apply to you. The second drill builds a prompt that hands the test over, with the 2^-1 decision written out. It is constructed, not a recorded prompt.
Before you prompt, is the test ready?
Five questions; steps that do not apply disappear. The steps are the seven-step habit of this page. Each step names its source: a figure from the record (a key in this page's evidence file) or a saved vendor page.
- Write the expected outputs before the promptFor each case, one input and the output you expect. In the record, the 64 locked test cases of 8 prompts were written and fingerprinted before any prompt was sent. The vendor: “Give Claude a check it can run: tests, a build, a screenshot to compare.” (Source:
a1.vectors_locked,lock.record; the saved best-practices page.) - Mark every case the prompt leaves open, and decide itRead the prompt against each case. In the v01 prompt nothing said what
2^-1gives; the locked test said0or0.5, and 10 of 11 recorded runs raised ValueError instead, 9 of them with a line that raises for a negative exponent. (Source:v01.prompt.silent_terms,v01.neg.failed,v01.reply.raise_line.) - Put those decisions in the prompt, or hand the agent the test fileEither way the agent can see what done means. The vendor's costs page: “Include test cases, paste screenshots, or define expected output in your prompt.” No run was made with a locked test file or vector list in its prompt, so this page cannot show what that changes. (Source: the saved costs page;
lock.vectors_in_prompt.) - Allow the exact test command, on its ownOne command that runs every test, allowed as written and kept apart from edits. In the record nobody could answer a permission prompt: 25 of 133 test commands were refused, and all 18 refused shell edits carried the test run on the same line. Name the command where the agent reads its instructions, as in the vendor's example “Run
npm testbefore committing”; in CI, the vendor's exact allow-list example isclaude -p "run the test suite" --permission-mode dontAsk --allowedTools "Bash(npm test)" "Read". (Source:a1b.pytest.refused,a1b.refused.shell_edit_with_test; the saved memory and permission-modes pages.) - Tell the agent not to edit the tests, and to stop when they passThe vendor's prompting guide: “It is unacceptable to remove or edit tests because this could lead to missing or buggy functionality.” The recorded bug-fix prompt ended “Do not edit the tests.” and said nothing about when to stop; the tests were unchanged in 75 of 75 runs (Class 98 published this). To make the stop a check, the vendor's hooks guide has an example: “This example verifies that tests pass before allowing Claude to stop:” (Source:
vq.pbp.no_edit_tests,a1b.tests_untouched,vq.hooks.stop.) - Ask for evidence: the command and its outputThe vendor: “Have Claude show evidence rather than asserting success: the test output, the command it ran and what it returned, or a screenshot of the result.” (Source:
vq.bp.evidence.) - Add the cases you think of later to the test file, not to a one-off commandIn the record, 6 refused commands chained
python -cafter the test to try extra cases, each its run's final call. A case in the test file runs under the allowed command every time. (Source:a1b.refused.test_and_check,a1b.refused.test_and_check_final.)
Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.
MeasuredQuotedn = 75 bug-fix runs and 88 coding runs The questions are constructed; the steps are the page's seven-step habit. Counts measured from the 75 recorded bug-fix runs on 5 small Python repositories, started 2026-10-03 on one Windows PC with Claude Code 2.1.284, tools Read, Edit and Bash, and nobody to answer a permission prompt, and from the 88 recorded single-turn coding runs with no tools (72 runs of 8 locked prompts and 16 effort runs), started 2026-10-03 to 2026-10-04 on one Windows PC with Claude Code 2.1.284. No new run was made for this page. Each step names its source; the keys are entries in this page's evidence file. Vendor behaviour is quoted from saved pages (2026-10-11), not tested on your setup.
Build a prompt that hands the test over
Six fields. The prompt updates as you type; copy it or download it as a text file. The defaults are a constructed example built on the v01 task, with the negative-exponent case written out as the open decision. It is not a recorded prompt, and no run used it.
Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.
Quotedn = 1 constructed example Constructed: no recorded run used a prompt like this one, so this page cannot say how an agent responds to it. The fields follow the vendor's saved pages: “Include test cases, paste screenshots, or define expected output in your prompt.” and “Have Claude show evidence rather than asserting success: the test output, the command it ran and what it returned, or a screenshot of the result.” No new run was made for this page. The file is built in your browser; nothing is sent anywhere.
Where to go next
- Class 98: AI Bug Fix teaches the bug-fix loop with the same 75 recorded runs, and has an interactive replay of one of them.
- Class 71: Evals and Testing AI Output takes testing past one function.
- Class 3: Prompting as Specification treats the prompt as a spec, the other half of this class.
- Class 95: Claude Opus 5.5 and Class 90: Claude Fable 5.1 first described the
2^-1miss, where the spec is silent. - Class 96: Claude Sonnet 5.5, measured against Opus 5.5 has the coding battery by model. This class does not compare models.
- Class 100: Claude Code Headless runs Claude Code from scripts, where checks like these run with nobody watching.
- Class 74: Deployment and CI/CD for AI-Built Code covers the pipeline that ships the code your tests guard.
One more class is planned, with no date and no promise that it will appear: What Your Coding Agent Is Allowed to Run.
What we did not verify
This is the page's own list of gaps. Everything here comes from runs recorded earlier, their saved grade and score files, and saved vendor pages. I made no new run, so each item is a limit on those records or on what was tested.
- Whether writing tests first raises pass rates. The two recorded families are not a controlled comparison: they differ in task, tools and prompt shape. No recorded run was given the locked test file or its vector list in its prompt. A run with them would be a new run, and none was made.
- Anything larger. The record covers 8 small prompts and 5 tiny repositories with one planted bug each. Nothing here was tried on a larger code base.
- Other setups. 3 models, one Windows PC, Claude Code
2.1.284. The single-turn runs had no tools; the bug-fix runs hadRead,EditandBashunder one fixed allow-list, with nobody to answer a permission prompt. Other models, versions, settings and machines were not run. - Small samples. 11 recorded runs per bench prompt and 75 bug-fix runs. The page claims no confidence interval for any count.
- The 6 refused spot-checks. What their extra cases would have shown is unknown: they did not run.
- The 19 cut-off re-run heads. Their full text was not opened for this page. That all 49 re-runs were green is Class 98's reading of the full records, cited and not re-derived.
- Why the 16 runs that ended on their edit did not try a re-run. The record shows a refused call earlier in each run, not a reason.
- Why each run chose what it chose for
2^-1, beyond what its own code and comments say. - Reasons read from code. Where the page gives a reason for an outcome (a
raiseline, an index pattern, a parse gap), it is READ: our reading of the saved text. No model-written code was run for this page. - The grader, run again. It was not; every outcome is read from its saved grade files.
- The hidden suite. The page reports only whether each run passed it, from the saved score files.
- The vendor's suggestions in practice. Whether a Stop hook, an exact CI allow-list or a CLAUDE.md line naming the test command would change any count on this page was not tested here.
- The vendor pages after 2026-10-11, when they were saved. They may have changed since.
- The drills' constructed cases are not recorded runs. Each drill says which of its cases are recorded.
Bottom line
Decide what done means, write it as a test before the prompt, hand the test over, let the agent run it, and stop on green. In 11 recorded single-turn runs of one prompt, 10 replies raised ValueError on a case that only the locked test had decided, and a test that expects the raise accepts 10 of the same 11. When the prompt itself was a failing test, 74 of 75 recorded bug-fix runs were solved, and in 49 of 49 re-runs the test run was the last tool call. No new run was made for this page.
Questions
What is TDD with AI?
We use the term TDD, short for test-driven development, for a habit: write a failing test first, change the code until it passes, then stop. With an AI coding agent it means writing the test before the prompt, so that the test, not the agent's own sense that the work looks done, decides when the work is done. In 11 recorded single-turn runs of one prompt, with no tools and no test shown to the model, 10 replies raised an error on a case the prompt never mentions; only the locked test had decided what that case should give. The vendor pages saved for this class do not use the term TDD; using it here is our choice, not the vendor's.
Why did 10 of 11 runs not meet the locked 2^-1 test?
The prompt asked for a function, evaluate, that returns an int, and said nothing about a negative exponent. The locked test passed when 2^-1 returned zero or one half; in the 11 recorded runs, the only way a reply failed it was by raising ValueError. 10 of 11 recorded replies raised ValueError there, and 9 of those 10 have a line in their own code that raises it for a negative exponent, on purpose by our reading of that line. That is not a model bug: the requirement lived only in the test. Writing the test before the prompt is how you find such a decision while you can still put it in the prompt.
Does writing tests first make an AI coding agent pass more often?
This class does not show that. The recorded runs are not a controlled comparison, and no recorded run was given the locked test file or its vector list in its prompt; such a run would be a new run, and none was made for this class. What the record shows is narrower: the test decides what done means. The locked test accepts 1 of the same 11 recorded replies, and a test that expects a raise accepts 10, with no change to the code.
Can a prompt that only points at a failing test work?
In 75 recorded bug-fix runs on 5 small Python repositories, every run got the same 15-word prompt: The test in tests/test_visible.py fails. Find and fix the bug. Do not edit the tests. It names no function, no bug and no expected value. 74 of 75 runs passed a hidden suite the agent never saw, and the visible result matched the hidden result in 75 of 75. Class 98 published the solved count and that no run passed the visible test while failing the hidden suite; the match in 75 of 75 is counted here from the same score files. The test carried what the prompt left out. That is 5 tiny repositories, not a large code base, and it is not a comparison with a longer prompt.
Does a coding agent stop when the tests pass?
In the recorded runs that re-ran the tests, the work stopped at the test result. Of the 74 bug-fix runs that edited code, 49 ran the tests again after the edit, and in all 49 that re-run was the final tool call, with 0 calls after it; the other 25 did not re-run, and the recorded prompt did not say to stop. Class 98's reading of the full records says all 49 re-runs passed. The vendor's best-practices page says: "Claude stops when the work looks done." It adds that a check Claude can run gives it a pass or fail to stop on. For a stop that does not rest on the prompt, the vendor's hooks guide shows a Stop hook that checks the tests.
Why were some test commands refused?
Each bug-fix run had one allow-list: Read, Edit, and the test command python -m pytest with or without arguments, and nobody could answer a permission prompt. 108 of 133 test calls ran. The refused ones carried something else on the same line: all 18 refused shell edits ran the test after the edit on one line, and 6 commands chained python -c after the test to try extra cases. The vendor's permissions page says: "A rule must match each subcommand independently."
What should I allow so the agent can run my tests?
The exact test command, with and without arguments, and keep edits and spot-checks off that line. Make each edit as its own call, and put extra cases in the test file instead of a python -c line. In the recorded runs, of the 25 refused calls that contained the test command, 18 chained a shell edit on the same line, and the rest chained a python -c spot-check: 6 after the run's edit and 1 before it. For CI, the vendor's permission modes page shows a run with an exact allow-list: one test command and Read.
Should I let the AI agent edit my tests?
Decide it and say it in the prompt. Every recorded bug-fix prompt said Do not edit the tests, and 75 of 75 runs left the test files unchanged; the record cannot say what they would have done without that sentence. The vendor's prompting best-practices page suggests reminding Claude that it is unacceptable to remove or edit tests. If you want new cases, add them yourself or name the test file the agent may add them to.
What can a passing test not tell me?
Only that its own cases pass. In the recorded battery, a check that expects an exception passes on any of three exception types, so one prompt's split between two kinds of error was not tested. A prompt rule, No eval, no ast., had no check at all: 0 of the 9 single-turn replies broke it, and nothing would have caught one that did. 2 of the 8 prompts name an exception and have no check for a raised exception at all. Read your test suite for what it leaves out.
How did a one-line test catch a real bug?
The v03 prompt says in words that array indices must have no leading zeros, and that '01' is invalid. 1 of 11 recorded replies still accepted '01': its comment states the rule and its code does not enforce it, by our reading of the saved code, which we did not run. A one-line test written from that sentence, expecting the pointer '/01' to raise an error, rejects that reply and no other. Leave it out and 11 of 11 pass.
Was a new run made for this class?
No. Every figure comes from runs recorded earlier on one Windows PC with Claude Code 2.1.284: 88 single-turn coding runs from 2026-10-03 to 2026-10-04 with no tools, graded afterwards against test cases locked before any prompt was sent, and 75 bug-fix runs on 2026-10-03 with the tools Read, Edit and Bash. No model-written code was run for this page; outcomes come from the saved grade and score files.
Is the class free, and do I need to sign up?
Yes, it is free, and you do not need to sign up or give an email address. It is an independent class, not affiliated with Anthropic. The figures come from saved runs, their saved grades and saved vendor pages; no new run was made for it.