Four things a test must not touch
A Sizzle engine on the service host depends on four things outside its own process. cwkPippa answers every reading and every conversation turn. The family brain authority behind Firelink says which brain each function uses. The data directory holds the journal, the mirror and the photos. The Mac's host profile can move the data root and the Mac each member is placed on. It cannot move a port: the profile accepts no port key, and engine_port reads the registry's port table directly. A test that touches any of them is testing the Mac it runs on. backend/tests/conftest.py replaces all four, and it makes the first replacement before anything else is imported.
Its first statement sets CWK_HOST_PROFILE to an empty string. The engine's settings read the family registry when they are imported, and the registry reads the host profile, so the variable must be set before backend is imported at all. That is why the imports under it carry # noqa: E402. With the profile pinned empty, every registry answer is the no-profile default, on every Mac.
FakePippa: the brain as five functions
The engine reaches cwkPippa only through backend/pippa.py. That module is the seam, and FakePippa replaces five of its functions with monkeypatch.setattr: read_plate, ensure_conversation, upload, ask and last_answer. Three choices make it more than a stub.
- It answers in the real wire shape.
read_platereturns the reading as a fenced JSON block, the way a model reply arrives, so the engine's parser and the rubric's validation run for real in every test that reads a plate. The fixture is a generic gambas al ajillo reading written in Korean, with one principle and two siblings. - It records what it was sent.
inputs,image_batches,reading_picksandaskedlet a test assert on what Pippa would have seen: that a correction rode into the next read, that every photo went along, which brain the slot resolved to. - It enforces the contract.
read_platefails the test if it is ever called without photos, because a reading always carries the plate's photos.
last_answer returns None when it is asked for an exact match. That is the case where cwkPippa holds no answer for an interrupted turn, so a restart in a test settles the turn as failed instead of inventing one.
FakeCentral: a fake at the HTTP seam
The brain authority is faked one layer lower. FakeCentral comes from the kit (kit_brain_control_testing). The fixture builds a real BrainControlClient whose http_get and http_send are the fake's methods, with its cache under tmp_path. The engine's startup declaration then registers Sizzle and its three slots against a blank authority, through the same client code production runs. Only the transport is pretend.
A real engine for every engine test
make_client builds create_app(tmp_path / "sizzle-db", serve_webui=False) and enters a FastAPI TestClient. Entering runs the lifespan, so every test that takes make_client gets the whole startup from lesson 1: the mirror rebuilt from an empty journal, the slots declared, the worker started, the turns resumed. Not every test takes it. The parser tests, the host-profile tests, the drift test and two helper tests in the threads file call their functions directly and boot nothing, because what they check needs no engine. And because the engine is a function of its data directory, a restart can be tested. The rubric test captures a plate, records a reading under an older rubric, exits the client, then starts a second engine on the same directory and checks that the plate was read exactly once more, under the current rubric.
| Suite | Count on the current main | How it runs |
|---|---|---|
| Engine (pytest, 8 files) | 40 tests from 34 functions; the parser tests are parametrized | python -m pytest backend/tests in the cwk-sizzle env |
| iOS hosted unit (XCTest) | 25 | scripts/test-mobile.sh on a dedicated simulator |
| iOS UI (XCUITest) | 2; one skips unless SIZZLE_BENCH_ENGINE is set | the same run |
| Web | no test runner | tsc -b && vite build is the check |
The drift check with teeth
One of the 40 is different. test_sibling_kit_drift.py is itself vendored from a kit template. It runs the kit's sync/sibling_kit_sync.py --check --repo cwkSizzle --repo-root against the checkout it sits in and asserts exit 0. The kit compares every file its manifest vendors into Sizzle, after stripping the provenance header, and a script that lost its executable bit counts as drift too. That set is 36 files and much wider than Python: the kit_*.py modules, the TypeScript and CSS under frontend/src/kit/, the shell scripts from the last lesson and the TestFlight pipeline, and one Swift file, PippaBrainCatalog.swift.
The teeth are in the missing-kit branch. With no cwkSiblingKit checkout beside the repo, a fleet Mac (one with a non-empty ~/.config/fleet-label) fails the test, because a skipped check there is a green that checked nothing. Off the fleet it skips and says why. Exit 1 reads as drift. Any other exit reads as a check that ended without a verdict, and the message names a kit checkout older than the copies as one cause.
What the phone suite does not do
The architecture doc says the SizzleMobile suite calls KitDriftCheck.run(repo:) from CwkKitTesting. For this app that is false. No Swift file in the repository names KitDriftCheck, and no commit has ever added it under clients/. The iOS tests import CwkKitTesting for ScriptedHTTPTransport, which feeds the engine client scripted replies. Only pytest runs the drift check. The Swift copy is still compared, but only when someone runs the engine suite. A phone-only change verified with scripts/test-mobile.sh alone never hears about Swift drift. sibling-kit-quest's a-check-with-teeth track is about exactly this question: whose suite runs the check.