Skip to content

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

Example · run it

Run this, and compare what you see with the output below.

Terminal window
python3 todo.py list
Output
1. [ ] 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.

Example · run it

Run the tests and keep only the last line of the output.

Terminal window
python3 -m unittest -q 2>&1 | tail -1
Output
OK

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.

Example · run it

Run clear and then list against a copy, so the committed file doesn’t change.

Terminal window
cp todos.json /tmp/todos.json
TODO_FILE=/tmp/todos.json python3 todo.py clear
TODO_FILE=/tmp/todos.json python3 todo.py list
Output
removed 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.

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.

Prompt (illustrative, not a recorded transcript)

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.

Response

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:

  1. Add test_clear.py with a test that calls store.remove_done on 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.
  2. Rewrite remove_done in store.py to build the list of kept items and assign it back in place, so the count and the callers keep working.
  3. Change render.list_items in render.py to skip done items, so a list after clear never shows one.
  4. Run python3 -m unittest -q and 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.

Checkpoint · choice

Which of these do you start in plan mode?

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.

Prompt (illustrative, not a recorded transcript)

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.

Response

Agreed. Revised plan:

  1. Add test_clear.py with the failing test for store.remove_done. Run it and confirm it fails.
  2. Rewrite remove_done in store.py to keep the open items and assign them back in place.
  3. Run python3 -m unittest -q and 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.

Checkpoint · scenario

The plan names the cause in store.py, and step 3 changes render.py to hide done items. What do you do?

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.

Example · run it

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.

Terminal window
python3 -m unittest -q 2>&1 | tail -1
Output
FAILED (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.

Checkpoint · choice

The agent writes the test, runs it, and reports that it passes. No other file has changed. What do you do?

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.

Example · run it

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.

Terminal window
python3 -m unittest -q 2>&1 | tail -1
Output
OK

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.

Terminal window
git status --short
git diff
removed = 0
for item in items:
if item["done"]:
items.remove(item)
removed += 1
return removed
kept = [item for item in items if not item["done"]]
removed = len(items) - len(kept)
items[:] = kept
return removed

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

Example · run it

Run clear and then list against a fresh copy of the committed list.

Terminal window
cp todos.json /tmp/todos.json
TODO_FILE=/tmp/todos.json python3 todo.py clear
TODO_FILE=/tmp/todos.json python3 todo.py list
Output
removed 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.

Checkpoint · order
  1. The agent writes a test that describes the wanted behavior
  2. Run the suite and confirm the new test fails
  3. The agent changes the code the plan named
  4. Run the suite yourself and confirm every test passes
  5. Read the diff and check it touched only the files in the plan

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.

Checkpoint · sort

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

  1. 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].
  2. Read the plan against the cause it names, and correct or drop a step that doesn’t follow while the correction costs a sentence.
  3. 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.
  4. 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.
  5. 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

  1. Anthropic. Best practices for Claude Code. Claude Code documentation. Reference. Claude Code best practices
  2. Anthropic. Claude Code 101. Claude Academy. Course. Academy claude-code-101
  3. Anthropic. Choose a permission mode. Claude Code documentation. Reference. Claude Code permission modes