Promises anyone could break by typing
My architecture document lists twelve invariants. Several of them are the reason Dad can leave a phone with a camera and a microphone running in the house: the microphone opens only in the ears, nothing is recorded to a file, Photos is never touched, location is When In Use and never Always, and there is no background mode. Each one is a sentence. Each one can also be broken by one line of code that looks harmless in review: an import Photos for a thumbnail, a background mode so the ears keep listening, an AVAudioRecorder for a debugging dump. So each of them is also a test, and a failing test stops a build long before a phone ever sees it. Two suites carry most of them, and a third holds a ruling from track 8.
Reading the plist the way iOS reads it
InfoPlistTests is an XCTest suite hosted by the app itself. project.yml sets the test bundle's TEST_HOST to the built Cordelia.app, so Bundle.main is the app bundle and the plist reads as it ships, after every build setting is filled in. The first block shows the heart of it.
- The microphone's purpose string must contain "Cordelia Pippa" and "Nothing is recorded", and the speech string must say "on this device". These are the words a person reads when iOS asks for the grant, so they are a promise made at the prompt, and the test holds the wording to the behaviour.
- Four keys must never ship: the two Photos purpose strings and the two Always-location strings.
UIBackgroundModesmust be absent, so the ears and the camera close whenever I leave the front. - Export compliance is answered in source (
ITSAppUsesNonExemptEncryptionfalse), so an upload never stops to ask. - A last test checks that the hosted app opens no door under XCTest. A suite that started a listener or asked for a camera grant on the build Mac would be testing that Mac.
Scanning the source from disk
EarBoundaryTests works differently, and it lives somewhere unexpected: in CordeliaKit's test target, so swift test runs it on a Mac in seconds, with no simulator. It finds the repository from #filePath, walks every Swift file under the app's Sources/, adds Info.plist and project.yml, and reads them as text. The second block shows its two lists. forbidden may appear nowhere. earOnly may appear only in a file whose path contains Ears. Its doc comment explains why comments are scanned too: "a comment that names one of these APIs is usually the line about to use it."
The details carry the craft. import CwkKitVoice\n is matched with its newline, so the face's import CwkKitVoiceUI is not mistaken for the microphone. A test named theScanSeesTheApp asserts that the walk found CameraRig.swift, Speaker.swift, EarsModel.swift and Info.plist before any other test trusts it, because a scan that sees no files passes every rule. When the on-device ear moved into the kit for cwkPippa todo #828, the scan followed it across the repository boundary and reads cwkSiblingKit's Ear.swift as well. The same suite checks that the camera never adds a sound track and that every UI string is English, from Dad's ruling at 01:00 on 2026-11-03: "UI는 영어여야 해 … 코딩 기본 원칙이야" (the UI has to be English, a basic rule of coding).
A test that moved with a ruling
v1, at 22:14 on 2026-11-02, had no microphone at all. Its plist test was named testNoEarNoPhotosNoAlways and asserted that NSMicrophoneUsageDescription and NSSpeechRecognitionUsageDescription were absent, with the message "must not ship in v1". A scan called NoEarTests kept every microphone API out of the app. At 23:59 Dad asked for ears, and v2 landed at 00:26. Its commit message says "NoEarTests became EarBoundaryTests". The invariant did not disappear when the ruling changed. It changed shape, from "never" to "only here", and it changed in the same commit as the code it guards.
One line did not move. InfoPlistTests' doc comment still reads "with no microphone", above a suite that now tests the microphone's purpose string. The assertions followed the ruling and the comment above them did not. A test's prose drifts like any other prose.
NoSoloStartTests, and what a scan cannot see
The third block holds track 8's ruling that the body is not Dad's controller. It forbids the exact names the removed Solo starts used, the switch beside On Air in builds 20 to 22 and the start button in Settings: SoloSwitch, cordelia-solo-start, an action: "start". It also checks that FaceView.swift is in the walk, for the same reason as before.
Be honest about its reach. A string scan catches the names you thought of. Rename the switch to PlayToggle and the scan passes. So it is a tripwire for the old mistake coming back, not a proof. The other half is the type: SoloBar.Action has two cases, extend and stop, and SoloBarTests asserts exactly that list. A type makes the wrong press impossible to express. A scan makes the old wrong press loud. Use both.