# 13.9 Tests first: the TDG loop (/building-ai-workers/programming-fundamentals/tests-first)

---
type: Document
title: "13.9 Tests first: the TDG loop"
description: "Decide the cases and their answers first, have the agent write them as tests and then the code that passes them, and know what passing does and does not show."
status: stable
order: 313.09
ksor:
  owner: team:panaversity
  audience: [ public ]
  approval:
    by: process:panaversity
    at: 2026-11-02T07:43:08Z
chapter: "13"
part: III
expert_status: builder-only
concepts: [ "13.9" ]
last_verified: 2026-10-30
generated:
  at: 2026-11-02T07:43:08Z
  by: process:panaversity
trust_tier: unverified
build_id: sha256:e1fbcb7d9a8f8b9b7cb96875022325c02c9ba4be94cef13b301cbd39a9edad85
dirty: true
ksor_version: 0.0.60
---

**In everyday life.** Before a contractor tiles a bathroom, you agree what done looks like: perfectly level, the lines between the tiles straight, and no cracked tiles. You keep your own copy, and the tiler never edits it. At the end, you check the job against your copy. A list written after the job tends to describe what was built, not what you wanted. And a job that passes your list can still hide what the list never named, such as missing waterproofing.

**What you will learn.** How to decide the tests before any code, have the agent write them and then make them pass without changing them, and know what passing does and does not show.

**Why it matters.** Only you can say what correct means. Write down the right answers before the agent starts, check its work against them, and never let it change them.

A **test** is a small program that runs your code with chosen inputs and checks the outputs with assertions. **Test-Driven Generation**, or **TDG**, is a way of working that puts the tests first. You decide the cases that define "correct", and the answer each one must give. The agent writes them as tests, and you read each test against your list. The tests run and fail, because the code does not exist yet. The agent writes the code, the tests run again, and you read the result. Programmers have long written tests first and then the code themselves. In TDG the agent writes both, and your cases decide whether the code is right, for the cases they name.

| Step | Who leads | [10-80-10](you-read-the-agent-writes.md#what-is-my-job-when-an-agent-writes-the-code) | [PRIMM-AI+](you-read-the-agent-writes.md#how-do-i-learn-to-read-code-with-an-agent-beside-me) |
| --- | --- | --- | --- |
| Decide the cases and answers, and read the failing tests written from them | You | The first 10 percent | Make |
| Generate the code that passes them | The agent | The middle 80 | |
| Read the test results, and repeat | You | The last 10 percent | Investigate, Modify |

### Why write the tests first?

For the same reason as the tile list above. Tests written after you have seen the result tend to describe that result, right or wrong.

**The test is the specification.** Each test says one thing the code must do, such as "Net 30 from October 7 is November 6" or "unknown terms raise an error". If you cannot say a case and its answer for something, you do not yet know what correct means for it, and neither does the agent.

**Failing for the right reason.** Have the new tests run before the code exists. They should fail because the function is missing, not because of a typo in the test. A test that passes before the code exists tests nothing. An empty floor has no cracked tiles, so a list that an empty floor passes proves nothing.

### Who owns the expected results?

You do.

**Two rules that protect the loop.**

1. **You own the expected results.** If the agent decides both the code and what the tests expect, it is checking its work against its own reading of the job, not yours. The agent may write the test code and suggest edge cases, the cases at the limit of a rule. You decide every expected result, and you read every test before it counts.
2. **The agent never changes a test to make it pass.** A failing test is your specification doing its job. If the agent edits the test instead of the code, stop it. The only time a test changes is when you decide the test itself was wrong. After every generation step, have the agent compare the tests with the commit where you saved them, and read the result yourself: `git diff <that commit> -- tests/` must show nothing. A plain `git diff tests/` is not enough: it shows only changes not yet added with `git add`, so it misses changes the agent has already added or committed. Count the tests too. The agent can change which files `npm test` runs, so the number that pass must be your new tests plus the old ones. A test file that the runner no longer runs is as bad as an edited one.

### What does green show, and what does it not?

**Passing is evidence, not proof.** Green tests, the ones that pass, show the code does what your tests say, for the inputs you chose. Nothing more. That is why the inputs matter: each term in the data, a month end, a year end, a leap day, and an input that must fail. The agent saw your tests while it worked, so its code can pass them by answering just those cases. So before you accept the code, read the lines that carry a rule, as [13.11](judgment.md#which-lines-carry-a-rule) shows.

**At work.** This is how `dueDate` began. Before any code exists, Ana writes five cases in plain words. Net 30 from 2026-10-07 is 2026-11-06, Net 15 from 2026-10-20 is 2026-11-04, Net 30 from 2026-12-20 is 2027-01-19, Net 15 from 2028-02-15 is 2028-03-01, and Net 10 must give an error, because Brightline has no Net 10 terms. She sends them: "Here are five cases for dueDate, each with its answer. Please write each one as a test, in both languages. Don't add cases of your own. If you think one is missing, tell me." The agent writes them beside the repository's 7 TypeScript and 2 Python tests. She reads each test against her list, and has the agent commit them while they fail, so the history shows the tests came first. Then she asks the agent: "Please write `src/due-date.ts` so the tests in `tests/due-date.test.ts` pass. Don't change the tests. If you think one is wrong, stop and tell me why." It does. She has it compare the tests with her commit, using `git diff 6cdbb3d -- tests/`, where `6cdbb3d` is that commit's id, and the output is empty. Then she has it run the type check. She does the Python version the same way.

**In TypeScript.** Predict the first run, before `src/due-date.ts` exists.

```ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { dueDate } from "../src/due-date.ts";

test("crosses a year end", () => {
  assert.equal(dueDate("2026-12-20", "Net 30"), "2027-01-19");
});

test("unknown terms raise an error", () => {
  assert.throws(() => dueDate("2026-10-01", "Net 10"), /Unknown terms: Net 10/);
});
```

```text
$ npm test
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '.../src/due-date.ts'
# fail 1
$ git add tests/
$ git commit -m "Failing tests for dueDate"
...the agent writes src/due-date.ts...
$ npm test
# pass 12
# fail 0
$ git diff 6cdbb3d -- tests/
$
```

`6cdbb3d` is the commit that holds the failing tests. Yours will have its own id. The test file stops at its first `import`, so Node counts one failure. Later, the 12 are her 5 tests plus the 7 already there.

**In Python.** `npm run test:py` runs pytest, which finds files whose names start with `test_` and runs every function whose name starts with `test`.

```python
import pytest

from brightline_ap.due_date import due_date

def test_crosses_a_year_end() -> None:
    assert due_date("2026-12-20", "Net 30") == "2027-01-19"

def test_unknown_terms_raise_an_error() -> None:
    with pytest.raises(ValueError, match="Unknown terms: Net 10"):
        due_date("2026-10-01", "Net 10")
```

```text
$ npm run test:py
E   ModuleNotFoundError: No module named 'brightline_ap.due_date'
...the agent writes py/brightline_ap/due_date.py...
$ npm run test:py
7 passed in 0.02s
```

The 7 are her 5 tests plus the 2 already there.

**Where the languages differ.** Node's test runner is built in: `test(...)`, `assert`, and `assert.throws` for an expected error, whose message must match the text between the slashes. Python uses pytest, a separate tool, with plain `assert` and `pytest.raises` for an expected error. The loop is the same in both.

**The operating rule:** decide the cases and their answers first, have the agent write them as tests, read each one, and watch them fail for the right reason. Then let the agent write the code, and check that the tests still match the commit where you saved them before you accept it.

![Figure 13.12. The TDG loop mapped to 10-80-10. You decide the test cases, and the agent writes them as failing tests (the first 10). The agent generates the code (the 80). You read the check results and the diff (the last 10). A failed check goes back to the agent to revise. Passing checks go to your review and then the commit.](img/tdg-loop.png)

*Figure 13.12. The TDG loop. Your test cases are the first 10 percent and decide the last 10. The agent writes them as tests, and never edits them once you approve.*

**Practice it.** [Practice: tests first](both-ai-vendors.md#practice-tests-first), in your own tool, fifteen minutes.
