Chapter 06 · Week 6

Test, Integrate, and Cut

Why does an honest builder look for his own faults?

Chapter 6 — Test, Integrate, and Cut

“Program testing can be used to show the presence of bugs, but never to show their absence.” — Edsger W. Dijkstra, Notes on Structured Programming (1970)

“Search me, O God, and know my heart! Try me and know my thoughts! And see if there be any grievous way in me, and lead me in the way everlasting!” — Psalm 139:23–24 (ESV)


Why This Matters

Last week you built the core. It works — on your machine, with your data, when you drive it the way you meant it to be driven. That is not the same as working, and this is the week you find out the difference.

This week you wear four hats in one twenty-hour block, and you wear them in this order: tester (make it fail), integrator (make the pieces be one system), change control board (decide what still gets in), and release engineer (freeze it and tag it). In the long edition of this course those are two separate weeks. Here they are one, because the arithmetic is what it is: you are six weeks into eight, you have burned about 120 of your 160 hours, and the 40 that remain are already spoken for — twenty for documentation, deployment, and handoff in Week 7, twenty for delivery in Week 8.

Read that again, because it is the whole shape of the week. After Friday, you have almost no discretionary build hours left. Whatever you write down on Thursday — the slice Week 5 left open, if there is one, and the fix queue under it — is very close to the last construction work this project ever gets. That is not a threat; it is a planning fact, and it is why the chapter ends with the word cut rather than the word finish.

Where you are in the life cycle: verification, reaching forward into release. Verification asks did I build the thing right — does the code satisfy the specification I wrote in Week 2? Validation asks did I build the right thing. Both are your job, both produce artifacts, and every one of those artifacts is a line on the Week-8 rubric that carries 50% of your grade. There is no reconstructing a defect log from memory in Week 8, and no faking a coverage matrix at midnight.

Here is the sentence that should make you uncomfortable. Right now, in your repository, there are defects you already suspect. The edge case you skipped because the demo did not need it. The error path that returns a 200 with an empty body. The date arithmetic you know is wrong at a boundary but that has never come up. You will not find those with a debugger. You will find them by opening docs/requirements.md, reading what you promised, and trying — one promise at a time — to prove you did not keep it.

The AI thread runs hot from both directions. As a tool, an assistant is genuinely excellent at generating test cases and edge cases from a requirement — one of its strongest uses in this whole course, and fast in exactly the way an eight-week term needs. It is also going to assert the wrong thing with total confidence and perfect syntax. As a workload, if your project calls a hosted model you now own a component that does not return the same answer twice, and a dependency that will not care that your presentation is at 2:15 on a Tuesday.

And underneath it all, the question this week actually turns on: why does an honest builder look for his own faults? Every instinct says to look away. The psalmist does the opposite — he asks to be searched. We will take that seriously in 6.11, because it is the difference between a tester and a person performing testing.


6.1 — The Test Plan: Scope, Levels, Environment, Exit Criteria

A test plan is a short document that answers five questions before you write a single assertion. Yours goes in docs/test-plan.md, it fits on one page, and the template is code/test-plan-lite.md.

  1. Scope — which requirements, at which build. Name the commit SHA. “The system” is not a scope.
  2. Out of scope — what you are deliberately not testing, and why. This is the section that separates a professional from a student who ran out of time: it is the only place you get to be honest about your limits in advance rather than apologizing afterward.
  3. Levels — unit, integration, acceptance: what runs at each level, by what command, roughly how many.
  4. Environment — runtime version, OS, database, third-party services (real, sandboxed, or stubbed), and the data set.
  5. Exit criteria — when you are allowed to say done.

The outline descends from IEEE 829, the old standard for software test documentation; the ISO/IEC/IEEE 29119 family has since taken over that ground. You need neither to pass this course — but when an employer hands you a test plan template in your first job, that is the lineage, and you will recognize every heading. Exit criteria are the whole point of the document. So here is the bad one next to the good one.

BAD EXIT CRITERIA
Testing is complete when the application is stable and the major
features work as expected with no significant bugs remaining.

GOOD EXIT CRITERIA
Verification is complete when ALL of the following hold at the tagged build:
  1. 100% of Must requirements have >= 1 passing acceptance test
     (per docs/traceability.csv).
  2. Zero open defects at severity S1 or S2.
  3. Every open S3/S4 defect is in docs/defect-log.md with a stated workaround.
  4. Every defect fixed this week has a named regression test that fails
     against the pre-fix commit.
  5. The full suite runs from `./script/test` and passes in CI on a clean clone.
  6. p95 for the dashboard route is <= 800 ms on the `large` seed set (NFR-02).

The bad version has four undefined words — stable, major, as expected, significant — and every one of them will be defined, on Thursday night, by a tired person who wants to go to bed. That is not a criterion. That is permission. In the good version you can be wrong about criterion 6; you cannot be vague about it. Every line is checkable by a stranger without asking you a question, which is exactly the test your Week-8 grader applies. Write them on Monday, before you know how the week is going. In a sixteen-week course you might get away with Wednesday. You have twenty hours. Monday.

Coach’s Note — In the long edition this plan runs to nine sections. Yours runs to five and fits on a page, and it is not a lesser document — it is the same document with the ceremony removed. Compression is not permission to skip a section; it is permission to write each one in three sentences instead of three paragraphs. The exit criteria do not compress. They are the load-bearing wall.


6.2 — From Acceptance Criteria to Test Cases, One to One

This is the load-bearing idea of the week, and it is why Week 2 made you write acceptance criteria at all.

Every acceptance criterion becomes at least one test case. Every test case names the criterion it verifies. If a criterion has no test, you have not verified that requirement — no matter what your coverage tool says.

That mapping is your traceability matrix. A working sample lives at code/traceability-matrix.csv; yours goes in docs/traceability.csv.

Here is a bad test case and a good one, for the same criterion — from PantryPilot, brief #1 in the Fast-Start Catalog (Appendix B). The requirement: FR-07 — the system shall list every pantry item whose expiry date falls within the next 3 days, inclusive of today, with FR-07-AC2: an item expiring today appears in "Expiring soon".

BAD TEST CASE
TC-021: Test the expiry warning.
  Steps: Add some items. Look at the dashboard.
  Expected: The right items are shown.

GOOD TEST CASE
TC-021  Verifies: FR-07-AC2      Level: unit      Data: seed set `small`
Preconditions: system date fixed at 2026-11-06 (clock injected, not the OS clock).
Steps:  1. Insert item "milk", expiry = 2026-11-06.
        2. Call expiring_soon(window_days=3).
Expected: the returned list contains exactly one item, name == "milk".
Fails if: the list is empty (off-by-one: the window starts tomorrow).

Nobody but the author can run the bad one, and the author cannot run it twice the same way. “The right items” is a memory, not an expectation. Four things made the good one good: it names the criterion it verifies, it pins the clock so it cannot pass in November and fail in December, it states the expected result as an observable value, and it names the specific failure it is hunting. A test that does not know what bug it is looking for usually is not looking.

Boundaries are where the bugs live. For any requirement with a number, a date, or a length in it, partition the input space into classes that should behave the same, then test the edges of each class — because the edge is where the < should have been a <=.

Partition for FR-07 (window = 3 days, inclusive)RepresentativeBoundary values worth a test
Already expired−5 days−1 day
In the window+2 days0 days (today), +3 days
Outside the window+10 days+4 days
Absentno expiry date setnull

Five boundary tests for one requirement. With three or four Must features — which is what 160 hours buys, and we will keep saying so — that is fifteen to twenty boundary tests, and they are the highest-yield assertions in your whole suite.

Levels, at this scale. A defensible shape here is roughly 20–40 unit tests, 3–8 integration tests, and one acceptance test per Must requirement. Not sacred numbers — just the shape you should have to argue your way out of. TraceLens, the contrasting example, is a command-line log analyzer with no UI and no auth, so its acceptance tests are golden-file tests: fixed input log, stdout compared byte-for-byte against a committed expected file. Same discipline, squatter pyramid. Write your project’s shape into the Levels section so the grader reads the shape you actually built.


6.3 — Verified-Requirement Coverage Beats Line Coverage

Line coverage tells you which lines a test touched. It does not tell you whether the test asserted anything true — you can reach 90% line coverage with a suite that calls every function and asserts nothing. Verified-requirement coverage cannot be gamed that way, because each point of it is a promise you kept: the fraction of your requirements whose every acceptance criterion has a passing test. Report that number in your milestone; report line coverage second, if at all.

python3 coverage_report.py                       # the sample matrix beside it
python3 coverage_report.py docs/traceability.csv # yours

code/coverage_report.py computes the number, and then does the more useful thing: it sorts your gaps into three buckets.

BucketWhat it meansYour move this week
No test case at allAn unverified promiseWrite the test, even if it fails
Tested and failingA real defectOpen a defect report (6.4)
Test exists, never runAn intention, not coverageRun it, or delete it and say so

That last bucket is the one that quietly lies to people: a test that has never executed is not coverage. Expect your first number to be low — 30% to 60% is normal on the first run, and it is not a judgment of you, it is a measurement you have never taken before. The script exits non-zero when a Must requirement is unverified, so you can wire it into script/test the moment you are willing to be held to it.

Coach’s Note — In an eight-week term, one week is one-eighth of the course. Falling a week behind is proportionally twice as expensive as it would be in the long edition, and this is the week where “behind” stops being a feeling and becomes a number you can read off a script. If your verified-requirement coverage on Tuesday is under 30%, that is not a testing problem. That is a scope problem, and 6.7 is where you fix it.


6.4 — Writing a Defect Report Someone Else Could Act On

A defect report is a message to a stranger — a grader, a future employer reading your public repository, or you, in Week 8, at eleven at night, with no memory of what you meant. The bar: someone who has never seen your project should be able to reproduce the defect from the report alone. Seven fields, every time, laid out in code/cut-kit.md: ID and title, environment, steps to reproduce, expected vs. actual, evidence, severity and priority, traceability and status.

BAD:  Expiry thing is broken. Doesn't work right sometimes. Fix before demo.

GOOD: DEF-014 — Expiry warning misses items expiring today
Environment: commit a1b2c3d, macOS 15, seed set `small`, clock pinned 2026-11-06
Requirement: FR-07 (FR-07-AC2)      Test case: TC-021
Steps: 1. Load seed set `small`.  2. Add item "milk", expiry = today.
       3. Open the dashboard.
Expected: "milk" listed under "Expiring soon" (FR-07-AC2 — inclusive of today).
Actual:   "Expiring soon" is empty; "milk" appears only under "All items".
Evidence: TC-021 fails. Log: `expiry_window: start=2026-11-07 end=2026-11-09`
          — the window starts tomorrow, not today.
Severity: S2 — a Must requirement is unmet; the feature's whole purpose fails.
Priority: P1 — on the demo path; suspected one-line off-by-one.
Status:   Open. Owner: <you>.

Of the bad one, ask: what build? Broken how? Which items? What did you expect instead? “Sometimes” is the most expensive word in a bug tracker, because it means the reporter never found the condition and the next person starts from zero. The good report costs four extra minutes and does something the bad one cannot: it narrows the bug. Writing expected-versus-actual in the observable world, with the log line beside it, is frequently the moment you realize you already know the fix.

Regression discipline, in one rule. When you fix a defect: write the failing test first, then fix, then prove the test was ever red.

git commit -m "test: TC-021b reproduces DEF-014 (expiry window excludes today)"
git commit -m "fix(FR-07): make expiry window inclusive of today — closes DEF-014"
git checkout HEAD~1 -- src/   # pre-fix source, new test still in place
./script/test                 # expect RED. Green here means the test never tested the bug.
git checkout HEAD -- src/     # restore the fix; expect GREEN

That third step is the one people skip, and it is the one that catches the embarrassing case where your new test passes against the broken code too.

Coach’s Note — Log the defect before you fix it, even when the fix is thirty seconds away. The log is graded, and more importantly it is the only evidence you found anything. A repository with an empty defect log tells a reviewer one of two things: the software is flawless, or the testing was. They will not guess in your favor.


6.5 — Severity Is Not Priority: Triage Against the Hours You Have Left

Students collapse these into one number and then make bad calls all week. Severity is a fact about the product: how much damage does this do to a user? Priority is a decision about the calendar: when do we fix it, given what remains? In industry a tester sets severity and an engineering lead sets priority. Here you set both, so state which hat you were wearing on each line.

P1 — fix nowP2 — this weekP3 — if hours remainP4 — will not fix
S1 data loss / security / won’t runAlways. Stop what you are doing.Never
S2 Must requirement unmetIf on the critical pathDefaultOnly with a documented workaroundRequires a written scope cut
S3 partly met, workaround existsRarelyIf cheapDefaultAcceptable, if logged
S4 cosmeticOnly if it is in the demo’s first 30 secondsRarelyDefaultCommon and fine

The off-diagonal cases are the ones that teach the distinction. High severity, low priority: the admin export crashes on data sets over 100,000 rows — S1, it is a crash, but P3, because no user in your scope has more than 800 rows and you will say so in the log. You ship it, documented. Low severity, high priority: the project’s name is misspelled on the landing page — S4, nothing is broken, but P1, because it is the first thing your grader sees and the fix takes ten seconds.

Now the accelerated part, and it is the number that makes this triage different from every other one you have done. Count your actual remaining build hours before you order the queue. Six hours left in Week 6 after the acceptance pass, plus a four-hour defect reserve you protect inside Week 7 — because Week 7’s other sixteen hours are documentation, deployment, handoff, and the clean-machine test, and all twenty of Week 8’s are the final package, the presentation, and delivery. Ten hours. That is the budget the slice work you carried out of Week 5 and the fix queue have to share, and it is why triage this week is a genuine engineering decision rather than a formality.

Carried construction comes off the top, before any defect. Week 5 asked for two slices, three only if the third was genuinely small, and this course sizes you to three or four Musts — so a good number of you arrive on Monday of Week 6 with one Must slice still open. It has the first claim on the ten hours, because an unbuilt Must is not something you can ship with a workaround and a note. It is a promise with nothing behind it, and the largest single line on the Week-8 rubric asks whether the release delivers every Must requirement. A carried Should gets no such claim, and do not give it one: it goes to 6.7 and gets cut, which is the entire purpose of the Should bucket. So subtract the Must before you order anything else, and price it honestly. A slice that is nearly closed — one path left, a validation rule, an error state, the test that never got written — costs two or three hours, and the queue keeps the rest. A slice that was never started costs what a whole vertical slice costs, seven to nine hours in a stack you know and more in one you do not, which does not fit inside ten and will not start fitting on Thursday. That is not a triage problem; it is a scope cut, and 6.7 is where you write it down — this week, while a cut is still a decision, rather than in Week 7, where it is an apology.

BAD TRIAGE — everything urgent, so nothing is
DEF-004  Sev: High   Pri: High   "Add item validation broken"
DEF-011  Sev: High   Pri: High   "Accessibility"
DEF-014  Sev: High   Pri: High   "Expiry off by one"

GOOD TRIAGE — against 10 remaining build hours, 2.5 of them already owed
CARRIED  FR-05 (Must) "mark an item used or thrown out" — store and list
                  done in Week 5, the confirm step and its test are not.
                  ~2.5 h to the Week-5 definition of done.       -> close first
                     ---- 7.5 h left for defects ----
DEF-014  S2 / P1  Must requirement FR-07 unmet; on the demo path; ~1 h.  -> fix now
DEF-011  S2 / P2  NFR-05: item form inputs have no labels; screen reader
                  unusable. Real user harm, and a Must NFR. ~2 h.       -> fix this week
DEF-004  S2 / P2  FR-03 rejects valid units ("dozen"); workaround: type
                  "12". ~1.5 h.                                          -> fix this week
DEF-022  S3 / P3  Sort is unstable for equal expiry dates; cosmetic.     -> reserve
DEF-025  S4 / P4  Footer year hard-coded. Will not fix in v1.0; logged.  -> ship

Each line carries a reason and an estimate, so a stranger — and your Friday self — can audit the judgment rather than just the outcome. Notice that the carried slice sits above the queue rather than inside it, and carries no severity: it is not a defect, it is unbuilt work, and it is the one line here that no workaround can rescue. Two and a half hours of construction and four and a half of fixes — seven committed, three in reserve. That is a plan. A defect that is S1/P1 needs no decision; everything else does, and the decision is worth more than the fix, because it depends on who your users are, what your demo shows, and how many hours you actually have. No assistant can make it for you.


6.6 — Integration, Performance Against Your Own Targets, and the Security Pass

Your unit tests proved each box works given the inputs you imagined. Integration proves the boxes agree about what those inputs are.

Open docs/architecture.md and find the component diagram from Week 3. Every line between two boxes is a seam. Seams fail in a small number of predictable ways, and you should go hunting for all of them on purpose.

Seam failureWhat it looks like
Contract driftOne side changed a field name, type, or nullability; the caller silently reads undefined/None
Unit mismatchGrams vs. ounces, seconds vs. milliseconds, cents vs. dollars — plausible numbers, so nobody notices
Time zone / encodingUTC vs. local, naive vs. aware, UTF-8 vs. whatever; off-by-one-day bugs, mojibake in names
IdentityTwo modules disagree about the primary key; duplicate rows, orphaned records
OrderingB assumes A already ran; works on your machine because your machine is slow enough
Error semanticsA returns null on failure; B treats null as “no results”; failures render as empty states forever
Partial failureA wrote, B did not, nobody rolled back; an invariant you thought was enforced is not

You do not have time to test every seam. Test the Tier 1 ones: a seam that crosses a process, network, or storage boundary and sits on an acceptance path. For a project this size that is usually two or three. For PantryPilot they are add-item → barcode lookup API → item store; item store → expiry calculator → dashboard; and suggestion engine → recipe model → dashboard. TraceLens has one that matters and it is brutal: parser → anomaly detector, where the parser’s “unknown line” representation and the detector’s assumptions about record completeness must agree, or the tool silently reports zero anomalies on a corrupt file.

Performance, measured rather than felt. In Week 2 you wrote non-functional requirements with numbers in them. Go read them; most students find the targets still there and never once measured. “It feels fast” is not a measurement, and you are the worst possible judge — local data, warm cache, three rows, the machine where you built it.

IDTargetMethodp50p95VerdictAction
NFR-02Dashboard < 800 ms p9530 reqs, deployed, large seed0.41 s2.30 sMISSindex on expires_at; re-measured 0.38 / 0.72 s — PASS
NFR-08Add item < 1.5 s p9530 reqs, deployed0.22 s0.51 sPASSnone
NFR-09Cold start < 10 s5 restarts9.4 s14.2 sMISSaccepted; the hosting tier idles the instance. CR-009, runbook note added

Report p95, not the average — averages hide exactly the tail your users complain about. Measure before you optimize; spending four of your twenty hours making the already-fast thing faster is a real and common failure. Fix at most one, re-measure, record both numbers. A target you missed and documented costs you far less than one you quietly deleted.

The security pass, timeboxed to ninety minutes. You are not becoming a security engineer this week. You are doing a structured pass, writing down what you found, and fixing what you can. Four areas, in the order that finds the most per minute: secrets (search tracked files and history — deleting a key in a later commit does not remove it); authorization (log in as user B and request user A’s object by ID, directly, not through the interface — hiding the button is not authorization, and this is the most common serious defect in student capstones); input (parameterized queries, never string concatenation, for anything reaching a database, a shell, a file path, or a template); and dependencies.

git grep -nIE '(AKIA[0-9A-Z]{16}|BEGIN [A-Z ]*PRIVATE KEY)'
git ls-files --error-unmatch .env 2>/dev/null && echo "TRACKED - fix this"

If you find a live credential, rotate it at the provider first. Rotation is the fix; cleaning the history is housekeeping you do afterward. For dependencies: run your ecosystem’s audit command, verify each advisory identifier at https://github.com/advisories or https://osv.dev/ — an assistant’s advisory ID is a claim, not proof, and fabricated identifiers are a routine failure mode — and record whether the vulnerable path is actually reachable from your code. A critical advisory in a path you never call is a different decision than a medium one in your login flow. Anything you cannot fix becomes a defect with a severity and a disposition; a known, documented weakness is a professional posture, an unknown one is just unknown.

All three of these records — the run, the performance table, the security pass — go into docs/test-results.md as three sections. The long edition gives each its own file. You have twenty hours and a grader who wants one place to look.

Coach’s Note — Graceful degradation is the cheapest way a capstone can visibly out-engineer a professional product. Pick your riskiest dependency, give the call a timeout, add a fallback path you control, and write an honest message for the failure state. Then demo it by breaking the dependency on purpose in front of the room. It costs an afternoon and it makes your demo unkillable — which matters, because on delivery day the network belongs to someone else.


6.7 — Change Control: Accept, Defer, or Refuse — in Writing

At the Week-4 design review your specification was baselined. That does not mean it cannot change. It means changes are decided, not absorbed. And the requests have been arriving. Your advisor has a suggestion. Someone at a demo says “what if it also…”. A defect turns out not to be a defect at all but a feature you never specified. Every one of those is a change request, and the heart of change control is an impact analysis that fits in three lines.

  1. Cost — build hours + test hours + doc hours, against the hours you actually have left.
  2. Blast radius — which requirement IDs, modules, interfaces, tests, and documents change.
  3. Risk — what accepting endangers, and what refusing gives up. Both directions. A one-directional risk line is advocacy, not analysis.

Then one of three dispositions: ACCEPT into this release, DEFER with a named destination and a size, or REJECT with a reason a stranger would accept. Every request gets its own file in docs/change-requests/CR-014-shared-shopping-list.md — and one row in the change table at the top of CHANGELOG.md. The folder is the record; the table is the index; a grader reads both. The template and two worked examples are in code/cut-kit.md.

Bad — the whole of docs/change-requests/CR-014-shopping-list.md: “Advisor suggested a shared shopping list. Shouldn’t be too bad. Will try to get to it.”

Good — the same request, as docs/change-requests/CR-014-shared-shopping-list.md:

CR-014 — Advisor asks for a shared household shopping list Cost: 9 build + 4 test + 1 doc = 14 h; 10 discretionary build hours remain in the entire project. Blast radius: new requirement; touches Pantry, Household, and Auth; one new endpoint, one new table with a migration; invalidates 6 integration tests; changes docs/architecture.md and README.md. Risk: accept → the documentation and deployment week is gone and the project does not ship; refuse → the demo shows single-user lists, which is what FR-09 always promised. Disposition: DEFER → backlog item B-07 (~14 h, post-handoff, needs acceptance criteria first). Decided by M. Ruiz, 2026-11-04.

The bad version is not bad because it is short. It is bad because it contains no facts, no decision, and — fatally — “will try to get to it,” which your advisor will read as yes. The good version lets somebody disagree with your decision without disputing your facts. That is what an analysis is for, and it took six minutes to write.

Now the cut. You are going to cut something. Nearly every accelerated capstone does, and the ones that cut in Week 6 finish while the ones that cut in Week 8 do not. Cutting scope is not failure. Cutting scope silently is. And if you are cutting, you are not failing the two-thirds rule — you are finally obeying it: 160 hours buys a project about two-thirds the size of a sixteen-week capstone, roughly three to four Must features, and a scope that fit in Week 2 on paper often turns out in Week 6 to have been a sixteen-week scope wearing an eight-week label. Re-sort your remaining requirements against the ten hours, not against your ambitions. Two cuts are usually right — the second, less-used path through a feature you have already shipped once (ship the one path completely), and the admin or configuration screen (document the manual procedure in the runbook instead; nobody grades a settings screen, everybody grades a runbook). One is usually wrong: cutting the tests, the documentation, or the deployment. Those are the bulk of your grade and 100% of whether the thing is real. Cut features to protect them.

A cut is a change request like any other, so it gets its own file with four things in it: what, why, where it went, and what the reader must know. Then — the part students skip — mark it Deferred in docs/requirements.md with a date and the CR reference. Do not delete the row. A grader comparing your Week-4 baseline to your Week-8 delivery will find the gap, and a deleted row reads exactly like “I hoped you wouldn’t notice.”

Coach’s Note — This edition has one signature failure mode, and it shows up here more than anywhere else: you adapted a brief from the Fast-Start Catalog and one of the requirements you inherited still quietly describes somebody else’s project. You will meet it this week as a test case you cannot write, or an acceptance criterion nothing in your code even addresses. That is not a defect in your software. It is a defect in your specification, and the honest move is a change request that says so — REJECT — inherited from the brief, never in scope for this adaptation, requirement withdrawn — rather than an untested row you leave in the matrix and hope nobody reads.


6.8 — Shipping With Known Defects, Honestly Recorded

You will not ship zero defects. Nobody does. Dijkstra’s line at the top of this chapter is not pessimism; it is arithmetic — testing samples an effectively infinite input space. So “quality” cannot mean “no bugs.” Quality means: every promise you wrote down is verified, and every defect you know about is written down, classified, and decided. Shipping with known defects is not a failure. Shipping with unrecorded defects is.

A release candidate is a specific claim: this build is what I intend to ship unless something disqualifying is found. It is a tagged, immutable point in your history, not a mood. The gate before you tag is a handful of commands, so you do not need a script for it — clean tree, clone, set up, test — and then an annotated tag, using Semantic Versioning (https://semver.org/) with a pre-release suffix, so it carries an author, a date, and a message:

git status --porcelain         # must be empty; an RC is built from a committed state
rm -rf /tmp/rc-check && git clone . /tmp/rc-check
(cd /tmp/rc-check && ./script/setup && ./script/test)
git tag -a v0.9.0-rc.1 -m "Release candidate 1: tested increment, Milestone 6"
git push origin v0.9.0-rc.1

That clone is the point. Not your working copy, which has six weeks of undeclared state in it — a clone, in a directory that has never seen your project. It is designed to fail the first time, and failing it now, in private, for twenty minutes, is the whole reason it exists. It is the small version of the clean-machine test you will run properly in Chapter 7. Finally, CHANGELOG.md — the “Keep a Changelog” convention (https://keepachangelog.com/), newest first, grouped under Added / Changed / Fixed / Security, with the CR change table at the top and a Known issues section at the bottom.

## [0.9.0-rc.1] - 2026-11-07
### Changed
- Barcode lookup now times out after 4 seconds and falls back to manual entry
  instead of blocking the add-item form (FR-11).
### Fixed
- The "Expiring soon" panel now includes items expiring today (FR-07, DEF-014).
### Known issues
- DEF-022 (S3): items with identical expiry dates sort inconsistently between
  page loads. Workaround: sort by name. Deferred to backlog B-09.
- DEF-025 (S4): footer copyright year is hard-coded.

Three lines of known issues cost you nothing and buy enormous credibility, because they prove you knew your own system well enough to find its edges. A release that claims everything works, with a defect log containing three entries all marked “fixed,” is a release nobody experienced believes. Never paste git log output into a change log: commits record typing, a change log records decisions.


6.9 — The Assistant: Generated Tests, and the Component That Will Not Sit Still

Generating test cases from a requirement is one of the highest-yield uses of an assistant in this entire course, and in a twenty-hour week it is close to essential. Use it, then audit it. Prompt it with the requirement, not the code. That is the whole trick:

Here is requirement FR-07 and its acceptance criteria, verbatim: <paste>.
Do NOT look at my implementation.
1. List the equivalence partitions and the boundary values for this requirement.
2. For each acceptance criterion, write one test case: preconditions, steps,
   expected result, and the specific bug it would catch.
3. List three edge cases my acceptance criteria do not currently cover, and say
   what the requirement should say about each.

Step 3 is worth the whole exercise: roughly a third of the time it surfaces a genuine hole in your requirement — a Week-2 defect discovered in Week 6, cheap now and expensive later. Log it and take it to change control. Now the failure modes, in the order they will bite you. The change detector: show the model your code and it writes tests asserting what the code does — if the code has an off-by-one, the generated test asserts the off-by-one, confidently, under a name like test_expiry_window_correct, and now your suite defends the bug. The empty assertion: a call wrapped in a try/except that asserts nothing meaningful; line coverage rises, verification does not. Invented API: methods that do not exist and fixtures you never defined — cheap to catch, but it eats minutes you do not have. Confident wrong oracles: an expected value derived from the sound of the requirement rather than its text. For FR-07 it may cheerfully assert that “within 3 days” excludes today. Only your specification decides that.

The rule for the week: an assistant may propose test cases; only you may approve an assertion. Log the use in docs/ai-usage.md — what you asked, what it produced, what you verified, what you changed.

The workload side: testing what does not sit still. If your project calls a hosted model, you own a component that returns a different answer to the same input. You cannot assert equality. You can assert four other things, all deterministic: structure (the output parses, the required keys exist, servings is an integer between 1 and 12, every ingredient named appears in the pantry list you passed in); properties (an invariant true of every valid output — no suggestion ever names an ingredient the household does not have); a golden set with a committed tolerance (ten to fifteen fixed inputs and a threshold written into the test plan: “at least 12 of 15 produce a valid, in-pantry recipe”); and failure behavior, which is the one that saves your demo.

Injected conditionRequired behavior
Provider returns HTTP 429Back off, retry once, then a clear user-facing message — not a stack trace
Provider times outTime out at your budget, degrade to the non-AI path
Provider returns malformed outputTyped parse error, logged, graceful fallback
API key missing or invalidFail at startup with a readable message, not on first user click
Provider is fully downThe rest of the application still works

All five are tests against a stub. Write them this week. Never let your automated suite make live paid API calls — stub by default, and put the small number of real-provider tests behind an explicit flag you run by hand. Provider pricing, rate limits, and free tiers change constantly; read your provider’s current terms rather than a number you remember, and set a spending alert if yours offers one.


6.10 — Interactive Lab: The Coverage & Cut Planner

Below this chapter on the website is The Coverage & Cut Planner. Do it before you touch your own matrix — twenty minutes, and it will change how you fill in both halves of this week. The first panel is coverage. Map your requirement identifiers to test cases and mark each one pass, fail, or not run. It renders the matrix and then does the thing a spreadsheet will not do for you: it highlights every Must requirement with no test at all and every acceptance criterion with no matching assertion, and it computes verified-requirement coverage live. Watch what happens to that number when you mark a test “not run.” It drops, as it should. That is the honesty a line-coverage tool cannot give you.

The second panel is the cut planner, and it is where this edition differs from the long one. Feed it your incoming defects and late change requests; each gets a severity, a priority, and an hour cost. Then give it the hours you actually have left — the real number from 6.5: what remains of this week plus the four-hour Week-7 reserve and nothing else, less whatever you still owe on a slice carried out of Week 5. The widget orders the fix queue against that budget and shows you exactly where the line falls: what gets fixed, what ships as a known open defect, and which late request has to be deferred or refused. Then it drafts the change-log entries that record the decision, so the paperwork is a by-product of the thinking rather than a chore after it.

Run it twice — once with a fantasy budget of thirty hours, once with ten. The gap between those two queues, item for item, is the exact content of what you are about to cut. Better to see it on a screen on Monday than discover it on Thursday night.


6.11 — Why Does an Honest Builder Look for His Own Faults?

“Search me, O God, and know my heart! Try me and know my thoughts! And see if there be any grievous way in me, and lead me in the way everlasting!” (Psalm 139:23–24, ESV)

Read that as a tester and it is startling. The psalmist does not ask to be told he is fine. He asks to be searched, tried, and shown the grievous way. He is requesting the defect report. Volunteering for the audit. And he does it while already knowing — the whole psalm is about this — that he is completely known already. There is nothing to hide, so the only reasonable posture is to stop hiding.

That is the psychological engine of good testing, and it is why testing your own code is harder than testing someone else’s. The natural instinct is self-protection: run the happy path, see it work, feel good, move on. The instinct is not stupidity — it is the same instinct that makes us defensive when a friend tells us something true. Jeremiah is blunt about it: “The heart is deceitful above all things, and desperately sick; who can understand it?” (Jeremiah 17:9, ESV). You are not a neutral observer of your own work. You have a stake — and a stake, in testing terms, is a bias in the measuring instrument.

The Christian answer to that bias is not “try harder to be objective.” It is ask to be searched. Psalm 19:12 names the problem exactly: “Who can discern his errors? Declare me innocent from hidden faults” (ESV). The faults that matter most are the ones you cannot see, which means no amount of sincerity will find them. You need something outside yourself. A test suite is a small mechanical version of that — claims you wrote when you were being honest, run by a machine with no stake in the answer, reporting whether the thing you built matches the thing you promised. It cannot be flattered. That is its entire value. And the faults you decline to look for do not disappear; they get transferred — to the user who loses the food she thought PantryPilot was tracking, to the successor who inherits the off-by-one at two in the morning. To decline to look is a decision that someone else will pay.

There is a temptation particular to a compressed term, and it is worth naming. In eight weeks the pressure is not primarily to lie about what works. It is to not check, and let the silence do the lying for you. An untested requirement makes no claim you can be caught on. That is precisely why the matrix has a “no test at all” bucket and why the milestone grades gaps rather than hiding them: the honest report of an unverified promise is worth more than a green badge over an unexamined one.

Then the cut, which is the other half of the week and has its own Scripture. You are going to refuse things — good ideas, from people whose opinion you want. “Let what you say be simply ‘Yes’ or ‘No’; anything more than this comes from evil” (Matthew 5:37, ESV) is a strange verse to find in a chapter about change control, and it is exactly on point. The offense is not refusal. It is the ambiguous answer — “I’ll try to fit that in” — comfortable now and deniable later, which transfers the whole cost of the confusion onto the person now planning around a commitment you never made. And Jesus, describing a builder: “For which of you, desiring to build a tower, does not first sit down and count the cost, whether he has enough to complete it?” (Luke 14:28, ESV). The unfinished tower there is not a monument to low ambition. It is a monument to uncounted cost — to eleven features at ninety percent.

Notice, finally, how the psalm ends. Not “search me and condemn me” but “lead me in the way everlasting.” The searching is for the leading. Your defect log is not a monument to your failures; it is the map of what to fix next. Your change-request folder is not a record of what you failed to build; it is evidence that you counted before you promised. An honest builder looks for his own faults because he intends to do something about them — and because a fault he refuses to see is one he has quietly decided to keep. That is why the log is graded. Not because we enjoy counting your bugs, but because a builder who can face his own work honestly is the only kind anyone should hire.


6.12 — Common Pitfalls

Pitfall: Testing the code instead of the requirement. Example: Reading expiring_soon() and writing a test that asserts exactly what the function currently returns — including the off-by-one. Fix: Derive every test case from the acceptance criterion text before you open the implementation file. If you have already read the code, have the assistant write the case from the requirement text alone.


Pitfall: Reporting line coverage as if it were verification. Example: “87% coverage” in the milestone, while a Must requirement has no test case at all. Fix: Report verified-requirement coverage from your matrix first. Run code/coverage_report.py. Line coverage is a diagnostic for finding untested code, not a claim that the software works.


Pitfall: Triaging against your ambitions instead of your remaining hours. Example: A nine-item P1 queue written on Thursday of Week 6, when ten discretionary build hours exist in the whole rest of the project. Fix: Write the hours number down first — what is left of this week plus your protected Week-7 reserve, minus any slice you carried out of Week 5 — then order the queue inside it. Everything that does not fit becomes a known open defect with a workaround, on purpose, in writing.


Pitfall: Keeping an inherited requirement that describes somebody else’s project. Example: A Fast-Start Catalog brief’s FR-06 sits in the matrix all term with no test, no code, and no acceptance criteria you ever adapted. Fix: Adapting is the accelerated skill. Withdraw it through change control with a one-line reason, mark it Deferred in docs/requirements.md with the CR reference, and say so in the change log. An honest withdrawal scores; a silent orphan does not.


Pitfall: Tests that depend on the current date, the network, or leftover database state. Example: The suite is green all week, then fails on the first of the month and again in the presentation room’s Wi-Fi. Fix: Inject the clock. Seed randomness. Stub the network by default. Recreate the database from checked-in seed data at the start of every run. If a test cannot run twice in a row with the same result, it is not a test yet.


Pitfall: Tagging a release candidate from a dirty working tree. Example: git tag v0.9.0-rc.1 with four uncommitted files that happen to be what makes it run. Fix: Clean tree, then a fresh clone into a temp directory that runs ./script/setup and ./script/test. If the clone fails, the tag would have been a lie.


6.13 — Where Your Hours Went This Week

Twenty hours, spent about like this. If yours looks very different, that is information — write it in docs/hours-log.csv with a sentence about why.

WorkHours
Test plan and exit criteria, written Monday before you look at the build1.5
Deriving test cases from acceptance criteria; building the traceability matrix2.5
Writing and automating unit and boundary tests; the suite green in CI3.0
Integration tests across your Tier 1 seams2.5
The first full acceptance pass, by hand, logging every defect as you find it2.0
Performance measured against your own NFRs; the ninety-minute security pass2.5
Closing the slice carried out of Week 5, to the Week-5 definition of done2.5
Triage, fixing the S1/S2 defects, regression test first every time1.5
Change control: CR files, the cut, CHANGELOG.md1.0
Release candidate: exit-criteria check, clean-clone gate, annotated tag0.5
Defect log, hours log, weekly quiz0.5
Total20.0

The number that surprises people is the 2.0 on the first acceptance pass. Running your own software deliberately, against a written script, trying to break it, is slow — and it is where most of your defects will come from. Do not let it get squeezed, and do not let integration eat it: if the seams cost you five hours instead of two and a half, cut a Should requirement rather than the acceptance pass.

The row to read twice is the carried slice, because it is the only construction line in the week and it is conditional. If Week 5 closed everything it promised, those two and a half hours go straight to the fix queue and you are genuinely ahead — say so in the log. If it did not, they are very nearly the last feature hours this project has, which is why 6.5 makes you subtract them before you order a single defect, and why a slice that needs seven gets cut in 6.7 instead of quietly overrunning the week.


6.14 — Reps

The reps are in the exercises, and this week they are not warmups. In order, they are the milestone. A preview: Rep 1 writes your scope, out-of-scope lines, and six exit criteria as checkable numbers, before you look at the build. Rep 3 builds the traceability matrix and runs code/coverage_report.py — write the real number down before you fix anything. Rep 5 is the first full acceptance pass, by hand, hunting on purpose. Rep 8 computes your remaining build hours, then triages ten defects to fit inside them. Rep 10 is three change requests, three three-line impact analyses, three dispositions — at least one REJECT and one DEFER.

Take the on-page Check Your Reps quiz when you finish the chapter. It is one of the eight that make up 15% of your grade, and it is the early-warning system for whether this week actually landed.


6.15 — This Week’s Milestone

Milestone 6Milestone 6: Tested Release Candidate, Defect Log & Change Log. A test plan with numeric exit criteria, a traceability matrix reporting honest verified-requirement coverage, an automated suite that runs from ./script/test and is green in CI, at least six honestly classified defects with regression tests named for every fix, a verification record carrying your performance and security results, one change-request file per request since the Week-4 baseline, a CHANGELOG.md with a Known-issues section, and an annotated release-candidate tag on a commit that clones clean.

Where it lands: docs/test-plan.md, docs/traceability.csv, docs/test-results.md, docs/defect-log.md, docs/change-requests/, CHANGELOG.md, tests/, and the tag itself.

Milestone 6 is graded twice — and by now you know exactly what that means. It scores in the 25% milestone bucket, and these artifacts are the Week-8 package that carries 50% of your grade. The full contract is in Appendix D. Producing them this week costs you twenty hours. Producing them in Week 8, alongside deployment, documentation, a handoff package, and a thirty-minute talk, costs you the grade.


6.16 — Coach’s Final Word

Something changes in a student the week they find their first real bug in their own code on purpose.

Up to that moment, testing feels like a tax — the thing you do after the fun part, to satisfy a rubric. Then you write the boundary case for a date window, watch it go red, and realize the software you were about to demo has been wrong since Week 5 in a way no amount of clicking around would have shown you. That is the moment testing stops being paperwork.

And then, an hour later, you do the harder half. You count the hours you actually have — ten, not thirty — and you write down what will not get fixed. In a sixteen-week course you can postpone that reckoning twice. Here you cannot: six weeks in, one week is one-eighth of your term, and the honest arithmetic is right in front of you. That arithmetic is not an insult to your ambition. It is the field you were given. The skill on trial is not coding — it is judgment under a finite budget, the rarest thing on any engineering team and the thing your first employer is actually buying. Anyone can say yes. Saying no, in writing, with a number, to somebody you respect, and then delivering exactly what you promised instead — that is the whole game.

So: search your own work. Write down what you find, all of it, in your own name. Fix what matters, decide about the rest in the open, tag the candidate, and ship the list. Then walk into Week 7 with something real to document instead of something you are still hoping to finish.

See you on Monday.


Up next: the exercises builds the release candidate rep by rep · Milestone 6 is Milestone 6 · then Chapter 7 — the README, the runbook, the handoff package, and the clean-machine test that proves any of it works. Reference: Appendix B (the Fast-Start Catalog — your brief’s what-to-cut-first list is written for exactly this week), Appendix C (the Document Kit), Appendix D (the grading contract), Appendix E (glossary: traceability, regression, release candidate, blast radius). Previous: Chapter 5.

Interactive Lab — Week 6
The Coverage & Cut Planner

End of Week 6 of eight. Forty of your 160 hours remain, and most are already owed. Panel 1 asks what you have actually verified, Panel 2 what you can still repair; Panel 3 turns the two into a ship-or-not decision and writes it down.

1 · Verified-requirement coverage

Six requirements from the PantryPilot brief: four Musts — about what 160 hours buys — and two Shoulds. Point each test at the requirement it should verify and set its result. A requirement is verified only when a test that actually asserts its acceptance criterion has been run and has passed. Two of these tests were generated from the requirement text by an assistant; both run green while asserting something else.

Requirement status

2 · The cut planner

Severity is how much damage it does: S1 critical (data loss, security exposure, or a Must feature with no workaround), S2 major, S3 minor, S4 trivial. Priority is when you will fix it: P1 now, P2 before this release, P3 later, P4 if time allows. Different questions, and allowed to disagree. Hours is your own estimate. The queue sorts by priority, then severity, then cost, and packs that order against the budget below.

12 h

Planning heuristic for this course: 40 hours remain — 20 in Week 7, 20 in Week 8. Roughly 28 are already owed to the documentation set, deployment and the clean-machine test, the tagged release and change log, and building and rehearsing the 30-minute presentation. That leaves about 12 uncommitted.

3 · Release gate and change log

Change-log entries this decision produces

 

Try: cover all six requirements the way a hurried reviewer would — T-02 for the pantry-list requirement, T-08 for the expiry requirement — and mark everything passing. One coverage number reads 100%; the other does not, and the other is the true one. Then give CR-007 priority P1 and leave the rest alone: a ten-hour change request no acceptance criterion ever asked for eats ten of your twelve repair hours, and three Must defects ship open behind it.

Check Your Reps

Week 6 Knowledge Check

Question 1 of 5
Monday of Week 6. This is the whole of section 5 of a classmate's docs/test-plan.md. What does Chapter 6 say happens to it on Thursday night?
Exit criteria:
  Testing is complete when the application is stable and the
  major features work as expected with no significant bugs
  remaining.
Why: Section 6.1. The good version is six numbered lines a stranger could check without asking you a question: a percentage of Must requirements with at least one passing acceptance test, zero open S1 or S2 defects, every open S3 or S4 in the defect log with a stated workaround, a named regression test for every fix, the suite passing from ./script/test in CI on a clean clone, and a p95 number lifted from an NFR. You can be wrong about one of those; you cannot be vague about it. Write them Monday, before you know how the week is going — criteria written on Friday are always, mysteriously, criteria the current build happens to meet.
Question 2 of 5
Thursday of Week 6. Here is your fix queue exactly as written. About 120 of your 160 hours are gone. What does Chapter 6 tell you to do first?
DEF-004  Sev: High   Pri: High   "Add item validation broken"
DEF-011  Sev: High   Pri: High   "Accessibility"
DEF-014  Sev: High   Pri: High   "Expiry off by one"
DEF-019  Sev: High   Pri: High   "Slow dashboard"
DEF-022  Sev: High   Pri: High   "Sort unstable"
Why: This is the chapter's own bad-triage example: everything urgent, so nothing is. Severity is a fact about the product — how much damage does this do to a user — and priority is a decision about the calendar, given what remains. The ten-hour number is what makes this triage a real engineering decision: six weeks in, the 40 remaining hours belong to documentation, deployment and handoff in Week 7 and to delivery in Week 8. Cutting the tests, the documentation or the deployment is the one cut section 6.7 calls usually wrong; you cut features to protect them.
Question 3 of 5
Four rows from docs/traceability.csv. Which row does Chapter 6 say "quietly lies to people," and what is the move?
FR-03,Must,FR-03-AC1,TC-008,unit,PASS,
FR-07,Must,FR-07-AC2,TC-021,unit,FAIL,DEF-014
FR-11,Must,FR-11-AC1,TC-030,integration,NOT RUN,
FR-16,Must,FR-16-AC1,,,,
Why: The three gap buckets in section 6.3 are no test case at all (an unverified promise — write the test, even if it fails), tested and failing (a real defect, which FR-07 already has as DEF-014), and test exists, never run. Only the last one pretends to be coverage. FR-16 is the opposite of choice four: gaps go in the matrix, visibly, because a grader would rather see an unverified requirement than a hidden one — and if FR-16 is a Fast-Start brief promise that never became your project, the honest move is to withdraw it through change control, mark it Deferred in the requirements with the CR reference, and never delete the row.
Question 4 of 5
A change request arrives: your advisor asks for a shared household shopping list. You price it at 9 build + 4 test + 1 doc hours, and about ten discretionary build hours remain in the entire project. Which response does Chapter 6 model?
Why: Fourteen hours against ten, so accepting means the documentation and deployment week is gone and the project does not ship. Section 6.7 requires a three-line impact analysis on every request — cost against the hours you actually have, blast radius across requirement ids, modules, interfaces, tests and documents, and risk in both directions, because a one-directional risk line is advocacy rather than analysis. "Later" is not a disposition; a named destination with a size is. And "will try to get to it" is the phrase the chapter calls fatal — your advisor will read it as yes.
Question 5 of 5
You are about to tag the release candidate and run this. What has just gone wrong, according to Chapter 6?
$ git status --porcelain
 M src/app.py
 M .env
?? seed/products.json

$ git tag -a v0.9.0-rc.1 -m "Release candidate 1"
Why: Section 6.8 and pitfall six: git status --porcelain must be empty, because a candidate tagged over four uncommitted files that happen to be what makes it run is a lie. Then the part students skip — the gate runs in a clone, in a directory that has never seen your project, not in a working copy carrying six weeks of undeclared state. It is designed to fail the first time, and failing it privately for twenty minutes is the whole reason it exists. Two details in this output are their own findings: .env shows as modified, which means it is tracked and should not be, and the seed fixture is untracked, which means your test data is not reproducible from the repository.
YOU FINISHED. NICE WORK.