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:
2026-06-22 23:21:09 -04:00
parent 513d7e7ac8
commit 389ac2e460
99 changed files with 1324 additions and 1315 deletions
+22 -22
View File
@@ -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.
@@ -1,16 +1,16 @@
# Demo app `tasks` (Module 13 copy)
# Demo app: `tasks` (Module 13 copy)
The same tiny task tracker from Modules 1 and 2, with one feature added: a `count` command backed
by `TaskList.pending_count()`. Use this copy for the Module 13 lab so everyone starts from the same
code including the same latent bug.
code, including the same latent bug.
If you already have a `tasks-app` from earlier modules, you can use that instead; just make sure it
has a `count` command (the Module 2 lab added one). The planted bug in this copy is there on purpose.
## Files
- `tasks.py` core logic (`Task`, `TaskList`), now with `pending_count()`.
- `cli.py` command-line front end. Adds `count`.
- `tasks.py`: core logic (`Task`, `TaskList`), now with `pending_count()`.
- `cli.py`: command-line front end. Adds `count`.
## Run it
@@ -22,4 +22,4 @@ python cli.py list
python cli.py count
```
Requires Python 3.10+. No third-party packages tests use the standard library `unittest`.
Requires Python 3.10+. No third-party packages; tests use the standard library `unittest`.
@@ -2,7 +2,7 @@
Same running example from Modules 1 and 2, carried forward. It has grown one feature since then:
a `pending_count()` helper that the AI added to back a `count` command. The feature "works" in
the obvious case which is exactly the kind of code this module teaches you to verify properly.
the obvious case, which is exactly the kind of code this module teaches you to verify properly.
"""
from dataclasses import dataclass, field