Plan first, then drive the change from a failing test
In this lesson we fix one bug with a coding agent, in two halves. In the first half the agent may read and may not edit: it works out where the bug is and writes down the steps it intends to take, and we correct one of them before any file changes. In the second half it writes a test that fails because of the bug and then changes the code until the test passes, and we read the diff. The bug is in a fixture that ships with this course, and it spans three files, which is the size of change where a few minutes of planning save more than they cost.
The vendor of Claude Code teaches this rhythm for its own tool, explore first, then plan, then code [1], and its short course walks through the same loop [2]. Where this lesson names a Claude Code key or flag, it says so, and the source is checked on the date at the end of the lesson. Other coding agents have a read-only mode under another name, and the rhythm is the same.
The fixture
Section titled “The fixture”The fixture is the to-do program from Your first session with a coding
agent, a few weeks later. It has grown a clear command that removes the
done items, and it has been split into three modules: todo.py holds the
commands, store.py reads, writes and prunes the list, and render.py
formats it. It is in the course repository under
site/examples/coding-with-agents/plan-then-test/fixture-repo/. Copy that
directory to a place of its own and change into it. A copy has no git
history, so the reset is to delete the copy and copy again, and the files
the session adds go with it. Working inside a clone of the course
repository also works, and there git checkout -- . is the reset, but the
agent then reads the course’s own instruction files from the directories
above the fixture. The copy is the better default.
The committed list has four items, two of them done. Run the program before you run the agent, so you know what the list looks like now.
Show the list
Section titled “Show the list”Run this, and compare what you see with the output below.
python3 todo.py list1. [ ] Buy milk 2. [x] Call the plumber 3. [x] Renew the passport 4. [ ] Water the plants 2 open, 2 done
Output verified in CI from site/examples/coding-with-agents/plan-then-test/list.py.
The tests pass. That is the first thing that is different from the last lesson: nothing here is red yet.
Run the tests
Section titled “Run the tests”Run the tests and keep only the last line of the output.
python3 -m unittest -q 2>&1 | tail -1OK
Output verified in CI from site/examples/coding-with-agents/plan-then-test/tests.py.
Now the bug. Clear the done items on a copy of the list, and show what is left.
Clear the done items
Section titled “Clear the done items”Run clear and then list against a copy, so the committed file doesn’t
change.
cp todos.json /tmp/todos.jsonTODO_FILE=/tmp/todos.json python3 todo.py clearTODO_FILE=/tmp/todos.json python3 todo.py listremoved 1 done item 1. [ ] Buy milk 2. [x] Renew the passport 3. [ ] Water the plants 2 open, 1 done
Output verified in CI from site/examples/coding-with-agents/plan-then-test/clear_bug.py.
clear removed one of the two done items. The other is still there,
still marked done. You have the evidence, and this time you don’t know
which of the three files holds the cause. That is the question the plan
answers before anything changes.
The examples in this lesson aren’t graded. Each one is a command you run in the fixture, with the output the course itself checks on every build. The checkpoints ask about the session, because running the session is what this lesson teaches.
Put the agent in plan mode
Section titled “Put the agent in plan mode”Plan mode is a read-only mode for the agent: it reads files, searches, and
runs commands to explore, and it edits nothing. Instead of a diff you get
a plan, a written list of the steps it intends to take, and the edits wait
until you approve it. In Claude Code, Shift+Tab cycles through the
permission modes until the status bar says plan mode is on, and
claude --permission-mode plan starts a session in it. A /plan prefix
puts one prompt in plan mode. Shift+Tab again leaves it without approving
anything [3].
Start your coding agent in plan mode from inside fixture-repo, and hand
it the evidence you gathered. The brief has the same parts as before: the
goal, the context, what it may touch, and how you know it is done. The one
new part is that you ask for a plan, and you ask it to name the cause
before it names the fix.
clear leaves a done item behind. With the committed todos.json copied to
/tmp/todos.json, TODO_FILE=/tmp/todos.json python3 todo.py clear prints
“removed 1 done item” and the list still shows “Renew the passport” as done.
Both done items should have gone. Read the three modules and the tests, then give me a
plan: where the cause is, which test you would write first, and which files
you would change. Don’t change anything yet. todos.json stays as it is.
I read todo.py, store.py, render.py and test_todo.py, and ran the
command you gave.
Cause: store.remove_done removes items from the list while it is looping
over the same list. After it removes item 2, item 3 moves into position 2,
and the loop moves on to position 3, so every done item that follows a
done item is skipped. render.py shows the list it is given, and it is
right to show the item.
Plan:
- Add
test_clear.pywith a test that callsstore.remove_doneon a list with two done items next to each other, and asserts that it returns 2 and that only the two open items remain. Run it and confirm it fails. - Rewrite
remove_doneinstore.pyto build the list of kept items and assign it back in place, so the count and the callers keep working. - Change
render.list_itemsinrender.pyto skip done items, so alistafterclearnever shows one. - Run
python3 -m unittest -qand confirm every test passes.
Files changed: test_clear.py (new), store.py, render.py.
The transcripts in this lesson are illustrative: they show what a good exchange looks like and don’t record a specific model’s words. Your agent phrases things differently, and its plan may have four steps or six.
Read the plan the way you read a diff. Steps 1 and 2 follow from the cause
the agent named, and step 3 has no cause behind it. The agent found the cause in
store.py, and then proposed a change to render.py anyway, because the
symptom showed there. Hiding done items in list would make the bug
invisible without fixing it, and it would change what list means for
every user who has a done item they haven’t cleared yet.
Which task gets a plan?
Section titled “Which task gets a plan?”Plan mode puts a coding agent in a read-only mode: it reads and explores and proposes steps, and it edits nothing until the plan is approved. A plan costs a few minutes at the start of a task.
Which of these do you start in plan mode?
For which task could you not write the diff yourself in one sentence?
Correct the plan while it is cheap
Section titled “Correct the plan while it is cheap”The agent is still in plan mode, so correcting it costs a sentence. Tell
it which step to drop and why, and ask for the plan again. In Claude Code
the prompt under a finished plan offers to approve it, to approve it and
review each edit, or to keep planning, and keep planning is the one you
want here [3]. The same page names a shortcut,
Ctrl+G, that opens the plan in your text editor so you can edit it
directly. A sentence in the chat does the same job for a one-step fix.
Drop step 3. You found the cause in store.py, and render.py is showing
the list correctly. Hiding done items would change what list does for
everyone. Keep steps 1, 2 and 4, and change only store.py and the new
test file.
Agreed. Revised plan:
- Add
test_clear.pywith the failing test forstore.remove_done. Run it and confirm it fails. - Rewrite
remove_doneinstore.pyto keep the open items and assign them back in place. - Run
python3 -m unittest -qand confirm every test passes.
Files changed: test_clear.py (new), store.py. render.py and
todos.json stay as they are.
Now the plan matches the cause. Approve it, and choose the option that still asks you before each edit. You know what the two edits should be, and the prompts let you check that those are the two you get.
The plan has a step that doesn't follow
Section titled “The plan has a step that doesn't follow”A learner has a coding agent in plan mode, a read-only mode where the agent proposes steps and edits nothing until the plan is approved. The agent's plan names the cause of a bug in one module and then also proposes a change to a second module where the symptom showed.
The plan names the cause in store.py, and step 3 changes render.py to
hide done items. What do you do?
Where is the cheapest place to remove a step: in the plan, in the diff, or in production?
The failing test first
Section titled “The failing test first”The first step of the approved plan is a test, and the agent writes it
before it touches store.py, because a test written after the fix asserts
whatever the fixed code does. A test written
before it has to describe the behavior you want, and it has to fail, which
proves it is looking at the bug at all. The vendor’s own guidance for a
bug brief takes the same order: ask for a test that fails on the bug before
any fix, and only then for the fix [1].
The agent creates test_clear.py and asks permission for the new file.
Read it before you approve.
class ClearTests(unittest.TestCase): def test_remove_done_removes_every_done_item(self): items = [ {"text": "Buy milk", "done": False}, {"text": "Call the plumber", "done": True}, {"text": "Renew the passport", "done": True}, {"text": "Water the plants", "done": False}, ] removed = store.remove_done(items) self.assertEqual(removed, 2) self.assertEqual([item["text"] for item in items], ["Buy milk", "Water the plants"])The list has its done items next to each other, because that is the case the bug skips. A test with one done item would pass today and prove nothing. Approve the file, and run the suite yourself.
Run the suite with the new test
Section titled “Run the suite with the new test”With test_clear.py in place, run the tests and keep the last line. The
course checks this against its own copy of the test, which is the one
shown above.
python3 -m unittest -q 2>&1 | tail -1FAILED (failures=1)
Output verified in CI from site/examples/coding-with-agents/plan-then-test/failing_test.py.
Red, for the reason you expected: the test reports that remove_done
returned 1. The test is the agent’s target now. It can run it on its own,
as many times as it needs, and it can’t say “done” until the line reads
OK.
The new test passes on the first run
Section titled “The new test passes on the first run”A learner is driving a bug fix from a failing test: the coding agent writes a test that captures the wanted behavior first, runs it and sees it fail, and then changes the code until it passes. The agent has written the test and run it, before changing any other file.
The agent writes the test, runs it, and reports that it passes. No other file has changed. What do you do?
What does a passing test prove about a bug that is still in the code?
Implement until it passes, then read the diff
Section titled “Implement until it passes, then read the diff”The agent moves to step 2, asks permission to edit store.py, and you
approve the one edit the plan named. Then it runs the suite, reads the
verdict, and reports. The report is a claim, so run the check yourself.
Run the suite after the fix
Section titled “Run the suite after the fix”Run the tests and keep the last line. The course applies the fix shown below to its own copy and checks this output against it.
python3 -m unittest -q 2>&1 | tail -1OK
Output verified in CI from site/examples/coding-with-agents/plan-then-test/tests_after_fix.py.
Then read the diff. It is the second piece of evidence, and the test passing is only proof if the diff touched what the plan said.
git status --shortgit diffremoved = 0for item in items: if item["done"]: items.remove(item) removed += 1return removedkept = [item for item in items if not item["done"]]removed = len(items) - len(kept)items[:] = keptreturn removedgit status names two files: store.py modified, test_clear.py new.
render.py isn’t in the list, and neither is todos.json. The diff builds
the list of kept items first and assigns it back into the same list, so
todo.py, which holds a reference to that list, sees the change. Your
agent’s version may use another construction. What matters is that it
changes remove_done and nothing else.
Prove it against the list one more time, on a copy.
Clear the done items after the fix
Section titled “Clear the done items after the fix”Run clear and then list against a fresh copy of the committed list.
cp todos.json /tmp/todos.jsonTODO_FILE=/tmp/todos.json python3 todo.py clearTODO_FILE=/tmp/todos.json python3 todo.py listremoved 2 done items 1. [ ] Buy milk 2. [ ] Water the plants 2 open, 0 done
Output verified in CI from site/examples/coding-with-agents/plan-then-test/clear_after_fix.py.
The last step of the plan was the cleanup, and there is little of it here: the test file has a name that says what it covers, and the fix has no leftover print or comment. When there is more, ask for it as its own step and read that diff too. Then end the session, as the last lesson taught. The next task gets a fresh one.
Put the test-driven steps in order
Section titled “Put the test-driven steps in order”Driving a change from a failing test: the test that captures the wanted behavior is written first, and the code is changed until it passes. The learner runs the checks and reads the diff at the end.
- The agent writes a test that describes the wanted behavior
- Run the suite and confirm the new test fails
- The agent changes the code the plan named
- Run the suite yourself and confirm every test passes
- Read the diff and check it touched only the files in the plan
What has to be red before anything turns green, and what do you read once it is green?
When a test beats a spec
Section titled “When a test beats a spec”A failing test suits a change whose wanted behavior fits in an assertion: a bug fix, or a bounded change to what one function or one command does. The test is precise, the agent can run it without you, and passing it is proof. It doesn’t suit a change where the question is the design: which modules exist, what the interface between them is, what the data looks like. For those you write the specification first and the agent works from it, and this course has its own lesson on working from a spec in small increments. The two aren’t rivals. A spec-driven feature has tests for each of its increments, and a test-driven fix has a one-line spec, the sentence that says what “fixed” means.
Test first, or spec first?
Section titled “Test first, or spec first?”A test-driven change starts from a failing test that captures the wanted behavior and suits bug fixes and bounded behavior changes. A spec-driven change starts from a written specification with success criteria and suits changes where the design matters more than any single test.
Can you write the wanted behavior as one assertion that fails today? If you can, which way is that?
Plan first, or go?
Section titled “Plan first, or go?”Plan mode puts a coding agent in a read-only mode: it reads, explores and proposes steps, and it edits nothing until the plan is approved. The lesson uses it for a change you can't place in one file.
Could you write the diff in one sentence before the agent starts?
Reading the plan
Section titled “Reading the plan”A coding agent in plan mode has proposed a plan that names the cause of a bug and lists the steps to fix it. Nothing is edited until the learner approves.
Which two things do you check before you approve the plan?
What does each step have to follow from?
The new test passes
Section titled “The new test passes”A learner drives a bug fix from a failing test. The bug: the export command drops the last row. The agent has written a test for it and run it before changing anything else.
The agent writes the test for the missing last row, runs it, and it passes. No other file has changed. What do you do?
Can a test that passes on the buggy code tell you when the bug is fixed?
Before the code changes
Section titled “Before the code changes”The lesson drives a change from a failing test that captures the wanted behavior, and the learner runs the checks and reads the diff at the end.
In a change driven by a failing test, what happens before the agent changes the code?
What does the test have to do on the code as it is now?
A test or a spec?
Section titled “A test or a spec?”A test-driven change starts from a failing test and suits a bug fix or a bounded behavior change. A spec-driven change starts from a written specification with success criteria and suits a change where the design matters more than any single test.
Match each change to where it starts.
Is it one behavior you can pin down in a test, or a design with several parts?
Exercise
In a fresh session, with the fixture reset, run the whole bug fix with
your coding agent: start in plan mode with the brief above, read the plan,
correct or drop any step that doesn’t follow from the cause it names, then
approve it and let the agent write the failing test and fix store.py.
Run python3 -m unittest -q yourself at the red step and at the green
step, and read git diff at the end. Plan on ten minutes. This is the
rhythm you use on any change too big to describe in one sentence.
A good result: the plan named store.remove_done as the cause, the test
failed before the fix and passed after it, and the diff touches
store.py and one new test file and nothing else. Finish by deleting your
copy and copying the fixture again, so it is ready for next time. Then answer one question: which step of the plan would you
have missed if the agent had gone straight to the diff?
Stretch: Now do the same for a change you choose, for example making `done` accept several numbers at once. Decide first whether it starts from a test or a spec, and say why in one line.
Recap
- For a change you can’t place in one file, start the agent in plan mode: it reads and proposes, and it edits nothing until you approve [3]. If you could write the diff in one sentence, skip the plan [1].
- Read the plan against the cause it names, and correct or drop a step that doesn’t follow while the correction costs a sentence.
- The failing test comes before the fix. It has to fail on the code as it is, or it isn’t looking at the bug.
- The agent implements until the test passes. You run the check yourself, and you read the diff to see that it touched only what the plan said.
- A test suits a bug fix or a bounded behavior change. A design question starts from a spec, and the two work together.
You can now
- Runs a session from setup to a reviewed diff
References
Section titled “References”- Anthropic. Best practices for Claude Code. Claude Code documentation. Reference.
Claude Code best practices - Anthropic. Claude Code 101. Claude Academy. Course.
Academy claude-code-101 - Anthropic. Choose a permission mode. Claude Code documentation. Reference.
Claude Code permission modes