Replace the pre-merge click-through with Maestro flows run locally on maestro-runner
Decision
Add a third test layer of black-box Maestro flows, written in YAML, that drive the app through the iOS accessibility tree against the real backend with seeded data. One local command replaces the manual pre-merge click-through. The flows select elements by explicit accessibility identifiers, which we now maintain as part of the app's interface. We run them with a pinned version of maestro-runner, locally before merging. CI does not run them.
The harness runs the branch's own server on a free port against a dedicated database and reseeds between mutating flows. The app finds that server through an endpoint override that exists only in debug builds.
Why
The only check on the full path from the app through GraphQL and Rust to Postgres used to be clicking through the app by hand before every merge. That was slow, easy to skip, and unreliable when tired. ADR-0004 keeps unit tests as the main layer and avoids UI automation as flaky and coupled to implementation. Black-box flows avoid that coupling because they know only what the user sees; the investment moves into stable accessibility identifiers.
Rejected alternatives
- XCUITest. It compiles a test target against the app and couples tests to how views are built, the brittleness ADR-0004 warned about.
- The official Maestro CLI as the runner. It needs a JVM, so every local run pays its startup and memory. maestro-runner is a single binary with built-in reports and app install. It reads the same YAML, so switching back costs nothing, and the official CLI remains the fallback when a flaky flow needs video.
- Run the suite in CI. iOS simulators need macOS runners, which cost roughly ten times Linux minutes, and the suite would run on every push instead of once before merging.
- A mock GraphQL transport. A mock able to drive real flows would reimplement the schema and exercise none of the Rust. The seed data already makes the real stack deterministic.
Consequences
A shipped build cannot be pointed at another backend, because the override is compiled out. Flows sign in as the seeded email-and-password user, because passkeys cannot be automated on a simulator against localhost. Flows that depend on a language model cannot be deterministic and are excluded from the default run. A user-facing element that a flow needs has to carry an accessibility identifier. A merge does not require a green run, so the suite catches a regression only on the machine of whoever runs it.