# Plan first, then drive the change from a failing test

> Put a coding agent in plan mode for a bug that spans three modules, correct one step of its plan while corrections are cheap, then have it write the failing test, fix the code until the test passes, and read the diff.

Page: https://schubergphilis.github.io/ai-training/coding-with-agents/plan-then-test/
License: CC BY-SA 4.0, https://creativecommons.org/licenses/by-sa/4.0/

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: read the
code, make a plan, and only then edit (Best practices for Claude Code, Claude Code documentation).
Its short course walks through the same loop (Claude Code 101, Claude Academy).
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

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: Show the list

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

```sh
python3 todo.py list
```

Output:

```text
1. [ ] Buy milk
2. [x] Call the plumber
3. [x] Renew the passport
4. [ ] Water the plants
2 open, 2 done
```

The tests pass. That is the first thing that is different from the last
lesson: nothing here is red yet.

#### Example: Run the tests

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

```sh
python3 -m unittest -q 2>&1 | tail -1
```

Output:

```text
OK
```

Now the bug. Clear the done items on a copy of the list, and show what is
left.

#### Example: Clear the done items

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

```sh
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:

```text
removed 1 done item
1. [ ] Buy milk
2. [x] Renew the passport
3. [ ] Water the plants
2 open, 1 done
```

`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

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 (Choose a permission mode, Claude Code documentation).

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)

```text
`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

```text
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: Which task gets a plan?

Which of these do you start in plan mode?

- Bump the version string in `pyproject.toml` from 1.4.0 to 1.4.1 for a release.
- A command gives the wrong result, and the code that could cause it is spread over three modules.
- Add one log line where an exception is caught.
- Rename one variable in one function.

## 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 (Choose a permission mode, Claude Code documentation). The same page names a shortcut,
`Ctrl+G`, that loads the plan into your editor so you can change it
there. A sentence in the chat does the same job for a one-step fix.

#### Prompt (illustrative, not a recorded transcript)

```text
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

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

#### Pitfall: A confident change in the wrong module

An engineer skips the plan and briefs the agent to "make `clear` remove all
the done items". The agent reads `render.py` first, because
the output comes from there, and changes `list_items` to leave done items out.
The symptom is gone, and `list` after `clear` doesn't show a done item. The bug is
still in `store.py`, the file still holds the item, and the numbers `list`
shows no longer match the positions in the file, so `done 3` marks whatever
is third in the file. The rule: for a change you can't place in one
file, ask for the cause and the steps first, and read the steps against
the cause before any of them runs.

#### Checkpoint: The plan has a step that doesn't follow

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

- Approve the plan. The agent has read more of the code than you have, and you can always revert the second file afterwards.
- Tell the agent which step to drop and why, and ask for the plan again before you approve anything.
- Leave plan mode, do the fix yourself in the first module, and ask the agent to write the test afterwards.
- Approve the plan and deny the permissions prompt for the second file when it comes.

## 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 (Best practices for Claude Code, Claude Code documentation).

The agent creates `test_clear.py` and asks permission for the new file.
Read it before you approve.

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

```sh
python3 -m unittest -q 2>&1 | tail -1
```

Output:

```text
FAILED (failures=1)
```

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: The new test passes on the first run

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

- Good news. The bug must have been fixed by an earlier change, so ask the agent to move on to the cleanup.
- Fine either way. Approve the fix to store.py and run the suite again afterwards.
- Delete the test and let the agent fix the code directly, since the tests are unreliable here.
- The test does not capture the bug. Ask the agent to change the test until it fails on the code as it is.

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

```sh
python3 -m unittest -q 2>&1 | tail -1
```

Output:

```text
OK
```

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.

```sh
git status --short
git diff
```

```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: Clear the done items after the fix

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

```sh
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:

```text
removed 2 done items
1. [ ] Buy milk
2. [ ] Water the plants
2 open, 0 done
```

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 *Your first session with a
coding agent* taught. The next task gets a fresh one.

#### Checkpoint: Put the test-driven steps in order

- Read the diff and check it touched only the files in the plan
- Run the suite and confirm the new test fails
- Run the suite yourself and confirm every test passes
- The agent changes the code the plan named
- The agent writes a test that describes the wanted behavior

## 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](https://schubergphilis.github.io/ai-training/coding-with-agents/spec-driven-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: Test first, or spec first?

Buckets:

- Start from a failing test
- Start from a spec

Items:

- The clear command leaves a done item behind
- The add command should refuse an empty text instead of adding a blank item
- Dates on items, with a due command that lists what is overdue
- Split the program into a package with one module per command and a shared store
- The clear command should print the text of each item it removed
- Sync the list with a calendar service

## 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 you can't state in one short 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 (Choose a permission mode, Claude Code documentation). Skip the plan when the whole change fits in one short sentence (Best practices for Claude Code, Claude Code documentation).
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.

## References

- Best practices for Claude Code, Claude Code documentation, https://code.claude.com/docs/en/best-practices
- Claude Code 101, Claude Academy, https://academy.claude.com/courses/claude-code-101
- Choose a permission mode, Claude Code documentation, https://code.claude.com/docs/en/permission-modes
