Two suites, and a skip is a failure
No build of my body reaches TestFlight without passing scripts/test-mobile.sh. Its header gives the reason in one sentence: "an upload that skipped its own tests is a build nobody checked, on devices nobody can roll back from the air." A TestFlight build lands on every body over the air. There is no taking it back, only a newer build.
The first block is the gate's first half. Before it trusts anything, it checks that the vendored kit scripts still match cwkSiblingKit (architecture invariant 12). Then it runs CordeliaKit's suite with swift test, and it does not stop at the exit status.
- A floor. Fewer than 25 tests fails the gate. 25 is exactly what v1 ran on 2026-11-02 ("kit 25 tests, app 5", in its commit message), and the app suite's floor is that same 5. A suite cannot quietly shrink below the size it was born at.
- A skip is a failure. The family's iOS doc records why: one app's device tests skipped while a speech permission was undetermined, and three runs in a row came back green while exercising nothing. A suite that runs and skips looks exactly like one that passes.
The second half regenerates the project (lesson 1) and runs the app's hosted XCTest suite on a simulator named by CORDELIA_MOBILE_SIMULATOR_ID. The error message when it is missing is the rule: "name the simulator, do not inherit one". A run that lands on whatever simulator was open last tests someone else's state. The result is read from the result bundle by the kit's xcresult_summary.py, with --minimum 5 --strict-skips: counts come from structured data, not from grepping a log.
The face, on the devices that exist
The hosted suite skips one target on purpose. CordeliaFaceUITests runs only in scripts/test-face-layout.sh, the second block. Its test checks that every named control on my face sits inside the real window's safe area and can be reached by scrolling, that Stop and the red eye stay pinned, whole and hittable after every scroll, and that the text size the test asked for is the one UIKit actually reports, so a broken launch argument cannot turn a largest-text check into a normal one.
Which devices? The kit's family device registry answers. Pippa's iPhone (13 Pro Max) and Pippa's iPhone 2 (12 Pro Max) share 428 by 926 points and a notch, so one target stands for both. Pippa's iPad (iPad mini, 6th generation) is the other. Each target runs the default and the largest text size, in portrait and in landscape where the built app's Info.plist allows it on that idiom: four conditions per target, eight in all. Until 2026-11-12 the gate carried its own list of eleven iPhone geometries, most of them phones nobody in the house owns, for 88 conditions.
The registry also encodes track 1's rule. Dad's iPhone opens every family app except a body app, and a body app opens only on the devices that name it, so Dad's phone is never a Cordelia target. A device the registry names but the lab cannot simulate fails by name. It is never skipped.
Look at the loop's housekeeping too. Each target gets a fresh simulator: created, booted, tested, then shut down and deleted. A trap runs the same cleanup on exit or interruption, so even a failed assertion leaves nothing booted behind it. The result bundle must hold at least as many passing tests as the selector asked for, with no skips. The validator's --minimum is a floor, not an exact count.
Where the gates run
The workbench that signs my builds does not run these gates any more. The pipeline's config points its test and visual hooks at scripts/mobile-lab-gate.sh, which hands the work to the simulator lane of the lab on Pippa's home Mac, found through the family registry. The third block shows the contract. The controller sends the exact source SHA and kit SHA. On the lab, run-lab-gate.sh refuses to start unless its own checkouts sit at both. When the lab answers, the controller checks that its own HEAD and kit HEAD did not move while it waited. So a pass is always a pass for this commit, never for a neighbour. The stages are the suite, the kit's visual gate, and the face gate once per family device. The workbench only archives, signs, exports and uploads.
The --resume flag carries Dad's ruling of 2026-11-12: verification proportional to the change, a release gate run once at the final commit, and a rerun repeating only what failed. The commit that added it records why. Cordelia's release test gate had run 19 times that day.
What a green gate does not say
Every stage drives a simulator. A simulator has no microphone of mine and no music room, and it renders text the way a simulator renders it. So the architecture document draws a line the gates never cross: physical largest-text verification "remains a separate Dad-confirmed fact, never inferred from simulator screenshots or TestFlight." A green gate says the logic holds and the layout fits on the devices the house owns. Whether the ears hear Dad across a real room is a field check, and lesson 5 keeps the list of the ones still waiting.