@ait-co/devtools 0.1.143 → 0.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 (223) hide show
  1. package/README.en.md +65 -163
  2. package/README.md +65 -192
  3. package/dist/in-app/auto.d.ts +1 -138
  4. package/dist/in-app/auto.js +28 -1102
  5. package/dist/in-app/auto.js.map +1 -1
  6. package/dist/in-app/index.d.ts +38 -547
  7. package/dist/in-app/index.d.ts.map +1 -1
  8. package/dist/in-app/index.js +62 -939
  9. package/dist/in-app/index.js.map +1 -1
  10. package/dist/mcp/cli.d.ts +1 -54
  11. package/dist/mcp/cli.js +33 -9720
  12. package/dist/mcp/cli.js.map +1 -1
  13. package/dist/mcp/server.d.ts +1 -88
  14. package/dist/mcp/server.js +36 -1076
  15. package/dist/mcp/server.js.map +1 -1
  16. package/dist/mock/index.d.ts +19 -20
  17. package/dist/mock/index.d.ts.map +1 -1
  18. package/dist/mock/index.js.map +1 -1
  19. package/dist/optional-peers-CDEFhlhJ.cjs +131 -0
  20. package/dist/optional-peers-CDEFhlhJ.cjs.map +1 -0
  21. package/dist/optional-peers-FdVbUJst.js +96 -0
  22. package/dist/optional-peers-FdVbUJst.js.map +1 -0
  23. package/dist/panel/index.js +1 -103
  24. package/dist/panel/index.js.map +1 -1
  25. package/dist/relay-url-store-CPZAn-T5.js +107 -0
  26. package/dist/relay-url-store-CPZAn-T5.js.map +1 -0
  27. package/dist/relay-url-store-DLjlvMSA.cjs +108 -0
  28. package/dist/relay-url-store-DLjlvMSA.cjs.map +1 -0
  29. package/dist/stubs/bin-devtools-mcp.js +58 -0
  30. package/dist/stubs/bin-devtools-mcp.js.map +1 -0
  31. package/dist/stubs/bin-devtools-test.d.ts +2 -0
  32. package/dist/stubs/bin-devtools-test.js +55 -0
  33. package/dist/stubs/bin-devtools-test.js.map +1 -0
  34. package/dist/test-runner/config.d.ts +1 -231
  35. package/dist/test-runner/config.js +41 -45
  36. package/dist/test-runner/config.js.map +1 -1
  37. package/dist/{tunnel-BVSMXctM.cjs → tunnel-BKZkOyQp.cjs} +27 -75
  38. package/dist/tunnel-BKZkOyQp.cjs.map +1 -0
  39. package/dist/{tunnel-D7xkimBu.js → tunnel-CqSCIrdU.js} +27 -75
  40. package/dist/tunnel-CqSCIrdU.js.map +1 -0
  41. package/dist/unplugin/index.cjs +17 -30
  42. package/dist/unplugin/index.cjs.map +1 -1
  43. package/dist/unplugin/index.d.cts +34 -5
  44. package/dist/unplugin/index.d.cts.map +1 -1
  45. package/dist/unplugin/index.d.ts +35 -6
  46. package/dist/unplugin/index.d.ts.map +1 -1
  47. package/dist/unplugin/index.js +17 -30
  48. package/dist/unplugin/index.js.map +1 -1
  49. package/dist/unplugin/tunnel.cjs +61 -74
  50. package/dist/unplugin/tunnel.cjs.map +1 -1
  51. package/dist/unplugin/tunnel.d.cts +22 -16
  52. package/dist/unplugin/tunnel.d.cts.map +1 -1
  53. package/dist/unplugin/tunnel.d.ts +22 -16
  54. package/dist/unplugin/tunnel.d.ts.map +1 -1
  55. package/dist/unplugin/tunnel.js +61 -74
  56. package/dist/unplugin/tunnel.js.map +1 -1
  57. package/package.json +17 -22
  58. package/dist/attach-orchestrator-0F0m_UqQ.js +0 -1845
  59. package/dist/attach-orchestrator-0F0m_UqQ.js.map +0 -1
  60. package/dist/attach-orchestrator-D65KxFy_.js +0 -1831
  61. package/dist/attach-orchestrator-D65KxFy_.js.map +0 -1
  62. package/dist/attach-orchestrator-DL3NQ9ca.js +0 -1846
  63. package/dist/attach-orchestrator-DL3NQ9ca.js.map +0 -1
  64. package/dist/bundle-C796JIwG.d.ts +0 -159
  65. package/dist/bundle-C796JIwG.d.ts.map +0 -1
  66. package/dist/capture-DsP525OZ.d.ts +0 -58
  67. package/dist/capture-DsP525OZ.d.ts.map +0 -1
  68. package/dist/cdp-connection-rP1WdnH5.d.ts +0 -287
  69. package/dist/cdp-connection-rP1WdnH5.d.ts.map +0 -1
  70. package/dist/cell-BaLvusOl.js +0 -68
  71. package/dist/cell-BaLvusOl.js.map +0 -1
  72. package/dist/cell-CBUS3-nT.js +0 -274
  73. package/dist/cell-CBUS3-nT.js.map +0 -1
  74. package/dist/cell-EBKKpAAT.js +0 -307
  75. package/dist/cell-EBKKpAAT.js.map +0 -1
  76. package/dist/chii-relay-BZ3HqWL5.js +0 -304
  77. package/dist/chii-relay-BZ3HqWL5.js.map +0 -1
  78. package/dist/chii-relay-D7eK2acz.cjs +0 -304
  79. package/dist/chii-relay-D7eK2acz.cjs.map +0 -1
  80. package/dist/debug-server-B3ABDrRI.js +0 -456
  81. package/dist/debug-server-B3ABDrRI.js.map +0 -1
  82. package/dist/debug-server-BWhwrVXa.js +0 -1158
  83. package/dist/debug-server-BWhwrVXa.js.map +0 -1
  84. package/dist/debug-server-CfQNxxGW.js +0 -600
  85. package/dist/debug-server-CfQNxxGW.js.map +0 -1
  86. package/dist/deeplink-B5-Hxu0Q.js +0 -62
  87. package/dist/deeplink-B5-Hxu0Q.js.map +0 -1
  88. package/dist/deeplink-BpO9qc-D.js +0 -62
  89. package/dist/deeplink-BpO9qc-D.js.map +0 -1
  90. package/dist/deeplink-BzdbA1gV.cjs +0 -62
  91. package/dist/deeplink-BzdbA1gV.cjs.map +0 -1
  92. package/dist/deeplink-DCScMYcp.cjs +0 -62
  93. package/dist/deeplink-DCScMYcp.cjs.map +0 -1
  94. package/dist/devtools-opener-3Drge_RJ.js +0 -75
  95. package/dist/devtools-opener-3Drge_RJ.js.map +0 -1
  96. package/dist/devtools-opener-B8nxrxqu.js +0 -71
  97. package/dist/devtools-opener-B8nxrxqu.js.map +0 -1
  98. package/dist/devtools-opener-BDY0w3_0.cjs +0 -68
  99. package/dist/devtools-opener-BDY0w3_0.cjs.map +0 -1
  100. package/dist/devtools-opener-BTl5A6Cd.js +0 -71
  101. package/dist/devtools-opener-BTl5A6Cd.js.map +0 -1
  102. package/dist/devtools-opener-CJpEsXXQ.js +0 -76
  103. package/dist/devtools-opener-CJpEsXXQ.js.map +0 -1
  104. package/dist/devtools-opener-CxtryS8c.js +0 -75
  105. package/dist/devtools-opener-CxtryS8c.js.map +0 -1
  106. package/dist/devtools-opener-iv1OwfJN.cjs +0 -68
  107. package/dist/devtools-opener-iv1OwfJN.cjs.map +0 -1
  108. package/dist/in-app/auto.d.ts.map +0 -1
  109. package/dist/mcp/cli.d.ts.map +0 -1
  110. package/dist/mcp/server.d.ts.map +0 -1
  111. package/dist/pool-DcaaOwUq.d.ts +0 -14761
  112. package/dist/pool-DcaaOwUq.d.ts.map +0 -1
  113. package/dist/qr-http-server--gl2-WKc.cjs +0 -1644
  114. package/dist/qr-http-server--gl2-WKc.cjs.map +0 -1
  115. package/dist/qr-http-server-Bb7lMeqi.js +0 -1644
  116. package/dist/qr-http-server-Bb7lMeqi.js.map +0 -1
  117. package/dist/qr-http-server-C_lqOrgc.js +0 -1644
  118. package/dist/qr-http-server-C_lqOrgc.js.map +0 -1
  119. package/dist/qr-http-server-CopuMbub.js +0 -1644
  120. package/dist/qr-http-server-CopuMbub.js.map +0 -1
  121. package/dist/qr-http-server-D-Off6K1.cjs +0 -1644
  122. package/dist/qr-http-server-D-Off6K1.cjs.map +0 -1
  123. package/dist/qr-http-server-DrbIVDjO.js +0 -1645
  124. package/dist/qr-http-server-DrbIVDjO.js.map +0 -1
  125. package/dist/qr-http-server-n1twN18z.js +0 -1644
  126. package/dist/qr-http-server-n1twN18z.js.map +0 -1
  127. package/dist/relay-factory-N9QobQxG.js +0 -206
  128. package/dist/relay-factory-N9QobQxG.js.map +0 -1
  129. package/dist/relay-secret-store-BPhN1upr.js +0 -240
  130. package/dist/relay-secret-store-BPhN1upr.js.map +0 -1
  131. package/dist/relay-secret-store-Bmyleu0A.js +0 -154
  132. package/dist/relay-secret-store-Bmyleu0A.js.map +0 -1
  133. package/dist/relay-secret-store-CQenfcSL.js +0 -154
  134. package/dist/relay-secret-store-CQenfcSL.js.map +0 -1
  135. package/dist/relay-secret-store-DKxs7zwq.js +0 -153
  136. package/dist/relay-secret-store-DKxs7zwq.js.map +0 -1
  137. package/dist/relay-secret-store-DWKdV-eY.cjs +0 -241
  138. package/dist/relay-secret-store-DWKdV-eY.cjs.map +0 -1
  139. package/dist/relay-secret-store-WJ8EGkIl.js +0 -153
  140. package/dist/relay-secret-store-WJ8EGkIl.js.map +0 -1
  141. package/dist/relay-url-store-1FGuSYAn.cjs +0 -115
  142. package/dist/relay-url-store-1FGuSYAn.cjs.map +0 -1
  143. package/dist/relay-url-store-BR2XodiO.js +0 -123
  144. package/dist/relay-url-store-BR2XodiO.js.map +0 -1
  145. package/dist/relay-url-store-Bskcyeg8.js +0 -114
  146. package/dist/relay-url-store-Bskcyeg8.js.map +0 -1
  147. package/dist/relay-url-store-CH63fVCm.js +0 -122
  148. package/dist/relay-url-store-CH63fVCm.js.map +0 -1
  149. package/dist/relay-url-store-DaY1QPes.js +0 -123
  150. package/dist/relay-url-store-DaY1QPes.js.map +0 -1
  151. package/dist/relay-url-store-xmUuTjXA.js +0 -122
  152. package/dist/relay-url-store-xmUuTjXA.js.map +0 -1
  153. package/dist/relay-worker-B5HKkGUY.js +0 -832
  154. package/dist/relay-worker-B5HKkGUY.js.map +0 -1
  155. package/dist/relay-worker-YdlpZQl9.d.ts +0 -214
  156. package/dist/relay-worker-YdlpZQl9.d.ts.map +0 -1
  157. package/dist/rolldown-runtime-DGkTqVfb.js +0 -15
  158. package/dist/rolldown-runtime-DUslC3ob.js +0 -14
  159. package/dist/runtime-kn9DxOeg.d.ts +0 -249
  160. package/dist/runtime-kn9DxOeg.d.ts.map +0 -1
  161. package/dist/test-runner/bin.js +0 -2584
  162. package/dist/test-runner/bin.js.map +0 -1
  163. package/dist/test-runner/bridge-stub.d.ts +0 -125
  164. package/dist/test-runner/bridge-stub.d.ts.map +0 -1
  165. package/dist/test-runner/bridge-stub.js +0 -92
  166. package/dist/test-runner/bridge-stub.js.map +0 -1
  167. package/dist/test-runner/bundle.d.ts +0 -2
  168. package/dist/test-runner/bundle.js +0 -439
  169. package/dist/test-runner/bundle.js.map +0 -1
  170. package/dist/test-runner/capture.d.ts +0 -2
  171. package/dist/test-runner/capture.js +0 -44
  172. package/dist/test-runner/capture.js.map +0 -1
  173. package/dist/test-runner/config.d.ts.map +0 -1
  174. package/dist/test-runner/method-pace.d.ts +0 -82
  175. package/dist/test-runner/method-pace.d.ts.map +0 -1
  176. package/dist/test-runner/method-pace.js +0 -120
  177. package/dist/test-runner/method-pace.js.map +0 -1
  178. package/dist/test-runner/pool.d.ts +0 -2
  179. package/dist/test-runner/pool.js +0 -136
  180. package/dist/test-runner/pool.js.map +0 -1
  181. package/dist/test-runner/relay-factory.d.ts +0 -11245
  182. package/dist/test-runner/relay-factory.d.ts.map +0 -1
  183. package/dist/test-runner/relay-factory.js +0 -206
  184. package/dist/test-runner/relay-factory.js.map +0 -1
  185. package/dist/test-runner/relay-worker.d.ts +0 -2
  186. package/dist/test-runner/relay-worker.js +0 -2
  187. package/dist/test-runner/report.d.ts +0 -163
  188. package/dist/test-runner/report.d.ts.map +0 -1
  189. package/dist/test-runner/report.js +0 -198
  190. package/dist/test-runner/report.js.map +0 -1
  191. package/dist/test-runner/rpc.d.ts +0 -56
  192. package/dist/test-runner/rpc.d.ts.map +0 -1
  193. package/dist/test-runner/rpc.js +0 -98
  194. package/dist/test-runner/rpc.js.map +0 -1
  195. package/dist/test-runner/runtime.d.ts +0 -2
  196. package/dist/test-runner/runtime.js +0 -659
  197. package/dist/test-runner/runtime.js.map +0 -1
  198. package/dist/test-runner/task-graph.d.ts +0 -38
  199. package/dist/test-runner/task-graph.d.ts.map +0 -1
  200. package/dist/test-runner/task-graph.js +0 -182
  201. package/dist/test-runner/task-graph.js.map +0 -1
  202. package/dist/throttle-DKKzX1qC.js +0 -59
  203. package/dist/throttle-DKKzX1qC.js.map +0 -1
  204. package/dist/totp-95OAa20j.js +0 -64
  205. package/dist/totp-95OAa20j.js.map +0 -1
  206. package/dist/totp-BjtoQNfu.cjs +0 -64
  207. package/dist/totp-BjtoQNfu.cjs.map +0 -1
  208. package/dist/totp-CZLLKfOC.js +0 -200
  209. package/dist/totp-CZLLKfOC.js.map +0 -1
  210. package/dist/totp-DAxys-r0.js +0 -199
  211. package/dist/totp-DAxys-r0.js.map +0 -1
  212. package/dist/totp-DIbrZtI7.js +0 -189
  213. package/dist/totp-DIbrZtI7.js.map +0 -1
  214. package/dist/totp-Df252ZdA.cjs +0 -192
  215. package/dist/totp-Df252ZdA.cjs.map +0 -1
  216. package/dist/totp-DfekTBk3.js +0 -211
  217. package/dist/totp-DfekTBk3.js.map +0 -1
  218. package/dist/totp-Dwft0Kz7.js +0 -3
  219. package/dist/totp-WY6l0ysP.js +0 -190
  220. package/dist/totp-WY6l0ysP.js.map +0 -1
  221. package/dist/tunnel-BVSMXctM.cjs.map +0 -1
  222. package/dist/tunnel-D7xkimBu.js.map +0 -1
  223. /package/dist/{test-runner/bin.d.ts → stubs/bin-devtools-mcp.d.ts} +0 -0
@@ -1,832 +0,0 @@
1
- import { m as logInfo } from "./attach-orchestrator-0F0m_UqQ.js";
2
- import { r as generateTotp } from "./totp-DAxys-r0.js";
3
- import { bundleTestFile } from "./test-runner/bundle.js";
4
- import { parseCaptureLines } from "./test-runner/capture.js";
5
- import { r as runPermissionPreflight, t as PERMISSION_PREFLIGHT_TIMEOUT_MS } from "./cell-CBUS3-nT.js";
6
- import { injectAndRunBundle } from "./test-runner/rpc.js";
7
- import { EventEmitter } from "node:events";
8
- import { WebSocket } from "ws";
9
- //#region src/shared/relay-auth-close.ts
10
- /**
11
- * Shared constants for the relay's named TOTP-auth rejection (issue #478).
12
- *
13
- * Before #478 the relay rejected an unauthenticated WebSocket upgrade with a
14
- * raw `HTTP/1.1 401` + `socket.destroy()`. A handshake aborted that way is
15
- * indistinguishable from a network failure on the browser side — the
16
- * WebSocket only ever sees close code 1006, so the phone (env-2 launcher PWA)
17
- * could not tell "stale TOTP code" apart from "tunnel down" and stayed
18
- * silent. The fix is accept-then-close: complete the handshake, then close
19
- * with an application close code that NAMES the rejection.
20
- *
21
- * Three parties share this contract:
22
- * - `src/mcp/chii-relay.ts` (Node) sends the close frame / HTTP error body;
23
- * - `src/in-app/attach.ts` (browser) observes relay-bound WebSockets and
24
- * surfaces the code to the launcher shell;
25
- * - `src/mcp/chii-connection.ts` (Node daemon client) recognises the code
26
- * as an auth failure on its own `/client` dial (defensive — #439's fresh
27
- * code mint means it should not normally hit this).
28
- *
29
- * This module is intentionally dependency-free (no Node, no DOM) so it is
30
- * safe to import from both the browser in-app bundle and the MCP daemon
31
- * bundle.
32
- *
33
- * SECRET-HANDLING: these are fixed enum values. The close reason / error body
34
- * must never grow to carry a secret, a TOTP code, or a host.
35
- */
36
- /**
37
- * WebSocket close code sent by the relay when TOTP auth is rejected.
38
- *
39
- * 4000–4999 is the application-reserved range (RFC 6455 §7.4.2); 4401 mirrors
40
- * HTTP 401 so it reads as "unauthorized" at a glance.
41
- */
42
- const RELAY_AUTH_REJECT_CLOSE_CODE = 4401;
43
- /**
44
- * Close reason string accompanying {@link RELAY_AUTH_REJECT_CLOSE_CODE}, and
45
- * the `error` value of the relay's HTTP 401 JSON body. Enum string only —
46
- * never interpolated with request data.
47
- */
48
- const RELAY_AUTH_REJECT_REASON = "totp-rejected";
49
- //#endregion
50
- //#region src/mcp/chii-connection.ts
51
- /**
52
- * Production `CdpConnection` backed by the local Chii relay.
53
- *
54
- * Topology (debug mode):
55
- * phone target.js --WS--> Chii relay :9100 <--WS-- this connection
56
- *
57
- * The phone connects to the relay as a `target`; this module connects as a
58
- * `client` (the role a CDP frontend would take) so CDP events the page emits
59
- * (`Runtime.consoleAPICalled`, `Network.*`) flow back here. We buffer recent
60
- * events in ring buffers the tool layer reads via `getBufferedEvents`.
61
- *
62
- * Node-only: imports `ws`. Never bundled into the browser/in-app entries.
63
- *
64
- * Attach reliability (#281):
65
- * `refreshTargets()` emits an internal 'target:attached' event whenever a
66
- * new target is added to the relay. `waitForFirstTarget()` awaits that event
67
- * (with a polling-interval fallback) so `start_attach`'s attach wait
68
- * resolves deterministically rather than racing between polling rounds.
69
- */
70
- /** Max events retained per domain ring buffer. */
71
- const DEFAULT_BUFFER_SIZE = 500;
72
- /**
73
- * Substrings that mark a "relay websocket is dead" class error, as produced by
74
- * `handleDisconnect('relay WebSocket 연결이 끊겼습니다')` (ws close handler) and
75
- * the fail-fast `sendCommand` rejection (`relay에 연결되어 있지 않습니다 (...)`).
76
- *
77
- * Exported so callers outside this module (e.g. `test-runner/relay-worker.ts`)
78
- * can detect the same error class without hardcoding a fragile full-sentence
79
- * match — mirrors the `EVALUATE_TIMEOUT_MARKER` precedent in
80
- * `test-runner/relay-worker.ts`. Kept in sync with `classifyToolError`'s
81
- * `relayDisconnectError` branch in `mcp/errors.ts`.
82
- */
83
- const RELAY_DISCONNECT_MARKERS = ["relay WebSocket", "relay에 연결되어 있지 않습니다"];
84
- /**
85
- * Returns true when `message` matches the relay-websocket-dead error class
86
- * (socket closed, socket errored, or fail-fast "not connected" rejection).
87
- */
88
- function isRelayDisconnectMessage(message) {
89
- return RELAY_DISCONNECT_MARKERS.some((marker) => message.includes(marker));
90
- }
91
- function isObject(value) {
92
- return typeof value === "object" && value !== null;
93
- }
94
- function parseInbound(raw) {
95
- let parsed;
96
- try {
97
- parsed = JSON.parse(raw);
98
- } catch {
99
- return null;
100
- }
101
- if (!isObject(parsed)) return null;
102
- const message = {};
103
- if (typeof parsed.id === "number") message.id = parsed.id;
104
- if (typeof parsed.method === "string") message.method = parsed.method;
105
- if ("params" in parsed) message.params = parsed.params;
106
- if ("result" in parsed) message.result = parsed.result;
107
- if (isObject(parsed.error) && typeof parsed.error.message === "string") message.error = { message: parsed.error.message };
108
- return message;
109
- }
110
- const PHASE_1_EVENTS = [
111
- "Runtime.consoleAPICalled",
112
- "Network.requestWillBeSent",
113
- "Network.responseReceived"
114
- ];
115
- /**
116
- * Ring buffer size for `Runtime.exceptionThrown`.
117
- *
118
- * Exceptions are rarer than console messages but each is heavier (stack
119
- * trace). 50 is generous enough to cover a crash scenario while keeping
120
- * memory bounded.
121
- *
122
- * **Lifecycle note**: the exception buffer intentionally survives `replaced` /
123
- * `crashed` / `destroyed` lifecycle events — it is NOT cleared on target
124
- * transitions. Rationale: an exception fired just before a crash is exactly
125
- * the signal we want to preserve for root-cause analysis. The buffer
126
- * represents "exceptions seen in this MCP session", not "exceptions in the
127
- * current page".
128
- */
129
- const EXCEPTION_BUFFER_SIZE = 50;
130
- /** Default per-command timeout if neither option nor env var is set. */
131
- const DEFAULT_COMMAND_TIMEOUT_MS = 3e4;
132
- /**
133
- * Production CDP connection. Polls the relay for the first attached target,
134
- * opens a client websocket to it, enables Phase 1 domains, and buffers events.
135
- */
136
- var ChiiCdpConnection = class {
137
- /** Authoritative connection kind (issue #348) — relay-backed. */
138
- kind = "relay";
139
- relayBaseUrl;
140
- bufferSize;
141
- commandTimeoutMs;
142
- totpSecret;
143
- emitter = new EventEmitter();
144
- buffers = /* @__PURE__ */ new Map();
145
- targets = /* @__PURE__ */ new Map();
146
- ws = null;
147
- connectionState = "idle";
148
- nextCommandId = 1;
149
- /**
150
- * The single active target id under the single-attach model.
151
- * Updated by `refreshTargets()` whenever a non-null target is present.
152
- * Used to detect a new (different) target attach and evict the previous one.
153
- */
154
- activeTargetId = null;
155
- /** In-flight enableDomains() promise — concurrent callers share it. */
156
- enablingPromise = null;
157
- /** Pending request→response commands keyed by CDP message id. */
158
- pending = /* @__PURE__ */ new Map();
159
- /**
160
- * Timestamp (ms since epoch) of the most recent crash/destroy/detach event,
161
- * or `null` if no crash has been detected since the last `enableDomains()`.
162
- */
163
- lastCrashDetectedAt = null;
164
- /**
165
- * Per-target last-seen timestamp (ms since epoch). Updated on any inbound
166
- * CDP message carrying data from a target. Keyed by target id.
167
- */
168
- targetLastSeenAt = /* @__PURE__ */ new Map();
169
- /** Active heartbeat interval handle (only when `AIT_CDP_HEARTBEAT_MS` is set). */
170
- heartbeatHandle = null;
171
- /** Lifecycle event listeners (crash / destroyed / detached). */
172
- lifecycleListeners = [];
173
- constructor(options) {
174
- this.relayBaseUrl = options.relayBaseUrl.replace(/\/$/, "");
175
- this.bufferSize = options.bufferSize ?? DEFAULT_BUFFER_SIZE;
176
- this.totpSecret = options.totpSecret;
177
- const envMs = process.env.AIT_CDP_COMMAND_TIMEOUT_MS ? Number(process.env.AIT_CDP_COMMAND_TIMEOUT_MS) : void 0;
178
- this.commandTimeoutMs = (envMs !== void 0 && Number.isFinite(envMs) && envMs > 0 ? envMs : void 0) ?? options.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
179
- for (const event of PHASE_1_EVENTS) this.buffers.set(event, []);
180
- this.buffers.set("Runtime.exceptionThrown", []);
181
- this.emitter.setMaxListeners(0);
182
- }
183
- /** Refresh the attached-target list from the relay's `GET /targets`. */
184
- async refreshTargets() {
185
- let targetsUrl = `${this.relayBaseUrl}/targets`;
186
- if (this.totpSecret) {
187
- const code = generateTotp(this.totpSecret);
188
- targetsUrl += `?at=${encodeURIComponent(code)}`;
189
- }
190
- const res = await fetch(targetsUrl);
191
- if (!res.ok) throw new Error(`Chii relay /targets returned HTTP ${res.status} ${res.statusText}`);
192
- const body = await res.json();
193
- const list = isObject(body) && Array.isArray(body.targets) ? body.targets : [];
194
- let newestTargetId = null;
195
- for (const item of list) {
196
- if (!isObject(item) || typeof item.id !== "string") continue;
197
- newestTargetId = item.id;
198
- }
199
- if (newestTargetId !== null && this.activeTargetId !== null && newestTargetId !== this.activeTargetId) {
200
- const prevId = this.activeTargetId;
201
- logInfo("page.detached", { prevTargetId: prevId });
202
- this.evictTarget(prevId);
203
- }
204
- this.targets.clear();
205
- for (const item of list) {
206
- if (!isObject(item) || typeof item.id !== "string") continue;
207
- if (item.id !== newestTargetId) continue;
208
- this.targets.set(item.id, {
209
- id: item.id,
210
- title: typeof item.title === "string" ? item.title : "",
211
- url: typeof item.url === "string" ? item.url : ""
212
- });
213
- }
214
- if (newestTargetId !== null) this.activeTargetId = newestTargetId;
215
- else this.activeTargetId = null;
216
- const result = [...this.targets.values()];
217
- if (newestTargetId !== null) this.emitter.emit("target:attached", result);
218
- return result;
219
- }
220
- listTargets() {
221
- return [...this.targets.values()];
222
- }
223
- /**
224
- * Waits until at least one target matching `filterFn` is attached, then
225
- * resolves with the full target list at that moment.
226
- *
227
- * Resolution happens on whichever comes first:
228
- * (a) a `'target:attached'` event from `refreshTargets()` (triggered by
229
- * the /targets poll finding a new target), OR
230
- * (b) a `'target:attached'` event from `handleMessage()` (triggered by
231
- * the first inbound CDP message from a target — confirms the relay
232
- * websocket has data from the phone, not just a target entry in the map).
233
- *
234
- * This dual-signal approach eliminates the polling race that previously
235
- * caused `wait_for_attach` to resolve before the first CDP message arrived.
236
- *
237
- * Falls back to checking `listTargets()` every `pollIntervalMs` in case the
238
- * EventEmitter is missed (defensive belt-and-suspenders).
239
- *
240
- * @param filterFn - Predicate that the returned targets must satisfy.
241
- * @param timeoutMs - Reject after this many ms (default 90 000). Pass a
242
- * non-finite value (`Infinity`) to disable the rejection timer entirely —
243
- * used by the test-runner's unbounded QR-attach wait (devtools#735). Node
244
- * clamps `setTimeout(fn, Infinity)` to ~1ms, so a non-finite value MUST
245
- * skip arming the timer rather than pass it through.
246
- * @param pollIntervalMs - Fallback poll interval (default 500ms).
247
- */
248
- waitForFirstTarget(filterFn, timeoutMs = 9e4, pollIntervalMs = 500) {
249
- const current = this.listTargets();
250
- if (filterFn(current)) return Promise.resolve(current);
251
- return new Promise((resolve, reject) => {
252
- let settled = false;
253
- let pollHandle = null;
254
- const settle = (targets) => {
255
- if (settled) return;
256
- settled = true;
257
- if (timeoutHandle !== null) clearTimeout(timeoutHandle);
258
- if (pollHandle !== null) {
259
- clearInterval(pollHandle);
260
- pollHandle = null;
261
- }
262
- this.emitter.off("target:attached", onAttach);
263
- resolve(targets);
264
- };
265
- const onAttach = (targets) => {
266
- if (filterFn(targets)) settle(targets);
267
- };
268
- const timeoutHandle = Number.isFinite(timeoutMs) ? setTimeout(() => {
269
- if (settled) return;
270
- settled = true;
271
- if (pollHandle !== null) {
272
- clearInterval(pollHandle);
273
- pollHandle = null;
274
- }
275
- this.emitter.off("target:attached", onAttach);
276
- reject(/* @__PURE__ */ new Error(`waitForFirstTarget: 타임아웃 (${timeoutMs}ms) — 폰이 relay에 attach되지 않았습니다.`));
277
- }, timeoutMs) : null;
278
- this.emitter.on("target:attached", onAttach);
279
- pollHandle = setInterval(() => {
280
- this.refreshTargets().then((targets) => {
281
- if (filterFn(targets)) settle(targets);
282
- }, () => {});
283
- }, pollIntervalMs);
284
- });
285
- }
286
- /**
287
- * Timestamp (ms since epoch) of the most recent crash/destroy/detach event
288
- * detected since the last `enableDomains()` call, or `null` if none.
289
- */
290
- getLastCrashDetectedAt() {
291
- return this.lastCrashDetectedAt;
292
- }
293
- /**
294
- * Last-seen timestamp (ms since epoch) for a given target id, or `null` if
295
- * the target is unknown / no message has been received from it yet.
296
- */
297
- getTargetLastSeenAt(targetId) {
298
- return this.targetLastSeenAt.get(targetId) ?? null;
299
- }
300
- /** Subscribe to target lifecycle events (crash / destroyed / detached). */
301
- onLifecycle(listener) {
302
- this.lifecycleListeners.push(listener);
303
- return () => {
304
- const idx = this.lifecycleListeners.indexOf(listener);
305
- if (idx !== -1) this.lifecycleListeners.splice(idx, 1);
306
- };
307
- }
308
- /**
309
- * Connect a client websocket to the first attached target and enable Phase 1
310
- * domains. Resolves once the socket is open and enable commands are sent.
311
- */
312
- async enableDomains() {
313
- if (this.ws && this.ws.readyState === WebSocket.OPEN) return;
314
- if (this.enablingPromise) return this.enablingPromise;
315
- this.enablingPromise = this._doEnableDomains().finally(() => {
316
- this.enablingPromise = null;
317
- });
318
- return this.enablingPromise;
319
- }
320
- async _doEnableDomains() {
321
- const target = (await this.refreshTargets())[0];
322
- if (!target) throw new Error("No mini-app page attached to the Chii relay yet.");
323
- let clientUrl = `${this.relayBaseUrl.replace(/^http/, "ws")}/client/${`devtools-mcp-${Date.now()}`}?target=${encodeURIComponent(target.id)}`;
324
- if (this.totpSecret) {
325
- const code = generateTotp(this.totpSecret);
326
- clientUrl += `&at=${encodeURIComponent(code)}`;
327
- }
328
- const ws = new WebSocket(clientUrl);
329
- this.ws = ws;
330
- await new Promise((resolve, reject) => {
331
- ws.once("open", () => resolve());
332
- ws.once("error", (err) => reject(err));
333
- ws.once("close", (code) => {
334
- if (code === 4401) reject(/* @__PURE__ */ new Error("relay 인증(TOTP)이 거부됐습니다 (close 4401). 코드가 만료됐을 수 있습니다 — 재연결 시 새 코드가 발급됩니다."));
335
- });
336
- });
337
- this.lastCrashDetectedAt = null;
338
- this.targetLastSeenAt.clear();
339
- this.connectionState = "connected";
340
- ws.on("message", (data) => this.handleMessage(data.toString()));
341
- ws.on("close", (code) => this.handleDisconnect(code === 4401 ? "relay 인증(TOTP)이 거부돼 연결이 종료됐습니다 (close 4401)" : "relay WebSocket 연결이 끊겼습니다"));
342
- ws.on("error", (err) => this.handleDisconnect(`relay WebSocket 오류: ${err.message}`));
343
- this.sendFireAndForget("Runtime.enable");
344
- this.sendFireAndForget("Network.enable");
345
- this.sendFireAndForget("DOM.enable");
346
- this.sendFireAndForget("Page.enable");
347
- this.sendFireAndForget("Inspector.enable");
348
- this.sendFireAndForget("Target.setDiscoverTargets", { discover: true });
349
- this.startHeartbeat(target.id);
350
- }
351
- /** Fire-and-forget CDP message (used for `*.enable`, no result awaited). */
352
- sendFireAndForget(method, params = {}) {
353
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
354
- const id = this.nextCommandId++;
355
- this.ws.send(JSON.stringify({
356
- id,
357
- method,
358
- params
359
- }));
360
- }
361
- /**
362
- * Issue a CDP command and resolve with its result (Phase 2). Rejects on a CDP
363
- * error frame or when no websocket is open (no page attached yet).
364
- *
365
- * @param opts.timeoutMs - Per-call override for this connection's command
366
- * watchdog (devtools#747) — see the `CdpConnection.send` docblock for the
367
- * contract callers racing their own longer timeout must follow.
368
- */
369
- send(method, params, opts) {
370
- return this.sendCommand(method, params ?? {}, opts);
371
- }
372
- /**
373
- * Issue an arbitrary request→response command over the relay and resolve with
374
- * its raw result. Both the typed CDP {@link send} and the AIT domain (Phase 3
375
- * `AIT.*` methods, forwarded over the same Chii channel) build on this.
376
- *
377
- * Rejects immediately if the connection is disconnected (fail-fast — no
378
- * auto-reconnect). Caller should re-run `list_pages` or `enableDomains` to
379
- * reattach.
380
- *
381
- * Times out after `opts.timeoutMs` when given, else `commandTimeoutMs`
382
- * (default 30s, env `AIT_CDP_COMMAND_TIMEOUT_MS`) — see devtools#747: the
383
- * default 30s watchdog used to undercut the test-runner's own longer
384
- * file-evaluate race no matter what `--timeout` the caller asked for. On
385
- * timeout the pending entry is cleaned up and the promise rejects with a
386
- * descriptive Korean error. `Number.isFinite` guards against a non-finite
387
- * override (e.g. `Infinity`, mirroring `waitForFirstTarget`'s convention)
388
- * so an intentional "no watchdog" override doesn't get clamped by
389
- * `setTimeout`.
390
- */
391
- sendCommand(method, params = {}, opts) {
392
- if (this.connectionState === "disconnected") return Promise.reject(/* @__PURE__ */ new Error(`relay에 연결되어 있지 않습니다 (${method}). list_pages로 attach 상태를 확인하고 enableDomains()로 재연결하세요.`));
393
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return Promise.reject(/* @__PURE__ */ new Error("No mini-app page attached to the Chii relay yet. Call enableDomains() first."));
394
- const id = this.nextCommandId++;
395
- const ws = this.ws;
396
- const timeoutMs = opts?.timeoutMs !== void 0 && Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0 ? opts.timeoutMs : this.commandTimeoutMs;
397
- return new Promise((resolve, reject) => {
398
- const handle = setTimeout(() => {
399
- this.pending.delete(id);
400
- reject(/* @__PURE__ */ new Error(`CDP 명령이 타임아웃됐습니다 (${method}, ${timeoutMs}ms). 폰 측 토스 앱이 백그라운드로 내려갔거나 미니앱이 unload됐을 수 있습니다. list_pages로 attach 상태를 확인하세요.`));
401
- }, timeoutMs);
402
- this.pending.set(id, {
403
- resolve: (v) => {
404
- clearTimeout(handle);
405
- resolve(v);
406
- },
407
- reject: (e) => {
408
- clearTimeout(handle);
409
- reject(e);
410
- }
411
- });
412
- ws.send(JSON.stringify({
413
- id,
414
- method,
415
- params
416
- }));
417
- });
418
- }
419
- /**
420
- * Called on WebSocket `close` or `error` after a successful connection.
421
- * Rejects all pending commands and marks the connection as disconnected so
422
- * subsequent `sendCommand` calls fail fast (no auto-reconnect).
423
- */
424
- handleDisconnect(reason) {
425
- if (this.connectionState === "disconnected") return;
426
- this.connectionState = "disconnected";
427
- this.ws = null;
428
- this.stopHeartbeat();
429
- const err = /* @__PURE__ */ new Error(`${reason}. list_pages로 attach 상태를 확인하고 enableDomains()로 재연결하세요.`);
430
- for (const waiter of this.pending.values()) waiter.reject(err);
431
- this.pending.clear();
432
- }
433
- /**
434
- * Evict a previously active target under the single-attach model.
435
- * Rejects pending commands with a 'replaced-by-new-attach' reason and emits
436
- * a 'replaced' lifecycle event. Does NOT clear all targets — only the specific
437
- * targetId. The caller is responsible for rebuilding the targets map afterwards.
438
- *
439
- * The error message uses 'replaced-by-new-attach' so test assertions can match it.
440
- */
441
- evictTarget(targetId) {
442
- const detectedAt = (/* @__PURE__ */ new Date()).toISOString();
443
- this.targets.delete(targetId);
444
- this.targetLastSeenAt.delete(targetId);
445
- const err = /* @__PURE__ */ new Error(`[ait-debug] replaced-by-new-attach — 이전 page 세션이 새 attach로 교체됐습니다 (targetId=${targetId}). list_pages로 현재 attach 상태를 확인하세요.`);
446
- for (const waiter of this.pending.values()) waiter.reject(err);
447
- this.pending.clear();
448
- const event = {
449
- kind: "replaced",
450
- targetId,
451
- detectedAt
452
- };
453
- for (const listener of this.lifecycleListeners) try {
454
- listener(event);
455
- } catch {}
456
- }
457
- /**
458
- * Handle a page-level crash or target destruction event.
459
- * Removes the target from the in-memory map, rejects all pending commands,
460
- * and emits a lifecycle event.
461
- *
462
- * @param kind - Event kind: 'crashed' | 'destroyed' | 'detached'
463
- * @param targetId - The target ID from the event params (may be null for
464
- * Inspector.targetCrashed which has no targetId in the params).
465
- */
466
- handleTargetGone(kind, targetId) {
467
- const detectedAt = (/* @__PURE__ */ new Date()).toISOString();
468
- this.lastCrashDetectedAt = Date.now();
469
- if (targetId !== null) {
470
- this.targets.delete(targetId);
471
- this.targetLastSeenAt.delete(targetId);
472
- if (this.activeTargetId === targetId) this.activeTargetId = null;
473
- } else {
474
- this.targets.clear();
475
- this.targetLastSeenAt.clear();
476
- this.activeTargetId = null;
477
- }
478
- const err = /* @__PURE__ */ new Error(`[ait-debug] ${kind === "crashed" ? "page crash (Inspector.targetCrashed)" : kind === "destroyed" ? "target 종료 (Target.targetDestroyed)" : "target detach (Target.detachedFromTarget)"} 감지됨 — relay에서 제거됐습니다. 새 attach가 필요합니다 (list_pages로 확인 → enableDomains()로 재연결).`);
479
- for (const waiter of this.pending.values()) waiter.reject(err);
480
- this.pending.clear();
481
- const event = {
482
- kind,
483
- targetId,
484
- detectedAt
485
- };
486
- for (const listener of this.lifecycleListeners) try {
487
- listener(event);
488
- } catch {}
489
- }
490
- /**
491
- * Start the optional CDP heartbeat loop.
492
- *
493
- * When `AIT_CDP_HEARTBEAT_MS` is set to a positive integer, every interval
494
- * we send `Runtime.evaluate({expression: '1'})` to each active target. If
495
- * the command times out (2 s hard deadline) or errors, we treat the target
496
- * as dead and call `handleTargetGone`.
497
- *
498
- * This is a zombie-detector fallback: cloudflared keeps-alive the tunnel ws
499
- * even when the phone app has crashed, so the ws-level disconnect (#252) won't
500
- * fire. The heartbeat catches this gap.
501
- *
502
- * Default: OFF. Only activates when `AIT_CDP_HEARTBEAT_MS` is set.
503
- */
504
- startHeartbeat(initialTargetId) {
505
- this.stopHeartbeat();
506
- const envMs = process.env.AIT_CDP_HEARTBEAT_MS ? Number(process.env.AIT_CDP_HEARTBEAT_MS) : void 0;
507
- if (envMs === void 0 || !Number.isFinite(envMs) || envMs <= 0) return;
508
- const PING_TIMEOUT_MS = 2e3;
509
- this.heartbeatHandle = setInterval(() => {
510
- const targetIds = this.targets.size > 0 ? [...this.targets.keys()] : [initialTargetId];
511
- for (const targetId of targetIds) {
512
- const pingPromise = this.sendCommand("Runtime.evaluate", {
513
- expression: "1",
514
- returnByValue: true,
515
- timeout: PING_TIMEOUT_MS
516
- });
517
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("heartbeat timeout")), PING_TIMEOUT_MS + 500));
518
- Promise.race([pingPromise, timeoutPromise]).catch(() => {
519
- if (this.targets.has(targetId)) this.handleTargetGone("destroyed", targetId);
520
- });
521
- }
522
- }, envMs);
523
- }
524
- stopHeartbeat() {
525
- if (this.heartbeatHandle !== null) {
526
- clearInterval(this.heartbeatHandle);
527
- this.heartbeatHandle = null;
528
- }
529
- }
530
- handleMessage(raw) {
531
- const message = parseInbound(raw);
532
- if (!message) return;
533
- if (typeof message.id === "number" && this.pending.has(message.id)) {
534
- const waiter = this.pending.get(message.id);
535
- this.pending.delete(message.id);
536
- if (waiter) if (message.error) waiter.reject(new Error(message.error.message));
537
- else waiter.resolve(message.result);
538
- return;
539
- }
540
- const now = Date.now();
541
- let firstMessageSeen = false;
542
- for (const targetId of this.targets.keys()) {
543
- if (!this.targetLastSeenAt.has(targetId)) firstMessageSeen = true;
544
- this.targetLastSeenAt.set(targetId, now);
545
- }
546
- if (firstMessageSeen && this.targets.size > 0) this.emitter.emit("target:attached", [...this.targets.values()]);
547
- if (typeof message.method !== "string") return;
548
- if (message.method === "Inspector.targetCrashed") {
549
- this.handleTargetGone("crashed", null);
550
- return;
551
- }
552
- if (message.method === "Target.targetDestroyed") {
553
- const targetId = isObject(message.params) && typeof message.params.targetId === "string" ? message.params.targetId : null;
554
- this.handleTargetGone("destroyed", targetId);
555
- return;
556
- }
557
- if (message.method === "Target.detachedFromTarget") {
558
- const targetId = isObject(message.params) && typeof message.params.targetId === "string" ? message.params.targetId : null;
559
- this.handleTargetGone("detached", targetId);
560
- return;
561
- }
562
- if (!this.buffers.has(message.method)) return;
563
- const event = message.method;
564
- const buffer = this.buffers.get(event);
565
- if (!buffer) return;
566
- buffer.push(message.params);
567
- const cap = event === "Runtime.exceptionThrown" ? EXCEPTION_BUFFER_SIZE : this.bufferSize;
568
- if (buffer.length > cap) buffer.shift();
569
- this.emitter.emit(event, message.params);
570
- }
571
- getBufferedEvents(event) {
572
- return this.buffers.get(event) ?? [];
573
- }
574
- on(event, listener) {
575
- this.emitter.on(event, listener);
576
- return () => this.emitter.off(event, listener);
577
- }
578
- /** Close the relay client websocket and reject any in-flight commands. */
579
- close() {
580
- const ws = this.ws;
581
- this.stopHeartbeat();
582
- this.handleDisconnect("Chii relay connection closed");
583
- ws?.close();
584
- }
585
- };
586
- //#endregion
587
- //#region src/test-runner/relay-worker.ts
588
- /**
589
- * Per-file evaluate timeout (ms) for manual-variant files (devtools#741) — a
590
- * human is expected to be tapping through a native sheet, so the timeout must
591
- * be far longer than the unattended default (60s, rpc.ts DEFAULT_TIMEOUT_MS).
592
- * Fixed constant (not configurable in v1) — documented here + CLI `--help`.
593
- */
594
- const MANUAL_FILE_TIMEOUT_MS = 5 * 6e4;
595
- /**
596
- * Sentinel string embedded in the error message by `injectAndRunBundle` when
597
- * the per-file evaluate race hits the timeout. Used by the retry guard so only
598
- * genuine timeouts get a second attempt — not bundle errors or parse failures.
599
- *
600
- * Exported for unit tests that assert the retry path is taken.
601
- */
602
- const EVALUATE_TIMEOUT_MARKER = "rpc: evaluate timed out after";
603
- /**
604
- * Runs all `files` sequentially over the given CDP `connection`.
605
- *
606
- * For each file:
607
- * 1. Bundle with esbuild (includes SDK shim + runtime).
608
- * 2. Inject into the attached page via `Runtime.evaluate`.
609
- * 3. Await the `RunReport` JSON response.
610
- * 4. Accumulate results.
611
- *
612
- * Returns a `RelayRunReport` with per-file results and flattened totals.
613
- *
614
- * This function does NOT open or manage the relay connection — the caller
615
- * is responsible for attaching and closing it.
616
- *
617
- * TODO (#645): implement the Vitest `PoolRunnerInitializer` interface here
618
- * so that `runTestFilesOverRelay` can be used as a Vitest pool entry.
619
- *
620
- * @param connection - Active CDP connection (relay or local kind).
621
- * @param files - Absolute paths to test files, run in order.
622
- * @param opts - Optional per-run overrides.
623
- */
624
- async function runTestFilesOverRelay(connection, files, opts) {
625
- const wallStart = Date.now();
626
- const startedAt = new Date(wallStart).toISOString();
627
- const fileResults = [];
628
- let domainsEnabled = false;
629
- try {
630
- await connection.enableDomains();
631
- domainsEnabled = true;
632
- } catch (e) {
633
- process.stderr.write(`relay-worker: enableDomains() failed before run — console capture may be empty (${e instanceof Error ? e.message : String(e)})\n`);
634
- }
635
- const collectCaptures = opts?.collectCaptures === true;
636
- const liveConsole = [];
637
- let unsubscribeConsole;
638
- if (collectCaptures && domainsEnabled) unsubscribeConsole = connection.on("Runtime.consoleAPICalled", (event) => {
639
- liveConsole.push(event);
640
- });
641
- let pendingReconnectCheck = false;
642
- /**
643
- * Attempts one `enableDomains()` reconnect. Logs the attempt and outcome to
644
- * stderr. Never throws — a failed reconnect just means the caller proceeds
645
- * as today (each remaining file / the retry will fail-fast on the still-dead
646
- * socket).
647
- *
648
- * `context` selects the fixed log literal: 'next-file' for the between-files
649
- * case (a file's FINAL result was a WS-dead-class error — the primary #731
650
- * scenario), 'retry-precheck' for the defensive reconnect before retrying a
651
- * timed-out file (cheap no-op via `enableDomains()`'s idempotency when the
652
- * socket never actually died).
653
- *
654
- * SECRET-HANDLING: fixed literals only — no relay wss URL, TOTP code, or
655
- * tunnel host.
656
- */
657
- const attemptReconnect = async (context) => {
658
- process.stderr.write(context === "next-file" ? "relay-worker: relay connection lost — attempting reconnect before next file\n" : "relay-worker: evaluate timed out — attempting reconnect before retry\n");
659
- try {
660
- await connection.enableDomains();
661
- process.stderr.write("relay-worker: reconnect succeeded\n");
662
- } catch (e) {
663
- process.stderr.write(`relay-worker: reconnect failed (${e instanceof Error ? e.message : String(e)}) — continuing, remaining files may fail\n`);
664
- }
665
- };
666
- const manualFiles = opts?.manualFiles;
667
- const manualTotal = manualFiles?.size ?? 0;
668
- let manualIndex = 0;
669
- const stubBlockingFiles = opts?.stubBlockingFiles;
670
- let preflightPermissions;
671
- if (files.length > 0) preflightPermissions = await runPermissionPreflight(connection, PERMISSION_PREFLIGHT_TIMEOUT_MS, opts?.preflightSdkLine !== "3.x");
672
- const paceMs = opts?.paceMs;
673
- let ranFirstFile = false;
674
- try {
675
- for (const file of files) {
676
- if (pendingReconnectCheck) {
677
- await attemptReconnect("next-file");
678
- pendingReconnectCheck = false;
679
- }
680
- if (paceMs !== void 0 && paceMs > 0 && ranFirstFile) await new Promise((resolve) => setTimeout(resolve, paceMs));
681
- ranFirstFile = true;
682
- const isStubbed = stubBlockingFiles?.has(file) === true;
683
- const isManual = !isStubbed && manualFiles?.has(file) === true;
684
- if (isManual) {
685
- manualIndex += 1;
686
- opts?.onManualFile?.(file, manualIndex, manualTotal);
687
- }
688
- const fileTimeoutMs = isManual ? MANUAL_FILE_TIMEOUT_MS : opts?.timeoutMs;
689
- let fileEntry;
690
- try {
691
- const { code } = await bundleTestFile(file, opts?.bundleOptions);
692
- /**
693
- * Runs one evaluate attempt and returns a FileResult, or `null` when the
694
- * result is a genuine timeout and the caller should retry.
695
- *
696
- * We need to distinguish:
697
- * - `rpcResult.ok = false` + timeout error → retry candidate (`return null`)
698
- * - `rpcResult.ok = false` + other error → final error, no retry
699
- * - `injectAndRunBundle` throws → CDP exceptionDetails OR a
700
- * relay-disconnect rejection from `connection.send()` (page engine
701
- * threw, or the ws died mid-evaluate); treated as a final
702
- * (non-retryable) error either way.
703
- *
704
- * The Promise.race timeout in rpc.ts RETURNS `{ok:false, error: '…'}` (it
705
- * does NOT throw/reject). Only genuine CDP `exceptionDetails` (or a dead
706
- * `connection.send()`) cause a throw. This distinction is what makes the
707
- * EVALUATE_TIMEOUT_MARKER gate below reachable — the timeout result
708
- * surfaces as `rpcResult.ok=false` with the marker string, not as a
709
- * caught exception.
710
- */
711
- const attempt = async () => {
712
- let rpcResult;
713
- try {
714
- rpcResult = await injectAndRunBundle(connection, code, fileTimeoutMs);
715
- } catch (e) {
716
- return {
717
- file,
718
- result: { error: e instanceof Error ? e.message : String(e) }
719
- };
720
- }
721
- if (rpcResult.ok) return {
722
- file,
723
- result: rpcResult.report
724
- };
725
- if (rpcResult.error.includes("rpc: evaluate timed out after")) return null;
726
- return {
727
- file,
728
- result: { error: rpcResult.error }
729
- };
730
- };
731
- const firstResult = await attempt();
732
- if (firstResult !== null) fileEntry = firstResult;
733
- else {
734
- await attemptReconnect("retry-precheck");
735
- process.stderr.write(`relay-worker: evaluate timed out for ${file} — retrying once\n`);
736
- const retryResult = await attempt();
737
- if (retryResult !== null) fileEntry = retryResult;
738
- else fileEntry = {
739
- file,
740
- result: { error: `${EVALUATE_TIMEOUT_MARKER} ${opts?.timeoutMs ?? 6e4}ms (after retry) — the device's app may have been backgrounded` }
741
- };
742
- }
743
- } catch (e) {
744
- fileEntry = {
745
- file,
746
- result: { error: e instanceof Error ? e.message : String(e) }
747
- };
748
- }
749
- if ("error" in fileEntry.result && isRelayDisconnectMessage(fileEntry.result.error)) pendingReconnectCheck = true;
750
- if (isStubbed) fileEntry.mode = "stubbed";
751
- else if (isManual) fileEntry.mode = "manual";
752
- fileResults.push(fileEntry);
753
- }
754
- } finally {
755
- unsubscribeConsole?.();
756
- }
757
- const captures = collectCaptures ? parseCaptureLines(liveConsole.map((e) => ({ text: renderConsoleLineText(e) }))) : [];
758
- const totals = fileResults.reduce((acc, { result }) => {
759
- if ("error" in result) {
760
- acc.failed += 1;
761
- acc.total += 1;
762
- } else {
763
- acc.passed += result.passed;
764
- acc.failed += result.failed;
765
- acc.skipped += result.skipped;
766
- acc.total += result.passed + result.failed + result.skipped;
767
- }
768
- return acc;
769
- }, {
770
- passed: 0,
771
- failed: 0,
772
- skipped: 0,
773
- total: 0
774
- });
775
- return {
776
- startedAt,
777
- duration: Date.now() - wallStart,
778
- files: fileResults,
779
- totals,
780
- captures,
781
- ...preflightPermissions ? { preflight: { permissions: preflightPermissions } } : {}
782
- };
783
- }
784
- /**
785
- * Renders one `Runtime.consoleAPICalled` event to a single line of text, the
786
- * same way `tools.ts#normalizeConsoleMessage` does (args rendered + space-
787
- * joined). Inlined here (≈8 lines) so this module avoids importing `tools.ts`,
788
- * which would drag the heavy MCP/Node graph (server-lock, parent-watcher, …)
789
- * onto the test-runner entry.
790
- *
791
- * SECRET-HANDLING: this only stringifies console args; the caller's
792
- * allowlist-prefix parser then discards everything that is not a genuine
793
- * `__AIT_CAPTURE__` line.
794
- */
795
- function renderConsoleLineText(event) {
796
- return event.args.map((arg) => {
797
- if (arg.value !== void 0) {
798
- if (typeof arg.value === "string") return arg.value;
799
- try {
800
- return JSON.stringify(arg.value);
801
- } catch {
802
- return String(arg.value);
803
- }
804
- }
805
- if (arg.description !== void 0) return arg.description;
806
- if (arg.className !== void 0) return arg.className;
807
- return arg.subtype ?? arg.type;
808
- }).join(" ");
809
- }
810
- /**
811
- * Flattens all test results from a `RelayRunReport` into a single array.
812
- * Files that errored during bundle/inject produce a synthetic failed entry.
813
- */
814
- function flattenResults(report) {
815
- const out = [];
816
- for (const { file, result } of report.files) if ("error" in result) out.push({
817
- file,
818
- name: `<bundle/inject error>`,
819
- status: "fail",
820
- duration: 0,
821
- error: result.error
822
- });
823
- else for (const t of result.tests) out.push({
824
- ...t,
825
- file
826
- });
827
- return out;
828
- }
829
- //#endregion
830
- export { ChiiCdpConnection as a, runTestFilesOverRelay as i, MANUAL_FILE_TIMEOUT_MS as n, RELAY_AUTH_REJECT_CLOSE_CODE as o, flattenResults as r, RELAY_AUTH_REJECT_REASON as s, EVALUATE_TIMEOUT_MARKER as t };
831
-
832
- //# sourceMappingURL=relay-worker-B5HKkGUY.js.map