@ait-co/devtools 0.1.136 → 0.1.138
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.
- package/dist/{attach-orchestrator-Cjdhir2U.js → attach-orchestrator-6kz_ZdWL.js} +5 -3
- package/dist/{attach-orchestrator-Cjdhir2U.js.map → attach-orchestrator-6kz_ZdWL.js.map} +1 -1
- package/dist/{attach-orchestrator-CE0S09YU.js → attach-orchestrator-BEiz3wAx.js} +5 -3
- package/dist/{attach-orchestrator-CMoDuG2A.js.map → attach-orchestrator-BEiz3wAx.js.map} +1 -1
- package/dist/{attach-orchestrator-CMoDuG2A.js → attach-orchestrator-BG8TGJOZ.js} +5 -3
- package/dist/{attach-orchestrator-CE0S09YU.js.map → attach-orchestrator-BG8TGJOZ.js.map} +1 -1
- package/dist/{bundle-BKqyhEK9.d.ts → bundle-C796JIwG.d.ts} +65 -2
- package/dist/bundle-C796JIwG.d.ts.map +1 -0
- package/dist/{capture-ltuV0gZa.d.ts → capture-DsP525OZ.d.ts} +1 -1
- package/dist/{capture-ltuV0gZa.d.ts.map → capture-DsP525OZ.d.ts.map} +1 -1
- package/dist/{cdp-connection-BYE9meXe.d.ts → cdp-connection-rP1WdnH5.d.ts} +1 -1
- package/dist/{cdp-connection-BYE9meXe.d.ts.map → cdp-connection-rP1WdnH5.d.ts.map} +1 -1
- package/dist/{cell-D1y4shoV.js → cell-BMlFh08P.js} +2 -2
- package/dist/cell-BMlFh08P.js.map +1 -0
- package/dist/{cell-DHA578lX.js → cell-Dtgtusog.js} +90 -16
- package/dist/cell-Dtgtusog.js.map +1 -0
- package/dist/{cell-BIdSQb9T.js → cell-KsRXFdDs.js} +122 -16
- package/dist/cell-KsRXFdDs.js.map +1 -0
- package/dist/{debug-server-BZtH2Z5b.js → debug-server-6c22qSWa.js} +150 -9
- package/dist/debug-server-6c22qSWa.js.map +1 -0
- package/dist/{debug-server-DApo0o3t.js → debug-server-BQHbaKPT.js} +152 -11
- package/dist/debug-server-BQHbaKPT.js.map +1 -0
- package/dist/{debug-server-D2wpPK3_.js → debug-server-DWat7d-9.js} +82 -6
- package/dist/debug-server-DWat7d-9.js.map +1 -0
- package/dist/devtools-opener-3Drge_RJ.js +75 -0
- package/dist/devtools-opener-3Drge_RJ.js.map +1 -0
- package/dist/devtools-opener-CJpEsXXQ.js +76 -0
- package/dist/devtools-opener-CJpEsXXQ.js.map +1 -0
- package/dist/devtools-opener-CxtryS8c.js +75 -0
- package/dist/devtools-opener-CxtryS8c.js.map +1 -0
- package/dist/in-app/auto.js +59 -12
- package/dist/in-app/auto.js.map +1 -1
- package/dist/in-app/index.d.ts +21 -2
- package/dist/in-app/index.d.ts.map +1 -1
- package/dist/in-app/index.js +60 -13
- package/dist/in-app/index.js.map +1 -1
- package/dist/mcp/cli.js +363 -73
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/mock/index.d.ts +64 -1
- package/dist/mock/index.d.ts.map +1 -1
- package/dist/mock/index.js +221 -14
- package/dist/mock/index.js.map +1 -1
- package/dist/panel/index.js +178 -23
- package/dist/panel/index.js.map +1 -1
- package/dist/{pool-C6TgrcyW.d.ts → pool-CrP5CPvU.d.ts} +3 -3
- package/dist/{pool-C6TgrcyW.d.ts.map → pool-CrP5CPvU.d.ts.map} +1 -1
- package/dist/{qr-http-server-ZkT6F7is.js → qr-http-server-D1hpoyBF.js} +1 -1
- package/dist/{qr-http-server-ZkT6F7is.js.map → qr-http-server-D1hpoyBF.js.map} +1 -1
- package/dist/qr-http-server-D4rGz8M7.js +1640 -0
- package/dist/qr-http-server-D4rGz8M7.js.map +1 -0
- package/dist/{qr-http-server-C7q8Gn04.js → qr-http-server-DIjCV7h_.js} +1 -1
- package/dist/{qr-http-server-C7q8Gn04.js.map → qr-http-server-DIjCV7h_.js.map} +1 -1
- package/dist/{relay-factory-DnuTH4v-.js → relay-factory-C1X3G3WY.js} +72 -15
- package/dist/relay-factory-C1X3G3WY.js.map +1 -0
- package/dist/{relay-secret-store-CLEGyHou.js → relay-secret-store-Bmyleu0A.js} +1 -1
- package/dist/{relay-secret-store-CLEGyHou.js.map → relay-secret-store-Bmyleu0A.js.map} +1 -1
- package/dist/{relay-secret-store-DhzAnnj-.js → relay-secret-store-CQenfcSL.js} +2 -2
- package/dist/{relay-secret-store-DhzAnnj-.js.map → relay-secret-store-CQenfcSL.js.map} +1 -1
- package/dist/{relay-secret-store-BcVrWwTq.js → relay-secret-store-DKxs7zwq.js} +1 -1
- package/dist/{relay-secret-store-DGduVJhs.js.map → relay-secret-store-DKxs7zwq.js.map} +1 -1
- package/dist/{relay-secret-store-DGduVJhs.js → relay-secret-store-WJ8EGkIl.js} +1 -1
- package/dist/{relay-secret-store-BcVrWwTq.js.map → relay-secret-store-WJ8EGkIl.js.map} +1 -1
- package/dist/{relay-url-store-CKW8RQzf.js → relay-url-store-BR2XodiO.js} +2 -2
- package/dist/{relay-url-store-CKW8RQzf.js.map → relay-url-store-BR2XodiO.js.map} +1 -1
- package/dist/{relay-url-store-CdA58fgw.js → relay-url-store-CH63fVCm.js} +2 -2
- package/dist/{relay-url-store-B0X8TsGr.js.map → relay-url-store-CH63fVCm.js.map} +1 -1
- package/dist/{relay-url-store-CwKT7i04.js → relay-url-store-DaY1QPes.js} +2 -2
- package/dist/{relay-url-store-CwKT7i04.js.map → relay-url-store-DaY1QPes.js.map} +1 -1
- package/dist/{relay-url-store-B0X8TsGr.js → relay-url-store-xmUuTjXA.js} +2 -2
- package/dist/{relay-url-store-CdA58fgw.js.map → relay-url-store-xmUuTjXA.js.map} +1 -1
- package/dist/{relay-worker-UK0nBfDX.js → relay-worker-BUXI0K0b.js} +13 -6
- package/dist/{relay-worker-UK0nBfDX.js.map → relay-worker-BUXI0K0b.js.map} +1 -1
- package/dist/{relay-worker-Dw3aIzIK.d.ts → relay-worker-DcyboK43.d.ts} +70 -11
- package/dist/relay-worker-DcyboK43.d.ts.map +1 -0
- package/dist/{runtime-DfHHZms2.d.ts → runtime-BiigOuvb.d.ts} +30 -3
- package/dist/runtime-BiigOuvb.d.ts.map +1 -0
- package/dist/test-runner/bin.js +481 -62
- package/dist/test-runner/bin.js.map +1 -1
- package/dist/test-runner/bridge-stub.d.ts +125 -0
- package/dist/test-runner/bridge-stub.d.ts.map +1 -0
- package/dist/test-runner/bridge-stub.js +92 -0
- package/dist/test-runner/bridge-stub.js.map +1 -0
- package/dist/test-runner/bundle.d.ts +2 -2
- package/dist/test-runner/bundle.js +107 -24
- package/dist/test-runner/bundle.js.map +1 -1
- package/dist/test-runner/capture.d.ts +1 -1
- package/dist/test-runner/config.d.ts +68 -5
- package/dist/test-runner/config.d.ts.map +1 -1
- package/dist/test-runner/config.js +1 -1
- package/dist/test-runner/method-pace.d.ts +82 -0
- package/dist/test-runner/method-pace.d.ts.map +1 -0
- package/dist/test-runner/method-pace.js +120 -0
- package/dist/test-runner/method-pace.js.map +1 -0
- package/dist/test-runner/pool.d.ts +1 -1
- package/dist/test-runner/pool.js +1 -1
- package/dist/test-runner/relay-factory.d.ts +67 -4
- package/dist/test-runner/relay-factory.d.ts.map +1 -1
- package/dist/test-runner/relay-factory.js +71 -14
- package/dist/test-runner/relay-factory.js.map +1 -1
- package/dist/test-runner/relay-worker.d.ts +1 -1
- package/dist/test-runner/relay-worker.js +1 -1
- package/dist/test-runner/report.d.ts +43 -17
- package/dist/test-runner/report.d.ts.map +1 -1
- package/dist/test-runner/report.js +33 -9
- package/dist/test-runner/report.js.map +1 -1
- package/dist/test-runner/rpc.d.ts +2 -2
- package/dist/test-runner/runtime.d.ts +2 -2
- package/dist/test-runner/runtime.js +83 -6
- package/dist/test-runner/runtime.js.map +1 -1
- package/dist/test-runner/task-graph.d.ts +1 -1
- package/dist/throttle-DKKzX1qC.js +59 -0
- package/dist/throttle-DKKzX1qC.js.map +1 -0
- package/dist/totp-Dwft0Kz7.js +3 -0
- package/package.json +1 -1
- package/dist/bundle-BKqyhEK9.d.ts.map +0 -1
- package/dist/cell-BIdSQb9T.js.map +0 -1
- package/dist/cell-D1y4shoV.js.map +0 -1
- package/dist/cell-DHA578lX.js.map +0 -1
- package/dist/debug-server-BZtH2Z5b.js.map +0 -1
- package/dist/debug-server-D2wpPK3_.js.map +0 -1
- package/dist/debug-server-DApo0o3t.js.map +0 -1
- package/dist/relay-factory-DnuTH4v-.js.map +0 -1
- package/dist/relay-worker-Dw3aIzIK.d.ts.map +0 -1
- package/dist/runtime-DfHHZms2.d.ts.map +0 -1
- package/dist/totp-D1pulXLa.js +0 -3
package/dist/mcp/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { i as generateTotp, n as assertRelayAuthConfigured, r as buildRelayVerifyAuth } from "../totp-WY6l0ysP.js";
|
|
3
|
-
import { t as loadRelaySecretReadOnly } from "../relay-secret-store-
|
|
3
|
+
import { t as loadRelaySecretReadOnly } from "../relay-secret-store-CQenfcSL.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { accessSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { argv } from "node:process";
|
|
@@ -25,33 +25,44 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
25
25
|
//#endregion
|
|
26
26
|
//#region src/in-app/gate.ts
|
|
27
27
|
/**
|
|
28
|
-
* The host suffix the Toss app uses to serve dogfood / private mini-apps.
|
|
29
|
-
*
|
|
30
|
-
* A `intoss-private://` (dogfood) entry maps to a host such as
|
|
31
|
-
* `aitc-sdk-example.private-apps.tossmini.com`. A production `intoss://`
|
|
32
|
-
* entry is served from `*.apps.tossmini.com` — the `.private-apps.` segment
|
|
33
|
-
* is absent. Confirmed live over CDP for mini-app 31146; the exact production
|
|
34
|
-
* host is to be re-confirmed once 31146 passes review (spec open question 2).
|
|
35
|
-
*/
|
|
36
|
-
const PRIVATE_APPS_HOST_SUFFIX = ".private-apps.tossmini.com";
|
|
37
|
-
/**
|
|
38
28
|
* The host suffix Cloudflare quick-tunnels serve from — the env 2 (PWA) entry.
|
|
39
29
|
* See {@link isTrycloudflareHost} for why this host kind bypasses Layer B1.
|
|
40
30
|
*/
|
|
41
31
|
const TRYCLOUDFLARE_HOST_SUFFIX = ".trycloudflare.com";
|
|
42
32
|
/**
|
|
43
|
-
*
|
|
44
|
-
* the host the Toss app reserves for dogfood / private mini-app entries.
|
|
33
|
+
* The parent host suffix for the whole Toss mini-app serving family.
|
|
45
34
|
*
|
|
46
|
-
* The
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
35
|
+
* The 3.0 runtime loader serves mini-app pages from tossmini.com hosts that
|
|
36
|
+
* are NOT `*.private-apps.tossmini.com` (observed live 2026-07-08 on mini-app
|
|
37
|
+
* 31146 with a 3.0-beta bundle: a 4-label host ending in `.tossmini.com`
|
|
38
|
+
* whose middle label is not `private-apps`, with `_deploymentId` consumed by
|
|
39
|
+
* the native loader and not propagated to the page URL — devtools#760).
|
|
40
|
+
*
|
|
41
|
+
* Under 3.0 the hostname therefore no longer distinguishes a dogfood
|
|
42
|
+
* candidate from a production entry, so for these hosts Layer B is demoted
|
|
43
|
+
* from a stage discriminator (#665) to a "Toss-owned host family" filter,
|
|
44
|
+
* and the effective boundary moves to Layer C: explicit `debug=1`, a valid
|
|
45
|
+
* `wss:` relay, and a MANDATORY `at=` TOTP code (see Layer C3 in
|
|
46
|
+
* {@link evaluateDebugGate}). A production user's entry URL carries none of
|
|
47
|
+
* those params, so an accidentally-shipped debug build stays dormant exactly
|
|
48
|
+
* as #665 intended; what changes is that a deliberate operator holding the
|
|
49
|
+
* TOTP secret can now attach on a 3.0-family host.
|
|
50
|
+
*
|
|
51
|
+
* The match is the same exact-suffix `endsWith` check as
|
|
52
|
+
* {@link isPrivateAppsHost} — never a substring `.includes()`, which would
|
|
53
|
+
* accept an attacker-controlled `x.tossmini.com.evil.example`. The leading
|
|
54
|
+
* `.` forces at least one subdomain label, so a bare `tossmini.com` does not
|
|
55
|
+
* match.
|
|
56
|
+
*/
|
|
57
|
+
const TOSSMINI_HOST_SUFFIX = ".tossmini.com";
|
|
58
|
+
/**
|
|
59
|
+
* Returns whether `hostname` is any `*.tossmini.com` subdomain — the host
|
|
60
|
+
* family the Toss app serves mini-app pages from. Includes the 2.x
|
|
61
|
+
* `*.private-apps.tossmini.com` dogfood hosts and the 3.0 unified serving
|
|
62
|
+
* hosts (devtools#760).
|
|
52
63
|
*/
|
|
53
|
-
function
|
|
54
|
-
return hostname.endsWith(
|
|
64
|
+
function isTossminiHost(hostname) {
|
|
65
|
+
return hostname.endsWith(TOSSMINI_HOST_SUFFIX);
|
|
55
66
|
}
|
|
56
67
|
/**
|
|
57
68
|
* The host suffix Cloudflare quick-tunnels use — the env 2 (PWA) entry.
|
|
@@ -98,18 +109,27 @@ function isLocalhostHost(hostname) {
|
|
|
98
109
|
* known debug-allowed host. The debug surface is ONLY active on:
|
|
99
110
|
* - localhost / loopback (env 1 desktop dev)
|
|
100
111
|
* - *.trycloudflare.com (env 2 PWA tunnel)
|
|
101
|
-
* - *.
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* is silently blocked. This is a positive allowlist —
|
|
105
|
-
* had debug surface regardless, but this function makes
|
|
106
|
-
* auditable in a single place.
|
|
112
|
+
* - *.tossmini.com (env 3 dog-food — 2.x private-apps hosts AND the 3.0
|
|
113
|
+
* unified serving family, devtools#760)
|
|
114
|
+
*
|
|
115
|
+
* Any other host is silently blocked. This is a positive allowlist —
|
|
116
|
+
* unlisted hosts never had debug surface regardless, but this function makes
|
|
117
|
+
* it explicit and auditable in a single place.
|
|
118
|
+
*
|
|
119
|
+
* #760 note on the #665 boundary: the former env 4 LIVE host family
|
|
120
|
+
* (`*.apps.tossmini.com`) now passes this coarse filter because the 3.0
|
|
121
|
+
* loader serves dogfood candidates and production entries from the same
|
|
122
|
+
* host family — the hostname alone can no longer separate them. The #665
|
|
123
|
+
* invariant ("no naked attach on a production-family host") is preserved
|
|
124
|
+
* one layer down: on tossmini hosts that are not `*.private-apps.*`, Layer
|
|
125
|
+
* C3 makes the TOTP `at=` code MANDATORY, and production entry URLs carry
|
|
126
|
+
* no debug/relay/at params at all.
|
|
107
127
|
*
|
|
108
128
|
* SECRET-HANDLING: the hostname value MUST NOT be logged or included in any
|
|
109
129
|
* error reason string — only benign labels ('host not in allowlist') are safe.
|
|
110
130
|
*/
|
|
111
131
|
function isDebugAllowedHost(hostname) {
|
|
112
|
-
return isLocalhostHost(hostname) || isTrycloudflareHost(hostname) ||
|
|
132
|
+
return isLocalhostHost(hostname) || isTrycloudflareHost(hostname) || isTossminiHost(hostname);
|
|
113
133
|
}
|
|
114
134
|
//#endregion
|
|
115
135
|
//#region src/shared/parent-watcher.ts
|
|
@@ -211,6 +231,39 @@ function startMaxAgeWatchdog(onExpired, opts = {}) {
|
|
|
211
231
|
} };
|
|
212
232
|
}
|
|
213
233
|
//#endregion
|
|
234
|
+
//#region src/test-runner/throttle.ts
|
|
235
|
+
/**
|
|
236
|
+
* Shared `APP_BRIDGE_THROTTLED` detection (devtools#767).
|
|
237
|
+
*
|
|
238
|
+
* 2026-07-10 real-device observation: the Toss app native side added a
|
|
239
|
+
* per-method rate limit to the 2.x bridge — calling the SAME bridge method
|
|
240
|
+
* twice within a short window rejects the second call immediately with a
|
|
241
|
+
* 2.x native envelope (`{name, code, userInfo, moduleName, __isError}`) whose
|
|
242
|
+
* `code` is `'APP_BRIDGE_THROTTLED'` and whose `message` reads
|
|
243
|
+
* `"Too many app bridge calls from <method>."`. The 3.x bridge path is
|
|
244
|
+
* unaffected (devtools#767 issue body, sdk-example#284 2026-07-10 comment).
|
|
245
|
+
*
|
|
246
|
+
* This module is the SINGLE source of the detection predicate. Today's only
|
|
247
|
+
* consumer is `runtime.ts` (in-page, per-test retry loop) — a plain static
|
|
248
|
+
* import; esbuild inlines this file into the same IIFE bundle (same
|
|
249
|
+
* mechanism as `bridge-stub.ts`, see `bundle.ts#getPageSideModulePath`). It
|
|
250
|
+
* is exported from this standalone, dependency-free module (rather than
|
|
251
|
+
* defined inline in `runtime.ts`) so a Node-side consumer could import the
|
|
252
|
+
* exact same predicate in future if whole-FILE throttle-aware retry is ever
|
|
253
|
+
* added on top of the per-test retry `runtime.ts` already performs — no such
|
|
254
|
+
* Node-side caller exists yet (whole-file retry today only triggers on an
|
|
255
|
+
* `injectAndRunBundle`-level timeout/exception, a distinct failure mode from
|
|
256
|
+
* a `RunReport` that completed normally but contains a THROTTLED-failed
|
|
257
|
+
* test — see `relay-worker.ts`'s `EVALUATE_TIMEOUT_MARKER` gate).
|
|
258
|
+
*
|
|
259
|
+
* Browser-compatible, dependency-free (no Node APIs) — safe to import from
|
|
260
|
+
* either side of the CDP boundary, mirroring `runtime.ts`'s own constraint.
|
|
261
|
+
*/
|
|
262
|
+
/** The native error code the Toss app bridge returns when rate-limited. */
|
|
263
|
+
const THROTTLED_ERROR_CODE = "APP_BRIDGE_THROTTLED";
|
|
264
|
+
/** The message substring the native envelope carries when rate-limited. */
|
|
265
|
+
const THROTTLED_MESSAGE_SUBSTRING = "Too many app bridge calls";
|
|
266
|
+
//#endregion
|
|
214
267
|
//#region src/mcp/deeplink.ts
|
|
215
268
|
/**
|
|
216
269
|
* URL of the AITC Sandbox launcher PWA.
|
|
@@ -2024,7 +2077,7 @@ async function readMcpSdkVersion() {
|
|
|
2024
2077
|
* some test environments that skip the build step).
|
|
2025
2078
|
*/
|
|
2026
2079
|
function readDevtoolsVersion() {
|
|
2027
|
-
return "0.1.
|
|
2080
|
+
return "0.1.138";
|
|
2028
2081
|
}
|
|
2029
2082
|
/**
|
|
2030
2083
|
* Derives the next recommended action from a completed diagnostics snapshot.
|
|
@@ -2617,7 +2670,7 @@ async function prepareAttach(deps, env, args, conn) {
|
|
|
2617
2670
|
const buildProjectRoot = typeof rawProjectRoot === "string" ? rawProjectRoot : void 0;
|
|
2618
2671
|
let tunnelHttpUrl = process.env.AIT_TUNNEL_BASE_URL?.trim() ?? "";
|
|
2619
2672
|
if (tunnelHttpUrl === "" && buildProjectRoot !== void 0) {
|
|
2620
|
-
const { readRelayUrls } = await import("../relay-url-store-
|
|
2673
|
+
const { readRelayUrls } = await import("../relay-url-store-DaY1QPes.js");
|
|
2621
2674
|
tunnelHttpUrl = (await readRelayUrls({ projectRoot: buildProjectRoot }))?.tunnelBaseUrl ?? "";
|
|
2622
2675
|
}
|
|
2623
2676
|
if (tunnelHttpUrl === "") return {
|
|
@@ -2694,10 +2747,12 @@ async function prepareAttach(deps, env, args, conn) {
|
|
|
2694
2747
|
tool: "start_attach",
|
|
2695
2748
|
msg: "no _deploymentId in scheme_url; matching on presence only"
|
|
2696
2749
|
});
|
|
2750
|
+
const wssForMatch = tunnelForBuild.wssUrl;
|
|
2751
|
+
const matchesRelay = (url) => url.includes(wssForMatch) || url.includes(encodeURIComponent(wssForMatch));
|
|
2697
2752
|
const isMatchingPage = (pages) => {
|
|
2698
2753
|
if (pages.length === 0) return false;
|
|
2699
2754
|
if (deploymentId === null) return true;
|
|
2700
|
-
return pages.some((p) => p.url.includes(deploymentId));
|
|
2755
|
+
return pages.some((p) => p.url.includes(deploymentId) || matchesRelay(p.url));
|
|
2701
2756
|
};
|
|
2702
2757
|
const buildTimeoutError = (baseText, timeoutSec, observed) => {
|
|
2703
2758
|
const observedUrls = observed.slice(0, 3).map((p) => p.url.slice(0, 80)).join(", ");
|
|
@@ -2956,8 +3011,43 @@ const PERMISSION_PROBE_MAP = {
|
|
|
2956
3011
|
contacts: "fetchContacts",
|
|
2957
3012
|
location: "getCurrentLocation"
|
|
2958
3013
|
};
|
|
2959
|
-
/**
|
|
2960
|
-
|
|
3014
|
+
/**
|
|
3015
|
+
* Inter-probe spacing (ms) applied BEFORE every probe after the first
|
|
3016
|
+
* (devtools#767). The native per-method bridge rate limit observed
|
|
3017
|
+
* 2026-07-10 rejects a SECOND call to the same method within a short window
|
|
3018
|
+
* with `APP_BRIDGE_THROTTLED` — the permission preflight's un-paced burst
|
|
3019
|
+
* (6 probes fired back-to-back) was the FIRST trigger of that limiter in the
|
|
3020
|
+
* wild (only the first probe passed; the remaining 5 came back throttled →
|
|
3021
|
+
* `unavailable`, cascading into skipped `__AIT_PERMS__`-branching tests).
|
|
3022
|
+
* Sequential-with-spacing is cheap insurance even though each probe targets a
|
|
3023
|
+
* DIFFERENT method — the limiter's exact scope (per-method vs. a shared
|
|
3024
|
+
* bridge-wide window) was not reverse-engineered (explicitly out of scope,
|
|
3025
|
+
* devtools#767) so spacing every probe is the conservative choice.
|
|
3026
|
+
*/
|
|
3027
|
+
const PROBE_INTER_CALL_DELAY_MS = 250;
|
|
3028
|
+
/**
|
|
3029
|
+
* Backoff delays (ms) between retry attempts when a probe is throttled. Its
|
|
3030
|
+
* `.length` IS the max retry count (devtools#767: "최대 2회 재시도") — the
|
|
3031
|
+
* generated page-side expression below derives `attempt < backoff.length`
|
|
3032
|
+
* from this same array (via `backoffArray`), so there is a single source for
|
|
3033
|
+
* both the delay ladder and the retry count; no separate max-retries
|
|
3034
|
+
* constant is kept in this module (unlike `runtime.ts`'s `TEST_MAX_RETRIES`,
|
|
3035
|
+
* which IS consumed directly by a Node-side loop condition).
|
|
3036
|
+
*/
|
|
3037
|
+
const PROBE_RETRY_BACKOFF_MS = [500, 1e3];
|
|
3038
|
+
/**
|
|
3039
|
+
* Non-fatal bound for the permission preflight `Runtime.evaluate` round-trip
|
|
3040
|
+
* (devtools#767: raised from the pre-pacing 10s to cover the new sequential +
|
|
3041
|
+
* backoff time budget).
|
|
3042
|
+
*
|
|
3043
|
+
* Worst-case budget with 6 probes, each requiring the full retry ladder:
|
|
3044
|
+
* - inter-probe spacing: only BEFORE probes 2‑6 (none before the first) =
|
|
3045
|
+
* 5 × {@link PROBE_INTER_CALL_DELAY_MS} = 1 250ms
|
|
3046
|
+
* - per-probe backoff (both retries taken): 6 × (500 + 1000) = 9 000ms
|
|
3047
|
+
* - probe round-trips themselves (headroom, not just the sleeps): ~4 500ms
|
|
3048
|
+
* total ≈ 14 750ms, rounded up to a clean 20 000ms for headroom.
|
|
3049
|
+
*/
|
|
3050
|
+
const PERMISSION_PREFLIGHT_TIMEOUT_MS = 2e4;
|
|
2961
3051
|
/**
|
|
2962
3052
|
* Builds the `Runtime.evaluate` expression for the permission-state preflight
|
|
2963
3053
|
* (devtools#739). Evaluated ONCE per relay session, before the first test
|
|
@@ -2972,6 +3062,29 @@ const PERMISSION_PREFLIGHT_TIMEOUT_MS = 1e4;
|
|
|
2972
3062
|
* caller, and a single slow/broken permission must not fail the others (each
|
|
2973
3063
|
* probe is awaited independently, not short-circuited).
|
|
2974
3064
|
*
|
|
3065
|
+
* devtools#767: probes run SEQUENTIALLY (not a parallel burst) with a
|
|
3066
|
+
* {@link PROBE_INTER_CALL_DELAY_MS} gap before each probe after the first, and
|
|
3067
|
+
* a probe that comes back `APP_BRIDGE_THROTTLED` (native per-method bridge
|
|
3068
|
+
* rate limit) is retried up to {@link PROBE_RETRY_BACKOFF_MS}`.length` times
|
|
3069
|
+
* with the same {@link PROBE_RETRY_BACKOFF_MS} ladder before falling back to
|
|
3070
|
+
* `'unavailable'`.
|
|
3071
|
+
* The THROTTLED detection mirrors `throttle.ts#isThrottledError` inline (this
|
|
3072
|
+
* expression is raw JS text evaluated via CDP, not an esbuild-bundled module,
|
|
3073
|
+
* so it cannot statically `import` that helper) — kept in sync by checking
|
|
3074
|
+
* BOTH `e.code === 'APP_BRIDGE_THROTTLED'` and a message substring match, the
|
|
3075
|
+
* same two signals `isThrottledError` checks.
|
|
3076
|
+
*
|
|
3077
|
+
* devtools#767 acceptance criteria 2 ("3.x run duration은 유의미하게 늘지 않음,
|
|
3078
|
+
* pacing은 opt-in 또는 throttle-adaptive") requires this NOT be an unconditional
|
|
3079
|
+
* cost — `pace` (default `true`, matching the pre-existing sequential+spacing
|
|
3080
|
+
* behavior for 2.x/unknown cells) lets a 3.x caller skip BOTH the inter-probe
|
|
3081
|
+
* `sleep(250)` AND the retry backoff ladder, since `APP_BRIDGE_THROTTLED` is a
|
|
3082
|
+
* 2.x-bridge-only limiter (see `--pace`'s own doc in `cli.ts`: "3.x cells are
|
|
3083
|
+
* unaffected by that limiter"). When `pace` is `false`, probes still run
|
|
3084
|
+
* sequentially (never a parallel burst) but back-to-back with no added delay,
|
|
3085
|
+
* and a THROTTLED result resolves to `'unavailable'` immediately (no retry) —
|
|
3086
|
+
* this matches the 3.x runtime, which never emits `APP_BRIDGE_THROTTLED`.
|
|
3087
|
+
*
|
|
2975
3088
|
* Tests (sdk-example#265) read `globalThis.__AIT_PERMS__` to branch
|
|
2976
3089
|
* deterministically per permission state instead of blanket outcome-branching.
|
|
2977
3090
|
*
|
|
@@ -2982,9 +3095,18 @@ const PERMISSION_PREFLIGHT_TIMEOUT_MS = 1e4;
|
|
|
2982
3095
|
*
|
|
2983
3096
|
* Pure function — no I/O, no CDP call. Callers pass the returned string to
|
|
2984
3097
|
* `Runtime.evaluate`.
|
|
3098
|
+
*
|
|
3099
|
+
* @param pace - Defaults to `true` (today's sequential+spacing+backoff
|
|
3100
|
+
* behavior). Pass `false` for a 3.x cell to skip the inter-probe delay and
|
|
3101
|
+
* the THROTTLED retry ladder entirely.
|
|
2985
3102
|
*/
|
|
2986
|
-
function buildPermissionPreflightExpression() {
|
|
2987
|
-
|
|
3103
|
+
function buildPermissionPreflightExpression(pace = true) {
|
|
3104
|
+
const probeCalls = Object.keys(PERMISSION_PROBE_MAP).map((key, i) => {
|
|
3105
|
+
const fnName = PERMISSION_PROBE_MAP[key];
|
|
3106
|
+
return `${i === 0 || !pace ? "" : ` await sleep(${PROBE_INTER_CALL_DELAY_MS});\n`} result[${JSON.stringify(key)}] = await probeWithRetry(${JSON.stringify(fnName)});`;
|
|
3107
|
+
}).join("\n");
|
|
3108
|
+
const backoffArray = pace ? `[${PROBE_RETRY_BACKOFF_MS.join(",")}]` : "[]";
|
|
3109
|
+
return `(async () => { const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const isThrottled = (e) => { if (!e) return false; if (e.code === ${JSON.stringify(THROTTLED_ERROR_CODE)}) return true; const msg = typeof e.message === 'string' ? e.message : ''; return msg.indexOf(${JSON.stringify(THROTTLED_MESSAGE_SUBSTRING)}) !== -1; }; const probe = async (fnName) => { const fn = globalThis.__sdk && globalThis.__sdk[fnName]; if (!fn || typeof fn.getPermission !== 'function') return { ok: true, value: 'unavailable' }; const status = await fn.getPermission(); if (status === 'allowed' || status === 'denied' || status === 'notDetermined') return { ok: true, value: status }; return { ok: true, value: 'unavailable' }; }; const backoff = ${backoffArray}; const probeWithRetry = async (fnName) => { for (let attempt = 0; attempt <= backoff.length; attempt++) { try { const r = await probe(fnName); return r.value; } catch (e) { if (isThrottled(e) && attempt < backoff.length) { await sleep(backoff[attempt]); continue; } return 'unavailable'; } } return 'unavailable'; }; const result = {};${probeCalls}\n globalThis.__AIT_PERMS__ = result; return JSON.stringify(result);})()`;
|
|
2988
3110
|
}
|
|
2989
3111
|
/**
|
|
2990
3112
|
* Runs the permission-state preflight (devtools#739) via `Runtime.evaluate`
|
|
@@ -3003,13 +3125,19 @@ function buildPermissionPreflightExpression() {
|
|
|
3003
3125
|
*
|
|
3004
3126
|
* @param conn - The live CDP connection (relay-attached page).
|
|
3005
3127
|
* @param timeoutMs - Round-trip bound. Defaults to {@link PERMISSION_PREFLIGHT_TIMEOUT_MS}.
|
|
3006
|
-
|
|
3007
|
-
|
|
3128
|
+
* @param pace - Forwarded to {@link buildPermissionPreflightExpression}.
|
|
3129
|
+
* Defaults to `true` (today's sequential+spacing+backoff behavior, correct
|
|
3130
|
+
* for 2.x/unknown cells). Callers that know the cell is 3.x should pass
|
|
3131
|
+
* `false` — devtools#767 acceptance criteria 2 requires 3.x run duration not
|
|
3132
|
+
* grow from this preflight, and 3.x never hits the 2.x-only
|
|
3133
|
+
* `APP_BRIDGE_THROTTLED` limiter this pacing exists to dodge.
|
|
3134
|
+
*/
|
|
3135
|
+
async function runPermissionPreflight(conn, timeoutMs = PERMISSION_PREFLIGHT_TIMEOUT_MS, pace = true) {
|
|
3008
3136
|
const TIMEOUT_SENTINEL = Symbol("permission-preflight-timeout");
|
|
3009
3137
|
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(TIMEOUT_SENTINEL), timeoutMs));
|
|
3010
3138
|
try {
|
|
3011
3139
|
const evalPromise = conn.send("Runtime.evaluate", {
|
|
3012
|
-
expression: buildPermissionPreflightExpression(),
|
|
3140
|
+
expression: buildPermissionPreflightExpression(pace),
|
|
3013
3141
|
returnByValue: true,
|
|
3014
3142
|
awaitPromise: true
|
|
3015
3143
|
});
|
|
@@ -3827,7 +3955,73 @@ const SDK_IMPORT_FILTER = new RegExp(`^${SDK_PACKAGE.replace(/[.*+?^${}()|[\]\\]
|
|
|
3827
3955
|
*
|
|
3828
3956
|
* If `window.__sdk` is absent (non-dog-food build), every access throws a
|
|
3829
3957
|
* descriptive error rather than returning `undefined` silently.
|
|
3958
|
+
*
|
|
3959
|
+
* Bridge-stub interception (devtools#740, DT-2): the virtual module runs the
|
|
3960
|
+
* resolved `window.__sdk` object through `wrapSdkWithStub` before exporting
|
|
3961
|
+
* it. `wrapSdkWithStub` is a pass-through (returns the same object,
|
|
3962
|
+
* unwrapped) unless `isStubBlockingEnabled()` reads `true` off
|
|
3963
|
+
* `globalThis.__AIT_STUB_BLOCKING__` — an opt-in flag injected the same way
|
|
3964
|
+
* `__AIT_CELL__` is (`cell.ts#injectGlobals`, wired from the CLI's
|
|
3965
|
+
* `--stub-blocking` flag). Default off => zero behavior change: every existing
|
|
3966
|
+
* (non-stub) test run resolves the exact same `window.__sdk` reference it did
|
|
3967
|
+
* before this plugin learned about stubbing.
|
|
3968
|
+
*
|
|
3969
|
+
* Per-method pacing (devtools#769): BEFORE the stub wrap, `window.__sdk` is
|
|
3970
|
+
* run through `wrapWithMethodPacing`, which enforces a minimum interval
|
|
3971
|
+
* between calls to the SAME named SDK function (`getPaceMethodMs()` reads the
|
|
3972
|
+
* `__AIT_PACE_METHOD_MS__` page global — see `method-pace.ts`). Composition
|
|
3973
|
+
* order is pacing INSIDE the stub (`wrapSdkWithStub(wrapWithMethodPacing(sdk,
|
|
3974
|
+
* gap), enabled)`): a stubbed name resolves instantly from
|
|
3975
|
+
* `bridge-stub.ts`'s fixtures without ever reaching the paced wrapper, and
|
|
3976
|
+
* only calls that fall through to the real bridge — the ones that can
|
|
3977
|
+
* actually trigger `APP_BRIDGE_THROTTLED` — pay the pacing cost. `gapMs <= 0`
|
|
3978
|
+
* (default, no `--pace-method` / non-2.x cell) is a no-op fast path — see
|
|
3979
|
+
* `method-pace.ts#wrapWithMethodPacing`'s doc for the full contract.
|
|
3980
|
+
*/
|
|
3981
|
+
/**
|
|
3982
|
+
* Builds the virtual CommonJS-style module contents the `sdk-redirect`
|
|
3983
|
+
* esbuild plugin loads for every `@apps-in-toss/web-framework` import.
|
|
3984
|
+
*
|
|
3985
|
+
* Pulled out to a pure, exported function (rather than inlined at the
|
|
3986
|
+
* `build.onLoad` call site) so unit tests can assert the exact composition
|
|
3987
|
+
* order (pacing wraps the raw SDK; the stub wraps the paced result) as a
|
|
3988
|
+
* string-level contract, independent of esbuild — esbuild cannot run inside
|
|
3989
|
+
* this repo's jsdom vitest environment (see `bundleTestFile`'s module doc's
|
|
3990
|
+
* lazy-import note), so a real bundling round-trip is out of unit-test reach
|
|
3991
|
+
* here; `wrapSdkWithStub`/`wrapWithMethodPacing`'s own composed-call behavior
|
|
3992
|
+
* is covered directly (see `bundle.test.ts`).
|
|
3993
|
+
*
|
|
3994
|
+
* Generates a virtual CommonJS-style module so that esbuild does NOT perform
|
|
3995
|
+
* strict named-export matching. When `format:'iife'` bundles a CJS module, it
|
|
3996
|
+
* wraps it with its own `__toCommonJS` helper and satisfies named imports via
|
|
3997
|
+
* property access on the `module.exports` object — which is our Proxy. This
|
|
3998
|
+
* means `import { getPlatformOS } from '...'` becomes
|
|
3999
|
+
* `__proxy.getPlatformOS` at runtime, which correctly reads from
|
|
4000
|
+
* `window.__sdk`.
|
|
4001
|
+
*
|
|
4002
|
+
* The bridge-stub and method-pace imports are STATIC imports of the real
|
|
4003
|
+
* `bridge-stub.ts`/`method-pace.ts` modules (not inlined JS) so esbuild
|
|
4004
|
+
* bundles each once and the fixture registry / pacing registry have a single
|
|
4005
|
+
* source of truth shared with their own unit tests.
|
|
4006
|
+
*
|
|
4007
|
+
* Exported for unit testing.
|
|
3830
4008
|
*/
|
|
4009
|
+
function buildSdkRedirectModuleContents() {
|
|
4010
|
+
return `
|
|
4011
|
+
import { wrapSdkWithStub, isStubBlockingEnabled } from ${JSON.stringify(getBridgeStubPath())};
|
|
4012
|
+
import { wrapWithMethodPacing, getPaceMethodMs } from ${JSON.stringify(getMethodPacePath())};
|
|
4013
|
+
var __rawSdk = (typeof window !== 'undefined' && window.__sdk)
|
|
4014
|
+
? window.__sdk
|
|
4015
|
+
: new Proxy({}, {
|
|
4016
|
+
get: function(_t, p) {
|
|
4017
|
+
throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));
|
|
4018
|
+
}
|
|
4019
|
+
});
|
|
4020
|
+
var __pacedSdk = wrapWithMethodPacing(__rawSdk, getPaceMethodMs());
|
|
4021
|
+
var __proxy = wrapSdkWithStub(__pacedSdk, isStubBlockingEnabled());
|
|
4022
|
+
module.exports = __proxy;
|
|
4023
|
+
`;
|
|
4024
|
+
}
|
|
3831
4025
|
function sdkRedirectPlugin() {
|
|
3832
4026
|
return {
|
|
3833
4027
|
name: "sdk-redirect",
|
|
@@ -3840,17 +4034,9 @@ function sdkRedirectPlugin() {
|
|
|
3840
4034
|
filter: /.*/,
|
|
3841
4035
|
namespace: "sdk-redirect"
|
|
3842
4036
|
}, () => ({
|
|
3843
|
-
contents:
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
: new Proxy({}, {
|
|
3847
|
-
get: function(_t, p) {
|
|
3848
|
-
throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));
|
|
3849
|
-
}
|
|
3850
|
-
});
|
|
3851
|
-
module.exports = __proxy;
|
|
3852
|
-
`,
|
|
3853
|
-
loader: "js"
|
|
4037
|
+
contents: buildSdkRedirectModuleContents(),
|
|
4038
|
+
loader: "js",
|
|
4039
|
+
resolveDir: process.cwd()
|
|
3854
4040
|
}));
|
|
3855
4041
|
}
|
|
3856
4042
|
};
|
|
@@ -3984,8 +4170,9 @@ function userFactoryPlugin(absPath) {
|
|
|
3984
4170
|
};
|
|
3985
4171
|
}
|
|
3986
4172
|
/**
|
|
3987
|
-
* Returns the absolute filesystem path to
|
|
3988
|
-
* (
|
|
4173
|
+
* Returns the absolute filesystem path to a page-side leaf module shipped
|
|
4174
|
+
* alongside `bundle.ts` in dist (e.g. `runtime.js`, `bridge-stub.js`) — a
|
|
4175
|
+
* fully self-contained module with no further page-side deps of its own.
|
|
3989
4176
|
*
|
|
3990
4177
|
* Rolldown code-splitting duplicates this bundling logic into shared chunks
|
|
3991
4178
|
* emitted at ARBITRARY dist depths: the `devtools-test` CLI pulls it from
|
|
@@ -3995,26 +4182,30 @@ function userFactoryPlugin(absPath) {
|
|
|
3995
4182
|
* is therefore wrong from at least one chunk — the live #697 regression.
|
|
3996
4183
|
*
|
|
3997
4184
|
* This resolves WITHOUT assuming chunk depth: from `import.meta.url`'s dir it
|
|
3998
|
-
* probes the co-located
|
|
4185
|
+
* probes the co-located `<name>.js` and the nested `test-runner/<name>.js`,
|
|
3999
4186
|
* then ascends one directory at a time (bounded) repeating both probes. The
|
|
4000
|
-
* nested probe catches dist/test-runner
|
|
4187
|
+
* nested probe catches dist/test-runner/<name>.js from the dist/ root level no
|
|
4001
4188
|
* matter which depth the chunk was hoisted to (root, dist/mcp/, or a future
|
|
4002
|
-
* relocation). The build always emits dist/test-runner
|
|
4003
|
-
* `'test-runner
|
|
4189
|
+
* relocation). The build always emits dist/test-runner/<name>.js (tsdown entry
|
|
4190
|
+
* `'test-runner/<name>'`; guarded by scripts/check-test-runner-dist.sh for
|
|
4191
|
+
* `runtime`).
|
|
4004
4192
|
*
|
|
4005
4193
|
* An ABSOLUTE path is returned deliberately: esbuild loads it as a literal file
|
|
4006
4194
|
* read, bypassing Node module resolution entirely, so this works identically in
|
|
4007
4195
|
* the npx-daemon context (its own dist tree) and the consumer-CLI context
|
|
4008
4196
|
* (the mini-app's installed @ait-co/devtools dist) — neither needs the package
|
|
4009
4197
|
* to be node-resolvable from the caller.
|
|
4198
|
+
*
|
|
4199
|
+
* @param moduleName - The leaf module's basename without extension, e.g.
|
|
4200
|
+
* `'runtime'` or `'bridge-stub'`.
|
|
4010
4201
|
*/
|
|
4011
|
-
function
|
|
4202
|
+
function getPageSideModulePath(moduleName) {
|
|
4012
4203
|
const startDir = path.dirname(fileURLToPath(import.meta.url));
|
|
4013
4204
|
const RELATIVE_PROBES = [
|
|
4014
|
-
[
|
|
4015
|
-
["test-runner",
|
|
4016
|
-
[
|
|
4017
|
-
["test-runner",
|
|
4205
|
+
[`${moduleName}.js`],
|
|
4206
|
+
["test-runner", `${moduleName}.js`],
|
|
4207
|
+
[`${moduleName}.ts`],
|
|
4208
|
+
["test-runner", `${moduleName}.ts`]
|
|
4018
4209
|
];
|
|
4019
4210
|
let dir = startDir;
|
|
4020
4211
|
for (let i = 0; i < 12; i++) {
|
|
@@ -4029,7 +4220,27 @@ function getRuntimePath() {
|
|
|
4029
4220
|
if (parent === dir) break;
|
|
4030
4221
|
dir = parent;
|
|
4031
4222
|
}
|
|
4032
|
-
return path.join(startDir,
|
|
4223
|
+
return path.join(startDir, `${moduleName}.js`);
|
|
4224
|
+
}
|
|
4225
|
+
/** Absolute path to the test-runner page-side runtime (describe/it/test/expect). */
|
|
4226
|
+
function getRuntimePath() {
|
|
4227
|
+
return getPageSideModulePath("runtime");
|
|
4228
|
+
}
|
|
4229
|
+
/**
|
|
4230
|
+
* Absolute path to the bridge-stub interceptor (devtools#740, DT-2) —
|
|
4231
|
+
* `wrapSdkWithStub`/`isStubBlockingEnabled`, statically imported by the
|
|
4232
|
+
* `sdk-redirect` virtual module.
|
|
4233
|
+
*/
|
|
4234
|
+
function getBridgeStubPath() {
|
|
4235
|
+
return getPageSideModulePath("bridge-stub");
|
|
4236
|
+
}
|
|
4237
|
+
/**
|
|
4238
|
+
* Absolute path to the per-method pacing wrapper (devtools#769) —
|
|
4239
|
+
* `wrapWithMethodPacing`/`getPaceMethodMs`, statically imported by the
|
|
4240
|
+
* `sdk-redirect` virtual module.
|
|
4241
|
+
*/
|
|
4242
|
+
function getMethodPacePath() {
|
|
4243
|
+
return getPageSideModulePath("method-pace");
|
|
4033
4244
|
}
|
|
4034
4245
|
/**
|
|
4035
4246
|
* Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.
|
|
@@ -4303,15 +4514,21 @@ async function runTestFilesOverRelay(connection, files, opts) {
|
|
|
4303
4514
|
const manualFiles = opts?.manualFiles;
|
|
4304
4515
|
const manualTotal = manualFiles?.size ?? 0;
|
|
4305
4516
|
let manualIndex = 0;
|
|
4517
|
+
const stubBlockingFiles = opts?.stubBlockingFiles;
|
|
4306
4518
|
let preflightPermissions;
|
|
4307
|
-
if (files.length > 0) preflightPermissions = await runPermissionPreflight(connection);
|
|
4519
|
+
if (files.length > 0) preflightPermissions = await runPermissionPreflight(connection, PERMISSION_PREFLIGHT_TIMEOUT_MS, opts?.preflightSdkLine !== "3.x");
|
|
4520
|
+
const paceMs = opts?.paceMs;
|
|
4521
|
+
let ranFirstFile = false;
|
|
4308
4522
|
try {
|
|
4309
4523
|
for (const file of files) {
|
|
4310
4524
|
if (pendingReconnectCheck) {
|
|
4311
4525
|
await attemptReconnect("next-file");
|
|
4312
4526
|
pendingReconnectCheck = false;
|
|
4313
4527
|
}
|
|
4314
|
-
|
|
4528
|
+
if (paceMs !== void 0 && paceMs > 0 && ranFirstFile) await new Promise((resolve) => setTimeout(resolve, paceMs));
|
|
4529
|
+
ranFirstFile = true;
|
|
4530
|
+
const isStubbed = stubBlockingFiles?.has(file) === true;
|
|
4531
|
+
const isManual = !isStubbed && manualFiles?.has(file) === true;
|
|
4315
4532
|
if (isManual) {
|
|
4316
4533
|
manualIndex += 1;
|
|
4317
4534
|
opts?.onManualFile?.(file, manualIndex, manualTotal);
|
|
@@ -4378,7 +4595,8 @@ async function runTestFilesOverRelay(connection, files, opts) {
|
|
|
4378
4595
|
};
|
|
4379
4596
|
}
|
|
4380
4597
|
if ("error" in fileEntry.result && isRelayDisconnectMessage(fileEntry.result.error)) pendingReconnectCheck = true;
|
|
4381
|
-
if (
|
|
4598
|
+
if (isStubbed) fileEntry.mode = "stubbed";
|
|
4599
|
+
else if (isManual) fileEntry.mode = "manual";
|
|
4382
4600
|
fileResults.push(fileEntry);
|
|
4383
4601
|
}
|
|
4384
4602
|
} finally {
|
|
@@ -4445,7 +4663,21 @@ USAGE
|
|
|
4445
4663
|
|
|
4446
4664
|
OPTIONS
|
|
4447
4665
|
--scheme-url <url> intoss-private:// URL from \`ait deploy --scheme-only\`
|
|
4448
|
-
(required for standalone relay attach / env3)
|
|
4666
|
+
(required for standalone relay attach / env3). Unused
|
|
4667
|
+
and ignored when --attach-launcher is set.
|
|
4668
|
+
--attach-launcher Attach over the env-2 AITC Sandbox PWA launcher
|
|
4669
|
+
(real-device WebKit) instead of the env-3 intoss
|
|
4670
|
+
scheme deep-link. The relay/QR/dashboard/attach-wait
|
|
4671
|
+
are identical; only the QR is a launcher deep-link.
|
|
4672
|
+
Requires --app-url. The SDK still hits the mock in
|
|
4673
|
+
env 2, so this measures the mock + standard Web API
|
|
4674
|
+
layer's engine-attributable behavior (env1↔env2
|
|
4675
|
+
equivalence), NOT native-bridge fidelity (that is env3).
|
|
4676
|
+
--app-url <url> The consumer dev server's HTTP tunnel URL (e.g. the
|
|
4677
|
+
*.trycloudflare.com URL from \`pnpm dev:phone:cdp\`)
|
|
4678
|
+
that the launcher PWA frames. REQUIRED with
|
|
4679
|
+
--attach-launcher; ignored otherwise. SECRET: a tunnel
|
|
4680
|
+
host — never printed; it rides only inside the QR.
|
|
4449
4681
|
--timeout <ms> Per-file evaluate timeout in ms (default: 60000).
|
|
4450
4682
|
Controls how long a single test file is allowed to run
|
|
4451
4683
|
before it is considered hung. Does NOT affect how long
|
|
@@ -4457,7 +4689,10 @@ OPTIONS
|
|
|
4457
4689
|
Pass a value to bound the wait for CI/headless runs.
|
|
4458
4690
|
--cell-sdk-line <line> SDK line to inject as __AIT_CELL__.sdkLine (2.x|3.x)
|
|
4459
4691
|
--cell-platform <plat> Platform to inject as __AIT_CELL__.platform
|
|
4460
|
-
(mock|ios|android, default: AIT_CELL_PLATFORM
|
|
4692
|
+
(mock|ios|android|ios-pwa, default: AIT_CELL_PLATFORM
|
|
4693
|
+
env). Use ios-pwa for env-2 (--attach-launcher) runs
|
|
4694
|
+
so env1(mock@desktop) and env2(mock@WebKit) captures
|
|
4695
|
+
stay distinguishable. An unknown value is rejected.
|
|
4461
4696
|
--report-dir <dir> Persist a runner-agnostic report + captures to <dir>
|
|
4462
4697
|
(report: <sdkLine>.<platform>.json; captures:
|
|
4463
4698
|
<dir>/.ait-capture/<category>.<sdkLine>.<platform>.json).
|
|
@@ -4472,6 +4707,28 @@ OPTIONS
|
|
|
4472
4707
|
--headless Disable browser auto-open (text QR only)
|
|
4473
4708
|
--project-root <dir> Project root for .ait_relay secret lookup
|
|
4474
4709
|
(default: current working directory)
|
|
4710
|
+
--pace <ms> Minimum delay in ms between test-to-test AND
|
|
4711
|
+
file-to-file bridge calls (default: 0, i.e. no
|
|
4712
|
+
added delay — today's behavior byte-for-byte).
|
|
4713
|
+
Falls back to the AIT_PACE env var when omitted
|
|
4714
|
+
(--pace takes precedence over the env var when both
|
|
4715
|
+
are given). Use on a 2.x cell scan when the native
|
|
4716
|
+
per-method bridge rate limit (APP_BRIDGE_THROTTLED,
|
|
4717
|
+
devtools#767) is rejecting rapid same-method calls —
|
|
4718
|
+
3.x cells are unaffected by that limiter and do not
|
|
4719
|
+
need this flag.
|
|
4720
|
+
--pace-method <ms> Minimum delay in ms BETWEEN calls to the SAME named
|
|
4721
|
+
SDK function — paces a same-method burst WITHIN a
|
|
4722
|
+
single test body (e.g. a clipboard happy-path loop
|
|
4723
|
+
calling setClipboardText/getClipboardText 8 times
|
|
4724
|
+
back to back), which --pace's test/file spacing
|
|
4725
|
+
cannot reach (devtools#769). Falls back to the
|
|
4726
|
+
AIT_PACE_METHOD env var when omitted (--pace-method
|
|
4727
|
+
takes precedence over the env var when both are
|
|
4728
|
+
given). Default: 250ms when --cell-sdk-line is 2.x
|
|
4729
|
+
(unset --cell-sdk-line also defaults to 2.x — see
|
|
4730
|
+
--cell-sdk-line), 0 (no added delay) otherwise. Pass
|
|
4731
|
+
--pace-method 0 to opt out even on a 2.x cell.
|
|
4475
4732
|
--manual-blocking Run manual-tagged test files (*.manual.ait.test.ts)
|
|
4476
4733
|
LAST, after all regular files, with a human present.
|
|
4477
4734
|
Before each manual file, the QR dashboard is pushed
|
|
@@ -4490,6 +4747,24 @@ OPTIONS
|
|
|
4490
4747
|
each manual file's report entry is stamped
|
|
4491
4748
|
mode: 'manual' — never diff a manual run against an
|
|
4492
4749
|
unattended baseline as if they were equivalent.
|
|
4750
|
+
--stub-blocking Run manual-tagged test files (*.manual.ait.test.ts)
|
|
4751
|
+
UNATTENDED (devtools#740, DT-2) by intercepting a
|
|
4752
|
+
fixed allowlist of blocking-UI SDK calls (ads
|
|
4753
|
+
show*, openPermissionDialog/requestPermission,
|
|
4754
|
+
saveBase64Data) in the page and answering them from
|
|
4755
|
+
fixtures captured by a real --manual-blocking run,
|
|
4756
|
+
instead of forwarding them to native UI. Implies
|
|
4757
|
+
--manual-blocking (manual files are included in the
|
|
4758
|
+
run); no human presence or QR-dashboard prompt is
|
|
4759
|
+
needed for them. HYBRID cell, not pure env3 — every
|
|
4760
|
+
other SDK call in the same run still hits the real
|
|
4761
|
+
native bridge. With --report-dir, files that ran
|
|
4762
|
+
under the stub are written to a SEPARATE
|
|
4763
|
+
<sdkLine>.<platform>.stubbed.json artifact (never
|
|
4764
|
+
merged into the standard or .manual.json report) and
|
|
4765
|
+
the report body is stamped cell.bridgeStub: true —
|
|
4766
|
+
never diff a stubbed run against a real device
|
|
4767
|
+
baseline (manual or unattended) as if equivalent.
|
|
4493
4768
|
--help, -h Show this help message
|
|
4494
4769
|
|
|
4495
4770
|
DESCRIPTION
|
|
@@ -4506,7 +4781,7 @@ DESCRIPTION
|
|
|
4506
4781
|
The test files run against the live relay connection started by this process;
|
|
4507
4782
|
no separate MCP daemon is required.
|
|
4508
4783
|
|
|
4509
|
-
EXAMPLE
|
|
4784
|
+
EXAMPLE (env 3 — intoss-private scheme)
|
|
4510
4785
|
devtools-test 'src/**/*.ait.test.ts' \\
|
|
4511
4786
|
--scheme-url "intoss-private://..." \\
|
|
4512
4787
|
--cell-sdk-line 3.x \\
|
|
@@ -4514,6 +4789,14 @@ EXAMPLE
|
|
|
4514
4789
|
--report-dir .ait-report \\
|
|
4515
4790
|
--timeout 60000
|
|
4516
4791
|
|
|
4792
|
+
EXAMPLE (env 2 — AITC Sandbox PWA launcher)
|
|
4793
|
+
devtools-test 'src/**/*.ait.test.ts' \\
|
|
4794
|
+
--attach-launcher \\
|
|
4795
|
+
--app-url "https://<subdomain>.trycloudflare.com" \\
|
|
4796
|
+
--cell-sdk-line 3.x \\
|
|
4797
|
+
--cell-platform ios-pwa \\
|
|
4798
|
+
--report-dir .ait-report
|
|
4799
|
+
|
|
4517
4800
|
`.trimStart();
|
|
4518
4801
|
/**
|
|
4519
4802
|
* Renders per-file result lines and the aggregate totals line to a string.
|
|
@@ -7205,7 +7488,7 @@ function createDebugServer(deps) {
|
|
|
7205
7488
|
};
|
|
7206
7489
|
const server = new Server({
|
|
7207
7490
|
name: "ait-debug",
|
|
7208
|
-
version: "0.1.
|
|
7491
|
+
version: "0.1.138"
|
|
7209
7492
|
}, { capabilities: { tools: { listChanged: true } } });
|
|
7210
7493
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
7211
7494
|
const conn = router.active;
|
|
@@ -7641,6 +7924,13 @@ function errorResult(err, name, isLocal = false) {
|
|
|
7641
7924
|
* SECRET-HANDLING: target `id`/`title`/`url` are not written to any log here.
|
|
7642
7925
|
* Only an attach-detected stderr line is emitted (no target details).
|
|
7643
7926
|
*
|
|
7927
|
+
* `server` is optional (devtools#772): the standalone test-runner path
|
|
7928
|
+
* (`test-runner/relay-factory.ts`) has no MCP `Server` instance to notify —
|
|
7929
|
+
* it only needs the `onAttach`/`onDetach` dashboard-push side effects. When
|
|
7930
|
+
* omitted, `sendToolListChanged()` is simply skipped; the signature-diff and
|
|
7931
|
+
* callback logic is byte-for-byte the same. Existing MCP daemon call sites
|
|
7932
|
+
* always pass a real `Server`, so their behavior is unchanged.
|
|
7933
|
+
*
|
|
7644
7934
|
* @returns `stop` — call this during shutdown to clear the interval.
|
|
7645
7935
|
*/
|
|
7646
7936
|
function startAttachWatcher(connection, server, intervalMs = 1e3, onAttach, onDetach) {
|
|
@@ -7650,7 +7940,7 @@ function startAttachWatcher(connection, server, intervalMs = 1e3, onAttach, onDe
|
|
|
7650
7940
|
}
|
|
7651
7941
|
let lastSignature = signature();
|
|
7652
7942
|
if (lastSignature !== "") {
|
|
7653
|
-
server
|
|
7943
|
+
server?.sendToolListChanged();
|
|
7654
7944
|
onAttach?.();
|
|
7655
7945
|
}
|
|
7656
7946
|
/** Compare current vs last signature and fire the appropriate callback. */
|
|
@@ -7660,7 +7950,7 @@ function startAttachWatcher(connection, server, intervalMs = 1e3, onAttach, onDe
|
|
|
7660
7950
|
const wasNonEmpty = lastSignature !== "";
|
|
7661
7951
|
lastSignature = current;
|
|
7662
7952
|
if (current !== "") {
|
|
7663
|
-
server
|
|
7953
|
+
server?.sendToolListChanged();
|
|
7664
7954
|
onAttach?.();
|
|
7665
7955
|
} else if (wasNonEmpty) onDetach?.();
|
|
7666
7956
|
}
|
|
@@ -7852,7 +8142,7 @@ async function readRelayLocalUrl(env = process.env, projectRoot) {
|
|
|
7852
8142
|
const envValue = (env.AIT_RELAY_LOCAL_URL ?? "").trim();
|
|
7853
8143
|
if (envValue !== "") return envValue;
|
|
7854
8144
|
if (projectRoot !== void 0) try {
|
|
7855
|
-
const { readRelayUrls } = await import("../relay-url-store-
|
|
8145
|
+
const { readRelayUrls } = await import("../relay-url-store-DaY1QPes.js");
|
|
7856
8146
|
const stored = await readRelayUrls({ projectRoot });
|
|
7857
8147
|
if (stored?.relayLocalUrl) return stored.relayLocalUrl;
|
|
7858
8148
|
} catch {}
|
|
@@ -7906,7 +8196,7 @@ async function readMobileRelayBaseUrl(env = process.env, projectRoot) {
|
|
|
7906
8196
|
const envValue = typeof raw === "string" ? raw.trim() : "";
|
|
7907
8197
|
if (envValue !== "") return envValue;
|
|
7908
8198
|
if (projectRoot !== void 0) {
|
|
7909
|
-
const { readRelayUrls } = await import("../relay-url-store-
|
|
8199
|
+
const { readRelayUrls } = await import("../relay-url-store-DaY1QPes.js");
|
|
7910
8200
|
const stored = await readRelayUrls({ projectRoot });
|
|
7911
8201
|
if (stored?.relayBaseUrl !== void 0) return stored.relayBaseUrl;
|
|
7912
8202
|
}
|
|
@@ -9189,7 +9479,7 @@ function createDevServer(deps = {}) {
|
|
|
9189
9479
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
9190
9480
|
const server = new Server({
|
|
9191
9481
|
name: "ait-devtools",
|
|
9192
|
-
version: "0.1.
|
|
9482
|
+
version: "0.1.138"
|
|
9193
9483
|
}, { capabilities: { tools: {} } });
|
|
9194
9484
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
9195
9485
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|