Skip to content

fix(ios): report the alert's own title and surface an unanswered launch confirmation - #3732

Open
thymikee wants to merge 9 commits into
mainfrom
maint/3702-launch-confirmation-title
Open

thymikee wants to merge 9 commits into
mainfrom
maint/3702-launch-confirmation-title

Conversation

@thymikee

@thymikee thymikee commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Summary

On iOS 17.5, open --launch-url left SpringBoard's first-deep-link confirmation unanswered and exited 0 with no launchConfirmation.

  1. Runner title read: preferredAlertTitle walked every descendant of the resolution root, so root-carried host chrome (the Home Screen page readout, labelled "0 pages" on 17.5) became the reported title and the launch-title rule refused it. It now reads the alert's own static texts first, then its own label; the walk stays as fallback.
  2. Silent exits are reported: one rule (applyAttemptReport) maps the final answer attempt to what the settle reports. A seen confirmation returns launchConfirmation: "unanswered" with typed launchConfirmationReason plus a reason-specific warning; the discriminated arms forbid a probe failure beside a seen outcome. When a probe step failed or could not be verified (alert read, accept, or runner — latest attempt wins over an earlier accepted), the response carries a typed launchConfirmationProbeFailure step plus a check-with-alert get warning — deliberately no launchConfirmation: the state is unknown, not observed. Proven-failed accepts still fail with launch_confirmation_unanswered. The read trims the iOS alert resolution reads every element of the app window when a popover is found #2905 full walk on the common path but does not close it. CHANGELOG and commands.md document the widened fields.

Part of #3702 — the 17.5 accepted-prompt proof is still owed; the issue stays open.

Validation

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.32 MB 5.33 MB +5.4 kB
Package (unpacked) 5.32 MB 5.33 MB +5.4 kB
Package (download) 1.60 MB 1.60 MB +1.6 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 23.3 ms 22.7 ms -0.5 ms
CLI --help 63.7 ms 64.2 ms +0.5 ms

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 18 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/__tests__/agent-device-client-apps.test.ts Outdated
Comment thread packages/platform-apple/src/open-policy.ts Outdated
Comment thread packages/contracts/src/application-lifecycle-runtime.ts Outdated
Comment thread packages/platform-apple/src/open-policy.ts Outdated
Comment thread src/client/client-normalizers.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/platform-apple/src/open-policy.ts Outdated
Comment thread packages/platform-apple/src/open-policy.ts Outdated
Comment thread packages/contracts/src/application-lifecycle-runtime.ts
Comment thread src/commands/management/app.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 6 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/contracts/src/application-lifecycle-runtime.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 4 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/platform-apple/src/open-policy.ts Outdated
Comment thread packages/platform-apple/src/open-policy.test.ts Outdated
@thymikee

Copy link
Copy Markdown
Member Author

The code at b55d10a looks sound, but the claim "Fixes #3702" is not proven yet. I found no run on iOS 17.5, or any 17.x, so the fix is unverified for the reported case.

The regression fixture (--agent-device-alert-title-page-chrome, in AgentDeviceRunnerApp.m) assumes a fixed tree where "0 pages" is a non-static-text element inside the dismiss-popup window root. #3702 says otherwise: on 17.5 a later alert get read the correct title through the same route (get -> resolveBlockingSystemModal -> springboard alerts -> preferredAlertTitle, see https://github.com/callstack/agent-device/blob/b55d10a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift#L52). So the misread happens only during presentation and depends on timing. If "0 pages" is a static text at that moment, or the title is not in the tree yet, the static-text-first read still reports "0 pages", and the fixture cannot tell these cases apart. On 17.5, open --launch-url may still leave the prompt unanswered. The new unanswered report now flags that instead of passing silently, but accepting the prompt on 17.5 is not shown. Please run this on an iOS 17.x simulator (17.5 preferred) at b55d10a: fresh simulator, simctl install, then the first open <app> --launch-url <scheme>://… --relaunch --json --debug. The proof is launchConfirmation: "accepted" and no ios_launch_confirmation_unanswered line in the request log, next to the same command at base 77137b0 showing title: "0 pages". Please also tie the iOS 27.1 accepted run and the French-locale unanswered run in the PR body to a SHA. If no 17.x runtime is available, change "Fixes #3702" to "Part of #3702" and keep the issue open for the 17.5 proof.

Not blocking, and fine to take or leave: LaunchConfirmation has shipped since v0.21.20 and now widens to "unanswered" with launchConfirmationReason, so CHANGELOG Unreleased and website/docs/docs/commands.md:100 need an entry; the typed probe-failure step (alert-read, alert-accept, runner) ends in warning prose in session-open-execution.ts:358 and has no field on AppOpenResult; the test.each at lifecycle-launch-confirmation.test.ts:183 has one row that always sets probeFailure, so the else branch at line 208 never runs and a plain test would do; alert-unrecognized fires for any alert seen while the app is not observable, yet the doc at application-lifecycle-runtime.ts:172 says "a system confirmation the launch itself raised", so reword it to "an alert seen in place of the launch"; and could AppOpenResult (client-app.ts:124) reuse the discriminated LaunchConfirmationOutcome instead of flat optional fields that launchConfirmationReport copies?

CI is red on all four failing jobs (Lint/Typecheck, Repo Guards, Integration, both Smoke) with linux.gap is not a function / TS2339 at test/integration/command-coverage/declarations.ts:558. This PR does not touch that file, and main fails the same way, with the fix pending in #3740. Because of that, the iOS simulator smoke and the integration iOS e2e files ran 0 scenarios, so CI says nothing about the changed open and alert route either way. No conflicts. I did not run the Swift or TS suites. Whether the fixture fails at base and passes at head was judged by reading the code. I also could not observe the real 17.5 alert tree during presentation.

Before merge, the 17.x first deep-link open at b55d10a needs to show launchConfirmation: "accepted", or the PR needs the "Part of #3702" wording. After that, please rebase once #3740 lands so the iOS smoke actually runs.

…ch confirmation

The runner read the alert title by walking every descendant of the resolution
root, so a SpringBoard confirmation whose root carries host chrome (a page
readout on iOS 17.5) reported that label instead of the alert's title, and the
launch-confirmation title rule refused it. The title now reads the alert's own
static texts, then its own label.

An unanswered launch confirmation now reaches the caller: open reports
launchConfirmation 'unanswered' with its typed reason and a warning pointing at
alert accept, instead of exiting 0 with no field.
The outcome and its reason travel as one discriminated report from the settle
through the daemon and client, so no printing site invents a fallback token. An
unrecognized alert warns to inspect it before answering; an unresolved URL
owner keeps the direct alert-accept guidance. The runner title read keeps the
labelled-descendant walk as the fallback after the static-text and own-label
reads.
…ir reasons

A hung or failing URL-owner lookup left the recognized prompt on screen while the
open succeeded silently; it now reports the same url-owner-unresolved outcome as
an unresolved owner, whose warning says the owner could not be resolved rather
than claiming none exists. The client reads the unanswered reason only for an
unanswered outcome, OpenApplicationOutcome derives its variants from
LaunchConfirmationOutcome, and the tests cover both reasons at the client and
daemon seams.
An unreadable alert read or an unresolvable runner left open silent about a
prompt that may still be up. The outcome now carries the failed step as a typed
launchConfirmationProbeFailure on the absent arm, and the daemon appends a
warning telling the caller to check with alert get. No launchConfirmation value
is invented: the state is unknown, not unanswered. The launch-URL confirmation
tests also split out of lifecycle.test.ts to stay under the test-size tripwire.
Every attempt now states the latest the probe knows: a failed accept read-back
or an unreadable probe after a re-hand replaces an earlier accepted with the
unknown prompt state, and a completed probe that sees nothing resolves the
unknown. The seen arms of the contract forbid a probe failure beside a
confirmation, and the versioned help documents the warnings for both unanswered
reasons and for an unreadable probe.
An unreadable alert accept was invoked and could not be verified, so "could not
run" was false for that step. The type and its comments now say the step failed
or its result could not be verified, which holds for every step.
The exception lived implied in two comments and contradicted the lookup case
they described. applyAttemptReport now owns the sentence: alert-read,
alert-accept and runner settle to the unknown state, while url-owner reports
the seen url-owner-unresolved. The settle doc and the test-table header point
at that rule instead of restating it.
…s arms client-side

The typed step named the failed read but ended as prose in a warning, so a
machine reader could not tell "no prompt" from "prompt state unreadable". It
now travels on the response beside its warning: the report builder passes the
absent arm through, buildOpenResult takes either arm, the normalizer reads the
step, the serializer carries it, and AppOpenResult reuses the discriminated
outcome union instead of flat optional fields.

Also: the LaunchConfirmation doc names it an alert seen in place of the
launch, the single-row runner-step test.each becomes a plain test, and the
CHANGELOG and commands.md document the widened launchConfirmation.
@thymikee
thymikee force-pushed the maint/3702-launch-confirmation-title branch from b55d10a to c7e29d8 Compare October 16, 2026 19:17
@github-actions

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://callstack.github.io/agent-device/pr-preview/pr-3732/

Built to branch gh-pages at 2026-10-16 19:19 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@thymikee

Copy link
Copy Markdown
Member Author

Rebased onto main after #3740 (clean, one Swift test-file conflict with #3715 resolved by keeping both tests) and pushed c7e29d8 with your non-blocking items. Point by point:

17.x proof. No 17.x runtime is installable or downloadable here (xcodebuild -downloadPlatform iOS -buildVersion 17.5 reports unavailable), so the body now says "Part of #3702" and states the 17.5 accepted-prompt proof is owed, keeping the issue open. Your fixture-timing concern is fair and untestable without a 17.x device: the fixture proves the static-text-first read beats labelled chrome when the title is in the tree, not that it survives a not-yet-populated tree on 17.5. Both live runs are now re-run at c7e29d8 and tied in the body: fresh iPhone-Duo iOS 27.1 sim + first install → launchConfirmation: "accepted" with no ios_launch_confirmation_unanswered line in the request log; French-locale 27.1 sim → unanswered + alert-unrecognized + warning, alert get reads the localized title.

CI / linux.gap. Agreed at the time (I hit the identical TS2339 on an unmodified base worktree at 77137b0 and recorded it as main-caused). #3740 is merged and the branch is rebased onto it; the local gate at c7e29d8 passes with exactCommit: true, so the iOS smoke and integration lanes now actually run.

Swift conflict verification. Both alert unit tests — testAlertTitleIsTheAlertsOwnTextNotLabelledChromeInsideTheRoot (this PR) and testAlertDismissOnADismissPopupWindowChoosesThePopoverCloseButton (#3715) — compile and pass together via xcodebuild test-without-building on an iOS 27.0 simulator.

CHANGELOG + commands.md. Added. commands.md gains the unanswered + launchConfirmationReason semantics and the launchConfirmationProbeFailure shape; the stale "returns as if none appeared" bullet is cut down to the still-true after-observed case.

Probe failure on AppOpenResult. Taken, and it was contained: the daemon's report builder now passes the absent arm through (launchConfirmationProbeFailure lands on the response beside its warning), buildOpenResult accepts either arm, the client normalizer reads the typed step, serializeOpenResult carries it, and AppOpenResult is now AppOpenResultBase & (LaunchConfirmationOutcome | LaunchConfirmationAbsent) — the flat optional fields are gone. open --help names the new field. Help/versioned-text and client tests cover it.

test.each → plain test. Done; the dead else branch is gone.

Doc reword. LaunchConfirmation now reads "an alert seen in place of the launch".

Ready for the 17.5 lane whenever someone can run it; everything else above is at c7e29d8.

@thymikee

Copy link
Copy Markdown
Member Author

Correction to the live-evidence paragraph above: for the accepted run at c7e29d8 I could not show the request log's absence of an ios_launch_confirmation_unanswered line — the per-request log file is not persisted once the session closes (the run's requestLogPath pointed at a file that no longer exists at inspection time). The claim stands on the response itself: success: true, launchConfirmation: "accepted", warnings absent — the unanswered line is only written when that reason path runs, which the response proves it did not. The body wording reflects this.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant