swipium 2.0.0 → 2.2.0

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 (152) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +32 -20
  3. package/THREAT_MODEL.md +109 -21
  4. package/dist/automationGen/run.js.map +1 -1
  5. package/dist/cli/init.js +101 -11
  6. package/dist/cli/init.js.map +1 -1
  7. package/dist/cli/verify.js +2 -2
  8. package/dist/cli/verify.js.map +1 -1
  9. package/dist/consent/consent.js +365 -17
  10. package/dist/consent/consent.js.map +1 -1
  11. package/dist/context/projectRoot.js +9 -4
  12. package/dist/context/projectRoot.js.map +1 -1
  13. package/dist/context/protocolEra.js +15 -0
  14. package/dist/context/protocolEra.js.map +1 -0
  15. package/dist/drivers/DirectDriver.js +5 -2
  16. package/dist/drivers/DirectDriver.js.map +1 -1
  17. package/dist/featureTesting/executionBootstrap.js +85 -77
  18. package/dist/featureTesting/executionBootstrap.js.map +1 -1
  19. package/dist/flows/run.js +9 -5
  20. package/dist/flows/run.js.map +1 -1
  21. package/dist/lib/abortScope.js +36 -3
  22. package/dist/lib/abortScope.js.map +1 -1
  23. package/dist/lib/android.js +15 -6
  24. package/dist/lib/android.js.map +1 -1
  25. package/dist/lib/codexEnv.js +112 -0
  26. package/dist/lib/codexEnv.js.map +1 -0
  27. package/dist/lib/logger.js +18 -0
  28. package/dist/lib/logger.js.map +1 -1
  29. package/dist/lib/result.js +188 -10
  30. package/dist/lib/result.js.map +1 -1
  31. package/dist/lib/schemaHash.js +20 -31
  32. package/dist/lib/schemaHash.js.map +1 -1
  33. package/dist/lib/simctl.js +102 -3
  34. package/dist/lib/simctl.js.map +1 -1
  35. package/dist/lib/toolSchema.js +143 -0
  36. package/dist/lib/toolSchema.js.map +1 -0
  37. package/dist/lib/wda.js +5 -2
  38. package/dist/lib/wda.js.map +1 -1
  39. package/dist/mobileAudit/runner.js +12 -4
  40. package/dist/mobileAudit/runner.js.map +1 -1
  41. package/dist/oracle/failures.js +2 -2
  42. package/dist/oracle/failures.js.map +1 -1
  43. package/dist/orchestration/testThis/execute.js +21 -4
  44. package/dist/orchestration/testThis/execute.js.map +1 -1
  45. package/dist/orchestration/testThis/pipeline.js +2 -0
  46. package/dist/orchestration/testThis/pipeline.js.map +1 -1
  47. package/dist/orchestration/testThis/plan.js.map +1 -1
  48. package/dist/report/export.js +1 -1
  49. package/dist/report/export.js.map +1 -1
  50. package/dist/server.js +564 -139
  51. package/dist/server.js.map +1 -1
  52. package/dist/services/prepareAndroid.js +3 -2
  53. package/dist/services/prepareAndroid.js.map +1 -1
  54. package/dist/services/prepareIos.js +31 -4
  55. package/dist/services/prepareIos.js.map +1 -1
  56. package/dist/services/smoke.js +38 -4
  57. package/dist/services/smoke.js.map +1 -1
  58. package/dist/session/processRegistry.js +12 -1
  59. package/dist/session/processRegistry.js.map +1 -1
  60. package/dist/snapshot/parse.js +64 -9
  61. package/dist/snapshot/parse.js.map +1 -1
  62. package/dist/snapshot/present.js +14 -4
  63. package/dist/snapshot/present.js.map +1 -1
  64. package/dist/snapshot/settle.js +14 -3
  65. package/dist/snapshot/settle.js.map +1 -1
  66. package/dist/tools/act.js +694 -670
  67. package/dist/tools/act.js.map +1 -1
  68. package/dist/tools/agent.js +15 -16
  69. package/dist/tools/agent.js.map +1 -1
  70. package/dist/tools/appControl.js +4 -4
  71. package/dist/tools/appControl.js.map +1 -1
  72. package/dist/tools/appMap.js +11 -13
  73. package/dist/tools/appMap.js.map +1 -1
  74. package/dist/tools/build.js +4 -6
  75. package/dist/tools/build.js.map +1 -1
  76. package/dist/tools/bundletool.js +3 -3
  77. package/dist/tools/bundletool.js.map +1 -1
  78. package/dist/tools/clearOverlay.js +2 -3
  79. package/dist/tools/clearOverlay.js.map +1 -1
  80. package/dist/tools/device.js +4 -3
  81. package/dist/tools/device.js.map +1 -1
  82. package/dist/tools/doctor.js +23 -11
  83. package/dist/tools/doctor.js.map +1 -1
  84. package/dist/tools/explore.js +15 -21
  85. package/dist/tools/explore.js.map +1 -1
  86. package/dist/tools/featureTesting.js +22 -6
  87. package/dist/tools/featureTesting.js.map +1 -1
  88. package/dist/tools/firstRun.js +4 -5
  89. package/dist/tools/firstRun.js.map +1 -1
  90. package/dist/tools/flow.js +13 -17
  91. package/dist/tools/flow.js.map +1 -1
  92. package/dist/tools/flowRepair.js +3 -5
  93. package/dist/tools/flowRepair.js.map +1 -1
  94. package/dist/tools/generate.js +10 -11
  95. package/dist/tools/generate.js.map +1 -1
  96. package/dist/tools/getArtifact.js +114 -10
  97. package/dist/tools/getArtifact.js.map +1 -1
  98. package/dist/tools/health.js +2 -1
  99. package/dist/tools/health.js.map +1 -1
  100. package/dist/tools/ios.js +35 -13
  101. package/dist/tools/ios.js.map +1 -1
  102. package/dist/tools/issues.js +8 -8
  103. package/dist/tools/issues.js.map +1 -1
  104. package/dist/tools/jobs.js +26 -10
  105. package/dist/tools/jobs.js.map +1 -1
  106. package/dist/tools/metro.js +4 -5
  107. package/dist/tools/metro.js.map +1 -1
  108. package/dist/tools/mobileAudit.js +11 -10
  109. package/dist/tools/mobileAudit.js.map +1 -1
  110. package/dist/tools/note.js +2 -3
  111. package/dist/tools/note.js.map +1 -1
  112. package/dist/tools/prepareIosTarget.js +102 -31
  113. package/dist/tools/prepareIosTarget.js.map +1 -1
  114. package/dist/tools/prepareTarget.js +3 -5
  115. package/dist/tools/prepareTarget.js.map +1 -1
  116. package/dist/tools/report.js +4 -5
  117. package/dist/tools/report.js.map +1 -1
  118. package/dist/tools/resolveArtifact.js +2 -3
  119. package/dist/tools/resolveArtifact.js.map +1 -1
  120. package/dist/tools/resolveTarget.js +3 -6
  121. package/dist/tools/resolveTarget.js.map +1 -1
  122. package/dist/tools/screenRecord.js +2 -3
  123. package/dist/tools/screenRecord.js.map +1 -1
  124. package/dist/tools/screenshot.js +2 -2
  125. package/dist/tools/screenshot.js.map +1 -1
  126. package/dist/tools/smoke.js +4 -2
  127. package/dist/tools/smoke.js.map +1 -1
  128. package/dist/tools/snapshot.js +6 -7
  129. package/dist/tools/snapshot.js.map +1 -1
  130. package/dist/tools/startSession.js +12 -16
  131. package/dist/tools/startSession.js.map +1 -1
  132. package/dist/tools/suite.js +2 -4
  133. package/dist/tools/suite.js.map +1 -1
  134. package/dist/tools/testSuite.js +13 -19
  135. package/dist/tools/testSuite.js.map +1 -1
  136. package/dist/tools/testThis.js +28 -13
  137. package/dist/tools/testThis.js.map +1 -1
  138. package/dist/tools/visual.js +7 -9
  139. package/dist/tools/visual.js.map +1 -1
  140. package/dist/tools/wait.js +130 -21
  141. package/dist/tools/wait.js.map +1 -1
  142. package/dist/tools/wda.js +199 -60
  143. package/dist/tools/wda.js.map +1 -1
  144. package/dist/version.js +1 -1
  145. package/docs/README.md +4 -4
  146. package/docs/ci-reports.md +39 -7
  147. package/docs/concepts.md +37 -25
  148. package/docs/flows.md +1 -1
  149. package/docs/mcp-server.md +281 -76
  150. package/docs/physical-devices.md +4 -4
  151. package/docs/tools.md +59 -30
  152. package/package.json +4 -3
@@ -1,44 +1,153 @@
1
1
  // qa_wait: non-shell synchronization for setup conditions, so agents don't shell out to
2
- // `sleep`/poll. Waits for: device_online or metro_ready (serving). Jobs are waited on with
3
- // qa_job_status waitMs. Returns timeout + current state + next steps.
2
+ // `sleep`/poll. Waits for: device_online (adb), metro_ready (serving), wda_ready (WDA /status
3
+ // ready) or simulator_booted (the session's iOS Simulator finished booting, e.g. after qa_ios boot
4
+ // returned status:"booting").
5
+ // Jobs are waited on with qa_job_status waitMs. Returns timeout + current state + next steps.
4
6
  import { z } from 'zod';
5
- import { qaOk, unknownSessionError } from '../lib/result.js';
7
+ import { cancelledResult, qaAnnotate, qaError, qaOk, unknownSessionError } from '../lib/result.js';
8
+ import { isAbortError, sleepOrCancel, throwIfCancelled } from '../lib/abortScope.js';
6
9
  import { resolveDevice } from '../session/attach.js';
7
10
  import { metroReadiness } from '../lib/metroState.js';
8
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
11
+ import { checkWda, isLoopbackWdaUrl, remoteWdaAllowedByUser, REMOTE_WDA_ENV } from '../lib/wda.js';
12
+ import { wdaUrlForSession } from './wda.js';
13
+ import { boundSimulatorUdid } from './ios.js';
14
+ import { simulatorBootState } from '../lib/simctl.js';
15
+ /** Default wait. Kept under common client tool timeouts (Codex: 60 s); the model can call again. */
16
+ export const DEFAULT_WAIT_TIMEOUT_MS = 45_000;
17
+ /** Effective upper bound for timeoutMs, same reasoning: one call must not outlive the client's
18
+ * timeout. Larger values (older docs said 60000/180000) are accepted and CLAMPED, with a note. */
19
+ export const MAX_WAIT_TIMEOUT_MS = 50_000;
20
+ const POLL_INTERVAL_MS = 1500;
9
21
  export function registerWait(server, sessions) {
10
22
  server.registerTool('qa_wait', {
11
23
  title: 'Wait for a setup condition',
12
- description: 'Block (bounded) until a setup condition holds instead of shelling out to sleep: for="device_online" (a device appears) or ' +
13
- '"metro_ready" (Metro serving the bundle). Returns satisfied/timedOut + current state. To wait for a job, use qa_job_status waitMs.',
24
+ description: 'Bounded wait for a setup condition instead of shell sleep: device_online (adb), metro_ready, wda_ready (after qa_wda ' +
25
+ 'start returned status:"starting"), or simulator_booted (the bound iOS Simulator, after qa_ios boot returned ' +
26
+ 'status:"booting"). Returns satisfied, or timedOut:true (call again). For jobs use qa_job_status waitMs.',
14
27
  inputSchema: {
15
28
  sessionId: z.string(),
16
- for: z.enum(['device_online', 'metro_ready']),
17
- timeoutMs: z.number().optional().describe('default 60000 (180000 for device_online).'),
29
+ for: z.enum(['device_online', 'metro_ready', 'wda_ready', 'simulator_booted']),
30
+ timeoutMs: z
31
+ .number()
32
+ .int()
33
+ .min(0)
34
+ .optional()
35
+ .describe(`default ${DEFAULT_WAIT_TIMEOUT_MS}; values above ${MAX_WAIT_TIMEOUT_MS} are clamped. On timedOut, call again to keep waiting.`),
18
36
  },
19
37
  }, async ({ sessionId, for: cond, timeoutMs }) => {
20
38
  const session = sessions.get(sessionId);
21
39
  if (!session)
22
40
  return unknownSessionError(sessionId);
23
- const deadline = Date.now() + (timeoutMs ?? (cond === 'device_online' ? 180000 : 60000));
24
- const intervalMs = 1500;
25
- while (Date.now() < deadline) {
26
- if (cond === 'device_online') {
27
- const dev = await resolveDevice(session);
28
- if (dev.available.length > 0)
29
- return qaOk({ satisfied: true, condition: cond, availableDevices: dev.available }, `device online: ${dev.available.join(', ')}`);
30
- }
31
- else {
41
+ const requested = timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
42
+ const effectiveMs = Math.min(requested, MAX_WAIT_TIMEOUT_MS);
43
+ const notes = requested > MAX_WAIT_TIMEOUT_MS
44
+ ? [
45
+ `timeoutMs ${requested} clamped to ${MAX_WAIT_TIMEOUT_MS}: one call stays under client tool timeouts (Codex: 60 s). Call qa_wait again to keep waiting.`,
46
+ ]
47
+ : [];
48
+ const done = (r) => qaAnnotate(r, notes);
49
+ const deadline = Date.now() + effectiveMs;
50
+ // wda_ready: the URL already used by this session (attached driver / last qa_wda start), else
51
+ // the configured one, which must pass the same non-loopback gate as qa_wda.
52
+ let wdaUrl;
53
+ if (cond === 'wda_ready') {
54
+ const resolved = wdaUrlForSession(session);
55
+ if (resolved.source === 'config' && !isLoopbackWdaUrl(resolved.url) && !remoteWdaAllowedByUser(resolved.url))
56
+ return qaError({
57
+ what: `Refused to poll non-loopback WDA URL ${resolved.url} from the repository config`,
58
+ changedState: false,
59
+ retrySafe: false,
60
+ failureCode: 'DESTRUCTIVE_REFUSED',
61
+ nextSteps: [
62
+ `Attach it first with qa_wda { action:"attach", webDriverAgentUrl, allowNonLoopback:true } (consent-gated), or set ${REMOTE_WDA_ENV}=<exact url> in the MCP server environment.`,
63
+ ],
64
+ });
65
+ wdaUrl = resolved.url;
66
+ }
67
+ let lastWda;
68
+ // simulator_booted: the iOS Simulator bound to this session (qa_ios boot binds it even when
69
+ // it returns status:"booting").
70
+ let simUdid;
71
+ if (cond === 'simulator_booted') {
72
+ simUdid = boundSimulatorUdid(session);
73
+ if (!simUdid)
74
+ return qaError({
75
+ what: 'No iOS Simulator bound to this session',
76
+ changedState: false,
77
+ retrySafe: true,
78
+ failureCode: 'NO_DEVICE',
79
+ nextSteps: ['Call qa_ios { action:"boot" } first; it binds the simulator even while it is still booting.'],
80
+ });
81
+ }
82
+ let lastSim;
83
+ // Cancellation (notifications/cancelled): the call's signal is scoped by the tool wrapper
84
+ // (abortScope), so every poll checks it and the pause between polls wakes on abort.
85
+ try {
86
+ while (Date.now() < deadline) {
87
+ throwIfCancelled();
88
+ if (cond === 'wda_ready') {
89
+ lastWda = await checkWda(wdaUrl, Math.max(250, Math.min(1500, deadline - Date.now())));
90
+ throwIfCancelled();
91
+ if (lastWda.ready)
92
+ return done(qaOk({ satisfied: true, condition: cond, webDriverAgentUrl: wdaUrl, wda: lastWda }, `WDA ready at ${wdaUrl}\nNext: qa_wda attach (if the session is not attached yet).`));
93
+ await sleepOrCancel(Math.min(POLL_INTERVAL_MS, Math.max(0, deadline - Date.now())));
94
+ continue;
95
+ }
96
+ if (cond === 'simulator_booted') {
97
+ lastSim = await simulatorBootState(simUdid, Math.max(250, Math.min(3000, deadline - Date.now())));
98
+ throwIfCancelled();
99
+ if (lastSim.state === 'booted') {
100
+ sessions.milestone(session, 'simulator_boot_end');
101
+ return done(qaOk({ satisfied: true, condition: cond, udid: simUdid, simulator: lastSim }, `Simulator ${lastSim.name ?? simUdid} booted\nNext: qa_ios install/launch (or qa_wda start).`));
102
+ }
103
+ if (lastSim.state === 'failed' || lastSim.state === 'shutdown' || lastSim.state === 'unknown')
104
+ return done(qaError({
105
+ what: lastSim.state === 'failed'
106
+ ? `Simulator ${simUdid} failed to boot: ${lastSim.error}`
107
+ : lastSim.state === 'shutdown'
108
+ ? `Simulator ${simUdid} is not booting (simctl state: ${lastSim.simctlState})`
109
+ : `Simulator ${simUdid} is not listed by simctl`,
110
+ changedState: false,
111
+ retrySafe: true,
112
+ failureCode: lastSim.state === 'failed' && /timed out/i.test(lastSim.error) ? 'SIMULATOR_BOOT_TIMEOUT' : 'SIMULATOR_BOOT_FAILED',
113
+ nextSteps: ['Boot it again with qa_ios { action:"boot" }, or pick another simulator from qa_ios { action:"list" }.'],
114
+ }));
115
+ await sleepOrCancel(Math.min(POLL_INTERVAL_MS, Math.max(0, deadline - Date.now())));
116
+ continue;
117
+ }
32
118
  const dev = await resolveDevice(session);
33
- if (dev.effective) {
119
+ throwIfCancelled();
120
+ if (cond === 'device_online') {
121
+ if (dev.available.length > 0)
122
+ return done(qaOk({ satisfied: true, condition: cond, availableDevices: dev.available }, `device online: ${dev.available.join(', ')}`));
123
+ }
124
+ else if (dev.effective) {
34
125
  const rd = await metroReadiness(dev.effective);
35
126
  if (rd.serving)
36
- return qaOk({ satisfied: true, condition: cond, metro: rd }, `Metro serving=${rd.serving} reverse=${rd.reverseSet} ready=${rd.ready}`);
127
+ return done(qaOk({ satisfied: true, condition: cond, metro: rd }, `Metro serving=${rd.serving} reverse=${rd.reverseSet} ready=${rd.ready}`));
37
128
  }
129
+ await sleepOrCancel(Math.min(POLL_INTERVAL_MS, Math.max(0, deadline - Date.now())));
38
130
  }
39
- await sleep(intervalMs);
40
131
  }
41
- return qaOk({ satisfied: false, timedOut: true, condition: cond }, `Timed out waiting for ${cond}. ${cond === 'device_online' ? 'Boot one with qa_prepare_target { bindOnly:true }.' : 'Start Metro with qa_metro action="start", or qa_metro diagnose.'}`);
132
+ catch (e) {
133
+ if (isAbortError(e))
134
+ return cancelledResult(`qa_wait ${cond} cancelled: the call was aborted before the condition held`);
135
+ throw e;
136
+ }
137
+ const hint = cond === 'device_online'
138
+ ? 'boot one with qa_prepare_target { bindOnly:true }.'
139
+ : cond === 'metro_ready'
140
+ ? 'start Metro with qa_metro action="start", or qa_metro diagnose.'
141
+ : cond === 'simulator_booted'
142
+ ? 'check qa_ios { action:"list" } (a cold boot of a fresh runtime can take a few minutes).'
143
+ : 'check qa_wda { action:"logs" } / { action:"diagnose" } (a managed WDA may have failed to launch).';
144
+ return done(qaOk({
145
+ satisfied: false,
146
+ timedOut: true,
147
+ condition: cond,
148
+ ...(cond === 'wda_ready' ? { webDriverAgentUrl: wdaUrl, wda: lastWda ?? null } : {}),
149
+ ...(cond === 'simulator_booted' ? { udid: simUdid, simulator: lastSim ?? null } : {}),
150
+ }, `Timed out waiting for ${cond}. Call qa_wait again to keep waiting, or ${hint}`));
42
151
  });
43
152
  }
44
153
  //# sourceMappingURL=wait.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"wait.js","sourceRoot":"","sources":["../../src/tools/wait.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"wait.js","sourceRoot":"","sources":["../../src/tools/wait.ts"],"names":[],"mappings":"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"}
package/dist/tools/wda.js CHANGED
@@ -2,7 +2,8 @@ import { z } from 'zod';
2
2
  import { spawn } from 'node:child_process';
3
3
  import { closeSync, existsSync, openSync, readFileSync, readdirSync, statSync } from 'node:fs';
4
4
  import { isAbsolute, join } from 'node:path';
5
- import { qaError, qaOk, unknownSessionError } from '../lib/result.js';
5
+ import { cancelledResult, qaError, qaOk, unknownSessionError } from '../lib/result.js';
6
+ import { isAbortError, runWithSignal } from '../lib/abortScope.js';
6
7
  import { consumeConsent, requireConsent } from '../consent/consent.js';
7
8
  import { sensitiveRefusal } from '../lib/sensitive.js';
8
9
  import { run } from '../lib/spawn.js';
@@ -12,6 +13,17 @@ import { recordWdaTiming, wdaRecommendations, wdaTimingSummary } from '../lib/wd
12
13
  import { WdaDriver } from '../drivers/WdaDriver.js';
13
14
  import * as sim from '../lib/simctl.js';
14
15
  import { findManagedWdaProcesses, killManagedWda, reclaimPid, registerManagedProcess, registeredWdaForSession, unregisterManagedProcess, } from '../session/processRegistry.js';
16
+ /** qa_wda actions that run xcodebuild and are consent-gated as `wda_<name>` (CONSENT_ACTIONS in consent.ts). */
17
+ export const WDA_XCODEBUILD_ACTIONS = ['build', 'start'];
18
+ function isWdaXcodebuildAction(a) {
19
+ return WDA_XCODEBUILD_ACTIONS.includes(a);
20
+ }
21
+ /** How long `qa_wda start` blocks inside ONE tool call waiting for /status. One call must stay
22
+ * under common client tool timeouts (Codex: 60 s), so a slower start returns status:"starting"
23
+ * and the agent polls with qa_wait { for:"wda_ready" } (bounded by ios.wda.startupTimeoutMs). */
24
+ export const WDA_START_CALL_WAIT_MS = 45_000;
25
+ /** Kill timer for the background `qa_wda build` job (a cold WDA build-for-testing is slow). */
26
+ export const WDA_BUILD_TIMEOUT_MS = 600_000;
15
27
  const managedProcesses = new Map();
16
28
  function pidIsAlive(pid) {
17
29
  if (!(pid > 0))
@@ -64,6 +76,19 @@ export function lastManagedWdaStart(session) {
64
76
  ...(port ? { port } : {}),
65
77
  };
66
78
  }
79
+ /** The WDA URL to probe for this session: the attached WdaDriver's, else the URL of the latest
80
+ * executed `qa_wda start` (both already passed the loopback/consent gate), else the configured
81
+ * one (source 'config': the caller must apply the non-loopback gate). Exported for qa_wait. */
82
+ export function wdaUrlForSession(session) {
83
+ if (session.driver instanceof WdaDriver)
84
+ return { url: session.driver.baseUrl, source: 'driver' };
85
+ const m = [...(session.mutations ?? [])]
86
+ .reverse()
87
+ .find((r) => r.tool === 'qa_wda' && r.action === 'wda_start' && r.status === 'executed' && typeof r.target?.webDriverAgentUrl === 'string');
88
+ if (m)
89
+ return { url: String(m.target.webDriverAgentUrl), source: 'wda_start' };
90
+ return { url: loadWdaConfig(session.root).url, source: 'config' };
91
+ }
67
92
  const isLoopback = isLoopbackWdaUrl;
68
93
  function latestWdaArtifacts(session) {
69
94
  const logs = [...session.artifacts].reverse().filter((a) => a.kind === 'wda');
@@ -120,19 +145,20 @@ function managedWdaBuildProductStatus(derivedDataPath) {
120
145
  export function registerWda(server, sessions) {
121
146
  server.registerTool('qa_wda', {
122
147
  title: 'WebDriverAgent diagnostics and attach',
123
- description: 'Diagnose, attach, or manage an iOS WebDriverAgent backend for structured iOS tap/type/snapshot. External WDA: ' +
124
- 'action:"attach" with webDriverAgentUrl (default http://127.0.0.1:8100). Managed WDA: build/start (consent-gated) from ' +
125
- 'wdaProjectPath or an installed Appium WDA; stop ends it. status/doctor/diagnose/logs/tune inspect a setup.',
148
+ description: 'Manage the iOS WebDriverAgent backend behind structured iOS tap/type/snapshot: attach an external WDA ' +
149
+ '(webDriverAgentUrl), build/start a managed one (consent-gated), stop it; status/doctor/diagnose/logs/tune inspect ' +
150
+ 'the setup. build runs as a background job (poll qa_job_status); start waits up to 45 s, then returns status:"starting" ' +
151
+ '(poll qa_wait { for:"wda_ready" }).',
126
152
  inputSchema: {
127
153
  sessionId: z.string(),
128
154
  action: z.enum(['status', 'doctor', 'build', 'start', 'stop', 'attach', 'diagnose', 'logs', 'tune']),
129
155
  webDriverAgentUrl: z.string().optional().describe('Default http://127.0.0.1:8100; non-loopback needs allowNonLoopback + consent.'),
130
- device: z.string().optional().describe('Simulator UDID behind this WDA (default: the session device).'),
156
+ device: z.string().optional().describe('Simulator UDID (default: session device).'),
131
157
  udid: z.string().optional().describe('Deprecated alias of device.'),
132
158
  bundleId: z.string().optional().describe('Default: the session appId.'),
133
159
  wdaProjectPath: z.string().optional().describe('WebDriverAgent.xcodeproj (default: Appium WDA if found).'),
134
160
  derivedDataPath: z.string().optional().describe('xcodebuild -derivedDataPath for reuse.'),
135
- scheme: z.string().optional().describe('WDA xcodebuild scheme. Defaults to WebDriverAgentRunner.'),
161
+ scheme: z.string().optional().describe('Default WebDriverAgentRunner.'),
136
162
  allowNonLoopback: z.boolean().optional().describe('Required to use a non-loopback external WDA URL.'),
137
163
  consentId: z.string().optional(),
138
164
  approve: z.boolean().optional(),
@@ -187,7 +213,7 @@ export function registerWda(server, sessions) {
187
213
  const explicitProjectPath = resolvePath(wdaProjectPath);
188
214
  // Managed build/start without wdaProjectPath: use a user-installed Appium WebDriverAgent
189
215
  // (~/.appium/…/appium-webdriveragent, global npm), reported as wdaProjectSource.
190
- const discoveredAppiumProject = !explicitProjectPath && (action === 'build' || action === 'start') ? discoverAppiumWdaProjects()[0] : undefined;
216
+ const discoveredAppiumProject = !explicitProjectPath && isWdaXcodebuildAction(action) ? discoverAppiumWdaProjects()[0] : undefined;
191
217
  const projectPath = explicitProjectPath ?? discoveredAppiumProject;
192
218
  const wdaProjectSource = explicitProjectPath ? 'argument' : discoveredAppiumProject ? 'appium-discovered' : null;
193
219
  const ddPath = resolvePath(derivedDataPath) ?? configured.derivedDataPath;
@@ -286,7 +312,7 @@ export function registerWda(server, sessions) {
286
312
  });
287
313
  return qaOk({ stopped: true, pid: proc.pid, logUri: proc.logUri }, `stopped managed WDA pid ${proc.pid}`);
288
314
  }
289
- if (action === 'build' || action === 'start') {
315
+ if (isWdaXcodebuildAction(action)) {
290
316
  if (!targetUdid) {
291
317
  return qaError({
292
318
  what: `qa_wda ${action} requires a simulator/device UDID`,
@@ -389,60 +415,30 @@ export function registerWda(server, sessions) {
389
415
  status: 'approved',
390
416
  });
391
417
  if (action === 'build') {
392
- sessions.milestone(session, 'wda_build_start');
393
- const r = await run('xcodebuild', args, { timeoutMs: 180000 });
394
- sessions.milestone(session, 'wda_build_end');
395
- const log = `${r.stdout}\n${r.stderr}`;
396
- const logUri = sessions.saveArtifact(session, 'wda', `wda-build-${Date.now()}.log`, log, 'text/plain', `WDA build ${r.code === 0 ? 'success' : 'failed'}`);
397
- sessions.addEnvChange(session, `wda build ${projectPath} ${targetUdid}`);
398
- if (r.code !== 0) {
399
- const failureCode = classifyWdaBuildFailure(log);
400
- sessions.recordMutation(session, {
401
- tool: 'qa_wda',
402
- action: 'wda_build',
403
- risk: 'medium',
404
- target: { udid: targetUdid, projectPath, derivedDataPath: ddPath, scheme: scheme ?? null },
405
- consent: { required: true, consentId, approved: true },
406
- status: 'blocked',
407
- ledgerUri: logUri,
408
- detail: `${failureCode}: exit ${r.code}`,
409
- });
410
- return qaError({
411
- what: failureCode === 'WDA_SIGNING_FAILED'
412
- ? `WDA signing/provisioning failed with exit ${r.code}`
413
- : `WDA build failed with exit ${r.code}`,
414
- changedState: true,
415
- retrySafe: failureCode !== 'WDA_SIGNING_FAILED',
416
- failureCode,
417
- artifactUri: logUri,
418
- nextSteps: failureCode === 'WDA_SIGNING_FAILED'
419
- ? [
420
- 'Open the WDA build log artifact, configure a valid development team/certificate/provisioning profile for the device, then retry.',
421
- ]
422
- : ['Open the WDA build log artifact, fix the Xcode build error, then retry.'],
423
- }, { xcode, logUri, timedOut: r.timedOut });
424
- }
425
- sessions.recordMutation(session, {
426
- tool: 'qa_wda',
427
- action: 'wda_build',
428
- risk: 'medium',
429
- target: { udid: targetUdid, projectPath, derivedDataPath: ddPath, scheme: scheme ?? null },
430
- consent: { required: true, consentId, approved: true },
431
- status: 'executed',
432
- ledgerUri: logUri,
433
- });
434
- const wdaBuildProduct = managedWdaBuildProductStatus(ddPath);
435
- return qaOk({
436
- built: true,
437
- logUri,
418
+ // xcodebuild build-for-testing can take minutes: run it as a background job so this call
419
+ // returns at once (one tool call stays under client timeouts). Consent was consumed above.
420
+ const job = sessions.createJob(session, 'wda_build');
421
+ const buildCtx = {
422
+ projectPath,
423
+ udid: targetUdid,
424
+ derivedDataPath: ddPath,
425
+ scheme,
426
+ consentId,
427
+ args,
438
428
  xcode,
439
- command: ['xcodebuild', ...args],
429
+ wdaProjectSource,
440
430
  wdaConfig: effectiveConfig,
431
+ };
432
+ void runWithSignal(sessions.abortSignal(session, job.jobId), () => runWdaBuildJob(sessions, session, job, buildCtx));
433
+ return qaOk({
434
+ jobId: job.jobId,
435
+ status: 'running',
436
+ kind: job.kind,
437
+ command: ['xcodebuild', ...args],
441
438
  wdaProjectPath: projectPath,
442
439
  wdaProjectSource,
443
440
  derivedDataPath: ddPath,
444
- wdaBuildProduct,
445
- }, `WDA build completed (${wdaProjectSource === 'appium-discovered' ? 'auto-discovered Appium WDA ' : ''}${projectPath}) > ${logUri}`);
441
+ }, `Started WDA build (${wdaProjectSource === 'appium-discovered' ? 'auto-discovered Appium WDA ' : ''}${projectPath}) as job ${job.jobId}.\nNext: qa_job_status { sessionId:"${session.id}", jobId:"${job.jobId}", waitMs:45000 }, then qa_wda start.`);
446
442
  }
447
443
  const logUri = sessions.saveArtifact(session, 'wda', `wda-start-${Date.now()}.log`, '', 'text/plain', 'WDA start log');
448
444
  const rec = sessions.findArtifact(logUri);
@@ -479,9 +475,43 @@ export function registerWda(server, sessions) {
479
475
  ledgerUri: logUri,
480
476
  detail: 'managed WDA process started',
481
477
  });
482
- const waited = await waitForWdaReady(url, configured.startupTimeoutMs);
478
+ // Cancelled while waiting: WDA stays started (managed + registered, so the next attach can
479
+ // adopt it); report CANCELLED, not a WDA startup failure.
480
+ // Bounded in-call wait: at most WDA_START_CALL_WAIT_MS here; the rest of the configured
481
+ // startupTimeoutMs is polled by the agent via qa_wait { for:"wda_ready" }.
482
+ const callWaitMs = Math.min(configured.startupTimeoutMs, WDA_START_CALL_WAIT_MS);
483
+ const waited = await waitForWdaReady(url, callWaitMs).catch((e) => {
484
+ if (isAbortError(e))
485
+ return null;
486
+ throw e;
487
+ });
488
+ if (!waited)
489
+ return cancelledResult('qa_wda start cancelled while waiting for WDA; the managed WDA process was left running', true);
483
490
  sessions.addMilestoneDuration(session, 'wda_startup_wait_ms', waited.durationMs);
491
+ const remainingMs = configured.startupTimeoutMs - waited.durationMs;
492
+ if (!waited.ready && remainingMs > 0 && pidIsAlive(child.pid ?? -1)) {
493
+ return qaOk({
494
+ started: true,
495
+ ready: false,
496
+ status: 'starting',
497
+ pid: child.pid ?? null,
498
+ logUri,
499
+ webDriverAgentUrl: url,
500
+ wdaProjectPath: projectPath,
501
+ wdaProjectSource,
502
+ command: ['xcodebuild', ...args],
503
+ wdaConfig: effectiveConfig,
504
+ wda: waited.status,
505
+ startupWaitMs: waited.durationMs,
506
+ startupTimeoutMs: configured.startupTimeoutMs,
507
+ remainingStartupMs: remainingMs,
508
+ }, `started managed WDA pid ${child.pid ?? 'unknown'}; /status not ready yet after ${Math.round(waited.durationMs / 1000)} s (still starting) > ${logUri}
509
+ ` +
510
+ `Next: qa_wait { sessionId:"${session.id}", for:"wda_ready" } (repeat while timedOut, up to ~${Math.ceil(remainingMs / 1000)} s more), then qa_wda attach.`);
511
+ }
484
512
  if (!waited.ready) {
513
+ const exited = remainingMs > 0; // only reached early when the xcodebuild process is gone
514
+ const why = `WDA not ready after ${waited.durationMs}ms${exited ? ' (the managed xcodebuild process exited)' : ''}`;
485
515
  sessions.recordMutation(session, {
486
516
  tool: 'qa_wda',
487
517
  action: 'wda_start',
@@ -497,10 +527,12 @@ export function registerWda(server, sessions) {
497
527
  consent: { required: true, consentId, approved: true },
498
528
  status: 'blocked',
499
529
  ledgerUri: logUri,
500
- detail: `WDA not ready after ${configured.startupTimeoutMs}ms`,
530
+ detail: why,
501
531
  });
502
532
  return qaError({
503
- what: `Managed WDA did not become ready at ${url} within ${configured.startupTimeoutMs}ms`,
533
+ what: exited
534
+ ? `Managed WDA exited before becoming ready at ${url} (waited ${waited.durationMs}ms)`
535
+ : `Managed WDA did not become ready at ${url} within ${configured.startupTimeoutMs}ms`,
504
536
  changedState: true,
505
537
  retrySafe: true,
506
538
  failureCode: 'WDA_START_FAILED',
@@ -732,4 +764,111 @@ export function registerWda(server, sessions) {
732
764
  }
733
765
  });
734
766
  }
767
+ /** Background body of `qa_wda build` (runs inside the job's cancellation scope). The job ends
768
+ * done (built) or failed (classified build failure); a cancelled job is left cancelled. Exported
769
+ * for tests. */
770
+ export async function runWdaBuildJob(sessions, session, job, ctx) {
771
+ const signal = sessions.abortSignal(session, job.jobId);
772
+ const upd = (patch) => sessions.updateJobIfRunning(session, job, patch);
773
+ const target = { udid: ctx.udid, projectPath: ctx.projectPath, derivedDataPath: ctx.derivedDataPath, scheme: ctx.scheme ?? null };
774
+ const consent = { required: true, consentId: ctx.consentId, approved: true };
775
+ upd({ progress: 'xcodebuild build-for-testing' });
776
+ sessions.milestone(session, 'wda_build_start');
777
+ let r;
778
+ try {
779
+ r = await run('xcodebuild', ctx.args, { timeoutMs: WDA_BUILD_TIMEOUT_MS, signal });
780
+ }
781
+ catch (e) {
782
+ if (isAbortError(e, signal))
783
+ return; // cancelled (qa_job_cancel / shutdown): the job already says so
784
+ const what = `xcodebuild could not run: ${String(e.message ?? e)}`;
785
+ sessions.recordMutation(session, {
786
+ tool: 'qa_wda',
787
+ action: 'wda_build',
788
+ risk: 'medium',
789
+ target,
790
+ consent,
791
+ status: 'blocked',
792
+ detail: what,
793
+ });
794
+ upd({
795
+ status: 'failed',
796
+ error: `WDA_BUILD_FAILED: ${what}`,
797
+ result: { failureCode: 'WDA_BUILD_FAILED', nextSteps: ['Check the Xcode installation (qa_wda doctor), then retry qa_wda build.'] },
798
+ resultText: `WDA build could not start: ${what}`,
799
+ endedAt: Date.now(),
800
+ });
801
+ return;
802
+ }
803
+ sessions.milestone(session, 'wda_build_end');
804
+ if (signal?.aborted)
805
+ return;
806
+ const log = `${r.stdout}\n${r.stderr}`;
807
+ const logUri = sessions.saveArtifact(session, 'wda', `wda-build-${Date.now()}.log`, log, 'text/plain', `WDA build ${r.code === 0 ? 'success' : 'failed'}`);
808
+ sessions.addEnvChange(session, `wda build ${ctx.projectPath} ${ctx.udid}`);
809
+ if (r.code !== 0) {
810
+ const failureCode = classifyWdaBuildFailure(log);
811
+ sessions.recordMutation(session, {
812
+ tool: 'qa_wda',
813
+ action: 'wda_build',
814
+ risk: 'medium',
815
+ target,
816
+ consent,
817
+ status: 'blocked',
818
+ ledgerUri: logUri,
819
+ detail: `${failureCode}: exit ${r.code}`,
820
+ });
821
+ const signing = failureCode === 'WDA_SIGNING_FAILED';
822
+ const what = signing
823
+ ? `WDA signing/provisioning failed with exit ${r.code}`
824
+ : `WDA build failed with exit ${r.code}${r.timedOut ? ` (timed out after ${WDA_BUILD_TIMEOUT_MS} ms)` : ''}`;
825
+ upd({
826
+ status: 'failed',
827
+ error: `${failureCode}: ${what}`,
828
+ artifactUris: [logUri],
829
+ result: {
830
+ failureCode,
831
+ logUri,
832
+ timedOut: r.timedOut,
833
+ retrySafe: !signing,
834
+ nextSteps: signing
835
+ ? [
836
+ 'Open the WDA build log artifact, configure a valid development team/certificate/provisioning profile for the device, then retry.',
837
+ ]
838
+ : ['Open the WDA build log artifact, fix the Xcode build error, then retry.'],
839
+ },
840
+ resultText: `${what}. Log: ${logUri}`,
841
+ endedAt: Date.now(),
842
+ });
843
+ return;
844
+ }
845
+ sessions.recordMutation(session, {
846
+ tool: 'qa_wda',
847
+ action: 'wda_build',
848
+ risk: 'medium',
849
+ target,
850
+ consent,
851
+ status: 'executed',
852
+ ledgerUri: logUri,
853
+ });
854
+ const wdaBuildProduct = managedWdaBuildProductStatus(ctx.derivedDataPath);
855
+ upd({
856
+ status: 'done',
857
+ progress: 'done',
858
+ artifactUris: [logUri],
859
+ result: {
860
+ built: true,
861
+ logUri,
862
+ xcode: ctx.xcode,
863
+ command: ['xcodebuild', ...ctx.args],
864
+ wdaConfig: ctx.wdaConfig,
865
+ wdaProjectPath: ctx.projectPath,
866
+ wdaProjectSource: ctx.wdaProjectSource,
867
+ derivedDataPath: ctx.derivedDataPath,
868
+ wdaBuildProduct,
869
+ },
870
+ resultText: `WDA build completed (${ctx.wdaProjectSource === 'appium-discovered' ? 'auto-discovered Appium WDA ' : ''}${ctx.projectPath}) > ${logUri}\nNext: qa_wda start.`,
871
+ endedAt: Date.now(),
872
+ });
873
+ }
735
874
  //# sourceMappingURL=wda.js.map