@@ -5,14 +5,17 @@ import {
55 type ProviderDeviceRuntime ,
66} from '../../../src/provider-device-runtime.ts' ;
77import type { DeviceInventoryProvider } from '../../../src/core/dispatch-resolve.ts' ;
8- import type { Interactor } from '../../../src/core/interactor-types.ts' ;
8+ import type { Interactor , RunnerContext } from '../../../src/core/interactor-types.ts' ;
99import type { LeaseLifecycleProvider } from '../../../src/daemon/handlers/lease.ts' ;
1010import type { DeviceLease } from '../../../src/daemon/lease-registry.ts' ;
1111import type { DaemonRequest } from '../../../src/daemon/types.ts' ;
1212import type { DeviceInfo } from '../../../src/kernel/device.ts' ;
1313import { createAppleInteractor } from '../../../src/platforms/apple/interactor.ts' ;
1414import type { RunnerCommand } from '../../../src/platforms/apple/core/runner/runner-contract.ts' ;
15- import type { AppleRunnerProvider } from '../../../src/platforms/apple/core/runner/runner-provider.ts' ;
15+ import type {
16+ AppleRunnerCommandOptions ,
17+ AppleRunnerProvider ,
18+ } from '../../../src/platforms/apple/core/runner/runner-provider.ts' ;
1619import { assertRpcOk } from './assertions.ts' ;
1720import { createProviderScenarioHarness , withProviderScenarioResource } from './harness.ts' ;
1821
@@ -27,6 +30,9 @@ const DEVICE: DeviceInfo = {
2730 booted : true ,
2831} ;
2932
33+ type RecordedRunnerCall = { command : RunnerCommand ; options : AppleRunnerCommandOptions } ;
34+ type RunnerTransportCalls = { runner : RecordedRunnerCall [ ] ; opens : number } ;
35+
3036// Issue #1297 acceptance path: a provider that only supplies an
3137// AppleRunnerProvider transport (plus its own `open`) reuses the SHARED Apple
3238// interactor — selector resolution, tap, fill, and snapshot all arrive at the
@@ -70,30 +76,73 @@ test('provider-supplied Apple runner transport reuses the shared interactor stac
7076 assertRpcOk (
7177 await daemon . callCommand ( 'click' , [ 'label="Provider Ready"' ] , request . flags , request ) ,
7278 ) ;
73- const tap = calls . commands . find ( ( command ) => command . command === 'tap' ) ;
79+ const tap = calls . runner . find ( ( call ) => call . command . command === 'tap' ) ;
7480 assert . ok ( tap , 'expected a runner-protocol tap on the provider transport' ) ;
7581 // The shared runtime resolved the selector against the transport's snapshot
7682 // and tapped the element center, whichever tap vehicle it picked.
77- assert . deepEqual ( { x : tap . x , y : tap . y } , { x : 60 , y : 30 } ) ;
83+ assert . deepEqual ( { x : tap . command . x , y : tap . command . y } , { x : 60 , y : 30 } ) ;
7884
7985 assertRpcOk (
8086 await daemon . callCommand ( 'fill' , [ 'label="Provider Input"' , 'hello' ] , request . flags , request ) ,
8187 ) ;
82- const fill = calls . commands . find (
83- ( command ) => command . command === 'type' && command . textEntryMode === 'replace' ,
88+ const fill = calls . runner . find (
89+ ( call ) => call . command . command === 'type' && call . command . textEntryMode === 'replace' ,
8490 ) ;
8591 assert . ok ( fill , 'expected a runner-protocol fill on the provider transport' ) ;
86- assert . equal ( fill . text , 'hello' ) ;
87- assert . deepEqual ( { x : fill . x , y : fill . y } , { x : 60 , y : 90 } ) ;
92+ assert . equal ( fill . command . text , 'hello' ) ;
93+ assert . deepEqual ( { x : fill . command . x , y : fill . command . y } , { x : 60 , y : 90 } ) ;
8894
89- const snapshots = calls . commands . filter ( ( command ) => command . command === 'snapshot' ) ;
95+ const snapshots = calls . runner . filter ( ( call ) => call . command . command === 'snapshot' ) ;
9096 assert . ok (
9197 snapshots . length >= 4 ,
9298 `expected shared snapshot runtime traffic on the transport, got ${ snapshots . length } ` ,
9399 ) ;
94100 } ) ;
95101} ) ;
96102
103+ // Daemon routes that issue runner commands OUTSIDE interactor methods (keyboard,
104+ // native alert, point read, iOS sequences) must reach the provider transport via
105+ // the request-boundary `appleRunnerProvider` scope instead of escaping to the
106+ // local XCTest runtime.
107+ test ( 'daemon direct runner routes reach the provider transport through the request scope' , async ( ) => {
108+ await withProviderScenarioResource ( createRunnerTransportWorld , async ( { daemon, calls } ) => {
109+ const lease = await allocateLease ( daemon ) ;
110+ const request = { flags : leaseFlags ( lease . leaseId ) , meta : leaseMeta ( lease . leaseId ) } ;
111+ assertRpcOk ( await daemon . callCommand ( 'open' , [ 'com.example.app' ] , request . flags , request ) ) ;
112+
113+ calls . runner . length = 0 ;
114+ assertRpcOk ( await daemon . callCommand ( 'keyboard' , [ 'dismiss' ] , request . flags , request ) ) ;
115+ const dismiss = calls . runner . find ( ( call ) => call . command . command === 'keyboardDismiss' ) ;
116+ assert . ok ( dismiss , 'expected keyboardDismiss on the provider transport, not local XCTest' ) ;
117+ } ) ;
118+ } ) ;
119+
120+ // Per-request RunnerContext threading: the daemon builds the interactor per
121+ // request, so runner calls carry the request's id (cancellation/accounting)
122+ // instead of the construction-time context.
123+ test ( 'provider transport runner calls carry the per-request id' , async ( ) => {
124+ await withProviderScenarioResource ( createRunnerTransportWorld , async ( { daemon, calls } ) => {
125+ const lease = await allocateLease ( daemon ) ;
126+ const flags = leaseFlags ( lease . leaseId ) ;
127+ const openMeta = { ...leaseMeta ( lease . leaseId ) , requestId : 'req-open-1' } ;
128+ assertRpcOk ( await daemon . callCommand ( 'open' , [ 'com.example.app' ] , flags , { meta : openMeta } ) ) ;
129+
130+ calls . runner . length = 0 ;
131+ const clickMeta = { ...leaseMeta ( lease . leaseId ) , requestId : 'req-click-1' } ;
132+ assertRpcOk (
133+ await daemon . callCommand ( 'click' , [ 'label="Provider Ready"' ] , flags , { meta : clickMeta } ) ,
134+ ) ;
135+ assert . ok ( calls . runner . length >= 1 , 'expected runner traffic for the click request' ) ;
136+ for ( const call of calls . runner ) {
137+ assert . equal (
138+ call . options . requestId ,
139+ 'req-click-1' ,
140+ `runner ${ call . command . command } lost the per-request id` ,
141+ ) ;
142+ }
143+ } ) ;
144+ } ) ;
145+
97146async function allocateLease (
98147 daemon : Awaited < ReturnType < typeof createProviderScenarioHarness > > ,
99148) : Promise < DeviceLease > {
@@ -104,7 +153,7 @@ async function allocateLease(
104153}
105154
106155async function createRunnerTransportWorld ( ) {
107- const calls = { commands : [ ] as RunnerCommand [ ] , opens : 0 } ;
156+ const calls : RunnerTransportCalls = { runner : [ ] , opens : 0 } ;
108157 const runtime = createProviderRuntime ( calls ) ;
109158 const providers = createProviderDeviceRuntimeRequestProviders ( [ runtime ] ) ;
110159 const daemon = await createProviderScenarioHarness ( {
@@ -121,11 +170,13 @@ async function createRunnerTransportWorld() {
121170 } ;
122171}
123172
124- function createProviderRuntime ( calls : {
125- commands : RunnerCommand [ ] ;
126- opens : number ;
127- } ) : ProviderDeviceRuntime {
128- const interactor = createRunnerTransportInteractor ( calls ) ;
173+ function createProviderRuntime ( calls : RunnerTransportCalls ) : ProviderDeviceRuntime {
174+ const transport : AppleRunnerProvider = {
175+ runCommand : async ( _device , command , options ) => {
176+ calls . runner . push ( { command, options } ) ;
177+ return runnerResultFor ( command ) ;
178+ } ,
179+ } ;
129180 const leaseLifecycle : LeaseLifecycleProvider = {
130181 allocate : async ( lease ) =>
131182 lease . leaseProvider === PROVIDER ? { provider : PROVIDER , deviceId : DEVICE . id } : undefined ,
@@ -137,23 +188,22 @@ function createProviderRuntime(calls: {
137188 leaseLifecycle,
138189 deviceInventoryProvider,
139190 ownsDevice : ( device ) => device . id === DEVICE . id ,
140- getInteractor : ( device ) => ( device . id === DEVICE . id ? interactor : undefined ) ,
191+ getInteractor : ( device , runnerContext ) =>
192+ device . id === DEVICE . id
193+ ? createRunnerTransportInteractor ( calls , transport , runnerContext )
194+ : undefined ,
195+ getAppleRunnerProvider : ( device ) => ( device . id === DEVICE . id ? transport : undefined ) ,
141196 shutdown : async ( ) => undefined ,
142197 } ;
143198}
144199
145- function createRunnerTransportInteractor ( calls : {
146- commands : RunnerCommand [ ] ;
147- opens : number ;
148- } ) : Interactor {
149- const transport : AppleRunnerProvider = {
150- runCommand : async ( _device , command ) => {
151- calls . commands . push ( command ) ;
152- return runnerResultFor ( command ) ;
153- } ,
154- } ;
200+ function createRunnerTransportInteractor (
201+ calls : RunnerTransportCalls ,
202+ transport : AppleRunnerProvider ,
203+ runnerContext : RunnerContext | undefined ,
204+ ) : Interactor {
155205 return {
156- ...createAppleInteractor ( DEVICE , { } , transport ) ,
206+ ...createAppleInteractor ( DEVICE , runnerContext ?? { } , transport ) ,
157207 // App lifecycle stays provider-owned: the transport seam covers runner
158208 // commands only, so the provider composes its own `open` on top.
159209 open : async ( ) => {
0 commit comments