Testing the Seams
Every layer passes, and the light stays off
By now each part of her behaves. The transcriber turns a WAV into words. The brain answers. The voice model speaks. The memory store writes a fact and reads it back. The hardware layer toggles a pin and writes down what it expected before it did. Every one of those has been exercised on its own, and every one of them works on its own. Then you assemble the whole assistant, say "turn on the light," and the light stays off. No traceback. Nothing in the log marked ERROR. She answers you politely and the room stays dark.
Here is what actually happened, in exact detail, because most of the bugs left in this
project look like this one. The transcriber returned
" Turn on the light.\n": a leading space, because that is how the model
emits a segment; a capital T; a period; a newline. The command extractor looks its input
up in a dictionary keyed by lowercase phrases. Both halves are correct. Both halves would
pass any test written against them alone. The lookup returns None, a
None command means she treats the sentence as conversation, and the pin is
never touched. The defect is not in either module. It sits in the six inches between
them, where one module's output becomes another's input.
The obvious way to catch this is to talk to her and watch. That works exactly once, for exactly the sentence you happened to say, and it needs a human, a microphone and a quiet room every time you want to repeat it. So this chapter builds the alternative, and the rule it runs on is this: a seam test is the contract between two modules written as code — hand the first one this input and the second one must produce that output. When a seam test goes red it does not say "the assistant is broken." It names the handoff.
Pytest collects any class whose name starts with Test. A dataclass has a
generated __init__, and pytest will not collect a class that takes
constructor arguments; it prints PytestCollectionWarning: cannot collect test
class 'TestCase' because it has a __init__ constructor and moves on. The
warning scrolls past, the class is silently skipped, and one day you wonder why the
suite reports fewer tests than you wrote. SeamTest avoids the collision
and says what the object actually is.
One seam, then a runner that does not care which is which
# labs/integration_tests.py
from collections.abc import Callable
from dataclasses import dataclass
COMMANDS = {"turn on the light": "light.on", "turn off the light": "light.off"}
@dataclass
class SeamTest:
name: str
description: str
def run(self) -> tuple[bool, str]:
raise NotImplementedError(f"{type(self).__name__} must implement run()")
@dataclass
class HeardToCommand(SeamTest):
transcribe: Callable[[str], str]
extract: Callable[[str], str | None]
clip: str
expect: str
def run(self) -> tuple[bool, str]:
heard = self.transcribe(self.clip)
got = self.extract(heard)
if got != self.expect:
return False, f"heard {heard!r}, extractor gave {got!r}, expected {self.expect!r}"
return True, f"heard {heard!r}, extractor gave {got!r}"
def fake_transcribe(clip: str) -> str:
return " Turn on the light.\n"
def extract_command(heard: str) -> str | None:
return COMMANDS.get(heard)
if __name__ == "__main__":
seam = HeardToCommand(
"heard_to_command",
"a transcript becomes the command it names",
fake_transcribe,
extract_command,
"glados/data/captured_audio.wav",
"light.on",
)
passed, message = seam.run()
print(f" [{'PASS' if passed else 'FAIL'}] {seam.name}: {message}")
$ uv run python labs/integration_tests.py
[FAIL] heard_to_command: heard ' Turn on the light.\n', extractor gave None, expected 'light.on'
The first seam test written for this book finds a real bug in one second, and the
message contains the whole diagnosis: what came out of the first module, what the
second one made of it, what you said it should have made of it. Two details in the
code are doing quiet work. The transcriber arrives as a field, so the same test object
runs against a canned string here and against the real model later without a line
changing. And HeardToCommand is itself a dataclass, so it inherits the
base's fields and appends its own: the constructor takes name,
description, then the four fields declared here, in that order. The fix is
one line, COMMANDS.get(heard.strip().lower().rstrip(".")), and the same
run then reads:
$ uv run python labs/integration_tests.py
[PASS] heard_to_command: heard ' Turn on the light.\n', extractor gave 'light.on'
@dataclass
class ReplyToSpeech(SeamTest):
reply: str
to_speech: Callable[[str], str]
def run(self) -> tuple[bool, str]:
spoken = self.to_speech(self.reply)
if not spoken.strip():
return False, "normalizer emptied a reply that had words in it"
markup = sorted({c for c in spoken if c in "*#`_"})
if markup:
return False, f"markup reached the voice model: {markup}"
return True, f"{len(spoken)} speakable characters, no markup"
@dataclass
class MemoryRoundTrip(SeamTest):
db_path: Path
key: str
value: str
def run(self) -> tuple[bool, str]:
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self.db_path.unlink(missing_ok=True)
con = sqlite3.connect(self.db_path)
con.execute("CREATE TABLE facts (key TEXT PRIMARY KEY, value TEXT)")
con.execute("INSERT INTO facts VALUES (?, ?)", (self.key, self.value))
con.commit()
con.close()
con = sqlite3.connect(self.db_path)
row = con.execute("SELECT value FROM facts WHERE key = ?", (self.key,)).fetchone()
con.close()
if row is None:
return False, f"{self.key!r} was gone after the reopen"
if row[0] != self.value:
return False, f"{self.key!r} came back as {row[0]!r}"
return True, f"{self.key}={row[0]!r} survived close and reopen"
class IntegrationRunner:
def __init__(self, tests: list[SeamTest]) -> None:
self.tests = tests
self.results: list[dict] = []
def run_all(self) -> bool:
for test in self.tests:
passed, message = test.run()
self.results.append({
"name": test.name,
"description": test.description,
"passed": passed,
"message": message,
})
print(f" [{'PASS' if passed else 'FAIL'}] {test.name}: {message}")
return all(r["passed"] for r in self.results)
$ uv run python labs/integration_tests.py
[PASS] heard_to_command: heard ' Turn on the light.\n', extractor gave 'light.on'
[PASS] reply_to_speech: 35 speakable characters, no markup
[PASS] memory_round_trip: last_calibration='servo 2 at 88 degrees' survived close and reopen
All passed: True
Three handoffs, three classes, and a runner that treats them identically. Look at what
run_all knows about its tests: their name, and that calling
run() gives back a boolean and a string. Nothing else. It never asks which
kind of seam it is holding, so a fourth kind is a new class and a list entry, with no
edit to the loop. The memory case earns its place by being real rather than mimed: it
writes to an actual SQLite file, closes the connection, opens a fresh one, and reads
the row back, which is the only version of that test that would have caught a fact
living in a transaction nobody committed.
def save_report(self, path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(self.results, indent=2))
def main() -> None:
runner = IntegrationRunner(build_suite(fake_transcribe))
all_passed = runner.run_all()
runner.save_report(REPORT_PATH)
print(f"Ran {len(runner.results)} seams. All passed: {all_passed}")
$ uv run python labs/integration_tests.py
[PASS] heard_to_command: heard ' Turn on the light.\n', extractor gave 'light.on'
[PASS] reply_to_speech: 35 speakable characters, no markup
[PASS] memory_round_trip: last_calibration='servo 2 at 88 degrees' survived close and reopen
Ran 3 seams. All passed: True
$ head -7 glados/data/integration_report.json
[
{
"name": "heard_to_command",
"description": "a transcript becomes the command it names",
"passed": true,
"message": "heard ' Turn on the light.\\n', extractor gave 'light.on'"
},
The terminal output is for you, now. The JSON is for the version of you that runs this
in three weeks and wants to know what changed, and for anything that reads results
without a human in front of it. Storing description alongside the verdict
is what makes the file readable months later: a bare "heard_to_command": false
needs an archaeologist, while the sentence next to it states the contract that broke.
mkdir(parents=True, exist_ok=True) means the first run on a fresh checkout
writes the report instead of dying on a missing directory, and
write_text truncates, so the file always describes the latest run and
never accumulates stale entries.
A green suite that checked nothing
You are about to touch the bench, so you want the hardware seams only. A filter on the name is the natural way to get them:
# run only the hardware seams before touching the bench
selected = [t for t in build_suite(fake_transcribe) if t.name.startswith("hardware_")]
runner = IntegrationRunner(selected)
all_passed = runner.run_all()
print(f"Ran {len(runner.results)} seams. All passed: {all_passed}")
$ uv run python labs/integration_tests.py
Ran 0 seams. All passed: True
No error. No warning. A confident green light over a suite that verified nothing at
all, which is the most dangerous kind of pass because it wears the same face as a real
one. Trace it back: no seam in the suite is named hardware_ anything, so
selected is empty, so the loop body never executes, so
self.results stays empty, and all([]) is True.
That last one is not a Python quirk to be angry at. all() answers "did
every element satisfy this?", and with no elements the answer is honestly yes; change
it and all(x > 0 for x in []) inside every other program on your machine
starts lying. The bug is the gate, which asked the wrong question. It wanted "did every
seam pass?" when it should have wanted "did seams run, and did every one of them
pass?" One bool() at the front of the return fixes it, and the same filter
now prints Ran 0 seams. All passed: False.
# labs/integration_tests.py — full file
import json
import sqlite3
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
REPORT_PATH = Path("glados/data/integration_report.json")
SUITE_DB = Path("glados/data/suite_memory.db")
COMMANDS = {"turn on the light": "light.on", "turn off the light": "light.off"}
@dataclass
class SeamTest:
name: str
description: str
def run(self) -> tuple[bool, str]:
raise NotImplementedError(f"{type(self).__name__} must implement run()")
@dataclass
class HeardToCommand(SeamTest):
transcribe: Callable[[str], str]
extract: Callable[[str], str | None]
clip: str
expect: str
def run(self) -> tuple[bool, str]:
heard = self.transcribe(self.clip)
got = self.extract(heard)
if got != self.expect:
return False, f"heard {heard!r}, extractor gave {got!r}, expected {self.expect!r}"
return True, f"heard {heard!r}, extractor gave {got!r}"
@dataclass
class ReplyToSpeech(SeamTest):
reply: str
to_speech: Callable[[str], str]
def run(self) -> tuple[bool, str]:
spoken = self.to_speech(self.reply)
if not spoken.strip():
return False, "normalizer emptied a reply that had words in it"
markup = sorted({c for c in spoken if c in "*#`_"})
if markup:
return False, f"markup reached the voice model: {markup}"
return True, f"{len(spoken)} speakable characters, no markup"
@dataclass
class MemoryRoundTrip(SeamTest):
db_path: Path
key: str
value: str
def run(self) -> tuple[bool, str]:
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self.db_path.unlink(missing_ok=True)
con = sqlite3.connect(self.db_path)
con.execute("CREATE TABLE facts (key TEXT PRIMARY KEY, value TEXT)")
con.execute("INSERT INTO facts VALUES (?, ?)", (self.key, self.value))
con.commit()
con.close()
con = sqlite3.connect(self.db_path)
row = con.execute("SELECT value FROM facts WHERE key = ?", (self.key,)).fetchone()
con.close()
if row is None:
return False, f"{self.key!r} was gone after the reopen"
if row[0] != self.value:
return False, f"{self.key!r} came back as {row[0]!r}"
return True, f"{self.key}={row[0]!r} survived close and reopen"
class IntegrationRunner:
def __init__(self, tests: list[SeamTest]) -> None:
self.tests = tests
self.results: list[dict] = []
def run_all(self) -> bool:
for test in self.tests:
passed, message = test.run()
self.results.append({
"name": test.name,
"description": test.description,
"passed": passed,
"message": message,
})
print(f" [{'PASS' if passed else 'FAIL'}] {test.name}: {message}")
return bool(self.results) and all(r["passed"] for r in self.results)
def save_report(self, path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(self.results, indent=2))
def fake_transcribe(clip: str) -> str:
return " Turn on the light.\n"
def extract_command(heard: str) -> str | None:
return COMMANDS.get(heard.strip().lower().rstrip("."))
def to_speech(reply: str) -> str:
text = reply.replace("**", "").replace("*", "").replace("`", "")
return " ".join(text.split())
def build_suite(transcribe: Callable[[str], str]) -> list[SeamTest]:
return [
HeardToCommand(
"heard_to_command",
"a transcript becomes the command it names",
transcribe,
extract_command,
"glados/data/captured_audio.wav",
"light.on",
),
ReplyToSpeech(
"reply_to_speech",
"an LLM reply reaches the voice model without markup",
"**Oh**, it's *you* again. `light.on` it is.",
to_speech,
),
MemoryRoundTrip(
"memory_round_trip",
"a stored fact survives closing the database",
SUITE_DB,
"last_calibration",
"servo 2 at 88 degrees",
),
]
def main() -> None:
runner = IntegrationRunner(build_suite(fake_transcribe))
all_passed = runner.run_all()
runner.save_report(REPORT_PATH)
print(f"Ran {len(runner.results)} seams. All passed: {all_passed}")
if __name__ == "__main__":
main()
build_suite takes the transcriber and nothing else, which is the seam
between fast and slow. Called with fake_transcribe, the whole suite
finishes in well under a second and you can run it after every edit. Called with the
real transcriber pointed at a committed WAV, the same three contracts get checked
against the actual model on the actual machine; that run is slower and its first
message will quote whatever the model heard, so your line will read differently from
the one above. The suite is identical either way. Only the collaborator changed.
Why this works: one loop, any number of contracts
Everything here rests on the runner refusing to know what it is running. It calls
test.run() and relies on the promise that every subclass gives back a
boolean and a string. That promise is the substitution principle from the object-oriented
literature, doing plain work in twelve lines: any SeamTest subclass can
stand in for any other wherever a SeamTest is expected. The alternative is a
function per seam and a hand-written sequence of calls, and that version grows a line in
the runner for every contract you add, which is how test suites get abandoned.
The base run() that raises is the enforcement half. Write a subclass, forget
the method, and the inherited version fires with the class name baked into the message:
NotImplementedError: WakeWordSeam must implement run(). Loud, immediate, and
it names the file to open. A silent default that returned True would be the
same species of lie as the empty suite.
There is a second reason the seams are classes and not functions, and it shows up the
moment you have ten of them. HeardToCommand describes a kind of
handoff; each instance describes one case of it. Ten sentences you want her to
understand cost ten list entries and zero new code, each carrying its own clip and its
own expected command. That split, one class per kind of contract and one instance per
case, is what keeps an integration suite from turning into a thousand lines of nearly
identical functions.
Checkpoint, and the interface that will not be assumed
- Shown a system where every module's own tests pass and the behaviour is still wrong, I can name where the defect must be and write the test that proves it.
- I can say what a dataclass subclass inherits from its base and what order the generated constructor takes the combined fields in.
- I can explain why
all([])isTrue, why that is the right answer forall(), and why it is the wrong answer for a test gate. - I can add a fourth kind of seam without editing
run_all, and state exactly what the runner relies on for that to keep working. - I can point at the line that makes this suite runnable in a second against fakes and on the bench against real models, without a second copy of the tests.
Exercise 1 — break it on purpose. Put
extract_command back to the plain COMMANDS.get(heard), run
the suite, and predict both the terminal output and what
integration_report.json holds afterwards.
The first line flips to [FAIL] heard_to_command: heard ' Turn on the
light.\n', extractor gave None, expected 'light.on', the other two seams
still pass because the runner keeps going after a failure, and the last line reads
All passed: False. The report holds all three entries with
"passed": false on the first, and its message is the
diagnosis in text: the two strings that failed to match, side by side. Then put the
fix back and watch the same file go green.
Exercise 2 — make the shell care. Add a
summary() method that returns how many seams passed out of how many ran,
and have main exit with a status code so a script can branch on the
result.
summary() is two lines:
passed = sum(1 for r in self.results if r["passed"]), then
return f"{passed}/{len(self.results)} seams passed". For the exit
code, end main with
raise SystemExit(0 if all_passed else 1). Run it with a broken seam
and echo $? prints 1; fix the seam and it prints
0. That single number is the whole point of a gate: something other
than a human can now read the verdict, which is what makes it possible to refuse a
deploy or refuse to power the servo rail.
Exercise 3 — run it against the real model. Record a short
clip of yourself saying "turn on the light," commit it under
glados/data/, and pass your real transcriber into
build_suite instead of fake_transcribe.
The suite goes from under a second to a few seconds, and the first message now quotes what the model actually heard from your voice and your room, so expect something close to but not identical to the canned string. If it passes, the contract holds against the real pipeline. If it fails on punctuation or a politeness word the model added, you have found the next normalization rule your extractor needs, which is precisely the class of bug this chapter exists to catch. Keep the clip in the repository: a recorded input is what makes a slow test repeatable.
The suite trusts something it has no way to check. build_suite accepts any
object with the right method name floating around on it, and finds out whether the
transcriber really has one only when the test calls it, mid-run. Next comes the stricter
version of that promise: STT, LLM and TTS contracts declared as abstract base classes, so
a component missing a method is refused at the moment you try to build it.