@ait-co/devtools 0.1.144 → 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 (181) hide show
  1. package/README.en.md +33 -217
  2. package/README.md +33 -246
  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/panel/index.js +1 -103
  20. package/dist/panel/index.js.map +1 -1
  21. package/dist/relay-url-store-CPZAn-T5.js +107 -0
  22. package/dist/relay-url-store-CPZAn-T5.js.map +1 -0
  23. package/dist/relay-url-store-DLjlvMSA.cjs +108 -0
  24. package/dist/relay-url-store-DLjlvMSA.cjs.map +1 -0
  25. package/dist/stubs/bin-devtools-mcp.js +58 -0
  26. package/dist/stubs/bin-devtools-mcp.js.map +1 -0
  27. package/dist/stubs/bin-devtools-test.d.ts +2 -0
  28. package/dist/stubs/bin-devtools-test.js +55 -0
  29. package/dist/stubs/bin-devtools-test.js.map +1 -0
  30. package/dist/test-runner/config.d.ts +1 -231
  31. package/dist/test-runner/config.js +41 -45
  32. package/dist/test-runner/config.js.map +1 -1
  33. package/dist/{tunnel-BGT9Curk.cjs → tunnel-BKZkOyQp.cjs} +1 -1
  34. package/dist/{tunnel-BGT9Curk.cjs.map → tunnel-BKZkOyQp.cjs.map} +1 -1
  35. package/dist/{tunnel-BOKmLzBO.js → tunnel-CqSCIrdU.js} +1 -1
  36. package/dist/{tunnel-BOKmLzBO.js.map → tunnel-CqSCIrdU.js.map} +1 -1
  37. package/dist/unplugin/index.cjs +9 -18
  38. package/dist/unplugin/index.cjs.map +1 -1
  39. package/dist/unplugin/index.d.cts +22 -3
  40. package/dist/unplugin/index.d.cts.map +1 -1
  41. package/dist/unplugin/index.d.ts +23 -4
  42. package/dist/unplugin/index.d.ts.map +1 -1
  43. package/dist/unplugin/index.js +10 -19
  44. package/dist/unplugin/index.js.map +1 -1
  45. package/package.json +10 -25
  46. package/dist/attach-orchestrator-0F0m_UqQ.js +0 -1845
  47. package/dist/attach-orchestrator-0F0m_UqQ.js.map +0 -1
  48. package/dist/attach-orchestrator-D65KxFy_.js +0 -1831
  49. package/dist/attach-orchestrator-D65KxFy_.js.map +0 -1
  50. package/dist/attach-orchestrator-DL3NQ9ca.js +0 -1846
  51. package/dist/attach-orchestrator-DL3NQ9ca.js.map +0 -1
  52. package/dist/bundle-C796JIwG.d.ts +0 -159
  53. package/dist/bundle-C796JIwG.d.ts.map +0 -1
  54. package/dist/capture-DsP525OZ.d.ts +0 -58
  55. package/dist/capture-DsP525OZ.d.ts.map +0 -1
  56. package/dist/cdp-connection-rP1WdnH5.d.ts +0 -287
  57. package/dist/cdp-connection-rP1WdnH5.d.ts.map +0 -1
  58. package/dist/cell-BaLvusOl.js +0 -68
  59. package/dist/cell-BaLvusOl.js.map +0 -1
  60. package/dist/cell-CBUS3-nT.js +0 -274
  61. package/dist/cell-CBUS3-nT.js.map +0 -1
  62. package/dist/cell-EBKKpAAT.js +0 -307
  63. package/dist/cell-EBKKpAAT.js.map +0 -1
  64. package/dist/chii-relay-B3ZhjGMi.js +0 -304
  65. package/dist/chii-relay-B3ZhjGMi.js.map +0 -1
  66. package/dist/chii-relay-CGMlePMd.cjs +0 -304
  67. package/dist/chii-relay-CGMlePMd.cjs.map +0 -1
  68. package/dist/debug-server-B3ABDrRI.js +0 -456
  69. package/dist/debug-server-B3ABDrRI.js.map +0 -1
  70. package/dist/debug-server-BWhwrVXa.js +0 -1158
  71. package/dist/debug-server-BWhwrVXa.js.map +0 -1
  72. package/dist/debug-server-CfQNxxGW.js +0 -600
  73. package/dist/debug-server-CfQNxxGW.js.map +0 -1
  74. package/dist/devtools-opener-3Drge_RJ.js +0 -75
  75. package/dist/devtools-opener-3Drge_RJ.js.map +0 -1
  76. package/dist/devtools-opener-CJpEsXXQ.js +0 -76
  77. package/dist/devtools-opener-CJpEsXXQ.js.map +0 -1
  78. package/dist/devtools-opener-CxtryS8c.js +0 -75
  79. package/dist/devtools-opener-CxtryS8c.js.map +0 -1
  80. package/dist/in-app/auto.d.ts.map +0 -1
  81. package/dist/mcp/cli.d.ts.map +0 -1
  82. package/dist/mcp/server.d.ts.map +0 -1
  83. package/dist/pool-DcaaOwUq.d.ts +0 -14761
  84. package/dist/pool-DcaaOwUq.d.ts.map +0 -1
  85. package/dist/qr-http-server-C_lqOrgc.js +0 -1644
  86. package/dist/qr-http-server-C_lqOrgc.js.map +0 -1
  87. package/dist/qr-http-server-CopuMbub.js +0 -1644
  88. package/dist/qr-http-server-CopuMbub.js.map +0 -1
  89. package/dist/qr-http-server-DrbIVDjO.js +0 -1645
  90. package/dist/qr-http-server-DrbIVDjO.js.map +0 -1
  91. package/dist/relay-factory-N9QobQxG.js +0 -206
  92. package/dist/relay-factory-N9QobQxG.js.map +0 -1
  93. package/dist/relay-secret-store-BR0YIkNv.cjs +0 -241
  94. package/dist/relay-secret-store-BR0YIkNv.cjs.map +0 -1
  95. package/dist/relay-secret-store-Bmyleu0A.js +0 -154
  96. package/dist/relay-secret-store-Bmyleu0A.js.map +0 -1
  97. package/dist/relay-secret-store-CQenfcSL.js +0 -154
  98. package/dist/relay-secret-store-CQenfcSL.js.map +0 -1
  99. package/dist/relay-secret-store-CYM8CBIF.js +0 -240
  100. package/dist/relay-secret-store-CYM8CBIF.js.map +0 -1
  101. package/dist/relay-secret-store-DKxs7zwq.js +0 -153
  102. package/dist/relay-secret-store-DKxs7zwq.js.map +0 -1
  103. package/dist/relay-secret-store-WJ8EGkIl.js +0 -153
  104. package/dist/relay-secret-store-WJ8EGkIl.js.map +0 -1
  105. package/dist/relay-url-store-BR2XodiO.js +0 -123
  106. package/dist/relay-url-store-BR2XodiO.js.map +0 -1
  107. package/dist/relay-url-store-C1as_m5G.cjs +0 -115
  108. package/dist/relay-url-store-C1as_m5G.cjs.map +0 -1
  109. package/dist/relay-url-store-CH63fVCm.js +0 -122
  110. package/dist/relay-url-store-CH63fVCm.js.map +0 -1
  111. package/dist/relay-url-store-CzFo_84F.js +0 -114
  112. package/dist/relay-url-store-CzFo_84F.js.map +0 -1
  113. package/dist/relay-url-store-DaY1QPes.js +0 -123
  114. package/dist/relay-url-store-DaY1QPes.js.map +0 -1
  115. package/dist/relay-url-store-xmUuTjXA.js +0 -122
  116. package/dist/relay-url-store-xmUuTjXA.js.map +0 -1
  117. package/dist/relay-worker-B5HKkGUY.js +0 -832
  118. package/dist/relay-worker-B5HKkGUY.js.map +0 -1
  119. package/dist/relay-worker-YdlpZQl9.d.ts +0 -214
  120. package/dist/relay-worker-YdlpZQl9.d.ts.map +0 -1
  121. package/dist/rolldown-runtime-DGkTqVfb.js +0 -15
  122. package/dist/rolldown-runtime-DUslC3ob.js +0 -14
  123. package/dist/runtime-kn9DxOeg.d.ts +0 -249
  124. package/dist/runtime-kn9DxOeg.d.ts.map +0 -1
  125. package/dist/test-runner/bin.js +0 -2584
  126. package/dist/test-runner/bin.js.map +0 -1
  127. package/dist/test-runner/bridge-stub.d.ts +0 -125
  128. package/dist/test-runner/bridge-stub.d.ts.map +0 -1
  129. package/dist/test-runner/bridge-stub.js +0 -92
  130. package/dist/test-runner/bridge-stub.js.map +0 -1
  131. package/dist/test-runner/bundle.d.ts +0 -2
  132. package/dist/test-runner/bundle.js +0 -439
  133. package/dist/test-runner/bundle.js.map +0 -1
  134. package/dist/test-runner/capture.d.ts +0 -2
  135. package/dist/test-runner/capture.js +0 -44
  136. package/dist/test-runner/capture.js.map +0 -1
  137. package/dist/test-runner/config.d.ts.map +0 -1
  138. package/dist/test-runner/method-pace.d.ts +0 -82
  139. package/dist/test-runner/method-pace.d.ts.map +0 -1
  140. package/dist/test-runner/method-pace.js +0 -120
  141. package/dist/test-runner/method-pace.js.map +0 -1
  142. package/dist/test-runner/pool.d.ts +0 -2
  143. package/dist/test-runner/pool.js +0 -136
  144. package/dist/test-runner/pool.js.map +0 -1
  145. package/dist/test-runner/relay-factory.d.ts +0 -11245
  146. package/dist/test-runner/relay-factory.d.ts.map +0 -1
  147. package/dist/test-runner/relay-factory.js +0 -206
  148. package/dist/test-runner/relay-factory.js.map +0 -1
  149. package/dist/test-runner/relay-worker.d.ts +0 -2
  150. package/dist/test-runner/relay-worker.js +0 -2
  151. package/dist/test-runner/report.d.ts +0 -163
  152. package/dist/test-runner/report.d.ts.map +0 -1
  153. package/dist/test-runner/report.js +0 -198
  154. package/dist/test-runner/report.js.map +0 -1
  155. package/dist/test-runner/rpc.d.ts +0 -56
  156. package/dist/test-runner/rpc.d.ts.map +0 -1
  157. package/dist/test-runner/rpc.js +0 -98
  158. package/dist/test-runner/rpc.js.map +0 -1
  159. package/dist/test-runner/runtime.d.ts +0 -2
  160. package/dist/test-runner/runtime.js +0 -659
  161. package/dist/test-runner/runtime.js.map +0 -1
  162. package/dist/test-runner/task-graph.d.ts +0 -38
  163. package/dist/test-runner/task-graph.d.ts.map +0 -1
  164. package/dist/test-runner/task-graph.js +0 -182
  165. package/dist/test-runner/task-graph.js.map +0 -1
  166. package/dist/throttle-DKKzX1qC.js +0 -59
  167. package/dist/throttle-DKKzX1qC.js.map +0 -1
  168. package/dist/totp-BqmCLSNA.js +0 -189
  169. package/dist/totp-BqmCLSNA.js.map +0 -1
  170. package/dist/totp-CMHR5lsW.cjs +0 -191
  171. package/dist/totp-CMHR5lsW.cjs.map +0 -1
  172. package/dist/totp-CZLLKfOC.js +0 -200
  173. package/dist/totp-CZLLKfOC.js.map +0 -1
  174. package/dist/totp-DAxys-r0.js +0 -199
  175. package/dist/totp-DAxys-r0.js.map +0 -1
  176. package/dist/totp-DfekTBk3.js +0 -211
  177. package/dist/totp-DfekTBk3.js.map +0 -1
  178. package/dist/totp-Dwft0Kz7.js +0 -3
  179. package/dist/totp-WY6l0ysP.js +0 -190
  180. package/dist/totp-WY6l0ysP.js.map +0 -1
  181. /package/dist/{test-runner/bin.d.ts → stubs/bin-devtools-mcp.d.ts} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"file":"runtime.js","names":["#received","#negated","#assert"],"sources":["../../src/test-runner/runtime.ts"],"sourcesContent":["/**\n * Thin test runtime for WebView execution.\n *\n * This file is bundled by `bundle.ts` together with the user's test file\n * into a single IIFE injected into the WebView via `Runtime.evaluate`.\n * It MUST stay browser-compatible — no Node.js APIs allowed.\n *\n * Design: rather than shipping @vitest/runner verbatim into a WebView (where\n * its Node-side internals cause issues), this runtime provides a minimal\n * compatible API surface — describe/it/test/expect globals — collects results\n * into a plain JSON-safe object, and exports `runTestModule` as the entry point.\n *\n * @vitest/runner and @vitest/expect are listed as dependencies so that the\n * package's type contracts are available and so the browser-compatible subsets\n * can be referenced. The Vitest custom pool that drives this runtime through\n * Vitest's `PoolRunnerInitializer` lives in `pool.ts`.\n *\n * NOTE: this file is imported by type from Node-side code (rpc.ts / relay-worker.ts)\n * for the RunReport / TestResult type shapes. The runtime ITSELF is not imported\n * at runtime on the Node side — only the types are used.\n */\n\nimport { isThrottledError } from './throttle.js';\n\n/* -------------------------------------------------------------------------- */\n/* Public result types (used by rpc.ts and relay-worker.ts) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Result of a single test case.\n * All fields are JSON-serialisable.\n */\nexport interface TestResult {\n /** Full dot-joined test name including nested suite names. */\n name: string;\n /** `'pass'` or `'fail'`. `'skip'` for skipped tests. */\n status: 'pass' | 'fail' | 'skip';\n /** Duration in milliseconds. */\n duration: number;\n /** Error message (fail only). Does NOT include the expression/secret. */\n error?: string;\n /**\n * Optional note attached to a skip — populated when the test body itself\n * calls `ctx.skip(cond, note)` (devtools#746), mirroring the static\n * `it.skip`/`it.skipIf` path which has no note today. `'skip'` status only.\n */\n note?: string;\n /**\n * Number of throttle-aware re-executions of the test BODY (devtools#767) —\n * present only when at least one retry was attempted (`>= 1`); omitted\n * (never `0`) for every test that never hit `APP_BRIDGE_THROTTLED`, so\n * existing consumers (sdk-example's diff script) see byte-identical reports\n * for the common case. A test that eventually PASSES after retrying still\n * carries this field — it is provenance, not a failure flag.\n */\n throttleRetries?: number;\n}\n\n/** Aggregate report returned by `runTestModule`. */\nexport interface RunReport {\n /** ISO timestamp of when `runTestModule` was called. */\n startedAt: string;\n /** Total elapsed milliseconds (wall-clock). */\n duration: number;\n passed: number;\n failed: number;\n skipped: number;\n tests: TestResult[];\n}\n\n/* -------------------------------------------------------------------------- */\n/* THROTTLED-aware test retry (devtools#767) */\n/* -------------------------------------------------------------------------- */\n\n/** Backoff delays (ms) between throttle-retry attempts: 1s, then 2s. */\nconst TEST_RETRY_BACKOFF_MS = [1_000, 2_000] as const;\n\n/** Max retry attempts per test after the initial run (devtools#767: \"최대 2회 재시도\"). */\nconst TEST_MAX_RETRIES = TEST_RETRY_BACKOFF_MS.length;\n\n/** Real `setTimeout`-backed sleep — the production default for {@link RunTestModuleOptions.sleep}. */\nconst DEFAULT_SLEEP = (ms: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, ms));\n\n/**\n * Test-only override surface for `runTestModule`. Never populated by the\n * generated bundle — see that function's doc.\n */\nexport interface RunTestModuleOptions {\n /**\n * Overrides the backoff/pacing sleep implementation. Defaults to a real\n * `setTimeout`-backed sleep. Unit tests pass a fake (e.g. an\n * immediately-resolving spy) so retry/pacing tests don't take real\n * wall-clock seconds.\n */\n sleep?: (ms: number) => Promise<void>;\n}\n\n/**\n * Renders a thrown value into a `TestResult.error` string. Handles three\n * shapes:\n * - a real `Error` (or subclass) — its `.message`;\n * - an error-LIKE plain object with a string `.message` (the 2.x native\n * bridge envelope this test-runner observes, e.g.\n * `{name, code, userInfo, moduleName, __isError}` — devtools#767's\n * `APP_BRIDGE_THROTTLED` rejections are exactly this shape, not an\n * `Error` instance) — its `.message` field;\n * - anything else — `String(e)` (the pre-#767 fallback, unchanged).\n *\n * Without the middle case, a native envelope's `.message` (the only useful\n * diagnostic text) was discarded in favor of the useless `\"[object Object]\"`.\n */\nfunction stringifyThrown(e: unknown): string {\n if (e instanceof Error) return e.message;\n if (typeof e === 'object' && e !== null) {\n const rec = e as { message?: unknown };\n if (typeof rec.message === 'string') return rec.message;\n }\n return String(e);\n}\n\n/* -------------------------------------------------------------------------- */\n/* Asymmetric matchers (devtools#692) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Brand property that tags asymmetric matcher markers (`expect.any(String)`,\n * `expect.objectContaining(...)`, etc). A reserved string key is used as the\n * brand so the guard stays a plain own-property check — markers are only ever\n * compared in-process by reference, so no cross-realm Symbol coordination is\n * needed.\n */\nconst ASYMMETRIC_BRAND = '$$asymmetricMatch' as const;\n\n/**\n * An asymmetric matcher: a placeholder that, when found on the `expected` side\n * of a deep comparison, runs its own `asymmetricMatch(actual)` predicate\n * instead of being structurally compared as a plain object. This is what makes\n * `toMatchObject({ hash: expect.any(String) })` match any string `hash`.\n */\ninterface AsymmetricMatcher {\n /** Brand discriminator — present only on asymmetric markers. */\n [ASYMMETRIC_BRAND]: true;\n /** Human-readable label used in failure messages. */\n toString(): string;\n /** Returns true when `actual` satisfies this matcher. */\n asymmetricMatch(actual: unknown): boolean;\n}\n\n/** Narrowing guard: is `v` an asymmetric matcher marker? */\nfunction isAsymmetric(v: unknown): v is AsymmetricMatcher {\n return (\n typeof v === 'object' &&\n v !== null &&\n (v as Record<string, unknown>)[ASYMMETRIC_BRAND] === true &&\n typeof (v as { asymmetricMatch?: unknown }).asymmetricMatch === 'function'\n );\n}\n\n/** The widest constructor type TypeScript allows without `any` (see toBeInstanceOf). */\ntype AnyCtor = abstract new (...args: never) => unknown;\n\n/**\n * `expect.any(Ctor)` — matches a value by its constructor. Primitive wrappers\n * (`String`/`Number`/`Boolean`) are matched by `typeof` (so the unboxed\n * primitive matches) AND by `instanceof` (so a boxed wrapper instance also\n * matches). `BigInt`/`Symbol` match by `typeof`. `Object` matches any non-null\n * object (NOT functions, matching vitest), `Array` uses `Array.isArray`,\n * `Function` uses `typeof === 'function'`, and any other constructor (user\n * classes) falls back to `instanceof`.\n */\nfunction makeAny(ctor: AnyCtor): AsymmetricMatcher {\n const name = (ctor as { name?: string }).name ?? String(ctor);\n const ctorAsFn = ctor as unknown as (new (...args: unknown[]) => unknown) | undefined;\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `Any<${name}>`,\n asymmetricMatch(actual: unknown): boolean {\n if (actual === null || actual === undefined) return false;\n switch (ctor as unknown) {\n case String:\n return typeof actual === 'string' || actual instanceof String;\n case Number:\n return typeof actual === 'number' || actual instanceof Number;\n case Boolean:\n return typeof actual === 'boolean' || actual instanceof Boolean;\n case BigInt:\n return typeof actual === 'bigint';\n case Symbol:\n return typeof actual === 'symbol';\n case Function:\n return typeof actual === 'function';\n case Object:\n // Match vitest: `expect.any(Object)` accepts objects only, NOT\n // functions (vitest checks `typeof other === 'object'`). `null` is\n // already excluded by the guard above.\n return typeof actual === 'object';\n case Array:\n return Array.isArray(actual);\n default:\n return ctorAsFn ? actual instanceof ctorAsFn : false;\n }\n },\n };\n}\n\n/** `expect.anything()` — matches any value except `null`/`undefined`. */\nfunction makeAnything(): AsymmetricMatcher {\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => 'Anything',\n asymmetricMatch: (actual: unknown): boolean => actual !== null && actual !== undefined,\n };\n}\n\n/** `expect.objectContaining(obj)` — partial-match every key of `obj` on `actual`. */\nfunction makeObjectContaining(expected: Record<string, unknown>): AsymmetricMatcher {\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `ObjectContaining<${safeStringify(expected)}>`,\n asymmetricMatch: (actual: unknown): boolean => deepMatchObject(actual, expected),\n };\n}\n\n/** `expect.arrayContaining(arr)` — `actual` is an array containing each element of `arr`. */\nfunction makeArrayContaining(expected: unknown[]): AsymmetricMatcher {\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `ArrayContaining<${safeStringify(expected)}>`,\n asymmetricMatch(actual: unknown): boolean {\n if (!Array.isArray(actual)) return false;\n return expected.every((exp) =>\n actual.some((act) => (isAsymmetric(exp) ? exp.asymmetricMatch(act) : deepEqual(act, exp))),\n );\n },\n };\n}\n\n/** `expect.stringContaining(sub)` — `actual` is a string containing `sub`. */\nfunction makeStringContaining(sub: string): AsymmetricMatcher {\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `StringContaining<${sub}>`,\n asymmetricMatch: (actual: unknown): boolean =>\n typeof actual === 'string' && actual.includes(sub),\n };\n}\n\n/** `expect.stringMatching(re)` — `actual` is a string matching `re` (string or RegExp). */\nfunction makeStringMatching(re: string | RegExp): AsymmetricMatcher {\n const pattern = typeof re === 'string' ? new RegExp(re) : re;\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `StringMatching<${String(pattern)}>`,\n asymmetricMatch: (actual: unknown): boolean =>\n typeof actual === 'string' && pattern.test(actual),\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Deep equality helpers */\n/* -------------------------------------------------------------------------- */\n\n/** JSON.stringify that never throws (cycles/BigInt) — for failure messages. */\nfunction safeStringify(v: unknown): string {\n try {\n return (\n JSON.stringify(v, (_k, val) =>\n isAsymmetric(val) ? val.toString() : typeof val === 'bigint' ? `${val}n` : val,\n ) ?? String(v)\n );\n } catch {\n return String(v);\n }\n}\n\n/**\n * Recursive structural equality — replaces JSON.stringify comparison to\n * handle key-order differences and undefined values correctly.\n *\n * When the `b` (expected) side is an asymmetric marker, its `asymmetricMatch`\n * predicate decides the result instead of structural comparison — markers are\n * placeholders, not plain objects, so deep-comparing them never matches.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (isAsymmetric(b)) return b.asymmetricMatch(a);\n if (Object.is(a, b)) return true;\n if (a === null || b === null) return false;\n if (typeof a !== 'object' || typeof b !== 'object') return false;\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!deepEqual(a[i], b[i])) return false;\n }\n return true;\n }\n if (Array.isArray(a) || Array.isArray(b)) return false;\n\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const aKeys = Object.keys(aObj);\n const bKeys = Object.keys(bObj);\n if (aKeys.length !== bKeys.length) return false;\n for (const key of aKeys) {\n if (!Object.hasOwn(bObj, key)) return false;\n if (!deepEqual(aObj[key], bObj[key])) return false;\n }\n return true;\n}\n\n/**\n * Partial-match: every key in `expected` must exist in `received` with a\n * recursively matching value. Extra keys in `received` are ignored.\n *\n * As in `deepEqual`, an asymmetric marker on the `expected` side delegates to\n * its `asymmetricMatch` predicate rather than being compared structurally.\n */\nfunction deepMatchObject(received: unknown, expected: unknown): boolean {\n if (isAsymmetric(expected)) return expected.asymmetricMatch(received);\n if (Object.is(received, expected)) return true;\n if (expected === null || received === null) return Object.is(received, expected);\n if (typeof expected !== 'object' || typeof received !== 'object')\n return Object.is(received, expected);\n\n if (Array.isArray(expected) && Array.isArray(received)) {\n if (expected.length !== received.length) return false;\n for (let i = 0; i < expected.length; i++) {\n if (!deepMatchObject(received[i], expected[i])) return false;\n }\n return true;\n }\n if (Array.isArray(expected) || Array.isArray(received)) return false;\n\n const expObj = expected as Record<string, unknown>;\n const recObj = received as Record<string, unknown>;\n for (const key of Object.keys(expObj)) {\n if (!Object.hasOwn(recObj, key)) return false;\n if (!deepMatchObject(recObj[key], expObj[key])) return false;\n }\n return true;\n}\n\n/**\n * Resolves a dot-separated property path on an object.\n * Returns `{ found: true, value }` or `{ found: false }`.\n */\nfunction resolvePath(obj: unknown, dotPath: string): { found: boolean; value?: unknown } {\n const parts = dotPath.split('.');\n let cur: unknown = obj;\n for (const part of parts) {\n if (cur === null || cur === undefined || typeof cur !== 'object') return { found: false };\n const record = cur as Record<string, unknown>;\n if (!Object.hasOwn(record, part)) return { found: false };\n cur = record[part];\n }\n return { found: true, value: cur };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Lightweight expect implementation */\n/* -------------------------------------------------------------------------- */\n\n/** Thrown by expect matchers on failure. */\nclass AssertionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'AssertionError';\n }\n}\n\n/** Minimal expect builder compatible with Vitest's jest-style API. */\nclass Expectation {\n #received: unknown;\n #negated = false;\n\n constructor(received: unknown) {\n this.#received = received;\n }\n\n get not(): this {\n const neg = new Expectation(this.#received) as this;\n neg.#negated = true;\n return neg;\n }\n\n #assert(pass: boolean, msg: string): void {\n const actual = this.#negated ? !pass : pass;\n if (!actual) {\n throw new AssertionError(this.#negated ? `Expected NOT: ${msg}` : msg);\n }\n }\n\n toBe(expected: unknown): void {\n this.#assert(\n Object.is(this.#received, expected),\n `Expected ${String(expected)}, received ${String(this.#received)}`,\n );\n }\n\n toEqual(expected: unknown): void {\n this.#assert(\n deepEqual(this.#received, expected),\n `Expected ${JSON.stringify(expected)}, received ${JSON.stringify(this.#received)}`,\n );\n }\n\n toBeTruthy(): void {\n this.#assert(Boolean(this.#received), `Expected truthy, received ${String(this.#received)}`);\n }\n\n toBeFalsy(): void {\n this.#assert(!this.#received, `Expected falsy, received ${String(this.#received)}`);\n }\n\n toBeNull(): void {\n this.#assert(this.#received === null, `Expected null, received ${String(this.#received)}`);\n }\n\n toBeUndefined(): void {\n this.#assert(\n this.#received === undefined,\n `Expected undefined, received ${String(this.#received)}`,\n );\n }\n\n toBeGreaterThan(n: number): void {\n this.#assert(\n typeof this.#received === 'number' && this.#received > n,\n `Expected > ${n}, received ${String(this.#received)}`,\n );\n }\n\n toBeLessThan(n: number): void {\n this.#assert(\n typeof this.#received === 'number' && this.#received < n,\n `Expected < ${n}, received ${String(this.#received)}`,\n );\n }\n\n toContain(sub: unknown): void {\n if (Array.isArray(this.#received)) {\n this.#assert(\n this.#received.includes(sub),\n `Expected array to contain ${JSON.stringify(sub)}, received ${JSON.stringify(this.#received)}`,\n );\n return;\n }\n this.#assert(\n typeof this.#received === 'string' && typeof sub === 'string' && this.#received.includes(sub),\n `Expected to contain \"${String(sub)}\", received \"${String(this.#received)}\"`,\n );\n }\n\n toThrow(msgFragment?: string): void {\n if (typeof this.#received !== 'function') {\n throw new AssertionError('toThrow: expected a function');\n }\n let threw = false;\n let errorMsg = '';\n try {\n (this.#received as () => unknown)();\n } catch (e) {\n threw = true;\n errorMsg = e instanceof Error ? e.message : String(e);\n }\n if (msgFragment !== undefined) {\n this.#assert(\n threw && errorMsg.includes(msgFragment),\n `Expected to throw containing \"${msgFragment}\", got \"${errorMsg}\"`,\n );\n } else {\n this.#assert(threw, 'Expected function to throw');\n }\n }\n\n // ---- New matchers (devtools#683) -----------------------------------------\n\n toMatchObject(expected: Record<string, unknown>): void {\n this.#assert(\n deepMatchObject(this.#received, expected),\n `Expected object to match ${JSON.stringify(expected)}, received ${JSON.stringify(this.#received)}`,\n );\n }\n\n toHaveProperty(dotPath: string, ...rest: unknown[]): void {\n const { found, value: actual } = resolvePath(this.#received, dotPath);\n if (rest.length > 0) {\n const value = rest[0];\n this.#assert(\n found && deepEqual(actual, value),\n `Expected property \"${dotPath}\" to equal ${JSON.stringify(value)}, got ${JSON.stringify(actual)}`,\n );\n } else {\n this.#assert(found, `Expected property \"${dotPath}\" to exist`);\n }\n }\n\n // The `abstract new (...args: never) => unknown` signature is the widest\n // constructor type that TypeScript allows without explicit `any`. Real-world\n // constructors like `ErrorConstructor` are assignable to it because `never`\n // in parameter position is contravariant (any arg list satisfies `never[]`).\n toBeInstanceOf(ctor: abstract new (...args: never) => unknown): void {\n this.#assert(\n this.#received instanceof (ctor as new (...args: unknown[]) => unknown),\n `Expected instance of ${(ctor as { name?: string }).name ?? String(ctor)}, received ${String(this.#received)}`,\n );\n }\n\n toBeTypeOf(typeStr: string): void {\n this.#assert(\n typeof this.#received === typeStr,\n `Expected typeof \"${typeStr}\", received \"${typeof this.#received}\"`,\n );\n }\n\n // ---- New matcher (devtools#807) -------------------------------------------\n\n toHaveLength(n: number): void {\n const length = (this.#received as { length?: unknown } | null | undefined)?.length;\n this.#assert(\n typeof length === 'number' && length === n,\n `Expected length ${n}, received ${typeof length === 'number' ? length : String(this.#received)}`,\n );\n }\n}\n\n/**\n * The `expect` callable plus its asymmetric-matcher static surface\n * (`expect.any`, `expect.objectContaining`, …). The user file accesses these as\n * `expect.any(String)`; the vitest redirect (`bundle.ts`) re-exports `expect` by\n * reading `globalThis.expect`, so attaching the statics to this function object\n * makes them reachable through the getter (a function's own properties survive\n * the getter that returns the function itself).\n */\ninterface ExpectStatic {\n (received: unknown): Expectation;\n any(ctor: AnyCtor): AsymmetricMatcher;\n anything(): AsymmetricMatcher;\n objectContaining(expected: Record<string, unknown>): AsymmetricMatcher;\n arrayContaining(expected: unknown[]): AsymmetricMatcher;\n stringContaining(sub: string): AsymmetricMatcher;\n stringMatching(re: string | RegExp): AsymmetricMatcher;\n}\n\n/** The `expect` function installed as a global. */\nconst expect: ExpectStatic = ((received: unknown): Expectation =>\n new Expectation(received)) as ExpectStatic;\n\n// Asymmetric matcher statics — vitest-compatible surface (devtools#692).\nexpect.any = makeAny;\nexpect.anything = makeAnything;\nexpect.objectContaining = makeObjectContaining;\nexpect.arrayContaining = makeArrayContaining;\nexpect.stringContaining = makeStringContaining;\nexpect.stringMatching = makeStringMatching;\n\n/* -------------------------------------------------------------------------- */\n/* Vitest-4-compatible task context (devtools#746) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Internal sentinel thrown by `ctx.skip()` to unwind the test body early.\n * Caught by the run loop (never surfaced as a failure) — see `runTestModule`'s\n * test-execution `catch` branch.\n */\nclass InPageSkipSentinel {\n constructor(readonly note: string | undefined) {}\n}\n\n/**\n * Minimal vitest-4-compatible task context passed as the first argument to a\n * test function, e.g. `it('...', async (ctx) => { ctx.skip(cond, note); })`.\n *\n * Real vitest's `TestContext` is much larger (`expect`, `onTestFailed`, …) —\n * this shim only implements what sdk-example's device-fixture suites actually\n * use: `ctx.skip` (vitest 4 semantics below) and a trivial `ctx.task.name`.\n */\nexport interface TestContext {\n /**\n * `ctx.skip()` (no args) unconditionally skips the remainder of the test\n * body. `ctx.skip(cond, note)` skips only when `cond` is truthy; when falsy,\n * this is a no-op and the body continues normally. Skipping throws an\n * internal sentinel — do not catch `InPageSkipSentinel` in user code.\n */\n skip(condition?: boolean, note?: string): void;\n /** Trivial task descriptor — nice-to-have, mirrors vitest's `ctx.task.name`. */\n task: { name: string };\n}\n\nfunction _makeTestContext(name: string): TestContext {\n return {\n skip(condition?: boolean, note?: string): void {\n if (condition === undefined || condition) {\n throw new InPageSkipSentinel(note);\n }\n },\n task: { name },\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* describe / it / test registry */\n/* -------------------------------------------------------------------------- */\n\n/** A test function as the user writes it — 0-arg or 1-arg (vitest task context). */\ntype TestFn = (ctx: TestContext) => void | Promise<void>;\n\ninterface PendingTest {\n suitePath: string[];\n name: string;\n fn: TestFn;\n skip: boolean;\n}\n\nconst _pendingTests: PendingTest[] = [];\nconst _suiteStack: string[] = [];\n\n/** Registers a suite scope; calls `fn` synchronously to collect inner tests. */\nfunction describe(name: string, fn: () => void): void {\n _suiteStack.push(name);\n fn();\n _suiteStack.pop();\n}\n\n/** Registers a test. */\nfunction it(name: string, fn: TestFn): void {\n _pendingTests.push({ suitePath: [..._suiteStack], name, fn, skip: false });\n}\n\n/** Alias for `it`. */\nconst test = it;\n\n/** Skipped test — registered but not executed. */\ndescribe.skip = (name: string, _fn: () => void): void => {\n void name;\n};\nit.skip = (name: string, _fn?: TestFn): void => {\n _pendingTests.push({ suitePath: [..._suiteStack], name, fn: () => {}, skip: true });\n};\ntest.skip = it.skip;\n\n/**\n * `it.skipIf(cond)(name, fn)` / `it.runIf(cond)(name, fn)` — conditional test\n * registration. `skipIf` skips when `cond` is truthy; `runIf` runs only when\n * `cond` is truthy (skips otherwise). sdk-example uses\n * `it.skipIf(cell.platform === 'mock')(...)` to skip real-SDK-only cases in env1.\n */\ntype ItRegistrar = (name: string, fn: TestFn) => void;\nfunction _conditionalIt(skip: boolean): ItRegistrar {\n return (name, fn) => {\n _pendingTests.push({ suitePath: [..._suiteStack], name, fn, skip });\n };\n}\nit.skipIf = (cond: unknown): ItRegistrar => _conditionalIt(Boolean(cond));\nit.runIf = (cond: unknown): ItRegistrar => _conditionalIt(!cond);\ntest.skipIf = it.skipIf;\ntest.runIf = it.runIf;\n\n/**\n * `describe.skipIf(cond)(name, fn)` / `describe.runIf(cond)(name, fn)`.\n * When skipped, the suite body still runs to register its tests but every test\n * inside is marked skipped (collected via a temporary skip flag is overkill —\n * a skipped describe simply does not invoke its body, mirroring `describe.skip`).\n */\ntype DescribeRegistrar = (name: string, fn: () => void) => void;\ndescribe.skipIf = (cond: unknown): DescribeRegistrar =>\n cond ? (name, _fn) => void name : (name, fn) => describe(name, fn);\ndescribe.runIf = (cond: unknown): DescribeRegistrar =>\n cond ? (name, fn) => describe(name, fn) : (name, _fn) => void name;\n\n/* -------------------------------------------------------------------------- */\n/* Lifecycle hooks (devtools#683) */\n/* -------------------------------------------------------------------------- */\n\ntype HookType = 'beforeAll' | 'afterAll' | 'beforeEach' | 'afterEach';\n\ninterface HookEntry {\n /** Suite path at the time of registration ([] = module scope). */\n suitePath: string[];\n type: HookType;\n fn: () => void | Promise<void>;\n}\n\nconst _hooks: HookEntry[] = [];\n\nfunction _registerHook(type: HookType, fn: () => void | Promise<void>): void {\n _hooks.push({ suitePath: [..._suiteStack], type, fn });\n}\n\n/**\n * Returns hooks whose suitePath is a prefix of `testSuitePath`\n * (i.e. the hook's scope contains the test).\n */\nfunction _hooksFor(type: HookType, testSuitePath: string[]): Array<() => void | Promise<void>> {\n return _hooks\n .filter((h) => {\n if (h.type !== type) return false;\n // Hook scope must be a prefix of the test's suite path\n if (h.suitePath.length > testSuitePath.length) return false;\n for (let i = 0; i < h.suitePath.length; i++) {\n if (h.suitePath[i] !== testSuitePath[i]) return false;\n }\n return true;\n })\n .map((h) => h.fn);\n}\n\n/** Runs an array of hook functions in order, awaiting each. */\nasync function _runHooks(fns: Array<() => void | Promise<void>>): Promise<Error | null> {\n for (const fn of fns) {\n try {\n await fn();\n } catch (e) {\n return e instanceof Error ? e : new Error(String(e));\n }\n }\n return null;\n}\n\nfunction beforeAll(fn: () => void | Promise<void>): void {\n _registerHook('beforeAll', fn);\n}\nfunction afterAll(fn: () => void | Promise<void>): void {\n _registerHook('afterAll', fn);\n}\nfunction beforeEach(fn: () => void | Promise<void>): void {\n _registerHook('beforeEach', fn);\n}\nfunction afterEach(fn: () => void | Promise<void>): void {\n _registerHook('afterEach', fn);\n}\n\n/* -------------------------------------------------------------------------- */\n/* vi shim (devtools#683) */\n/* -------------------------------------------------------------------------- */\n\ninterface SpyCall {\n args: unknown[];\n returnValue: unknown;\n}\n\ninterface MockFn<T extends unknown[], R> {\n (...args: T): R;\n mock: { calls: SpyCall[] };\n mockImplementation(fn: (...args: T) => R): this;\n mockReturnValue(value: R): this;\n mockRestore(): void;\n}\n\ninterface SpyRecord {\n obj: Record<string, unknown>;\n method: string;\n original: unknown;\n}\n\nconst _spyRegistry: SpyRecord[] = [];\n\nfunction _createMockFn<T extends unknown[], R>(impl?: (...args: T) => R): MockFn<T, R> {\n let currentImpl: ((...args: T) => R) | undefined = impl;\n const calls: SpyCall[] = [];\n\n const mockFn = ((...args: T): R => {\n const returnValue = currentImpl ? currentImpl(...args) : (undefined as unknown as R);\n calls.push({ args, returnValue });\n return returnValue;\n }) as MockFn<T, R>;\n\n mockFn.mock = { calls };\n\n mockFn.mockImplementation = function (fn: (...args: T) => R): typeof mockFn {\n currentImpl = fn;\n return this;\n };\n\n mockFn.mockReturnValue = function (value: R): typeof mockFn {\n currentImpl = () => value;\n return this;\n };\n\n // No-op restore for standalone fn (only spyOn-created fns have a real restore)\n mockFn.mockRestore = (): void => {};\n\n return mockFn;\n}\n\nconst vi = {\n /** Creates a spy on `obj[method]`, replacing it with a mock function. */\n spyOn<T extends Record<string, unknown>, K extends keyof T>(\n obj: T,\n method: K,\n ): MockFn<unknown[], unknown> {\n const original = obj[method];\n const spy = _createMockFn<unknown[], unknown>(\n typeof original === 'function' ? (original as (...args: unknown[]) => unknown) : undefined,\n );\n\n spy.mockRestore = (): void => {\n obj[method] = original as T[K];\n };\n\n _spyRegistry.push({ obj: obj as Record<string, unknown>, method: method as string, original });\n obj[method] = spy as unknown as T[K];\n return spy;\n },\n\n /** Creates a standalone mock function, optionally wrapping `impl`. */\n fn<T extends unknown[], R>(impl?: (...args: T) => R): MockFn<T, R> {\n return _createMockFn(impl);\n },\n\n /** Restores all spies created via `vi.spyOn` to their original values. */\n restoreAllMocks(): void {\n for (const { obj, method, original } of _spyRegistry) {\n obj[method] = original;\n }\n _spyRegistry.length = 0;\n },\n};\n\n/* -------------------------------------------------------------------------- */\n/* Runtime entry point */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Runtime globals object — exported for direct use in unit tests so that\n * test factories can reference runtime's own `it`/`expect`/`beforeAll`/etc.\n * without depending on `globalThis` injection.\n *\n * In a real WebView bundle these are accessed via globals installed by\n * `runTestModule`; in Node tests, import from here directly.\n */\nexport const runtimeGlobals = {\n describe,\n it,\n test,\n expect,\n beforeAll,\n afterAll,\n beforeEach,\n afterEach,\n vi,\n} as const;\n\n/**\n * Installs describe/it/test/expect/afterAll/afterEach/beforeAll/beforeEach/vi\n * as globals, invokes `moduleFactory` to register the user's tests, then\n * executes them and returns a RunReport.\n *\n * This function is exported as `__testBundle.runTestModule` by the IIFE wrapper\n * that bundle.ts generates. The Node-side rpc.ts calls it via `Runtime.evaluate`.\n *\n * @param moduleFactory - A zero-argument function that contains the user's\n * top-level test code (describe/it/test calls). The bundler wraps the entire\n * test module so that its top-level statements become the body of this factory.\n * @param opts - Test-only overrides (see {@link RunTestModuleOptions}). Never\n * passed by the generated bundle (`rpc.ts#buildRunTestsExpression` calls\n * `runTestModule(__userFactory)` with exactly one argument) — this second\n * parameter exists so unit tests can inject a fake sleep and assert the\n * THROTTLED-retry timing without real delays.\n */\nexport async function runTestModule(\n moduleFactory?: () => void | Promise<void>,\n opts?: RunTestModuleOptions,\n): Promise<RunReport> {\n const sleep = opts?.sleep ?? DEFAULT_SLEEP;\n // Reset state for re-entrant calls within the same page context.\n _pendingTests.length = 0;\n _suiteStack.length = 0;\n _hooks.length = 0;\n _spyRegistry.length = 0;\n\n // Install globals that user test code expects.\n type G = typeof globalThis & {\n describe: typeof describe;\n it: typeof it;\n test: typeof test;\n expect: typeof expect;\n beforeAll: typeof beforeAll;\n afterAll: typeof afterAll;\n beforeEach: typeof beforeEach;\n afterEach: typeof afterEach;\n vi: typeof vi;\n };\n const g = globalThis as G;\n g.describe = describe;\n g.it = it;\n g.test = test;\n g.expect = expect;\n g.beforeAll = beforeAll;\n g.afterAll = afterAll;\n g.beforeEach = beforeEach;\n g.afterEach = afterEach;\n g.vi = vi;\n\n // Run the factory (which registers describe/it/test blocks via globals).\n if (moduleFactory) {\n await moduleFactory();\n }\n\n const wallStart = Date.now();\n const startedAt = new Date(wallStart).toISOString();\n const results: TestResult[] = [];\n\n // Determine unique suite scopes from registered tests, in discovery order.\n // We need to fire beforeAll/afterAll once per scope (grouped by suitePath).\n //\n // Simplified model: we run beforeAll hooks before the first test in a scope\n // and afterAll hooks after the last test in a scope. Scope identity is the\n // entire suitePath string (e.g. \"Suite A > Suite B\").\n //\n // For module-scope hooks (suitePath=[]), they wrap the entire test run.\n\n // Group tests by their suite key to track beforeAll/afterAll per scope.\n // We keep a Set of scopes for which beforeAll has been fired.\n const firedBeforeAll = new Set<string>();\n // Map from scope key → last index in _pendingTests that belongs to that scope.\n const lastIndexForScope = new Map<string, number>();\n for (let i = 0; i < _pendingTests.length; i++) {\n const key = _pendingTests[i].suitePath.join('\\0');\n lastIndexForScope.set(key, i);\n // Also compute for parent scopes (module scope = '')\n const path = _pendingTests[i].suitePath;\n for (let depth = 0; depth < path.length; depth++) {\n const parentKey = path.slice(0, depth).join('\\0');\n const cur = lastIndexForScope.get(parentKey) ?? -1;\n if (i > cur) lastIndexForScope.set(parentKey, i);\n }\n }\n // Module scope key\n const MODULE_KEY = '';\n if (!lastIndexForScope.has(MODULE_KEY) && _pendingTests.length > 0) {\n lastIndexForScope.set(MODULE_KEY, _pendingTests.length - 1);\n }\n\n // Fire module-scope beforeAll before any tests\n if (_pendingTests.length > 0) {\n const baFns = _hooksFor('beforeAll', []);\n const baErr = await _runHooks(baFns);\n firedBeforeAll.add(MODULE_KEY);\n if (baErr) {\n // If module beforeAll fails, mark all tests as failed.\n for (const pending of _pendingTests) {\n const fullName = [...pending.suitePath, pending.name].join(' > ');\n results.push({\n name: fullName,\n status: 'fail',\n duration: 0,\n error: `beforeAll failed: ${baErr.message}`,\n });\n }\n const duration = Date.now() - wallStart;\n const passed = results.filter((r) => r.status === 'pass').length;\n const failed = results.filter((r) => r.status === 'fail').length;\n const skipped = results.filter((r) => r.status === 'skip').length;\n // Still run module afterAll for cleanup (e.g. flushCapture)\n const aaFns = _hooksFor('afterAll', []);\n await _runHooks(aaFns);\n return { startedAt, duration, passed, failed, skipped, tests: results };\n }\n }\n\n // devtools#767 --pace: read the page-global __AIT_PACE_MS__ (injected by\n // relay-factory.ts the same way __AIT_CELL__ is, via cell.ts#injectGlobals)\n // ONCE per run. Absent/0 (the default) means zero behavior change — the\n // wait below becomes a no-op sleep(0) that resolves on the next microtask\n // tick, same as today's unpaced loop for every existing caller.\n const paceMs = (() => {\n const raw = (globalThis as { __AIT_PACE_MS__?: unknown }).__AIT_PACE_MS__;\n return typeof raw === 'number' && raw > 0 ? raw : 0;\n })();\n let ranFirstTest = false;\n\n for (let i = 0; i < _pendingTests.length; i++) {\n const pending = _pendingTests[i];\n const fullName = [...pending.suitePath, pending.name].join(' > ');\n\n if (pending.skip) {\n results.push({ name: fullName, status: 'skip', duration: 0 });\n continue;\n }\n\n // --pace inter-test spacing: wait BEFORE every test after the first one\n // that actually runs (skipped tests above never reach here). Mirrors the\n // permission preflight's \"no wait before the first probe\" pacing model\n // (cell.ts#buildPermissionPreflightExpression).\n if (paceMs > 0 && ranFirstTest) {\n await sleep(paceMs);\n }\n ranFirstTest = true;\n\n // Fire suite-scoped beforeAll for any new scopes entered by this test\n for (let depth = 1; depth <= pending.suitePath.length; depth++) {\n const scopePath = pending.suitePath.slice(0, depth);\n const scopeKey = scopePath.join('\\0');\n if (!firedBeforeAll.has(scopeKey)) {\n // Only hooks registered exactly at this scope (not broader/narrower)\n const scopedFns = _hooks\n .filter((h) => h.type === 'beforeAll' && h.suitePath.join('\\0') === scopeKey)\n .map((h) => h.fn);\n const baErr = await _runHooks(scopedFns);\n firedBeforeAll.add(scopeKey);\n if (baErr) {\n // Mark remaining tests in this scope as failed\n results.push({\n name: fullName,\n status: 'fail',\n duration: 0,\n error: `beforeAll failed: ${baErr.message}`,\n });\n }\n }\n }\n\n // beforeEach\n const beFns = _hooksFor('beforeEach', pending.suitePath);\n const beErr = await _runHooks(beFns);\n\n const tStart = Date.now();\n let testErr: string | undefined;\n let inPageSkip: InPageSkipSentinel | undefined;\n let throttleRetries = 0;\n\n if (beErr) {\n testErr = `beforeEach failed: ${beErr.message}`;\n } else {\n // THROTTLED-aware retry (devtools#767): only the test BODY is retried —\n // beforeEach (above) and afterEach (below) each run exactly once\n // regardless of how many times the body is attempted. This keeps any\n // capture-emitting hook (e.g. sdk-example's flushCapture) firing exactly\n // once per test, so a retried body can never duplicate a capture record\n // (see relay-worker.ts's harvesting model — it's a passive listener over\n // the whole run, not a per-attempt flush/drain, so this invariant is what\n // keeps a retry from polluting the capture stream).\n for (let attempt = 0; ; attempt++) {\n try {\n await pending.fn(_makeTestContext(fullName));\n testErr = undefined;\n break;\n } catch (e) {\n if (e instanceof InPageSkipSentinel) {\n inPageSkip = e;\n break;\n }\n testErr = stringifyThrown(e);\n if (isThrottledError(e) && attempt < TEST_MAX_RETRIES) {\n throttleRetries += 1;\n await sleep(TEST_RETRY_BACKOFF_MS[attempt]);\n continue;\n }\n break;\n }\n }\n }\n\n // afterEach — always run even if test failed or was skipped mid-body.\n const aeFns = _hooksFor('afterEach', pending.suitePath);\n const aeErr = await _runHooks(aeFns);\n if (aeErr && !testErr && !inPageSkip) testErr = `afterEach failed: ${aeErr.message}`;\n\n if (inPageSkip) {\n // ctx.skip() unwound the body — record as skipped (not pass/fail), same\n // as the static it.skip path. The note (if any) rides on TestResult.note.\n results.push({\n name: fullName,\n status: 'skip',\n duration: Date.now() - tStart,\n ...(inPageSkip.note !== undefined ? { note: inPageSkip.note } : {}),\n });\n } else if (testErr !== undefined) {\n results.push({\n name: fullName,\n status: 'fail',\n duration: Date.now() - tStart,\n error: testErr,\n ...(throttleRetries > 0 ? { throttleRetries } : {}),\n });\n } else {\n results.push({\n name: fullName,\n status: 'pass',\n duration: Date.now() - tStart,\n ...(throttleRetries > 0 ? { throttleRetries } : {}),\n });\n }\n\n // Fire suite-scoped afterAll when this is the last test in each scope\n for (let depth = pending.suitePath.length; depth >= 1; depth--) {\n const scopePath = pending.suitePath.slice(0, depth);\n const scopeKey = scopePath.join('\\0');\n const lastIdx = lastIndexForScope.get(scopeKey) ?? -1;\n if (lastIdx === i) {\n const scopedFns = _hooks\n .filter((h) => h.type === 'afterAll' && h.suitePath.join('\\0') === scopeKey)\n .map((h) => h.fn);\n await _runHooks(scopedFns);\n }\n }\n }\n\n // Fire module-scope afterAll after all tests — critical for flushCapture\n const moduleAfterAllFns = _hooks\n .filter((h) => h.type === 'afterAll' && h.suitePath.length === 0)\n .map((h) => h.fn);\n await _runHooks(moduleAfterAllFns);\n\n const duration = Date.now() - wallStart;\n const passed = results.filter((r) => r.status === 'pass').length;\n const failed = results.filter((r) => r.status === 'fail').length;\n const skipped = results.filter((r) => r.status === 'skip').length;\n\n return { startedAt, duration, passed, failed, skipped, tests: results };\n}\n"],"mappings":"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"}
@@ -1,38 +0,0 @@
1
- import { t as RunReport } from "../runtime-kn9DxOeg.js";
2
- import { File, TaskEventPack, TaskResultPack } from "@vitest/runner";
3
-
4
- //#region src/test-runner/task-graph.d.ts
5
- /**
6
- * Rebuilds a single `File` task graph from one file's flat `RunReport`.
7
- *
8
- * @param filepath Absolute path to the test file.
9
- * @param root Project root (used for the file id hash).
10
- * @param projectName Vitest project name (or undefined for the default project).
11
- * @param pool Pool name to stamp on the file task (our relay pool name).
12
- * @param report The page-runtime report for this file.
13
- */
14
- declare function synthesizeFileTask(filepath: string, root: string, projectName: string | undefined, pool: string, report: RunReport): File;
15
- /**
16
- * Flattens a File task subtree into the {@link TaskResultPack} tuples Vitest's
17
- * `state.updateTasks` consumes: `[id, result, meta]`.
18
- *
19
- * Includes every task (file, suites, tests). Suites/files without a result
20
- * report `undefined` in the result slot — Vitest tolerates that.
21
- */
22
- declare function toTaskResultPacks(file: File): TaskResultPack[];
23
- /**
24
- * Builds the {@link TaskEventPack} sequence reporters expect: a `suite-prepare`
25
- * before a suite's children, `test-prepare`/`test-finished` per test, and a
26
- * `suite-finished` after. This drives reporter progress output.
27
- */
28
- declare function toTaskEventPacks(file: File): TaskEventPack[];
29
- /**
30
- * Deterministic synthetic id for a file that failed before any task graph could
31
- * be built (bundle/inject error). Uses the same `generateHash` Vitest uses, on
32
- * the root-relative path + projectName, matching `generateFileHash` semantics so
33
- * the id is stable across runs and won't collide with real file ids.
34
- */
35
- declare function syntheticFileId(relativePath: string, projectName: string | undefined): string;
36
- //#endregion
37
- export { synthesizeFileTask, syntheticFileId, toTaskEventPacks, toTaskResultPacks };
38
- //# sourceMappingURL=task-graph.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"task-graph.d.ts","names":[],"sources":["../../src/test-runner/task-graph.ts"],"mappings":";;;;AA6MA;;;;;;;;;AAAA,iBA5DgB,kBAAA,CACd,QAAA,UACA,IAAA,UACA,WAAA,sBACA,IAAA,UACA,MAAA,EAAQ,SAAA,GACP,IAAA;;;;;;;;iBAmCa,iBAAA,CAAkB,IAAA,EAAM,IAAA,GAAO,cAAA;;;;;;iBAmB/B,gBAAA,CAAiB,IAAA,EAAM,IAAA,GAAO,aAAA;;;;;;;iBAgC9B,eAAA,CAAgB,YAAA,UAAsB,WAAA"}
@@ -1,182 +0,0 @@
1
- import { calculateSuiteHash, createFileTask, generateHash } from "@vitest/runner/utils";
2
- //#region src/test-runner/task-graph.ts
3
- /** Separator the page runtime uses to join suite path + test name. */
4
- const NAME_SEPARATOR = " > ";
5
- /**
6
- * Maps a page-runtime status to a Vitest {@link TaskState}.
7
- * `skip` collapses to Vitest's `skip` run-mode; `pass`/`fail` are terminal.
8
- */
9
- function toTaskState(status) {
10
- switch (status) {
11
- case "pass": return "pass";
12
- case "fail": return "fail";
13
- case "skip": return "skip";
14
- }
15
- }
16
- /**
17
- * Builds a {@link TaskResult} for a finished test. Suites and files only carry
18
- * a result on collection/hook error (we leave those undefined — the page never
19
- * reports suite-level errors separately).
20
- */
21
- function toTaskResult(t) {
22
- const result = {
23
- state: toTaskState(t.status),
24
- duration: t.duration
25
- };
26
- if (t.status === "fail") result.errors = [{
27
- message: t.error ?? "Test failed",
28
- name: "AssertionError"
29
- }];
30
- return result;
31
- }
32
- /**
33
- * Creates a structural suite task under `parent`. Mirrors the partial-object
34
- * pattern Vitest uses in `createFileTask`; ids are assigned by
35
- * `calculateSuiteHash` afterwards.
36
- */
37
- function makeSuite(parent, file, name) {
38
- return {
39
- id: "",
40
- type: "suite",
41
- name,
42
- fullName: "",
43
- mode: "run",
44
- meta: {},
45
- file,
46
- suite: parent,
47
- tasks: []
48
- };
49
- }
50
- /**
51
- * Finds-or-creates a child suite named `name` under `parent`.
52
- * The caller recomputes ids once the whole file tree is built.
53
- */
54
- function ensureSuite(parent, file, name) {
55
- const existing = parent.tasks.find((t) => t.type === "suite" && t.name === name);
56
- if (existing) return existing;
57
- const suite = makeSuite(parent, file, name);
58
- parent.tasks.push(suite);
59
- return suite;
60
- }
61
- /**
62
- * Creates a structural test task and appends it to `parent`.
63
- *
64
- * `Test` carries runtime-only required fields (context/timeout/annotations/
65
- * artifacts) that are never read off a *reported* task — reporters consume
66
- * id/name/type/mode/result/suite/file. We satisfy the type at this one cast
67
- * boundary instead of fabricating fake runtime values.
68
- */
69
- function appendTest(parent, file, t, name) {
70
- const test = {
71
- id: "",
72
- type: "test",
73
- name,
74
- fullName: "",
75
- mode: t.status === "skip" ? "skip" : "run",
76
- meta: {},
77
- file,
78
- suite: parent,
79
- result: toTaskResult(t),
80
- annotations: []
81
- };
82
- parent.tasks.push(test);
83
- }
84
- /**
85
- * Rebuilds a single `File` task graph from one file's flat `RunReport`.
86
- *
87
- * @param filepath Absolute path to the test file.
88
- * @param root Project root (used for the file id hash).
89
- * @param projectName Vitest project name (or undefined for the default project).
90
- * @param pool Pool name to stamp on the file task (our relay pool name).
91
- * @param report The page-runtime report for this file.
92
- */
93
- function synthesizeFileTask(filepath, root, projectName, pool, report) {
94
- const file = createFileTask(filepath, root, projectName, pool);
95
- for (const t of report.tests) {
96
- const segments = t.name.split(NAME_SEPARATOR);
97
- const testName = segments.pop() ?? t.name;
98
- let parent = file;
99
- for (const suiteName of segments) parent = ensureSuite(parent, file, suiteName);
100
- appendTest(parent, file, t, testName);
101
- }
102
- calculateSuiteHash(file);
103
- if (report.tests.length === 0 && report.failed > 0) file.result = {
104
- state: "fail",
105
- errors: [{
106
- message: "File failed to run",
107
- name: "Error"
108
- }]
109
- };
110
- return file;
111
- }
112
- /**
113
- * Flattens a File task subtree into the {@link TaskResultPack} tuples Vitest's
114
- * `state.updateTasks` consumes: `[id, result, meta]`.
115
- *
116
- * Includes every task (file, suites, tests). Suites/files without a result
117
- * report `undefined` in the result slot — Vitest tolerates that.
118
- */
119
- function toTaskResultPacks(file) {
120
- const packs = [];
121
- const walk = (task) => {
122
- packs.push([
123
- task.id,
124
- task.result,
125
- task.meta
126
- ]);
127
- if (task.type === "suite") for (const child of task.tasks) walk(child);
128
- };
129
- walk(file);
130
- return packs;
131
- }
132
- /**
133
- * Builds the {@link TaskEventPack} sequence reporters expect: a `suite-prepare`
134
- * before a suite's children, `test-prepare`/`test-finished` per test, and a
135
- * `suite-finished` after. This drives reporter progress output.
136
- */
137
- function toTaskEventPacks(file) {
138
- const events = [];
139
- const isFile = (task) => task.type === "suite" && task.file === task;
140
- const walk = (task) => {
141
- if (task.type === "test") {
142
- events.push([
143
- task.id,
144
- "test-prepare",
145
- void 0
146
- ]);
147
- events.push([
148
- task.id,
149
- "test-finished",
150
- void 0
151
- ]);
152
- return;
153
- }
154
- const emitSuiteEvents = task.type === "suite" && !isFile(task);
155
- if (emitSuiteEvents) events.push([
156
- task.id,
157
- "suite-prepare",
158
- void 0
159
- ]);
160
- for (const child of task.tasks) walk(child);
161
- if (emitSuiteEvents) events.push([
162
- task.id,
163
- "suite-finished",
164
- void 0
165
- ]);
166
- };
167
- walk(file);
168
- return events;
169
- }
170
- /**
171
- * Deterministic synthetic id for a file that failed before any task graph could
172
- * be built (bundle/inject error). Uses the same `generateHash` Vitest uses, on
173
- * the root-relative path + projectName, matching `generateFileHash` semantics so
174
- * the id is stable across runs and won't collide with real file ids.
175
- */
176
- function syntheticFileId(relativePath, projectName) {
177
- return generateHash(`${relativePath}${projectName ?? ""}`);
178
- }
179
- //#endregion
180
- export { synthesizeFileTask, syntheticFileId, toTaskEventPacks, toTaskResultPacks };
181
-
182
- //# sourceMappingURL=task-graph.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"task-graph.js","names":[],"sources":["../../src/test-runner/task-graph.ts"],"sourcesContent":["/**\n * Synthesises a Vitest task graph from a relay `RunReport`.\n *\n * The page-side runtime (`runtime.ts`) only knows how to collect a flat list\n * of `TestResult { name: 'suite > sub > test', status, duration, error }`.\n * Vitest's reporters/watch/UI ecosystem, however, expect a populated task\n * graph (`File` → `Suite` → `Test`) plus `TaskResultPack`/`TaskEventPack`\n * tuples reported through `vitest.state`.\n *\n * This module bridges the two: it rebuilds the nested suite/test tree from the\n * ` > `-joined names, assigns Vitest-compatible stable ids via the official\n * `@vitest/runner/utils` helpers (NEVER hand-rolled — ids must match Vitest's\n * own scheme so reruns and reporter lookups line up), and emits the result\n * packs Vitest consumes.\n *\n * Pure functions only — no CDP, no Node IO. This keeps the synthesis unit\n * testable with a plain `RunReport` input (no phone/relay needed).\n *\n * Implementation note: like Vitest's own `createFileTask`, we build structural\n * task objects and fill only the fields reporters read (id/name/type/mode/meta/\n * result/tasks/suite/file). Runtime-only fields on `Test` (context/timeout/\n * annotations/artifacts) are never read off a reported task, so we satisfy the\n * type at a single explicit boundary rather than fabricating fake values.\n */\n\nimport type {\n File,\n Suite,\n Task,\n TaskEventPack,\n TaskResult,\n TaskResultPack,\n TaskState,\n Test,\n} from '@vitest/runner';\nimport { calculateSuiteHash, createFileTask, generateHash } from '@vitest/runner/utils';\nimport type { RunReport, TestResult } from './runtime.js';\n\n/** Separator the page runtime uses to join suite path + test name. */\nconst NAME_SEPARATOR = ' > ';\n\n/**\n * Maps a page-runtime status to a Vitest {@link TaskState}.\n * `skip` collapses to Vitest's `skip` run-mode; `pass`/`fail` are terminal.\n */\nfunction toTaskState(status: TestResult['status']): TaskState {\n switch (status) {\n case 'pass':\n return 'pass';\n case 'fail':\n return 'fail';\n case 'skip':\n return 'skip';\n }\n}\n\n/**\n * Builds a {@link TaskResult} for a finished test. Suites and files only carry\n * a result on collection/hook error (we leave those undefined — the page never\n * reports suite-level errors separately).\n */\nfunction toTaskResult(t: TestResult): TaskResult {\n const result: TaskResult = {\n state: toTaskState(t.status),\n duration: t.duration,\n };\n if (t.status === 'fail') {\n // SECRET-HANDLING: `error` is the matcher message only — rpc.ts already\n // strips the expression/value before it reaches us. Pass it through as the\n // single error object Vitest reporters render.\n result.errors = [{ message: t.error ?? 'Test failed', name: 'AssertionError' }];\n }\n return result;\n}\n\n/**\n * Creates a structural suite task under `parent`. Mirrors the partial-object\n * pattern Vitest uses in `createFileTask`; ids are assigned by\n * `calculateSuiteHash` afterwards.\n */\nfunction makeSuite(parent: Suite, file: File, name: string): Suite {\n const suite: Suite = {\n id: '',\n type: 'suite',\n name,\n fullName: '',\n mode: 'run',\n meta: {},\n file,\n suite: parent,\n tasks: [],\n };\n return suite;\n}\n\n/**\n * Finds-or-creates a child suite named `name` under `parent`.\n * The caller recomputes ids once the whole file tree is built.\n */\nfunction ensureSuite(parent: Suite, file: File, name: string): Suite {\n const existing = parent.tasks.find((t): t is Suite => t.type === 'suite' && t.name === name);\n if (existing) return existing;\n const suite = makeSuite(parent, file, name);\n parent.tasks.push(suite);\n return suite;\n}\n\n/**\n * Creates a structural test task and appends it to `parent`.\n *\n * `Test` carries runtime-only required fields (context/timeout/annotations/\n * artifacts) that are never read off a *reported* task — reporters consume\n * id/name/type/mode/result/suite/file. We satisfy the type at this one cast\n * boundary instead of fabricating fake runtime values.\n */\nfunction appendTest(parent: Suite, file: File, t: TestResult, name: string): void {\n const test = {\n id: '',\n type: 'test',\n name,\n fullName: '',\n mode: t.status === 'skip' ? 'skip' : 'run',\n meta: {},\n file,\n suite: parent,\n result: toTaskResult(t),\n annotations: [],\n // `Test` has runtime-only required fields (context/timeout/artifacts)\n // populated by Vitest at execution time and never read off a *reported*\n // task. We satisfy the type via `unknown` (not `any`) at this single\n // boundary, mirroring how Vitest's own createFileTask builds a partial\n // structural task object.\n } as unknown as Test;\n parent.tasks.push(test);\n}\n\n/**\n * Rebuilds a single `File` task graph from one file's flat `RunReport`.\n *\n * @param filepath Absolute path to the test file.\n * @param root Project root (used for the file id hash).\n * @param projectName Vitest project name (or undefined for the default project).\n * @param pool Pool name to stamp on the file task (our relay pool name).\n * @param report The page-runtime report for this file.\n */\nexport function synthesizeFileTask(\n filepath: string,\n root: string,\n projectName: string | undefined,\n pool: string,\n report: RunReport,\n): File {\n // createFileTask assigns the canonical stable file id (generateFileHash on the\n // root-relative path + projectName) and self-references file.file = file.\n const file = createFileTask(filepath, root, projectName, pool);\n\n for (const t of report.tests) {\n const segments = t.name.split(NAME_SEPARATOR);\n const testName = segments.pop() ?? t.name;\n let parent: Suite = file;\n for (const suiteName of segments) {\n parent = ensureSuite(parent, file, suiteName);\n }\n appendTest(parent, file, t, testName);\n }\n\n // Assign Vitest-stable position-based ids to every suite/test under the file\n // (file id stays as createFileTask set it).\n calculateSuiteHash(file);\n\n // Surface a file-level error result when the whole file failed to run with no\n // collected tests (bundle/inject error path surfaces as a synthetic file).\n if (report.tests.length === 0 && report.failed > 0) {\n file.result = { state: 'fail', errors: [{ message: 'File failed to run', name: 'Error' }] };\n }\n\n return file;\n}\n\n/**\n * Flattens a File task subtree into the {@link TaskResultPack} tuples Vitest's\n * `state.updateTasks` consumes: `[id, result, meta]`.\n *\n * Includes every task (file, suites, tests). Suites/files without a result\n * report `undefined` in the result slot — Vitest tolerates that.\n */\nexport function toTaskResultPacks(file: File): TaskResultPack[] {\n const packs: TaskResultPack[] = [];\n // Note: a File task also has `type: 'suite'` (File extends Suite) — the suite\n // branch covers both, so we never need a separate 'file' check.\n const walk = (task: Task): void => {\n packs.push([task.id, task.result, task.meta]);\n if (task.type === 'suite') {\n for (const child of task.tasks) walk(child);\n }\n };\n walk(file);\n return packs;\n}\n\n/**\n * Builds the {@link TaskEventPack} sequence reporters expect: a `suite-prepare`\n * before a suite's children, `test-prepare`/`test-finished` per test, and a\n * `suite-finished` after. This drives reporter progress output.\n */\nexport function toTaskEventPacks(file: File): TaskEventPack[] {\n const events: TaskEventPack[] = [];\n // A File task also has `type: 'suite'`. Emit suite-prepare/finished for the\n // file and every nested suite; test-prepare/finished for each test.\n const isFile = (task: Task): boolean => task.type === 'suite' && task.file === task;\n const walk = (task: Task): void => {\n if (task.type === 'test') {\n events.push([task.id, 'test-prepare', undefined]);\n events.push([task.id, 'test-finished', undefined]);\n return;\n }\n // Don't emit suite events for the file root itself — reporters track the\n // module separately; only inner suites get suite-prepare/finished.\n const emitSuiteEvents = task.type === 'suite' && !isFile(task);\n if (emitSuiteEvents) {\n events.push([task.id, 'suite-prepare', undefined]);\n }\n for (const child of task.tasks) walk(child);\n if (emitSuiteEvents) {\n events.push([task.id, 'suite-finished', undefined]);\n }\n };\n walk(file);\n return events;\n}\n\n/**\n * Deterministic synthetic id for a file that failed before any task graph could\n * be built (bundle/inject error). Uses the same `generateHash` Vitest uses, on\n * the root-relative path + projectName, matching `generateFileHash` semantics so\n * the id is stable across runs and won't collide with real file ids.\n */\nexport function syntheticFileId(relativePath: string, projectName: string | undefined): string {\n return generateHash(`${relativePath}${projectName ?? ''}`);\n}\n"],"mappings":"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"}
@@ -1,59 +0,0 @@
1
- //#region src/test-runner/throttle.ts
2
- /**
3
- * Shared `APP_BRIDGE_THROTTLED` detection (devtools#767).
4
- *
5
- * 2026-07-10 real-device observation: the Toss app native side added a
6
- * per-method rate limit to the 2.x bridge — calling the SAME bridge method
7
- * twice within a short window rejects the second call immediately with a
8
- * 2.x native envelope (`{name, code, userInfo, moduleName, __isError}`) whose
9
- * `code` is `'APP_BRIDGE_THROTTLED'` and whose `message` reads
10
- * `"Too many app bridge calls from <method>."`. The 3.x bridge path is
11
- * unaffected (devtools#767 issue body, sdk-example#284 2026-07-10 comment).
12
- *
13
- * This module is the SINGLE source of the detection predicate. Today's only
14
- * consumer is `runtime.ts` (in-page, per-test retry loop) — a plain static
15
- * import; esbuild inlines this file into the same IIFE bundle (same
16
- * mechanism as `bridge-stub.ts`, see `bundle.ts#getPageSideModulePath`). It
17
- * is exported from this standalone, dependency-free module (rather than
18
- * defined inline in `runtime.ts`) so a Node-side consumer could import the
19
- * exact same predicate in future if whole-FILE throttle-aware retry is ever
20
- * added on top of the per-test retry `runtime.ts` already performs — no such
21
- * Node-side caller exists yet (whole-file retry today only triggers on an
22
- * `injectAndRunBundle`-level timeout/exception, a distinct failure mode from
23
- * a `RunReport` that completed normally but contains a THROTTLED-failed
24
- * test — see `relay-worker.ts`'s `EVALUATE_TIMEOUT_MARKER` gate).
25
- *
26
- * Browser-compatible, dependency-free (no Node APIs) — safe to import from
27
- * either side of the CDP boundary, mirroring `runtime.ts`'s own constraint.
28
- */
29
- /** The native error code the Toss app bridge returns when rate-limited. */
30
- const THROTTLED_ERROR_CODE = "APP_BRIDGE_THROTTLED";
31
- /** The message substring the native envelope carries when rate-limited. */
32
- const THROTTLED_MESSAGE_SUBSTRING = "Too many app bridge calls";
33
- /**
34
- * True when `err` looks like a native `APP_BRIDGE_THROTTLED` rejection —
35
- * either the 2.x native envelope's `code` field equals
36
- * {@link THROTTLED_ERROR_CODE}, or the error's message contains
37
- * {@link THROTTLED_MESSAGE_SUBSTRING}. Either signal alone is sufficient: the
38
- * `code` field is the precise native signal, but a caller may only have a
39
- * stringified message (e.g. `Error#message` after the native envelope was
40
- * flattened) — checking both keeps detection robust across both shapes.
41
- *
42
- * Accepts `unknown` because callers may pass a caught page-side exception
43
- * (whose shape is not statically known), a `TestResult.error` string, or a
44
- * `FileResult` error string — never throws.
45
- */
46
- function isThrottledError(err) {
47
- if (err === null || err === void 0) return false;
48
- if (typeof err === "string") return err.includes(THROTTLED_MESSAGE_SUBSTRING);
49
- if (typeof err === "object") {
50
- const rec = err;
51
- if (rec.code === "APP_BRIDGE_THROTTLED") return true;
52
- if (typeof rec.message === "string" && rec.message.includes("Too many app bridge calls")) return true;
53
- }
54
- return false;
55
- }
56
- //#endregion
57
- export { THROTTLED_MESSAGE_SUBSTRING as n, isThrottledError as r, THROTTLED_ERROR_CODE as t };
58
-
59
- //# sourceMappingURL=throttle-DKKzX1qC.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"throttle-DKKzX1qC.js","names":[],"sources":["../src/test-runner/throttle.ts"],"sourcesContent":["/**\n * Shared `APP_BRIDGE_THROTTLED` detection (devtools#767).\n *\n * 2026-07-10 real-device observation: the Toss app native side added a\n * per-method rate limit to the 2.x bridge — calling the SAME bridge method\n * twice within a short window rejects the second call immediately with a\n * 2.x native envelope (`{name, code, userInfo, moduleName, __isError}`) whose\n * `code` is `'APP_BRIDGE_THROTTLED'` and whose `message` reads\n * `\"Too many app bridge calls from <method>.\"`. The 3.x bridge path is\n * unaffected (devtools#767 issue body, sdk-example#284 2026-07-10 comment).\n *\n * This module is the SINGLE source of the detection predicate. Today's only\n * consumer is `runtime.ts` (in-page, per-test retry loop) — a plain static\n * import; esbuild inlines this file into the same IIFE bundle (same\n * mechanism as `bridge-stub.ts`, see `bundle.ts#getPageSideModulePath`). It\n * is exported from this standalone, dependency-free module (rather than\n * defined inline in `runtime.ts`) so a Node-side consumer could import the\n * exact same predicate in future if whole-FILE throttle-aware retry is ever\n * added on top of the per-test retry `runtime.ts` already performs — no such\n * Node-side caller exists yet (whole-file retry today only triggers on an\n * `injectAndRunBundle`-level timeout/exception, a distinct failure mode from\n * a `RunReport` that completed normally but contains a THROTTLED-failed\n * test — see `relay-worker.ts`'s `EVALUATE_TIMEOUT_MARKER` gate).\n *\n * Browser-compatible, dependency-free (no Node APIs) — safe to import from\n * either side of the CDP boundary, mirroring `runtime.ts`'s own constraint.\n */\n\n/** The native error code the Toss app bridge returns when rate-limited. */\nexport const THROTTLED_ERROR_CODE = 'APP_BRIDGE_THROTTLED';\n\n/** The message substring the native envelope carries when rate-limited. */\nexport const THROTTLED_MESSAGE_SUBSTRING = 'Too many app bridge calls';\n\n/**\n * True when `err` looks like a native `APP_BRIDGE_THROTTLED` rejection —\n * either the 2.x native envelope's `code` field equals\n * {@link THROTTLED_ERROR_CODE}, or the error's message contains\n * {@link THROTTLED_MESSAGE_SUBSTRING}. Either signal alone is sufficient: the\n * `code` field is the precise native signal, but a caller may only have a\n * stringified message (e.g. `Error#message` after the native envelope was\n * flattened) — checking both keeps detection robust across both shapes.\n *\n * Accepts `unknown` because callers may pass a caught page-side exception\n * (whose shape is not statically known), a `TestResult.error` string, or a\n * `FileResult` error string — never throws.\n */\nexport function isThrottledError(err: unknown): boolean {\n if (err === null || err === undefined) return false;\n\n if (typeof err === 'string') {\n return err.includes(THROTTLED_MESSAGE_SUBSTRING);\n }\n\n if (typeof err === 'object') {\n const rec = err as { code?: unknown; message?: unknown };\n if (rec.code === THROTTLED_ERROR_CODE) return true;\n if (typeof rec.message === 'string' && rec.message.includes(THROTTLED_MESSAGE_SUBSTRING)) {\n return true;\n }\n }\n\n return false;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAa,uBAAuB;;AAGpC,MAAa,8BAA8B;;;;;;;;;;;;;;AAe3C,SAAgB,iBAAiB,KAAuB;AACtD,KAAI,QAAQ,QAAQ,QAAQ,KAAA,EAAW,QAAO;AAE9C,KAAI,OAAO,QAAQ,SACjB,QAAO,IAAI,SAAS,4BAA4B;AAGlD,KAAI,OAAO,QAAQ,UAAU;EAC3B,MAAM,MAAM;AACZ,MAAI,IAAI,SAAA,uBAA+B,QAAO;AAC9C,MAAI,OAAO,IAAI,YAAY,YAAY,IAAI,QAAQ,SAAA,4BAAqC,CACtF,QAAO;;AAIX,QAAO"}
@@ -1,189 +0,0 @@
1
- import { createHmac, timingSafeEqual } from "node:crypto";
2
- //#region src/mcp/totp.ts
3
- /**
4
- * RFC 6238 TOTP implementation (Node.js, node:crypto only).
5
- *
6
- * External TOTP libraries (otplib, speakeasy, …) are intentionally NOT used
7
- * to keep the dependency surface minimal. This hand-roll is ~30 lines and
8
- * covers exactly what relay-side auth needs.
9
- *
10
- * Algorithm summary (RFC 6238 + RFC 4226):
11
- * T = floor(now / 30) — 30-second time step counter
12
- * K = Buffer.from(secret, 'hex') — shared secret (raw bytes, hex-encoded)
13
- * MAC = HMAC-SHA1(K, T as 8-byte big-endian uint64)
14
- * offset = MAC[19] & 0x0f
15
- * code = (MAC[offset..offset+4] & 0x7fffffff) % 10^6 — 6 digits
16
- *
17
- * Security note (keep this comment accurate):
18
- * The baked-in secret in a dog-food build is extractable from the bundle by a
19
- * determined reverse engineer. This mechanism raises the bar from
20
- * "anyone with the URL" to "URL + bundle extraction + live TOTP calculation".
21
- * Casual URL leaks (Slack paste, QR screenshot, shoulder-surfing) are
22
- * blocked; deliberate reverse engineering is not. See threat model in
23
- * src/mcp/chii-relay.ts and umbrella CLAUDE.md §4.
24
- *
25
- * SECRET-HANDLING: secret values and computed codes MUST NOT appear in any
26
- * log, error message, or string visible outside this module. Only boolean
27
- * pass/fail and reason enum values are safe to surface.
28
- */
29
- /** Time step window in seconds (RFC 6238 default). */
30
- const TIME_STEP = 30;
31
- /** Number of digits in the generated code. */
32
- const DIGITS = 6;
33
- /**
34
- * Derives a 6-digit TOTP code from a hex-encoded secret at the given wall-
35
- * clock time.
36
- *
37
- * @param secret - The shared secret as a hex string (e.g. 64 hex chars = 32
38
- * bytes). Must be the output of `generateAttachToken()` or compatible.
39
- * @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.
40
- * @returns A zero-padded 6-digit decimal string, e.g. `"042193"`.
41
- */
42
- function generateTotp(secret, when = Date.now()) {
43
- const key = Buffer.from(secret, "hex");
44
- const counter = Math.max(0, Math.floor(when / 1e3 / TIME_STEP));
45
- const counterBuf = Buffer.alloc(8);
46
- const hi = Math.floor(counter / 4294967296);
47
- const lo = counter >>> 0;
48
- counterBuf.writeUInt32BE(hi, 0);
49
- counterBuf.writeUInt32BE(lo, 4);
50
- const mac = createHmac("sha1", key).update(counterBuf).digest();
51
- const offset = mac[19] & 15;
52
- return (((mac[offset] & 127) << 24 | (mac[offset + 1] & 255) << 16 | (mac[offset + 2] & 255) << 8 | mac[offset + 3] & 255) % 10 ** DIGITS).toString().padStart(DIGITS, "0");
53
- }
54
- /**
55
- * Verifies a TOTP code against the secret, accepting ±`skew` time steps to
56
- * tolerate clock drift between the relay host and the client device.
57
- *
58
- * Uses `timingSafeEqual` for constant-time comparison to prevent timing
59
- * side-channel attacks.
60
- *
61
- * @param secret - Hex-encoded shared secret.
62
- * @param code - The 6-digit code to verify (string or numeric).
63
- * @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.
64
- * @param skew - Number of adjacent steps to accept on either side. Default 1
65
- * (accepts T-1, T, T+1 — a 90-second acceptance window).
66
- * @returns `true` if the code matches any accepted step, `false` otherwise.
67
- */
68
- function verifyTotp(secret, code, when = Date.now(), skew = 1) {
69
- const normalised = String(code).padStart(DIGITS, "0");
70
- if (normalised.length !== DIGITS || !/^\d{6}$/.test(normalised)) return false;
71
- const candidateBuf = Buffer.from(normalised, "utf8");
72
- for (let delta = -skew; delta <= skew; delta++) {
73
- const expected = generateTotp(secret, when + delta * TIME_STEP * 1e3);
74
- if (timingSafeEqual(Buffer.from(expected, "utf8"), candidateBuf)) return true;
75
- }
76
- return false;
77
- }
78
- /**
79
- * Minimum length (in hex characters) accepted for `AIT_DEBUG_TOTP_SECRET`.
80
- *
81
- * The secret is hex-encoded (see {@link generateTotp} — `Buffer.from(secret,
82
- * 'hex')`). 32 hex chars = 16 bytes = 128 bits, the floor for an HMAC-SHA1 key
83
- * we are willing to gate a public relay behind. `generateAttachToken()` emits
84
- * 64 hex chars (32 bytes), comfortably above this bar.
85
- */
86
- const MIN_SECRET_HEX_CHARS = 32;
87
- /** Hex string: one or more hex digits, case-insensitive (RFC 4648 base16). */
88
- const HEX_RE = /^[0-9a-fA-F]+$/;
89
- /**
90
- * Human-facing guidance printed when {@link assertRelayAuthConfigured} fails.
91
- *
92
- * SECRET-HANDLING: this message states only the REQUIREMENT (≥32 hex chars) and
93
- * how to mint one. It NEVER echoes the configured value, its length, or any
94
- * fragment derived from it — see {@link assertRelayAuthConfigured}.
95
- *
96
- * Note on encoding: the secret is hex (base16), not base32 — `generateTotp`
97
- * decodes it with `Buffer.from(secret, 'hex')`. A base32 string would be
98
- * silently mis-decoded and every TOTP code would fail to match, so the minting
99
- * command emits hex.
100
- */
101
- const RELAY_AUTH_SECRET_MISSING_MESSAGE = [
102
- "[ait-debug] AIT_DEBUG_TOTP_SECRET이 필수입니다. 32자 이상 16진수(hex) 문자열을 설정하세요.",
103
- "발급: openssl rand -hex 32",
104
- "데몬은 start_debug의 projectRoot 인자로 받은 디렉토리에서 .ait_relay 파일을 읽어 이 시크릿을 채웁니다.",
105
- "프로젝트에서 pnpm dev:phone:cdp를 한 번 띄우면 unplugin이 .ait_relay를 자동 생성하니(tunnel.cdp 옵션 필요), projectRoot를 전달하세요.",
106
- "자세히: https://docs.aitc.dev/guides/relay-auth-totp"
107
- ].join("\n");
108
- /**
109
- * Whether `secret` is a well-formed relay-auth TOTP secret: a hex string of at
110
- * least {@link MIN_SECRET_HEX_CHARS} characters with an even length (an odd
111
- * length would have its trailing nibble silently dropped by `Buffer.from(...,
112
- * 'hex')`, weakening the key without warning).
113
- *
114
- * Pure predicate so callers can test the validation independently of the
115
- * fail-fast side effect in {@link assertRelayAuthConfigured}.
116
- *
117
- * SECRET-HANDLING: returns only a boolean — the input value is never returned,
118
- * logged, or echoed.
119
- */
120
- function isValidRelayAuthSecret(secret) {
121
- if (secret === void 0 || secret === "") return false;
122
- if (secret.length < MIN_SECRET_HEX_CHARS) return false;
123
- if (secret.length % 2 !== 0) return false;
124
- return HEX_RE.test(secret);
125
- }
126
- /**
127
- * Fail-fast guard enforcing that a relay-auth TOTP secret is configured before
128
- * a public-internet-exposed relay is booted (issue #250).
129
- *
130
- * Relay-auth (the §4 Layer C TOTP gate) is the only fail-fast layer that closes
131
- * the real gap: a leaked `wss://…trycloudflare.com` URL otherwise lets a third
132
- * party attach a debugger to a dog-food/live mini-app. Without a secret the relay
133
- * comes up unauthenticated, so this guard is called at every relay-boot site —
134
- * `bootRelayFamily` (intoss env 3/4) and `bootExternalRelayFamily` (env-2 PWA),
135
- * both eager and lazy. Local-only sessions never boot a relay and so never reach
136
- * this guard, matching the issue's exemption for non-relay debugging.
137
- *
138
- * Throws when the secret is unset, empty, too short, or not a valid hex string.
139
- * The thrown message is the bin entry's fatal stderr (see `cli.ts` `main().catch`)
140
- * — the same fatal model as the missing-`AIT_RELAY_BASE_URL` path.
141
- *
142
- * SECRET-HANDLING: the env value is read once, passed ONLY to the boolean
143
- * predicate, and never logged. The thrown message names the requirement, never
144
- * the value, its length, or any derived fragment.
145
- *
146
- * @param env - Environment to read from. Defaults to `process.env`; injectable
147
- * for tests so they never mutate the real process environment.
148
- */
149
- function assertRelayAuthConfigured(env = process.env) {
150
- if (!isValidRelayAuthSecret(env.AIT_DEBUG_TOTP_SECRET)) throw new Error(RELAY_AUTH_SECRET_MISSING_MESSAGE);
151
- }
152
- /**
153
- * Reads `AIT_DEBUG_TOTP_SECRET` from `process.env` at runtime and builds a
154
- * `verifyAuth` predicate for the Chii relay's WebSocket upgrade gate.
155
- *
156
- * The predicate checks the `at` query parameter against the current and
157
- * adjacent TOTP time steps (±{@link RELAY_VERIFY_SKEW_STEPS} skew) using
158
- * {@link verifyTotp}. This gives the issued code a minimum validity of ~3
159
- * minutes, which is enough to cover the QR-scan → launcher-attach flow even
160
- * when the launcher PWA needs to load or reinstall (#490).
161
- *
162
- * Returns `undefined` when the env var is not set — callers treat that as
163
- * "auth disabled" (no predicate registered on the relay). Note that since
164
- * issue #250 the secret is MANDATORY at every relay-boot site (enforced by
165
- * {@link assertRelayAuthConfigured} BEFORE the relay starts), so in production
166
- * this never returns `undefined` for a relay that actually boots; the
167
- * `undefined` branch only matters for the no-relay local path and tests.
168
- *
169
- * Lives here (not in the MCP server) so the unplugin's env-2 relay can wire the
170
- * same gate without importing the heavy MCP server module graph. Re-exported
171
- * from `debug-server.ts` for back-compat.
172
- *
173
- * SECRET-HANDLING: The secret value read from env is captured in a closure and
174
- * is NEVER written to any log, error message, or process output.
175
- */
176
- function buildRelayVerifyAuth(env = process.env) {
177
- const secret = env.AIT_DEBUG_TOTP_SECRET;
178
- if (!secret) return void 0;
179
- return (req) => {
180
- const rawUrl = req.url ?? "";
181
- const qIndex = rawUrl.indexOf("?");
182
- const queryStr = qIndex === -1 ? "" : rawUrl.slice(qIndex + 1);
183
- return verifyTotp(secret, new URLSearchParams(queryStr).get("at") ?? "", void 0, 6);
184
- };
185
- }
186
- //#endregion
187
- export { assertRelayAuthConfigured, buildRelayVerifyAuth, isValidRelayAuthSecret };
188
-
189
- //# sourceMappingURL=totp-BqmCLSNA.js.map