Repository navigation
fix(ios-runner): app-launch policy is decided by the retry flag; querySelector launches a stopped app #2890
Description
Purpose
CommandTraits.readOnly is documented as one axis and consumed as several, so opting a command out of one decision silently opts it out of the others. querySelector is the live instance: it is deliberately not retried, and as a side effect it is no longer refused when its app is stopped, so it bare-launches the app instead of answering APP_NOT_RUNNING.
This issue separates app-launch policy from retry eligibility inside the existing exhaustive classification, and fixes the command's behavior.
Citations are at f47e9c9ef4.
Current behavior
RunnerTests+Models.swift:52-53documentsreadOnlyas "Whether the command is eligible for the session-invalidating retry". The struct comment at:44-48presents the table as three independent axes.- Five runner-side decisions read that one flag: retry (
RunnerTests+Lifecycle.swift:454), the stopped-app refusalnotRunningReadResponse(:321-325), activation routing (RunnerTests+CommandDispatch.swift:445-451), recorded-failure→session invalidation (:536, jointly withisLifecycle), and remembered-text-entry tap clearing (RunnerTests+CommandExecution.swift:11). querySelectoris classifiedreadOnly: .never(RunnerTests+Models.swift:116, comment at:114). Because the refusal and the activation both read the retry bit, aquerySelectorwhose target app is.notRunningskipsnotRunningReadResponseand reachesactivateTarget(bundleId:reason:"bundle_changed")atCommandDispatch.swift:451. fix(ios): never launch a not-running session app from an observation #2852 established that a runner read must not launch a stopped app.- TypeScript classifies the same command read-only (
packages/platform-apple/src/runner/runner-command-traits.ts:65).
Required behavior
- A read command never causes a bare launch.
querySelectoragainst a stopped app answersAPP_NOT_RUNNINGand stays non-retried. - Launch policy is its own declared fact — e.g.
noApp | existingApp | mayLaunch— resolved in the same exhaustiveCommandType.traitsswitch, and the not-running refusal lives under it. A change to retry eligibility must not be able to change it. - Payload-dependent traits resolve once against
Command, removingCommandTraits.ReadOnly.conditionaland thealert-specific resolver atRunnerTests+Lifecycle.swift:465-473. Conditionality is per fact:alertgetpermits read retry,accept/dismissdo not, and everyalertaction skips app-activation preflight (RunnerTests+CommandDispatch.swift:549-551). - The activation bypass and the recorded-failure exemption stop sharing
isLifecycle. The comment atRunnerTests+Models.swift:100-104exists only to explain that collision and should be deleted by this change. - Five named wrappers over one retry bit do not satisfy this issue. The coupling to break is that launch policy reads the retry bit.
Out of scope
Do not force the two trait tables equal. They are homonyms, not twins: TS readOnly also gates readiness preflight (runner-session.ts:1205), waitForRunner versus a single send (:1034-1045), and transport recovery (runner-command-recovery.ts:247,322). querySelector and uptime deliberately allow the TS resend while forbidding the Swift session-loss retry. Genuinely shared facts belong in the table that already exists for them (contracts/fixtures/alert-command-traits.json, read by both RunnerTests+LifecycleTests.swift and packages/platform-apple/src/runner/__tests__/runner-command-traits.test.ts); everything else stays per-side.
Completion conditions
- One per-command table asserts every runner-side consumer together: retry, not-running refusal, activation route, recorded-failure conversion.
- On a live simulator,
querySelectoragainst a stopped app returnsAPP_NOT_RUNNING, and atapin the same state still activates. ReadOnly.conditionaland theModels.swift:100-104comment are gone.- A new
CommandTypecase cannot compile without classifying its launch policy. pnpm check:affected --runand the runner XCTest lanes pass on the pushed commit.
Dependencies
Found by the #2803 audit. Related: #2881 (request shapes, not trait semantics), #2852, #2862.
Activity
test(ios-e2e): relaunch the deep link a confirmation held
docs(ios-runner): scope the existing-app refusal to where it is enforced
fix(ios-runner): keep the presented surface's route for no-app comman…