Test the app on the simulator
Drive the running app on the iOS simulator, and for anything non-trivial verify against the live app state, not just the screen. Many bugs are UI/data desyncs the screenshot won't show.
Three tools do the work:
ios-simulatorMCP — drive the UI (the app is bare RN, sotestIDs are queryable).scripts/dump-rn-state.js— read the live Redux state (globalThis.state).liftosaur-localMCP — set up the environment (programs, exercises, history, settings) on the same account the simulator is signed into.
Focus: $ARGUMENTS
Auth preflight (throwaway test account)
The simulator app and the liftosaur-local MCP must be on the SAME account. The MCP's API key
is static (in ~/.claude.json → this project's mcpServers.liftosaur-local.headers), and
debugLogin rebinds that key server-side to a freshly created account — one call aligns both.
- Get the key:
python3 -c "import json,os;c=json.load(open(os.path.expanduser('~/.claude.json')));print(c['projects']['/Users/anton/projects/liftosaur']['mcpServers']['liftosaur-local']['headers']['Authorization'].split()[-1])" - Check the current account:
node scripts/dump-rn-state.js --eval "globalThis.state.user". If it's already thetest_account you're mid-scenario with, skip step 3 — eachdebugLogincall starts a NEW empty account. - Create a fresh throwaway account (empty storage, active subscription — premium features work)
and rebind the MCP key to it:
node scripts/dump-rn-state.js --eval "globalThis.debugLogin('<key>')" # → {"userId":"test_xxxxxxxx","email":"test_xxxxxxxx@test.liftosaur.com","apiKeyBound":true} - Verify:
list_programsvia MCP should reflect the same empty account the sim now shows.
Requires the local dev server (npm run start:server) — the sign-in bypass and key-rebind
endpoint are Utils_isLocal()-gated, and globalThis.debugLogin exists only in __DEV__
builds. This signs the simulator out of whatever account it was on (that account's local data
stays on-device; a normal sign-in restores it) — leave the sim on the test account when done
and tell the user which account you left it on.
The core loop
- Set up the scenario via
liftosaur-localMCP (or by driving the UI). - Act in the UI via
ios-simulatorMCP — target bytestID, never pixel-guess. - Verify with
dump-rn-state.js: does the live state match what the screen shows and what you intended? A mismatch between control ↔ state ↔ intent is a bug. - Reset between cases (in-app undo/discard, or re-seed via MCP) so each test starts clean.
Nuclear option:
globalThis.debugLogin('<key>')again — brand-new empty account, MCP follows.
Tools
ios-simulator MCP (drive the UI)
get_booted_sim_idonce (bundle idcom.liftosaur.www).ui_find_element(["<testID>"])→ tap the returned frame center.testIDs surface asAXUniqueId. Prefer semantic targeting over coordinates.ui_view— cheap screenshot for a visual/state check.ui_describe_all— full a11y tree; use sparingly, it can exceed the token limit. If it's dumped to a file, pull the ids out:python3 -c "import re;print('\n'.join(dict.fromkeys(re.findall(r'\"AXUniqueId\":\"([^\"]+)\"',open('FILE').read()))))"ui_typeafter tapping a field;ui_swipeto scroll;launch_app(..., terminate_running:true)to relaunch. See mainCLAUDE.mdfor idb setup.
dump-rn-state.js (read live state)
node scripts/dump-rn-state.js storage.currentProgramId # dotted path
node scripts/dump-rn-state.js --eval "Object.keys(globalThis.state)" # arbitrary expr
node scripts/dump-rn-state.js --eval "globalThis.state.storage.settings.units"
Hermes' JSON.stringify drops undefined/functions, so you get clean IState. Useful roots:
storage (programs, settings, history, stats), progress (in-progress workout),
editProgramStates / editProgramExerciseStates (edit buffers). Needs Metro connected — a
"Fast Refresh disconnected" banner means it dropped; relaunch the app.
liftosaur-local MCP (set up the environment)
Same account as the sim. Read the DSL guide first — it is not guessable:
get_liftoscript_reference, then get_liftoscript_examples.
- Programs:
create_program/update_program(validate withrun_playgroundBEFORE saving),list_programs,get_program,delete_program. - Also: custom exercises/equipment/gyms,
create_history_record, measurements,set_exercise_data.
After creating/updating data via MCP, the device won't see it until it syncs — relaunch with
launch_app(bundle_id, terminate_running:true) to force a full sync, then switch to it in-app
(Me → Program) if needed.
Understand the target first
Before driving the UI, know the testIDs and the handlers behind the feature. Fastest path:
spawn an Explore agent to grep testID= in the relevant components and report each control,
its handler, and how it serializes/writes state (with file:line). Note handler asymmetries
(one path preserves a field, a sibling resets it) — those are prime bug candidates.
Native code lives in src/components/** (.tsx using RN primitives; check for .native.tsx
variants). State shapes are in src/types.ts and src/models/state.ts.
Gotchas (learned the hard way)
- Coordinates:
ui_find_elementframes are in full-scroll coords; taps use screen points. The tab bar sits at ~y≥762 and overlaps list rows — scroll the target above it before tapping, or you'll hit a footer tab. - Inputs commit on blur — after
ui_typeinto a numeric/text field, dismiss the keypad (its keyboard-icon) before dumping state, or the change won't be in state yet. - CodeMirror / inlined-HTML WebViews (e.g. the Liftoscript full-text editor) are ONE opaque a11y node — you can't tap inside. Set that content via the MCP instead.
- Reload to sync MCP-made data:
launch_app(..., terminate_running:true). - Verify the DATA, not just the screen — the screen can look right while state is wrong.
- Leave the account as you found it (or tell the user what you changed): switch the active program back, delete throwaway programs/history, and confirm nothing unintended was saved.
Reporting
When bug-hunting, write findings to lambda/scripts/bugs/<area>-<date>.md. Per bug: severity,
numbered repro steps, a before/after state dump proving it, any UI-vs-state mismatch, and the
file:line root cause. Reproduce from a clean state before reporting, and list what you verified
working too.
Example: program-editor QA
A worked scenario — reuse/repeat mechanics on the Edit Program screens — including a ready-made
fixture program and the exact bugs it surfaced, is in
lambda/scripts/bugs/program-editor-bugs-2026-07-01.md. Use it as a model for structuring a run.