style(no-slop): remove every em-dash + banned words across all modules + capstone
Apply the no-ai-slop standard (now binding in AGENTS.md): the em-dash character is banned outright (restructured, not blind-replaced), plus the banned word/phrase list (delve, leverage, robust, seamless, truly, unlock, etc.). 0 em-dashes remain in modules + capstone; the only "robust" left is the planted M10 ai-change.patch trap. Module H1 titles use a colon separator. All deliberate teaching devices preserved; labs compile/parse (py/sh/yaml/json); no junk. AGENTS.md updated with the hard no-slop rules. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TfzV5QvtPDz8LJS3Pu5VLT
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# Module 13 — Testing in the AI Era
|
||||
# Module 13: Testing in the AI Era
|
||||
|
||||
> **AI writes code that looks right and passes a human skim. That's exactly the code that needs a
|
||||
> test.** The same AI that produces the risk is excellent at writing the tests that catch it, once
|
||||
@@ -8,10 +8,10 @@
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Module 1** — the `tasks-app` running example you'll be testing, and a working Python + terminal.
|
||||
- **Module 2** — commits as checkpoints and reading `git diff`. Tests and a clean commit history are
|
||||
- **Module 1**: the `tasks-app` running example you'll be testing, and a working Python + terminal.
|
||||
- **Module 2**: commits as checkpoints and reading `git diff`. Tests and a clean commit history are
|
||||
the two halves of "I can trust this change."
|
||||
- **Module 10** — reviewing a diff the AI produced for *plausibility traps*, not just correctness.
|
||||
- **Module 10**: reviewing a diff the AI produced for *plausibility traps*, not just correctness.
|
||||
This module is the automated, repeatable version of that same instinct: a test reviews the code for
|
||||
you, the same way, every time.
|
||||
|
||||
@@ -29,10 +29,10 @@ setup for the next module.
|
||||
|
||||
By the end of this module you can:
|
||||
|
||||
1. Say what a test actually *is* — a small program that runs your code and asserts what should be
|
||||
true — and run one with Python's built-in `unittest`, no installs.
|
||||
1. Say what a test actually *is*: a small program that runs your code and asserts what should be
|
||||
true, and run one with Python's built-in `unittest`, no installs.
|
||||
2. Explain why AI-generated code specifically needs automated verification, beyond a careful read.
|
||||
3. Direct an AI to write *meaningful* tests for code — and recognize the trap where it writes tests
|
||||
3. Direct an AI to write *meaningful* tests for code, and recognize the trap where it writes tests
|
||||
that merely re-state current behavior instead of encoding intent.
|
||||
4. Use a test to expose a real bug in code that looked correct, then fix the code (not the test) and
|
||||
watch the suite go green.
|
||||
@@ -49,7 +49,7 @@ that runs a piece of your code and asserts that the result is what it should be.
|
||||
holds, the test passes silently. If it doesn't, the test fails loudly and tells you exactly which
|
||||
expectation broke.
|
||||
|
||||
You've already been testing — by hand. Every time you ran `python cli.py list` and eyeballed the
|
||||
You've already been testing, by hand. Every time you ran `python cli.py list` and eyeballed the
|
||||
output, you ran a manual test: *do something, check the result looks right.* The problem with the
|
||||
manual version is the same problem copy-paste had in Module 1: it doesn't scale across files or
|
||||
across time. You can't re-run "eyeball every command" on every change, so you don't, so regressions
|
||||
@@ -101,7 +101,7 @@ of the thing.
|
||||
Here's the failure mode that makes this module non-optional. AI-generated code has a property normal
|
||||
buggy code doesn't: **it is optimized to look correct.** The model produces code that reads
|
||||
plausibly, uses the right function names, follows the conventions it saw in your file, and passes a
|
||||
human skim — because "looks like correct code" is close to what it was trained to produce. Correct
|
||||
human skim, because "looks like correct code" is close to what it was trained to produce. Correct
|
||||
*behavior* is a separate thing the model is often right about and sometimes confidently wrong about,
|
||||
and the surface gives you almost no signal about which.
|
||||
|
||||
@@ -131,7 +131,7 @@ Ask an AI to "write tests for this function" with no further direction and you w
|
||||
that are subtly worthless, in a specific way: **they assert whatever the code currently does, rather
|
||||
than what the code is supposed to do.** The model reads the implementation, sees that it returns `5`
|
||||
for some input, and writes `assertEqual(result, 5)`. The test passes. It will keep passing. It is a
|
||||
tautology — it tests that the code does what the code does.
|
||||
tautology; it tests that the code does what the code does.
|
||||
|
||||
This is catastrophic in the AI era, because if the code the AI wrote is *wrong*, an AI test that was
|
||||
written *from that same code* will faithfully assert the wrong answer and lock the bug in. You now
|
||||
@@ -148,7 +148,7 @@ Concretely, that changes how you direct the AI. Don't say "write tests for `pend
|
||||
|
||||
- Weak (invites tautology): *"Write unit tests for the `pending_count` method."*
|
||||
- Strong (encodes intent): *"`pending_count` should return the number of tasks that are still
|
||||
pending — not completed. Write `unittest` tests for that behavior: empty list returns 0; tasks
|
||||
pending, not completed. Write `unittest` tests for that behavior: empty list returns 0; tasks
|
||||
added but none done returns the full count; after completing some, returns only the still-pending
|
||||
count; all done returns 0. Derive the expected values from that description, not from the current
|
||||
implementation."*
|
||||
@@ -166,12 +166,12 @@ intent has to come from you.
|
||||
### Tests are the content the next module automates
|
||||
|
||||
One more framing before the lab. A test file just sitting in your repo is useful when you remember to
|
||||
run it — which, like the manual eyeball check, you eventually won't. The full payoff comes in
|
||||
run it; like the manual eyeball check, you eventually won't. The full payoff comes in
|
||||
**Module 14**, where Continuous Integration runs this exact `python -m unittest` command
|
||||
automatically on every push, so a regression can't reach `main` without something going red first.
|
||||
|
||||
That's why this module comes immediately before CI: **tests are the content CI runs.** You can't
|
||||
automate a check you don't have. So the deliverable here isn't just "I understand testing" — it's a
|
||||
automate a check you don't have. So the deliverable here isn't just "I understand testing"; it's a
|
||||
real, committed `test_tasks.py` that the next module will pick up and run for you forever. Leave this
|
||||
module with that file and Module 14 is half-built already.
|
||||
|
||||
@@ -220,7 +220,7 @@ to catch a bug that has been sitting in the code looking perfectly fine.
|
||||
Sub your own agent if you prefer (`claude --version # sub your own agent`).
|
||||
- Git initialized in your working copy (Module 2), so the agent can commit the test file at the end.
|
||||
|
||||
### Part A — Write and run a first test by hand
|
||||
### Part A: Write and run a first test by hand
|
||||
|
||||
Do this once yourself so the tool isn't magic. From inside your working copy of the app:
|
||||
|
||||
@@ -249,7 +249,7 @@ Do this once yourself so the tool isn't magic. From inside your working copy of
|
||||
|
||||
You should see one test, and `OK`. That's the entire mechanism. Everything else is more of these.
|
||||
|
||||
### Part B — Direct the AI to write tests that encode intent
|
||||
### Part B: Direct the AI to write tests that encode intent
|
||||
|
||||
3. Now hand Claude Code the job, but direct it properly. Point it at `tasks.py` with a prompt that
|
||||
supplies **intent**, not just "write tests." Something like:
|
||||
@@ -263,13 +263,13 @@ Do this once yourself so the tool isn't magic. From inside your working copy of
|
||||
Note what you did: you described a case (*one completed*) where a correct `pending_count` and a
|
||||
wrong one give different answers. That's the case that can catch a bug.
|
||||
|
||||
4. Claude Code writes `test_tasks.py` next to `tasks.py`. **Review it before running it** — this is
|
||||
4. Claude Code writes `test_tasks.py` next to `tasks.py`. **Review it before running it**; this is
|
||||
the Module 10 skill applied to tests. For each test ask: *if `pending_count` were wrong, would this
|
||||
one notice?* A test that only ever adds tasks (never completes one) would pass no matter what
|
||||
`pending_count` returns, because with nothing done, total and pending are the same number. That
|
||||
test is a tautology; the "one completed" test is the one with teeth.
|
||||
|
||||
### Part C — Catch the bug
|
||||
### Part C: Catch the bug
|
||||
|
||||
5. Run the suite:
|
||||
|
||||
@@ -298,12 +298,12 @@ Do this once yourself so the tool isn't magic. From inside your working copy of
|
||||
return len(self.pending())
|
||||
```
|
||||
|
||||
Re-run `python -m unittest -v` — green. Confirm the app agrees:
|
||||
Re-run `python -m unittest -v`; green. Confirm the app agrees:
|
||||
`python cli.py add a && python cli.py add b && python cli.py done 0 && python cli.py count`
|
||||
should report **1 task(s) pending**.
|
||||
|
||||
> Using your own app from earlier modules instead? If your `count` command was already correct,
|
||||
> don't skip the lesson — *plant* the bug to feel it: temporarily change your pending-count logic
|
||||
> don't skip the lesson; *plant* the bug to feel it: temporarily change your pending-count logic
|
||||
> to `len(self.tasks)`, confirm an intent-encoding test goes red, then fix it. The muscle is
|
||||
> "write the test that would have caught this," and you build it by watching it catch something.
|
||||
|
||||
@@ -327,7 +327,7 @@ against it *after* you've written your own.
|
||||
The honest limits, because a green suite invites overconfidence:
|
||||
|
||||
- **Passing tests prove presence, not absence.** A green run means the behaviors you *wrote tests
|
||||
for* work. It says nothing about the behaviors you didn't think to test — which, with AI-written
|
||||
for* work. It says nothing about the behaviors you didn't think to test, which, with AI-written
|
||||
code, includes the edge cases the model also didn't think about. Tests narrow risk; they don't
|
||||
eliminate it. "All tests pass" is not "the code is correct."
|
||||
- **Tests written from the implementation are worse than no tests.** A suite that locks in current
|
||||
@@ -357,10 +357,10 @@ The honest limits, because a green suite invites overconfidence:
|
||||
- You watched an intent-encoding test **fail**, traced it to the real `pending_count` bug, fixed the
|
||||
*code*, and watched it pass.
|
||||
- You can articulate, in your own words, the difference between a test that asserts current behavior
|
||||
(a tautology that can't fail) and one that encodes intent (one that can) — and why the second is
|
||||
(a tautology that can't fail) and one that encodes intent (one that can), and why the second is
|
||||
the only kind worth having for AI-written code.
|
||||
- You have a committed `test_tasks.py` in the repo, ready for Module 14 to run automatically on every
|
||||
push.
|
||||
|
||||
If a test that can't possibly fail now reads to you as obviously useless, you've got the core idea —
|
||||
If a test that can't possibly fail now reads to you as obviously useless, you've got the core idea,
|
||||
and you're ready for **Module 14**, where these tests stop depending on you remembering to run them.
|
||||
|
||||
Reference in New Issue
Block a user