August 11, 2026 · BariBariGood
Testing iOS apps without a Mac fleet: mock mode
Most of the code in an agent-drives-a-simulator pipeline isn't macOS code. It's leases, queues, action payloads, wait loops, journal exports — protocol and logic. But testing any of it used to require a Mac with Xcode, which means CI needs Mac runners and contributors on Linux can't run anything at all. v0.5.0's --mock fixes that: the daemon runs anywhere, and mock targets carry a full deterministic action backend, not just a fake target list.
Start the daemon
git clone https://github.com/BariBariGood/manzanas
cd manzanas && make build
./bin/manzanasd --addr :7433 --mock
# 3 mock simulators, full action backend, journal enabledOn a non-macOS host the daemon falls back to mock mode automatically; the flag just makes it explicit. You get three mock simulators, and every one of them renders the same synthetic app: a 390×844 login screen with a username field, a password field, a Wi-Fi switch, a Sign In button, and a footer that starts below the fold so scrolling is real. State transitions are synchronous and deterministic — repeated runs produce identical trees, hashes, and screenshots.
Drive the full loop
This is the same wire protocol an agent uses against a real Mac — lease, boot, batch of actions, release:
D=http://localhost:7433
L=$(curl -s -X POST $D/v0/leases \
-d '{"labels":["ios26"],"agent_id":"demo","ttl_seconds":300}')
LID=$(echo "$L" | jq -r .id); UDID=$(echo "$L" | jq -r .target_udid)
curl -s -X POST $D/v0/targets/$UDID/boot -d "{\"lease_id\":\"$LID\"}"
curl -s -X POST $D/v0/actions:batch -d "{
\"lease_id\":\"$LID\", \"stop_on_error\":true, \"actions\":[
{\"kind\":\"type_into_element\",\"payload\":{\"id\":\"username\",\"text\":\"agent\",\"require_focus\":true}},
{\"kind\":\"type_into_element\",\"payload\":{\"id\":\"password\",\"text\":\"pw\"}},
{\"kind\":\"tap_element\",\"payload\":{\"label\":\"Sign In\"}},
{\"kind\":\"wait_for_element\",\"payload\":{\"label\":\"Welcome, agent!\",\"timeout_ms\":5000}},
{\"kind\":\"audit\",\"payload\":{\"inline\":false}},
{\"kind\":\"screenshot\",\"payload\":{\"inline\":false}}]}"
curl -s -X DELETE $D/v0/leases/$LIDThe interesting part is what's not mocked. The action pipeline reuses the production backend handlers — observe compaction, the predicate/matcher DSL, wait loops, composite element actions, batches, audit checks, screenshot transcoding — with only the simulator process boundary replaced by an in-process synthetic app. The audit step runs the real checks over the synthetic tree and returns findings with an annotated screenshot. What CI exercises through mock mode is the same code an agent hits in production, minus the simulator itself.
Now make it one call
Raw HTTP is for understanding; day to day you'd wrap the loop in a run:
# mock-smoke.yaml
name: mock-smoke
target:
labels: [ios26]
steps:
- action: type_into_element
with: {id: username, text: agent, require_focus: true}
- action: type_into_element
with: {id: password, text: pw}
- action: tap_element
with: {label: "Sign In"}
- action: wait_for_element
with: {label: "Welcome, agent!", timeout_ms: 5000}
- name: quality gate
action: audit$ ./bin/manzanas run mock-smoke.yaml -o evidence.md
run run_…: passed
journal run: lse_…
target: MOCK-UDID-1
step 0 type_into_element: ok
...
step 4 audit: okevidence.md is the journal's PR-ready markdown export — steps, tree hashes, the audit findings, the screenshots. Since mock screenshots are rendered from the synthetic tree, the pixels match the accessibility tree byte-for-byte on every run, which makes it a nice fixture for testing your own evidence tooling too.
What it's for (and not for)
I use mock mode for three things: CI for the daemon and clients on Linux runners; developing agent prompts and run-specs without burning a real simulator lease; and demos — the entire quickstart on this site works on a $5 VPS. What it is not for is performance numbers (mock latency is near-zero; use a real host and make bench) or testing your actual app — there is one fixed synthetic screen, and launch_app resets it whatever bundle ID you pass. When you outgrow it, point the same specs at a real daemon and nothing else changes. Details in docs/mock.md.