Expo Smoke Test
Prove two things on each platform: the current change works, and the app's core loop still works. Static inspection, type checks, unit tests, and a successful bundle support the result but do not replace device verification.
Establish the run
Inspect the request, the current diff, the Expo config, and the package manifest to identify the flow the change affects and its observable result.
Read root PRODUCT.md when it exists and derive one representative journey
from its core loop. Read that file only; never create or edit it. When
PRODUCT.md or its core loop is absent, verify the change flow alone and
report the missing regression coverage.
Use the installed CLI's version-matched guidance instead of remembered command shapes:
agent-device help workflow
agent-device help react-native
If agent-device is unavailable, report that runtime verification is blocked
and give npm install -g agent-device@latest as the setup command.
Point each platform's client at this project's own dev server rather than a
default port another checkout may already hold. Pass the port as a session
runtime hint with --metro-port, and confirm from the running tools which port
this project actually serves.
Pass preflight before behavior
Keep three gates explicit for each platform:
- Runtime readiness: the exact target, development build, project-owned Metro server, and observation path are usable, and the loaded bundle or native build belongs to this checkout.
- Known initial state: app-local data, OS permissions, login or fixture state, and every affected backend boundary match the journey's premise.
- Behavior verification: the prepared app is driven and the change flow plus core loop reach their exact outcomes. Passing preparation never satisfies this gate.
Use narrow checks for routine readiness. Run agent-device doctor only when
the user asks for environment diagnosis or a failed readiness check suggests an
unhealthy device, app, development server, or runner.
Choose and report one state profile per platform:
- Known app state is the default. Prepare only the app data, explicit permissions, login, and fixture state the journey needs while preserving unrelated device state. Empty and seeded states both qualify when they are explicit and reproducible. App-state clearing and permission reset are separate boundaries; report each one actually changed.
- Fresh device applies when the request requires a fresh install or the
change depends on first launch, onboarding, OS permission history, secure
device storage, application identity, native configuration, deep links, push
behavior, another device-level surface, or evidenced device-state
contamination. Reset only a simulator or emulator the request or project
allocation establishes as dedicated to this run. Fresh-device preparation
requires
agent-device0.20.10 or newer, whose device claims reject foreign local mutations; earlier releases advertise claims without enforcing them. Acquire each exact target through its finalsmoke-iosorsmoke-androidsession withagent-device open, confirmagent-device device statusnames that session and workspace, and keep the claim through reset, boot, reinstall, Metro reconnection, and worker handoff. If the claim cannot be acquired or held across the selected reset mechanism, stop before reset. Absence from an advisory claim list is not proof of ownership. Address one exact target, never all devices. After reset, re-establish the allowed backend fixture before behavior verification. - Preserved or prior state retains or seeds the earlier application and backend state needed for returning-user, upgrade, or migration behavior. Once that premise is prepared, do not clear app data or erase the target before driving the flow.
Never erase a physical, user-owned, shared, or otherwise unowned target. When a fresh-device claim has no eligible virtual target, report that platform's runtime-readiness blocker. When a usable target exists but its required state cannot safely be established, report the known-initial-state blocker. Do not weaken the scenario or mutate another owner's environment.
Treat Supabase or another stateful service as a separate affected surface. Use only the repository's deterministic local or test readiness, migration, reset, and seed path needed by the journey. A ready client never proves backend state, and ordinary verification authority never permits a destructive linked or remote reset.
Choose devices and sessions
Use a local simulator and emulator as the default targets. Use a physical
device when one is connected and the user asks for it. A physical iOS target
additionally needs Developer Mode and a signed XCTest runner; when that setup
is missing, report the exact prerequisites from
agent-device help physical-device as a blocker rather than substituting a
simulator.
Run every platform against a development build of the project itself. A
development build is the binary this change will ship in, so it is the only
client that can carry the project's own native modules, config plugins,
permissions, and entitlements. Expo Go is a fixed prebuilt shell: passing there
proves the JavaScript behaved inside a different binary than the one being
delivered, which is not the evidence this check exists to produce. When no
development build is installed, build one with the repository-supported command,
normally expo run:ios or expo run:android through the project's own package
manager, before verifying. npx aborts with EBADDEVENGINES where
package.json pins another package manager through devEngines.
Give each platform its own named session, so both apps can run at once. Target
devices by the names agent-device devices reports, which are not adb serials
or simulator UDIDs, and open the development build by its own app id:
agent-device devices --platform ios
agent-device apps --platform ios
agent-device open <app-id> --platform ios --device "<name>" --session smoke-ios --relaunch
agent-device open <app-id> --platform android --device "<name>" --session smoke-android --relaunch
Open the actual installed app identifier or development-client URL reported by
the running tools; do not invent one. A URL target rejects --relaunch, so open
the app id when the journey needs a clean process and pass the URL afterward
only if the build needs to be pointed at a specific dev server.
Carry --session <name> on every command in a named-session flow. Without it a
command targets the implicit default session instead of the platform you meant.
Keep state-changing commands serial inside a single session. Run the two sessions concurrently through the two platform workers described below. Concurrent runs contend for CPU and device I/O, so allow longer per-command timeouts than a solo run needs.
Classify the runtime path
Classify the whole change once, before launching:
- Metro path: JavaScript or TypeScript behavior, React components, styles,
navigation code, and bundle-loaded assets that do not alter the native app.
Reuse the installed development build and its Metro server, and let Fast
Refresh or
agent-device metro reloadapply the change. - Native path: app config that affects the binary, config plugins, native modules or dependencies, permissions, entitlements, icons or splash configuration, native project files, SDK or React Native upgrades, and startup behavior. Rebuild with the repository-supported command, install the resulting development build, and relaunch it.
Both paths run on a development build; the classification decides whether that build must be rebuilt, not which client to use. Use the native path for a mixed change or when native impact remains uncertain. Preserve the project's managed or checked-in native workflow rather than regenerating native directories as an incidental verification step. Native builds for the two platforms may run one after another while verification still runs per session.
Delegate exactly two platform workers
The coordinator owns everything shared: inspect the change and PRODUCT.md,
classify the runtime path once, select both targets and app identifiers, prepare
the development builds, start or identify the project-owned Metro server,
select each platform's state profile, prepare every destructive or backend
prerequisite, and define the changed flow plus core-loop journey. Finish native
builds serially when they would contend for the same project state. Do not make
either platform worker rediscover, reset, or mutate this shared setup.
Once each platform's preflight is ready or explicitly blocked, use the harness's subagent capability to spawn exactly two workers concurrently:
- one
expo-smoke-runnerfor iOS with sessionsmoke-ios; - one
expo-smoke-runnerfor Android with sessionsmoke-android.
Give a ready worker the project directory, target device, app id or development-client URL, Metro port, runtime path, selected state profile, readiness evidence, prepared and preserved state boundaries, explicit initial-state assertions, changed flow and exact expected outcome, core-loop journey or its confirmed absence, evidence requirements, and its fixed session name. The worker confirms the assigned initial state, drives the app, and returns evidence; it never covers the other platform, resets a device or backend, or spawns a nested agent.
Give a blocked worker only the platform, fixed session name, selected state profile, coordinator-established failed gate and evidence, preserved boundaries, exact prerequisite, and project directory. Target, app, runtime, and initial-state fields may be absent because preflight did not establish them. The worker returns the blocker without mutating the environment. The other worker may still return direct platform evidence, but a blocked platform prevents both-platform completion.
Wait for both workers, then aggregate their reports against the completion gate. One worker finishing or passing never substitutes for the other. Name the two workers explicitly: a generic concurrency hint leaves both sessions in the coordinator instead of creating isolated test contexts.
Select the custom agent named expo-smoke-runner when the installation exposes
it. If subagents are available but that profile is not installed, still spawn
exactly two general-purpose subagents and include this skill's complete
per-platform verification contract in each assignment. This keeps the skill
self-contained. If the harness exposes no subagent capability at all, run both
named sessions directly, preserve the same evidence gate, and report that
platform-context isolation was unavailable.
Verify each platform
On each platform, exercise the smallest complete flow the change affects, then the core-loop journey. Check four layers for each:
Before the first behavior interaction, assert the assigned initial-state premise on the running app. A mismatch is a known-initial-state failure, not an application regression, and the worker does not repair it with an unassigned reset.
- Loaded: the intended Metro update or native build is running on that target, with no bundle, build, or incompatible-client error.
- Healthy: reproduce inside a focused log window and check for relevant JavaScript errors, native crashes, RedBox or LogBox failures, and rejected network requests.
- Correct: drive the flow with
press,fill,scroll,back, and other appropriate commands using--settle; verify the named outcome with an exactwait,is,get, orfindassertion. A screenshot alone does not prove a behavioral expectation. - Sound at the claimed layer: inspect the React tree, props, hooks, re-renders, native performance, network data, or traces only when the change makes a claim about that layer.
Use refs from the latest snapshot or settled diff. A state-changing command invalidates earlier refs; refresh the snapshot or use a stable id, label, role, or testID before the next action. Prefer semantic selectors and use coordinates only when the accessibility surface cannot expose the target, recording that limitation with visual evidence.
Collect evidence proportional to the claim: a screenshot for visual output, focused logs for runtime behavior, network output for request behavior, and performance artifacts for performance claims.
When an overlay dismissal reports that the overlay is still visible, capture a screenshot before concluding it blocks the run. A development client's own floating controls can register as an overlay while the app UI stays fully reachable; report what the screenshot shows rather than retrying the dismissal.
Drive the core loop
Drive the core-loop journey on the running app every run, on each platform.
Derive it from PRODUCT.md so that file stays its single definition, and
confirm the derived journey with the user when the core loop admits more than
one reasonable reading.
Assert readiness before the first interaction, then work from the current screen: read it, act, and assert the outcome each step establishes. End on an assertion that proves the journey reached its destination.
agent-device wait 'id="<first-target>"' 60000 --session smoke-ios
agent-device fill 'id="<input>"' "<value>" --settle --session smoke-ios
agent-device press 'id="<action>"' --settle --session smoke-ios
agent-device wait 'id="<destination>"' 15000 --session smoke-ios
A replayed recording does not satisfy this. Replay checks each step against the element identity captured when the script was made and stops at the first mismatch, so a diverging run leaves the rest of the journey unexercised while reporting a failure the app did not cause, and a passing run certifies fingerprints rather than behavior. Report a core loop as verified only from this run driving it on that platform's running app.
Read a failure for what it is before reporting it. An assertion that fails on a loaded, healthy app is a regression; a daemon timeout, a lost device, or a snapshot the accessibility layer could not capture says nothing about the app, so recover and re-drive rather than reporting the core loop as broken.
Finish the run
Finish only when each platform separately shows the current change loaded, the change flow passing its exact expectation, the core-loop journey passing or its absence reported, and no relevant runtime errors during the reproduction. Report each platform's target, state profile, readiness evidence, prepared and preserved state boundaries, flows, assertions, and artifact paths on its own, separating observed results from remaining inference. Evidence from one platform never establishes the other.
If verification is blocked, name the app, platform, session, failed gate, and
the exact next command or user action needed. Each worker closes its own session
when finished; the coordinator closes any session it had to run directly. Leave
a healthy Metro server running for the next edit loop unless the user requested
cleanup; in CI, release the devices with agent-device close --shutdown.
When a workaround such as clearing a cache or rebuilding leaves its root cause
open, or you observe an out-of-scope defect with evidence, record it at the
moment of discovery through the project-knowledge skill. If that skill is
unavailable, write the symptom, observed evidence, suspected cause, what was
tried, and a proposed next step to docs/follow-ups/<slug>.md yourself.
Reporting it only in conversation loses it.