Skip to content

fix(ios-runner): app-launch policy is decided by the retry flag; querySelector launches a stopped app #2890

Description

@thymikee

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-53 documents readOnly as "Whether the command is eligible for the session-invalidating retry". The struct comment at :44-48 presents the table as three independent axes.
  • Five runner-side decisions read that one flag: retry (RunnerTests+Lifecycle.swift:454), the stopped-app refusal notRunningReadResponse (:321-325), activation routing (RunnerTests+CommandDispatch.swift:445-451), recorded-failure→session invalidation (:536, jointly with isLifecycle), and remembered-text-entry tap clearing (RunnerTests+CommandExecution.swift:11).
  • querySelector is classified readOnly: .never (RunnerTests+Models.swift:116, comment at :114). Because the refusal and the activation both read the retry bit, a querySelector whose target app is .notRunning skips notRunningReadResponse and reaches activateTarget(bundleId:reason:"bundle_changed") at CommandDispatch.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. querySelector against a stopped app answers APP_NOT_RUNNING and stays non-retried.
  • Launch policy is its own declared fact — e.g. noApp | existingApp | mayLaunch — resolved in the same exhaustive CommandType.traits switch, 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, removing CommandTraits.ReadOnly.conditional and the alert-specific resolver at RunnerTests+Lifecycle.swift:465-473. Conditionality is per fact: alert get permits read retry, accept/dismiss do not, and every alert action skips app-activation preflight (RunnerTests+CommandDispatch.swift:549-551).
  • The activation bypass and the recorded-failure exemption stop sharing isLifecycle. The comment at RunnerTests+Models.swift:100-104 exists 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, querySelector against a stopped app returns APP_NOT_RUNNING, and a tap in the same state still activates.
  • ReadOnly.conditional and the Models.swift:100-104 comment are gone.
  • A new CommandType case cannot compile without classifying its launch policy.
  • pnpm check:affected --run and 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions