agent-device 0.14.7 → 0.14.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +119 -9
  2. package/android-snapshot-helper/README.md +4 -2
  3. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.14.7.apk → agent-device-android-snapshot-helper-0.14.9.apk} +0 -0
  4. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.14.9.apk.sha256 +1 -0
  5. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.14.7.manifest.json → agent-device-android-snapshot-helper-0.14.9.manifest.json} +6 -6
  6. package/dist/src/180.js +1 -1
  7. package/dist/src/208.js +1 -0
  8. package/dist/src/221.js +3 -3
  9. package/dist/src/6108.js +26 -0
  10. package/dist/src/7462.js +1 -0
  11. package/dist/src/7719.js +1 -0
  12. package/dist/src/9542.js +2 -2
  13. package/dist/src/9639.js +2 -2
  14. package/dist/src/9671.js +1 -0
  15. package/dist/src/9818.js +1 -1
  16. package/dist/src/android-adb.d.ts +11 -2
  17. package/dist/src/android-snapshot-helper.d.ts +12 -2
  18. package/dist/src/cli.js +82 -0
  19. package/dist/src/command-schema.js +382 -0
  20. package/dist/src/contracts.d.ts +1 -0
  21. package/dist/src/finders.d.ts +1 -0
  22. package/dist/src/index.d.ts +6 -0
  23. package/dist/src/index.js +1 -1
  24. package/dist/src/internal/bin.js +2 -461
  25. package/dist/src/internal/daemon.js +20 -20
  26. package/dist/src/io.js +1 -1
  27. package/dist/src/remote-config.js +1 -1
  28. package/dist/src/selectors.d.ts +1 -0
  29. package/dist/src/server.js +20 -0
  30. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +86 -13
  31. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +160 -93
  32. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +1 -0
  33. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +3 -0
  34. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +15 -0
  35. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +1 -0
  36. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TvRemote.swift +185 -0
  37. package/package.json +33 -6
  38. package/server.json +21 -0
  39. package/skills/agent-device/SKILL.md +11 -1
  40. package/skills/dogfood/SKILL.md +3 -1
  41. package/smithery.yaml +1 -0
  42. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.14.7.apk.sha256 +0 -1
  43. package/dist/src/2007.js +0 -26
  44. package/skills/react-devtools/SKILL.md +0 -48
package/dist/src/io.js CHANGED
@@ -1 +1 @@
1
- import{promises as t}from"node:fs";import e from"node:os";import i from"node:path";import{AppError as a}from"./9152.js";function r(n={}){let l=n.cwd??process.cwd(),c=n.tempDir??e.tmpdir(),p=n.rootDir?o(n.rootDir,l):void 0;return{resolveInput:async t=>{if("uploadedArtifact"===t.kind)throw new a("UNSUPPORTED_OPERATION","Uploaded artifact inputs require a custom artifact adapter");return{path:o(t.path,l,p)}},reserveOutput:async(e,a)=>{let r,n=a.visibility??"client-visible",d=e?.kind==="path"?o(e.path,l,p):i.join(r=await t.mkdtemp(i.join(c,`agent-device-${a.field}-`)),`${a.field}${a.ext}`);return await t.mkdir(i.dirname(d),{recursive:!0}),{path:d,visibility:n,...r?{cleanup:async()=>{await t.rm(r,{recursive:!0,force:!0})}}:{},publish:async()=>e?.kind==="downloadableArtifact"?{kind:"localPath",field:a.field,path:d,fileName:e.fileName??i.basename(e.clientPath??d)}:void 0}},createTempFile:async e=>{let a=await t.mkdtemp(i.join(c,`${e.prefix}-`));return{path:i.join(a,`file${e.ext}`),visibility:"internal",cleanup:async()=>{await t.rm(a,{recursive:!0,force:!0})}}}}}function o(t,e,r){var o,n;let l,c=i.isAbsolute(t)?t:i.resolve(e,t);if(r&&(o=c,n=r,""!==(l=i.relative(n,o))&&(l.startsWith("..")||i.isAbsolute(l))))throw new a("INVALID_ARGS","Local path is outside the artifact adapter root",{path:c,rootDir:r});return c}export{r as createLocalArtifactAdapter};
1
+ export{createLocalArtifactAdapter}from"./7719.js";
@@ -1 +1 @@
1
- import e from"node:fs";import t from"node:path";import{AppError as n}from"./9152.js";import{resolveUserPath as r}from"./3267.js";let o=new Set(["1","true","yes","on"]),i=new Set(["0","false","no","off"]);function a(e){return`AGENT_DEVICE_${e.replace(/([A-Z])/g,"_$1").replace(/[^A-Za-z0-9_]/g,"_").toUpperCase()}`}function s(e,t,r,o){if(e.multiple)return(Array.isArray(t)?t:[t]).map(t=>s({...e,multiple:!1},t,r,o));if("boolean"===e.type){var i=t,a=r,u=o;if("boolean"==typeof i)return i;if("string"==typeof i){let e=l(i);if(void 0!==e)return e}throw new n("INVALID_ARGS",`Invalid value for "${u}" in ${a}. Expected boolean.`)}if("booleanOrString"===e.type){if("boolean"==typeof t)return t;if("string"==typeof t&&void 0!==l(t))return l(t);if("string"==typeof t&&t.trim().length>0)return t;throw new n("INVALID_ARGS",`Invalid value for "${o}" in ${r}. Expected boolean or non-empty string.`)}if("string"===e.type){if("string"==typeof t&&t.trim().length>0)return t;throw new n("INVALID_ARGS",`Invalid value for "${o}" in ${r}. Expected non-empty string.`)}if("enum"===e.type){if(void 0!==e.setValue){var y=e,f=t,m=r,p=o;let i=y.setValue;if(f===i)return i;if("string"==typeof f){let e=f.trim();if(""===e||"true"===e||"1"===e)return i;if("false"===e||"0"===e)return}if(!0===f)return i;if(!1!==f)throw new n("INVALID_ARGS",`Invalid value for "${p}" in ${m}. Expected boolean-like value for enum flag.`);return}if("string"!=typeof t||!e.enumValues?.includes(t))throw new n("INVALID_ARGS",`Invalid value for "${o}" in ${r}. Expected one of: ${e.enumValues?.join(", ")}.`);return t}let c="number"==typeof t?t:"string"==typeof t?Number(t):NaN;if(!Number.isFinite(c)||!Number.isInteger(c))throw new n("INVALID_ARGS",`Invalid value for "${o}" in ${r}. Expected integer.`);if("number"==typeof e.min&&c<e.min)throw new n("INVALID_ARGS",`Invalid value for "${o}" in ${r}. Must be >= ${e.min}.`);if("number"==typeof e.max&&c>e.max)throw new n("INVALID_ARGS",`Invalid value for "${o}" in ${r}. Must be <= ${e.max}.`);return c}function l(e){let t=e.trim().toLowerCase();return!!o.has(t)||!i.has(t)&&void 0}let u=[{key:"stateDir",type:"string",path:!0},{key:"daemonBaseUrl",type:"string"},{key:"daemonAuthToken",type:"string"},{key:"daemonTransport",type:"enum",enumValues:["auto","socket","http"]},{key:"daemonServerMode",type:"enum",enumValues:["socket","http","dual"]},{key:"tenant",type:"string"},{key:"sessionIsolation",type:"enum",enumValues:["none","tenant"]},{key:"runId",type:"string"},{key:"leaseId",type:"string"},{key:"leaseBackend",type:"enum",enumValues:["ios-simulator","ios-instance","android-instance"]},{key:"platform",type:"enum",enumValues:["ios","macos","android","linux","apple"]},{key:"target",type:"enum",enumValues:["mobile","tv","desktop"]},{key:"device",type:"string"},{key:"udid",type:"string"},{key:"serial",type:"string"},{key:"iosSimulatorDeviceSet",type:"string",path:!0,legacyEnvNames:["IOS_SIMULATOR_DEVICE_SET"]},{key:"androidDeviceAllowlist",type:"string",legacyEnvNames:["ANDROID_DEVICE_ALLOWLIST"]},{key:"session",type:"string"},{key:"metroProjectRoot",type:"string",path:!0},{key:"metroKind",type:"enum",enumValues:["auto","react-native","expo"]},{key:"metroPublicBaseUrl",type:"string"},{key:"metroProxyBaseUrl",type:"string"},{key:"metroBearerToken",type:"string",legacyEnvNames:["AGENT_DEVICE_PROXY_TOKEN"]},{key:"metroPreparePort",type:"int",min:1,max:65535},{key:"metroListenHost",type:"string"},{key:"metroStatusHost",type:"string"},{key:"metroStartupTimeoutMs",type:"int",min:1},{key:"metroProbeTimeoutMs",type:"int",min:1},{key:"metroRuntimeFile",type:"string",path:!0},{key:"metroNoReuseExisting",type:"boolean"},{key:"metroNoInstallDeps",type:"boolean"}],y=new Map(u.map(e=>[e.key,e]));function f(e){let t=e.env??process.env;return r(e.configPath,{cwd:e.cwd,env:t})}function m(o){let i=function(o){let i,a,l=o.env??process.env,u=f(o);if(!e.existsSync(u))throw new n("INVALID_ARGS",`Remote config file not found: ${u}`);try{i=e.readFileSync(u,"utf8")}catch(e){throw new n("INVALID_ARGS",`Failed to read remote config file: ${u}`,{cause:e instanceof Error?e.message:String(e)})}try{a=JSON.parse(i)}catch(e){throw new n("INVALID_ARGS",`Invalid JSON in remote config file: ${u}`,{cause:e instanceof Error?e.message:String(e)})}if(!a||"object"!=typeof a||Array.isArray(a))throw new n("INVALID_ARGS",`Remote config file must contain a JSON object: ${u}`);let m={},p=a,c=t.dirname(u);for(let[e,t]of Object.entries(p)){let o=y.get(e);if(!o)throw new n("INVALID_ARGS",`Unsupported remote config key "${e}" in remote config file ${u}.`);let i=s(o,t,`remote config file ${u}`,e);m[o.key]="string"==typeof i&&"path"in o&&o.path?r(i,{cwd:c,env:l}):i}return{resolvedPath:u,profile:m}}(o);return{resolvedPath:i.resolvedPath,profile:function(...e){let t={};for(let n of e)if(n)for(let e of u){let r=n[e.key];void 0!==r&&(t[e.key]=r)}return t}(function(e=process.env){let t={};for(let n of u){let r=(function(e){let t=y.get(e);return[a(e),...t?.legacyEnvNames??[]]})(n.key).map(t=>({name:t,value:e[t]})).find(e=>"string"==typeof e.value&&e.value.trim().length>0);r&&(t[n.key]=s(n,r.value,`environment variable ${r.name}`,r.name))}return t}(o.env),i.profile)}}export{u as REMOTE_CONFIG_FIELD_SPECS,a as buildPrimaryEnvVarName,s as parseSourceValue,f as resolveRemoteConfigPath,m as resolveRemoteConfigProfile};
1
+ export{resolveRemoteConfigPath,resolveRemoteConfigProfile}from"./208.js";
@@ -35,6 +35,7 @@ declare type RawSnapshotNode = {
35
35
  rect?: Rect;
36
36
  enabled?: boolean;
37
37
  selected?: boolean;
38
+ focused?: boolean;
38
39
  hittable?: boolean;
39
40
  depth?: number;
40
41
  parentIndex?: number;
@@ -0,0 +1,20 @@
1
+ import{buildCommandUsageText as e,buildUsageText as t}from"./command-schema.js";import{readVersion as r}from"./9671.js";let n="agent-device",o=["workflow","debugging","react-devtools","remote","macos","dogfood"];function i(e={}){let t=!1===e.global?"npx -y agent-device mcp":"agent-device mcp",r=!1===e.global?"No global install required.":"npm install -g agent-device",n=e.client?`
2
+ Client hint: ${e.client}`:"";return`${r}
3
+
4
+ MCP server command:
5
+ ${t}
6
+
7
+ Generic MCP JSON:
8
+ {
9
+ "mcpServers": {
10
+ "agent-device": {
11
+ "command": "${!1===e.global?"npx":"agent-device"}",
12
+ "args": ${JSON.stringify(!1===e.global?["-y","agent-device","mcp"]:["mcp"])}
13
+ }
14
+ }
15
+ }
16
+
17
+ Use this server for discovery and routing only. For device actions, call the CLI commands returned by the help tool, starting with:
18
+ agent-device help workflow${n}
19
+ `}function a(r={}){if(r.topic&&r.command)throw Error("Provide either topic or command, not both.");let n=r.topic??r.command;if(!n)return t();if(r.topic&&!d(r.topic))throw Error(`Unknown help topic: ${r.topic}. Expected one of: ${o.join(", ")}`);let i=e(n);if(!i)throw Error(`Unknown command or help topic: ${n}`);return i}function s(){return[{uri:"agent-device://install",name:"agent-device MCP install",description:"Install and client configuration snippets.",mimeType:"text/markdown"},{uri:"agent-device://help",name:"agent-device command list",description:"Version-matched command list and global flags.",mimeType:"text/plain"},...o.map(e=>({uri:`agent-device://help/${e}`,name:`agent-device help ${e}`,description:`Version-matched ${e} workflow guidance.`,mimeType:"text/plain"}))]}function c(){return[l("agent-device-workflow","Plan a normal app automation loop."),l("agent-device-debugging","Collect focused debugging evidence."),l("agent-device-dogfood","Run exploratory QA with reproducible evidence."),l("agent-device-react-native-performance","Inspect React Native renders."),l("agent-device-macos","Inspect a macOS app or surface.")]}function l(e,t){return{name:e,description:t,arguments:[{name:"target",description:"Optional app, device, or task target.",required:!1}]}}function d(e){return o.includes(e)}function p(e){if("2.0"!==e.jsonrpc||"string"!=typeof e.method)return g(e.id??null,-32600,"Invalid JSON-RPC request.");if(void 0===e.id)return null;try{var l,p;return l=e.id,p=function(e,l){switch(e){case"initialize":return{protocolVersion:"2025-11-25",capabilities:{tools:{},resources:{},prompts:{}},serverInfo:{name:n,version:r()}};case"ping":return{};case"tools/list":return{tools:[{name:"status",description:"Report the installed agent-device MCP router and CLI metadata.",inputSchema:{type:"object",properties:{},additionalProperties:!1}},{name:"install",description:"Return install and MCP client configuration snippets for agent-device.",inputSchema:{type:"object",properties:{client:{type:"string",description:"Optional client name for labeling the returned guidance."},global:{type:"boolean",description:"Use a global agent-device binary when true; use npx when false."}},additionalProperties:!1}},{name:"help",description:"Return version-matched CLI help for a workflow topic or command.",inputSchema:{type:"object",properties:{topic:{type:"string",enum:o,description:"Agent workflow topic."},command:{type:"string",description:"CLI command name such as snapshot, open, logs, or react-devtools."}},additionalProperties:!1}}]};case"tools/call":var p,g,w,y=l;let b=m(y),$=h(b,"name"),k=f(b.arguments);try{if("status"===$)return u(JSON.stringify({name:n,registryName:"io.github.callstackincubator/agent-device",version:r(),transport:"stdio",command:"agent-device mcp",node:process.version,capabilities:{tools:["status","install","help"],resources:s().map(e=>e.uri),prompts:c().map(e=>e.name)},note:"This MCP server routes agents to the agent-device CLI. It does not expose device automation or shell execution tools."},null,2));if("install"===$)return u(i(k));if("help"===$)return u(a(k));throw Error(`Unknown tool: ${$}`)}catch(e){return u(e instanceof Error?e.message:String(e),!0)}return;case"resources/list":return{resources:s()};case"resources/read":let x;return p=l,{contents:[{uri:x=h(m(p),"uri"),mimeType:"agent-device://install"===x?"text/markdown":"text/plain",text:function(e){if("agent-device://install"===e)return i();if("agent-device://help"===e)return t();let r=e.startsWith("agent-device://help/")?e.slice(20):"";if(d(r))return a({topic:r});throw Error(`Unknown resource: ${e}`)}(x)}]};case"prompts/list":return{prompts:c()};case"prompts/get":let C;return g=l,C=m(g),function(e,t={}){let r={"agent-device-workflow":"workflow","agent-device-debugging":"debugging","agent-device-dogfood":"dogfood","agent-device-react-native-performance":"react-devtools","agent-device-macos":"macos"}[e];if(!r)throw Error(`Unknown prompt: ${e}`);let n=t.target?` Target: ${t.target}.`:"";return{description:`Use agent-device help ${r} before planning commands.${n}`,messages:[{role:"user",content:{type:"text",text:`Read the agent-device ${r} guidance through the MCP help tool, then produce a concise command plan using agent-device CLI commands only.${n}`}}]}}(h(C,"name"),(w=C.arguments,Object.fromEntries(Object.entries(f(w)).filter(e=>"string"==typeof e[1]))));default:throw new v(`Unsupported MCP method: ${e}`)}}(e.method,e.params),{jsonrpc:"2.0",id:l,result:p}}catch(t){if(t instanceof v)return g(e.id,-32601,t.message);return g(e.id,-32602,t instanceof Error?t.message:String(t))}}function u(e,t=!1){return{isError:t,content:[{type:"text",text:e}]}}function g(e,t,r){return{jsonrpc:"2.0",id:e,error:{code:t,message:r}}}function m(e){if(!e||"object"!=typeof e||Array.isArray(e))throw Error("Expected object parameters.");return e}function f(e){return void 0===e?{}:m(e)}function h(e,t){let r=e[t];if("string"!=typeof r||0===r.length)throw Error(`Expected ${t} to be a non-empty string.`);return r}class v extends Error{}async function w(){let e=new y(e=>{let t=function(e){if(Array.isArray(e)){let t=e.flatMap(e=>{var t;return(t=p(e))?[t]:[]});return t.length>0?t:null}return p(e)}(e);t&&b(t)});process.stdin.setEncoding("utf8"),process.stdin.on("data",t=>{try{e.push(t)}catch(e){b({jsonrpc:"2.0",id:null,error:{code:-32700,message:e instanceof Error?e.message:String(e)}})}}),await new Promise(e=>{process.stdin.on("end",e),process.stdin.on("close",e),process.stdin.resume()})}class y{buffer="";sink;constructor(e){this.sink=e}push(e){for(this.buffer+=e;;){let e=this.tryReadLineMessage();if(void 0!==e){this.emit(e);continue}break}}tryReadLineMessage(){let e=this.buffer.indexOf("\n");if(-1===e)return;let t=this.buffer.slice(0,e).trim();return this.buffer=this.buffer.slice(e+1),t.length>0?t:void 0}emit(e){let t=JSON.parse(e);Array.isArray(t),this.sink(t)}}function b(e){process.stdout.write(`${JSON.stringify(e)}
20
+ `)}export{w as runAgentDeviceMcpServer};
@@ -13,6 +13,18 @@ extension RunnerTests {
13
13
  return (gestureStartUptimeMs, currentUptimeMs())
14
14
  }
15
15
 
16
+ private func unsupportedResponse(for outcome: RunnerInteractionOutcome) -> Response? {
17
+ switch outcome {
18
+ case .performed:
19
+ return nil
20
+ case .unsupported(let message):
21
+ return Response(
22
+ ok: false,
23
+ error: ErrorPayload(code: "UNSUPPORTED_OPERATION", message: message)
24
+ )
25
+ }
26
+ }
27
+
16
28
  func execute(command: Command) throws -> Response {
17
29
  if Thread.isMainThread {
18
30
  return try executeOnMainSafely(command: command)
@@ -231,11 +243,15 @@ extension RunnerTests {
231
243
  case .tap:
232
244
  if let text = command.text {
233
245
  if let element = findElement(app: activeApp, text: text) {
246
+ var outcome = RunnerInteractionOutcome.performed
234
247
  let timing = measureGesture {
235
248
  withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
236
- element.tap()
249
+ outcome = activateElement(app: activeApp, element: element, action: "tap by text")
237
250
  }
238
251
  }
252
+ if let response = unsupportedResponse(for: outcome) {
253
+ return response
254
+ }
239
255
  return Response(
240
256
  ok: true,
241
257
  data: DataPayload(
@@ -249,11 +265,15 @@ extension RunnerTests {
249
265
  }
250
266
  if let x = command.x, let y = command.y {
251
267
  let touchFrame = resolvedTouchVisualizationFrame(app: activeApp, x: x, y: y)
268
+ var outcome = RunnerInteractionOutcome.performed
252
269
  let timing = measureGesture {
253
270
  withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
254
- tapAt(app: activeApp, x: x, y: y)
271
+ outcome = tapAt(app: activeApp, x: x, y: y)
255
272
  }
256
273
  }
274
+ if let response = unsupportedResponse(for: outcome) {
275
+ return response
276
+ }
257
277
  return Response(
258
278
  ok: true,
259
279
  data: DataPayload(
@@ -309,13 +329,19 @@ extension RunnerTests {
309
329
  let doubleTap = command.doubleTap ?? false
310
330
  let touchFrame = resolvedTouchVisualizationFrame(app: activeApp, x: x, y: y)
311
331
  if doubleTap {
332
+ var outcome = RunnerInteractionOutcome.performed
312
333
  let timing = measureGesture {
313
334
  withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
314
335
  runSeries(count: count, pauseMs: intervalMs) { _ in
315
- doubleTapAt(app: activeApp, x: x, y: y)
336
+ if case .performed = outcome {
337
+ outcome = doubleTapAt(app: activeApp, x: x, y: y)
338
+ }
316
339
  }
317
340
  }
318
341
  }
342
+ if let response = unsupportedResponse(for: outcome) {
343
+ return response
344
+ }
319
345
  return Response(
320
346
  ok: true,
321
347
  data: DataPayload(
@@ -329,13 +355,19 @@ extension RunnerTests {
329
355
  )
330
356
  )
331
357
  }
358
+ var outcome = RunnerInteractionOutcome.performed
332
359
  let timing = measureGesture {
333
360
  withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
334
361
  runSeries(count: count, pauseMs: intervalMs) { _ in
335
- tapAt(app: activeApp, x: x, y: y)
362
+ if case .performed = outcome {
363
+ outcome = tapAt(app: activeApp, x: x, y: y)
364
+ }
336
365
  }
337
366
  }
338
367
  }
368
+ if let response = unsupportedResponse(for: outcome) {
369
+ return response
370
+ }
339
371
  return Response(
340
372
  ok: true,
341
373
  data: DataPayload(
@@ -354,11 +386,15 @@ extension RunnerTests {
354
386
  }
355
387
  let duration = (command.durationMs ?? 800) / 1000.0
356
388
  let touchFrame = resolvedTouchVisualizationFrame(app: activeApp, x: x, y: y)
389
+ var outcome = RunnerInteractionOutcome.performed
357
390
  let timing = measureGesture {
358
391
  withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
359
- longPressAt(app: activeApp, x: x, y: y, duration: duration)
392
+ outcome = longPressAt(app: activeApp, x: x, y: y, duration: duration)
360
393
  }
361
394
  }
395
+ if let response = unsupportedResponse(for: outcome) {
396
+ return response
397
+ }
362
398
  return Response(
363
399
  ok: true,
364
400
  data: DataPayload(
@@ -377,11 +413,15 @@ extension RunnerTests {
377
413
  }
378
414
  let holdDuration = min(max((command.durationMs ?? 60) / 1000.0, 0.016), 10.0)
379
415
  let dragFrame = resolvedDragVisualizationFrame(app: activeApp, x: x, y: y, x2: x2, y2: y2)
416
+ var outcome = RunnerInteractionOutcome.performed
380
417
  let timing = measureGesture {
381
418
  withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
382
- dragAt(app: activeApp, x: x, y: y, x2: x2, y2: y2, holdDuration: holdDuration)
419
+ outcome = dragAt(app: activeApp, x: x, y: y, x2: x2, y2: y2, holdDuration: holdDuration)
383
420
  }
384
421
  }
422
+ if let response = unsupportedResponse(for: outcome) {
423
+ return response
424
+ }
385
425
  return Response(
386
426
  ok: true,
387
427
  data: DataPayload(
@@ -407,18 +447,25 @@ extension RunnerTests {
407
447
  return Response(ok: false, error: ErrorPayload(message: "dragSeries pattern must be one-way or ping-pong"))
408
448
  }
409
449
  let holdDuration = min(max((command.durationMs ?? 60) / 1000.0, 0.016), 10.0)
450
+ var outcome = RunnerInteractionOutcome.performed
410
451
  let timing = measureGesture {
411
452
  withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
412
453
  runSeries(count: count, pauseMs: pauseMs) { idx in
454
+ guard case .performed = outcome else {
455
+ return
456
+ }
413
457
  let reverse = pattern == "ping-pong" && (idx % 2 == 1)
414
458
  if reverse {
415
- dragAt(app: activeApp, x: x2, y: y2, x2: x, y2: y, holdDuration: holdDuration)
459
+ outcome = dragAt(app: activeApp, x: x2, y: y2, x2: x, y2: y, holdDuration: holdDuration)
416
460
  } else {
417
- dragAt(app: activeApp, x: x, y: y, x2: x2, y2: y2, holdDuration: holdDuration)
461
+ outcome = dragAt(app: activeApp, x: x, y: y, x2: x2, y2: y2, holdDuration: holdDuration)
418
462
  }
419
463
  }
420
464
  }
421
465
  }
466
+ if let response = unsupportedResponse(for: outcome) {
467
+ return response
468
+ }
422
469
  return Response(
423
470
  ok: true,
424
471
  data: DataPayload(
@@ -427,6 +474,18 @@ extension RunnerTests {
427
474
  gestureEndUptimeMs: timing.gestureEndUptimeMs
428
475
  )
429
476
  )
477
+ case .remotePress:
478
+ guard let button = tvRemoteButton(from: command.remoteButton) else {
479
+ return Response(ok: false, error: ErrorPayload(message: "remotePress requires remoteButton"))
480
+ }
481
+ let duration = (command.durationMs ?? 0) / 1000.0
482
+ guard pressTvRemote(button, duration: duration) else {
483
+ return Response(
484
+ ok: false,
485
+ error: ErrorPayload(code: "UNSUPPORTED_OPERATION", message: "remotePress is only supported on tvOS")
486
+ )
487
+ }
488
+ return Response(ok: true, data: DataPayload(message: "remote pressed"))
430
489
  case .type:
431
490
  guard let text = command.text else {
432
491
  return Response(ok: false, error: ErrorPayload(message: "type requires text"))
@@ -633,13 +692,23 @@ extension RunnerTests {
633
692
  return Response(ok: false, error: ErrorPayload(message: "alert not found"))
634
693
  }
635
694
  if action == "accept" {
636
- let button = alert.buttons.allElementsBoundByIndex.first
637
- button?.tap()
695
+ guard let button = alert.buttons.allElementsBoundByIndex.first else {
696
+ return Response(ok: false, error: ErrorPayload(message: "alert accept button not found"))
697
+ }
698
+ let outcome = activateElement(app: activeApp, element: button, action: "alert accept")
699
+ if let response = unsupportedResponse(for: outcome) {
700
+ return response
701
+ }
638
702
  return Response(ok: true, data: DataPayload(message: "accepted"))
639
703
  }
640
704
  if action == "dismiss" {
641
- let button = alert.buttons.allElementsBoundByIndex.last
642
- button?.tap()
705
+ guard let button = alert.buttons.allElementsBoundByIndex.last else {
706
+ return Response(ok: false, error: ErrorPayload(message: "alert dismiss button not found"))
707
+ }
708
+ let outcome = activateElement(app: activeApp, element: button, action: "alert dismiss")
709
+ if let response = unsupportedResponse(for: outcome) {
710
+ return response
711
+ }
643
712
  return Response(ok: true, data: DataPayload(message: "dismissed"))
644
713
  }
645
714
  let buttonLabels = alert.buttons.allElementsBoundByIndex.map { $0.label }
@@ -648,8 +717,12 @@ extension RunnerTests {
648
717
  guard let scale = command.scale, scale > 0 else {
649
718
  return Response(ok: false, error: ErrorPayload(message: "pinch requires scale > 0"))
650
719
  }
720
+ var outcome = RunnerInteractionOutcome.performed
651
721
  let timing = measureGesture {
652
- pinch(app: activeApp, scale: scale, x: command.x, y: command.y)
722
+ outcome = pinch(app: activeApp, scale: scale, x: command.x, y: command.y)
723
+ }
724
+ if let response = unsupportedResponse(for: outcome) {
725
+ return response
653
726
  }
654
727
  return Response(
655
728
  ok: true,