-
-
Notifications
You must be signed in to change notification settings - Fork 313
feat(ios): appstate reads the session app's XCUIApplication state through the runner #2929
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
thymikee
merged 1 commit into
callstack:main
from
okwasniewski:oskar/ios-appstate-runner
Sep 25, 2026
+554
−29
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,22 @@ | ||
| import type { AppleApplicationState } from '@agent-device/kernel/snapshot'; | ||
|
|
||
| export type { AppleApplicationState } from '@agent-device/kernel/snapshot'; | ||
|
|
||
| /** Which app a session-scoped read is about; the Android foreground read needs nothing. */ | ||
| export type AppStateRuntimeInput = Readonly<{ appBundleId?: string }>; | ||
|
|
||
| /** Neutral foreground identity returned by a selected platform/provider runtime. */ | ||
| export type AppStateRuntimeResult = Readonly<{ | ||
| package?: string; | ||
| activity?: string; | ||
| /** | ||
| * Apple: how the app named by the input is running, as a live runner reads it. It says nothing | ||
| * about which app is frontmost; a session app in a background state has left the foreground. | ||
| * Absent when no runner session is live to ask, so the read never starts one. | ||
| */ | ||
| applicationState?: AppleApplicationState; | ||
| }>; | ||
|
|
||
| export type AppStateRuntimeOperations = Readonly<{ | ||
| appState(): Promise<AppStateRuntimeResult>; | ||
| appState(input?: AppStateRuntimeInput): Promise<AppStateRuntimeResult>; | ||
| }>; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import { expect, test, vi } from 'vitest'; | ||
| import type { Interactor } from '@agent-device/contracts/interactor-types'; | ||
| import type { DeviceInfo } from '@agent-device/kernel/device'; | ||
| import { bindAppleAppStateRuntime } from './app-state-runtime.ts'; | ||
|
|
||
| const device: DeviceInfo = { | ||
| platform: 'apple', | ||
| appleOs: 'ios', | ||
| id: 'sim-1', | ||
| name: 'iPhone 17 Pro', | ||
| kind: 'simulator', | ||
| target: 'mobile', | ||
| booted: true, | ||
| }; | ||
|
|
||
| function bind(liveRunner: boolean) { | ||
| const appState = vi.fn(async () => ({ applicationState: 'runningBackground' as const })); | ||
| const resolveInteractor = vi.fn(async () => ({ appState }) as unknown as Interactor); | ||
| const hasLiveRunnerSession = vi.fn(async () => liveRunner); | ||
| const operations = bindAppleAppStateRuntime( | ||
| { appleApplications: { hasLiveRunnerSession } as never }, | ||
| { device, signal: new AbortController().signal, resolveInteractor }, | ||
| ); | ||
| return { operations, appState, resolveInteractor, hasLiveRunnerSession }; | ||
| } | ||
|
|
||
| test('a live runner session answers the session app state through the interactor', async () => { | ||
| const { operations, appState, resolveInteractor } = bind(true); | ||
| await expect(operations.appState({ appBundleId: 'com.example.app' })).resolves.toEqual({ | ||
| applicationState: 'runningBackground', | ||
| }); | ||
| expect(appState).toHaveBeenCalledTimes(1); | ||
| expect(resolveInteractor).toHaveBeenCalledWith( | ||
| device, | ||
| expect.objectContaining({ appBundleId: 'com.example.app' }), | ||
| ); | ||
| }); | ||
|
|
||
| test('without a live runner session the read answers nothing and resolves no interactor', async () => { | ||
| // Resolving the interactor is what would start a runner; a session-state read never does. | ||
| const { operations, resolveInteractor, hasLiveRunnerSession } = bind(false); | ||
| await expect(operations.appState({ appBundleId: 'com.example.app' })).resolves.toEqual({}); | ||
| expect(hasLiveRunnerSession).toHaveBeenCalledWith(device, {}); | ||
| expect(resolveInteractor).not.toHaveBeenCalled(); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import type { | ||
| AppStateRuntimeInput, | ||
| AppStateRuntimeOperations, | ||
| AppStateRuntimeResult, | ||
| } from '@agent-device/contracts/app-state-runtime'; | ||
| import type { Interactor, RunnerContext } from '@agent-device/contracts/interactor-types'; | ||
| import type { PlatformRuntimeHost } from '@agent-device/contracts/platform-runtime-operations'; | ||
| import type { DeviceInfo } from '@agent-device/kernel/device'; | ||
| import { AppError } from '@agent-device/kernel/errors'; | ||
|
|
||
| /** Resolves the selected owner's interactor, exactly as the element text runtime does. */ | ||
| export type AppStateInteractorResolver = ( | ||
| device: DeviceInfo, | ||
| runner: RunnerContext, | ||
| ) => Promise<Interactor>; | ||
|
|
||
| /** | ||
| * Binds the runner's read of the session app's state for the lifetime of a request binding, on the | ||
| * `Interactor` seam the point reads use. The read never starts a runner: with no live runner session | ||
| * (a bridge simulator right after `open`, an idle-stopped runner, a device whose runner is down) it | ||
| * answers nothing and the session record alone answers upstream. That is what keeps `appState` on | ||
| * the simulator host in the runner-demand table, since it demands no runner of its own. | ||
| */ | ||
| export function bindAppleAppStateRuntime( | ||
| host: Pick<PlatformRuntimeHost, 'appleApplications'>, | ||
| params: Readonly<{ | ||
| device: DeviceInfo; | ||
| signal: AbortSignal; | ||
| resolveInteractor: AppStateInteractorResolver; | ||
| }>, | ||
| ): AppStateRuntimeOperations { | ||
| return Object.freeze({ | ||
| appState: async (input?: AppStateRuntimeInput): Promise<AppStateRuntimeResult> => { | ||
| params.signal.throwIfAborted(); | ||
| if (!(await host.appleApplications.hasLiveRunnerSession(params.device, {}))) return {}; | ||
| const interactor = await params.resolveInteractor(params.device, { | ||
| appBundleId: input?.appBundleId, | ||
| signal: params.signal, | ||
| }); | ||
| if (typeof interactor.appState !== 'function') { | ||
| // Facts advertised the read but the interactor cannot perform it: a contract bug (ADR 0019 | ||
| // §2), not a refusal, so nothing upstream may answer from the session record instead. | ||
| throw new AppError( | ||
| 'COMMAND_FAILED', | ||
| 'Runtime owner advertised appState without an interactor implementation', | ||
| { | ||
| reason: 'runtime-contract-invalid', | ||
| hint: 'This is an agent-device runtime contract bug; report the selected device and command.', | ||
| }, | ||
| ); | ||
| } | ||
| return await interactor.appState(); | ||
| }, | ||
| }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[claude-fable-5-1] responding on behalf of Oskar
Done in d8c243f:
sourceis an enum ofsessionandrunner,stateis theAPPLE_APPLICATION_STATESenum, andsrc/mcp/__tests__/command-tools-appstate-schema.test.tsvalidates both iOS answers and the Android one and rejects an undeclared state.