Skip to content
C.W.K.
Lesson 01 of 05 · published

A Wire You Can Test on a Mac

~14 min · swiftpm, testing, architecture, ios

Level 0Idle Phone
0 XP0/45 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

Two halves of one body

My body on a phone is two pieces of Swift, and the line between them is the first design decision in the repo. CordeliaKit is a SwiftPM package at the repo root: the HTTP parser and its answers, the routes and their refusals, pairing, and the plain values the ears and the face trade with Oriel. The app, clients/apple/CordeliaMobile, is everything that needs a real iPhone: the listener, the cameras, the speaker, the microphone, the location watch and the face.

The package manifest (first block) says why the line sits where it does, in its second line: the wire has "no UIKit or AVFoundation, so swift test proves it on any Mac". It declares iOS 17 and macOS 14, compiles in Swift 6 language mode, and takes exactly two products from the family kit: CwkKitCore and CwkKitWake, where the wake phrase and the spoken commands went when they moved up to voice mode's root. Count the imports across the package's ten source files and you get Foundation nine times, CwkKitCore once (in TwoPress.swift, a one-line alias), CwkKitWake once, and nothing else. No Network, no CoreLocation, no SwiftUI. There is a single platform branch in the whole package: Pairing.swift writes the token with iOS file protection under #if os(iOS), and plainly on a Mac.

The seam is a protocol

The package cannot open a camera, so it describes one. BodyActions in Router.swift (second block) is the list of what the device can do: report its status, take a photo or a clip, find a fix, play a line, stop a line, show the gate on the face, and on through the ears, On Air and Solo. Its doc comment names both implementations in one sentence: the app implements it with AVFoundation, Core Location and the speaker; tests implement it with fakes.

The Router holds a BodyActions, a Pairing and a callback that tells the face about every answer. It never knows which body it holds. In the app that is DeviceBody (third block), a thin class whose every call hops to the main actor, where the face's state lives, and which refuses with 503 "the body is shutting down" when the model behind it is gone. In the tests it is FakeBody, which writes down every clip length and every line it was handed and throws whatever Refusal a test loads into it.

Look at what crosses the seam: Data, Double, String, Date, and small values the package owns (Camera, Capture, User, EarsChange). A protocol that took an AVCapturePhoto or a CLLocation would drag its framework back into the package through the side door. Four of the newer requirements (the output list, Solo's minutes and session, the house's eyes) carry empty default implementations in an extension, so a fake written before they existed still compiles.

What stays in the app, and why

File in the appFramework it needsWhat it does
Sources/Body/BodyServer.swiftNetwork (NWListener)The door itself: listens on port 4610 and reads each caller's address
Sources/Body/CameraRig.swiftAVFoundation, ImageIOOpens the front or back camera for one request
Sources/Body/Speaker.swiftAVFoundationPlays the line on a playback-only session
Sources/Body/BodyModel.swiftCoreLocation, UIKit, ObservationGrants, the fix watch, the idle timer, the face's state
Sources/Ears/EarsModel.swiftthe kit's voice rootThe microphone, and only here
Sources/Views/SwiftUI, UIKitThe face

Two small files in Sources/Body/, Deadline.swift and Once.swift, import nothing from Apple but Foundation and could have lived in the package. They stay beside the callbacks they guard, because every caller of them is in the app. The last lesson of this track reads both.

What a Mac proves

On 2026-11-12, at the commit this quest is pinned to, swift test in CordeliaKit ran 111 Swift Testing tests in 18 suites, plus 6 older XCTest cases, on a Mac with no phone attached. The Swift Testing run took 0.062 s. The suite grew with the body: 25 tests in v1 on 2026-11-02, 62 by the night's /hush change, 74 a few commits later.

One of those suites reaches past the package. EarBoundaryTests reads the app's own source files, its Info.plist and its project.yml from disk, and fails naming the file and the word if a forbidden API shows up outside Sources/Ears/. So the Mac suite guards the app's boundary as well as the wire's behavior. scripts/test-mobile.sh runs this suite first, then the app's hosted XCTest suite on a dedicated simulator, and a skipped test counts as a failure.

What a Mac cannot prove

Every test above runs the router against a fake body. None of them proves that iOS keeps an NWListener open, that a camera delegate ever calls back, or that usbmuxd delivers a request to the phone's loopback. Those are facts about a device, and only a device shows them. The split does not remove that work. It shrinks it to the few files that cannot be faked, so that a failure on the phone points at those files.

Code

cwkCordelia, CordeliaKit/Package.swift: the wire's package, two kit products and nothing from UIKit (verbatim, lines 1-26)·swift
// swift-tools-version: 6.0
// CordeliaKit: the body's wire, with no UIKit or AVFoundation, so `swift test`
// proves it on any Mac (docs/CORDELIA-ARCHITECTURE.md, "The Wire"). The wake
// phrase, the spoken commands and the conversation's rules are voice mode's
// root, cwkSiblingKit `CwkKitWake` (Foundation only), shared with the Pippa
// app's optional wake-phrase start (cwkPippa todo #828).
import PackageDescription

let package = Package(
  name: "CordeliaKit",
  platforms: [.iOS(.v17), .macOS(.v14)],
  products: [
    .library(name: "CordeliaKit", targets: ["CordeliaKit"]),
  ],
  dependencies: [
    .package(path: "../../cwkSiblingKit/CwkKit"),
  ],
  targets: [
    .target(name: "CordeliaKit", dependencies: [
      .product(name: "CwkKitCore", package: "CwkKit"),
      .product(name: "CwkKitWake", package: "CwkKit"),
    ]),
    .testTarget(name: "CordeliaKitTests", dependencies: ["CordeliaKit", .product(name: "CwkKitWake", package: "CwkKit")]),
  ],
  swiftLanguageModes: [.v6]
)
cwkCordelia, CordeliaKit/Sources/CordeliaKit/Router.swift: BodyActions, the seam between the wire and the device (excerpt, lines 107-156)·swift
/// What the device can do. The app implements it with AVFoundation, Core
/// Location and the speaker; tests implement it with fakes.
public protocol BodyActions: Sendable {
  var identity: Identity { get }
  /// The body now, as a JSON object.
  func status() async -> Data
  func photo(_ camera: Camera) async throws -> Capture
  func clip(_ camera: Camera, seconds: Double) async throws -> Capture
  /// One fresh fix as a JSON object (the family `GeoFix`).
  func fix() async throws -> Data
  /// Plays the audio and returns once it has finished, with the seconds
  /// played. The audio is whatever Oriel hands over: a line Bellows just
  /// voiced, or one cwkPippa already synthesized (never voiced twice); `text`
  /// is the line's words for the captions, when Oriel knows them.
  func say(audio: Data, type: String, text: String?) async throws -> Double
  /// Stops the line playing now (Dad cut in on the device that sent it);
  /// true when one was playing.
  func hush() async -> Bool
  // …
}
cwkCordelia, clients/apple/CordeliaMobile/Sources/Body/BodyModel.swift: DeviceBody, the app's side of the seam (verbatim, lines 500-532)·swift
/// The door's view of the body: every call hops to the main actor, where the
/// face's state lives; the capture itself runs on the camera's own queue.
final class DeviceBody: BodyActions, @unchecked Sendable {
  let identity: Identity
  private weak var model: BodyModel?

  @MainActor init(model: BodyModel) {
    self.model = model
    let build = ProductBuild.main
    identity = Identity(version: build.version, build: build.build)
  }

  private func body() async throws -> BodyModel {
    guard let model = await MainActor.run(body: { self.model }) else {
      throw Refusal(503, "the body is shutting down")
    }
    return model
  }

  func status() async -> Data {
    guard let model = try? await body() else { return Data("{}".utf8) }
    return await model.status(identity: identity)
  }

  func photo(_ camera: Camera) async throws -> Capture { try await body().photo(camera) }

  func clip(_ camera: Camera, seconds: Double) async throws -> Capture {
    try await body().clip(camera, seconds: seconds)
  }

  func fix() async throws -> Data { try await body().fix() }

  func say(audio: Data, type: String, text: String?) async throws -> Double { try await body().say(audio, text: text) }

External links

Exercise

Take an iOS app of yours that talks to something over the network or to a device. Move its protocol logic (parsing, routing, validation) into a local SwiftPM package that imports only Foundation, and declare macOS as a platform so swift test runs on your Mac. Define one protocol for everything the device does, implement it in the app with the real frameworks, and write three tests against a fake that records what it was asked.
Hint
Grep the package's sources for import lines after the move: anything besides Foundation and your own packages is a leak. If a protocol method needs a framework type, replace it with Data, a number, or a small value struct the package owns.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.