@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/test-runner/bin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { m as logInfo } from "../attach-orchestrator-
|
|
2
|
+
import { m as logInfo } from "../attach-orchestrator-BEiz3wAx.js";
|
|
3
3
|
import { r as generateTotp } from "../totp-CZLLKfOC.js";
|
|
4
|
-
import {
|
|
4
|
+
import { r as runPermissionPreflight, t as PERMISSION_PREFLIGHT_TIMEOUT_MS } from "../cell-KsRXFdDs.js";
|
|
5
5
|
import * as path$1 from "node:path";
|
|
6
6
|
import path, { basename, isAbsolute, resolve } from "node:path";
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
@@ -93,12 +93,13 @@ function partitionManualTests(files) {
|
|
|
93
93
|
//#endregion
|
|
94
94
|
//#region src/test-runner/relay-factory.ts
|
|
95
95
|
/**
|
|
96
|
-
* Builds a {@link RelayConnectionFactory} that opens a standalone
|
|
97
|
-
* connection
|
|
96
|
+
* Builds a {@link RelayConnectionFactory} that opens a standalone relay
|
|
97
|
+
* connection for env 3 (intoss-private scheme, default) or env 2 (AITC Sandbox
|
|
98
|
+
* PWA launcher deep-link, when `opts.attachLauncher` is set — devtools#774).
|
|
98
99
|
*
|
|
99
100
|
* `open()` performs the full attach lifecycle and BLOCKS while a human scans
|
|
100
|
-
* the rendered QR with their phone — there is no way around the manual scan
|
|
101
|
-
*
|
|
101
|
+
* the rendered QR with their phone — there is no way around the manual scan.
|
|
102
|
+
* By default the wait is UNBOUNDED (`opts.timeoutMs` omitted): the
|
|
102
103
|
* runner stays up until the user stops it (Ctrl-C/SIGTERM), since QR-scan is
|
|
103
104
|
* a human-paced action with no sound default bound (devtools#735). Passing an
|
|
104
105
|
* explicit `timeoutMs` opts into the old bounded behavior (CI/headless
|
|
@@ -115,14 +116,17 @@ function createRelayConnectionFactory(opts) {
|
|
|
115
116
|
const headless = opts.headless === true;
|
|
116
117
|
let family;
|
|
117
118
|
let qrServer;
|
|
119
|
+
let attachWatcher;
|
|
118
120
|
let phase = "active";
|
|
119
121
|
let manualPrompt = null;
|
|
120
122
|
return {
|
|
121
123
|
async open() {
|
|
122
|
-
const { prepareAttach, renderAndMaybeWait, mintAttachUrl } = await import("../attach-orchestrator-
|
|
123
|
-
const { injectDebugIndicator, injectGlobals } = await import("../cell-
|
|
124
|
-
const { loadRelaySecretReadOnly } = await import("../relay-secret-store-
|
|
125
|
-
const { bootRelayFamily, buildRelayVerifyAuth } = await import("../debug-server-
|
|
124
|
+
const { prepareAttach, renderAndMaybeWait, mintAttachUrl } = await import("../attach-orchestrator-BEiz3wAx.js").then((n) => n.i);
|
|
125
|
+
const { injectDebugIndicator, injectGlobals } = await import("../cell-KsRXFdDs.js").then((n) => n.n);
|
|
126
|
+
const { loadRelaySecretReadOnly } = await import("../relay-secret-store-Bmyleu0A.js");
|
|
127
|
+
const { bootRelayFamily, buildRelayVerifyAuth, startAttachWatcher } = await import("../debug-server-DWat7d-9.js");
|
|
128
|
+
const { buildChiiInspectorUrl } = await import("../devtools-opener-CJpEsXXQ.js");
|
|
129
|
+
const { generateTotp } = await import("../totp-CZLLKfOC.js").then((n) => n.i);
|
|
126
130
|
await loadRelaySecretReadOnly({ projectRoot });
|
|
127
131
|
let resolveTunnelUp;
|
|
128
132
|
const tunnelReady = new Promise((resolve) => {
|
|
@@ -152,29 +156,73 @@ function createRelayConnectionFactory(opts) {
|
|
|
152
156
|
canOpenBrowser: () => !headless
|
|
153
157
|
};
|
|
154
158
|
try {
|
|
155
|
-
const { startQrHttpServer } = await import("../qr-http-server-
|
|
159
|
+
const { startQrHttpServer } = await import("../qr-http-server-DIjCV7h_.js");
|
|
156
160
|
const getDashboardState = () => ({
|
|
157
161
|
tunnel: attachDeps.getTunnelStatus(),
|
|
158
|
-
pages:
|
|
162
|
+
pages: booted.connection.listTargets().map((t) => ({
|
|
163
|
+
id: t.id,
|
|
164
|
+
url: t.url
|
|
165
|
+
})),
|
|
159
166
|
attachUrl: lastAttachParts ? mintAttachUrl(attachDeps, lastAttachParts) : null,
|
|
160
|
-
mode: "relay-dev",
|
|
167
|
+
mode: opts.attachLauncher === true ? "relay-mobile" : "relay-dev",
|
|
161
168
|
phase,
|
|
162
169
|
manualPrompt
|
|
163
170
|
});
|
|
164
|
-
|
|
171
|
+
const getDirectInspectorUrl = () => {
|
|
172
|
+
if (!booted.relayHttpUrl) return {
|
|
173
|
+
ok: false,
|
|
174
|
+
reason: "relayDown"
|
|
175
|
+
};
|
|
176
|
+
const targets = booted.connection.listTargets();
|
|
177
|
+
if (targets.length === 0) return {
|
|
178
|
+
ok: false,
|
|
179
|
+
reason: "noTarget"
|
|
180
|
+
};
|
|
181
|
+
const totpSecret = process.env.AIT_DEBUG_TOTP_SECRET;
|
|
182
|
+
if (!totpSecret) return {
|
|
183
|
+
ok: false,
|
|
184
|
+
reason: "totpUnavailable"
|
|
185
|
+
};
|
|
186
|
+
const url = buildChiiInspectorUrl(booted.relayHttpUrl, targets[0].id, () => generateTotp(totpSecret, Date.now()));
|
|
187
|
+
if (url === null) return {
|
|
188
|
+
ok: false,
|
|
189
|
+
reason: "totpUnavailable"
|
|
190
|
+
};
|
|
191
|
+
return {
|
|
192
|
+
ok: true,
|
|
193
|
+
url
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
qrServer = await startQrHttpServer(getDashboardState, {
|
|
197
|
+
dashboardPort: opts.dashboardPort,
|
|
198
|
+
getDirectInspectorUrl
|
|
199
|
+
});
|
|
165
200
|
attachDeps.qrHttpServer = qrServer;
|
|
166
201
|
attachDeps.onAttachUrlBuilt = (parts) => {
|
|
167
202
|
lastAttachParts = parts;
|
|
168
203
|
qrServer?.notifyStateChange();
|
|
169
204
|
};
|
|
205
|
+
attachWatcher = startAttachWatcher(booted.connection, void 0, 1e3, () => qrServer?.notifyStateChange(), () => qrServer?.notifyStateChange());
|
|
170
206
|
process.stderr.write(`devtools-test: QR dashboard: http://127.0.0.1:${qrServer.port}/\n`);
|
|
171
207
|
} catch {}
|
|
172
208
|
const TUNNEL_BOOT_TIMEOUT_MS = 15e3;
|
|
173
209
|
await Promise.race([tunnelReady, new Promise((resolve) => setTimeout(resolve, TUNNEL_BOOT_TIMEOUT_MS))]);
|
|
174
|
-
|
|
210
|
+
let prep;
|
|
211
|
+
if (opts.attachLauncher === true) {
|
|
212
|
+
const priorTunnelBaseUrl = process.env.AIT_TUNNEL_BASE_URL;
|
|
213
|
+
process.env.AIT_TUNNEL_BASE_URL = opts.appUrl ?? "";
|
|
214
|
+
try {
|
|
215
|
+
prep = await prepareAttach(attachDeps, "relay-mobile", {}, booted.connection);
|
|
216
|
+
} finally {
|
|
217
|
+
if (priorTunnelBaseUrl === void 0) delete process.env.AIT_TUNNEL_BASE_URL;
|
|
218
|
+
else process.env.AIT_TUNNEL_BASE_URL = priorTunnelBaseUrl;
|
|
219
|
+
}
|
|
220
|
+
} else prep = await prepareAttach(attachDeps, "relay-dev", { scheme_url: opts.schemeUrl }, booted.connection);
|
|
175
221
|
if (!prep.ok) {
|
|
176
222
|
booted.stop();
|
|
177
223
|
family = void 0;
|
|
224
|
+
attachWatcher?.stop();
|
|
225
|
+
attachWatcher = void 0;
|
|
178
226
|
await qrServer?.close();
|
|
179
227
|
qrServer = void 0;
|
|
180
228
|
throw new Error("createRelayConnectionFactory: attach preparation failed — check the scheme_url and that the relay tunnel is up");
|
|
@@ -183,6 +231,8 @@ function createRelayConnectionFactory(opts) {
|
|
|
183
231
|
if (waitResult.isError) {
|
|
184
232
|
booted.stop();
|
|
185
233
|
family = void 0;
|
|
234
|
+
attachWatcher?.stop();
|
|
235
|
+
attachWatcher = void 0;
|
|
186
236
|
await qrServer?.close();
|
|
187
237
|
qrServer = void 0;
|
|
188
238
|
const timeoutSec = Math.round(timeoutMs / 1e3);
|
|
@@ -204,12 +254,17 @@ function createRelayConnectionFactory(opts) {
|
|
|
204
254
|
if (lastEnableError !== void 0) {
|
|
205
255
|
booted.stop();
|
|
206
256
|
family = void 0;
|
|
257
|
+
attachWatcher?.stop();
|
|
258
|
+
attachWatcher = void 0;
|
|
207
259
|
await qrServer?.close();
|
|
208
260
|
qrServer = void 0;
|
|
209
261
|
throw new Error(`createRelayConnectionFactory: page did not become ready after ${PAGE_READY_RETRIES} attempts (${Math.round(PAGE_READY_RETRIES * PAGE_READY_RETRY_DELAY_MS / 1e3)}s) — the mini-app page may have disconnected before enableDomains() could open the CDP websocket`);
|
|
210
262
|
}
|
|
211
263
|
await injectDebugIndicator(booted.connection);
|
|
212
264
|
if (opts.cell !== void 0) await injectGlobals(booted.connection, { __AIT_CELL__: opts.cell });
|
|
265
|
+
if (opts.stubBlocking === true) await injectGlobals(booted.connection, { __AIT_STUB_BLOCKING__: true });
|
|
266
|
+
if (opts.paceMs !== void 0 && opts.paceMs > 0) await injectGlobals(booted.connection, { __AIT_PACE_MS__: opts.paceMs });
|
|
267
|
+
if (opts.paceMethodMs !== void 0 && opts.paceMethodMs > 0) await injectGlobals(booted.connection, { __AIT_PACE_METHOD_MS__: opts.paceMethodMs });
|
|
213
268
|
return booted.connection;
|
|
214
269
|
},
|
|
215
270
|
onSessionPhase(next) {
|
|
@@ -226,11 +281,13 @@ function createRelayConnectionFactory(opts) {
|
|
|
226
281
|
qrServer?.notifyStateChange();
|
|
227
282
|
}
|
|
228
283
|
try {
|
|
229
|
-
const { injectDebugIndicator } = await import("../cell-
|
|
284
|
+
const { injectDebugIndicator } = await import("../cell-KsRXFdDs.js").then((n) => n.n);
|
|
230
285
|
await injectDebugIndicator(connection, { state: "disconnected" });
|
|
231
286
|
} catch {}
|
|
232
287
|
family?.stop();
|
|
233
288
|
family = void 0;
|
|
289
|
+
attachWatcher?.stop();
|
|
290
|
+
attachWatcher = void 0;
|
|
234
291
|
await qrServer?.close();
|
|
235
292
|
qrServer = void 0;
|
|
236
293
|
}
|
|
@@ -906,7 +963,73 @@ const SDK_IMPORT_FILTER = new RegExp(`^${SDK_PACKAGE.replace(/[.*+?^${}()|[\]\\]
|
|
|
906
963
|
*
|
|
907
964
|
* If `window.__sdk` is absent (non-dog-food build), every access throws a
|
|
908
965
|
* descriptive error rather than returning `undefined` silently.
|
|
966
|
+
*
|
|
967
|
+
* Bridge-stub interception (devtools#740, DT-2): the virtual module runs the
|
|
968
|
+
* resolved `window.__sdk` object through `wrapSdkWithStub` before exporting
|
|
969
|
+
* it. `wrapSdkWithStub` is a pass-through (returns the same object,
|
|
970
|
+
* unwrapped) unless `isStubBlockingEnabled()` reads `true` off
|
|
971
|
+
* `globalThis.__AIT_STUB_BLOCKING__` — an opt-in flag injected the same way
|
|
972
|
+
* `__AIT_CELL__` is (`cell.ts#injectGlobals`, wired from the CLI's
|
|
973
|
+
* `--stub-blocking` flag). Default off => zero behavior change: every existing
|
|
974
|
+
* (non-stub) test run resolves the exact same `window.__sdk` reference it did
|
|
975
|
+
* before this plugin learned about stubbing.
|
|
976
|
+
*
|
|
977
|
+
* Per-method pacing (devtools#769): BEFORE the stub wrap, `window.__sdk` is
|
|
978
|
+
* run through `wrapWithMethodPacing`, which enforces a minimum interval
|
|
979
|
+
* between calls to the SAME named SDK function (`getPaceMethodMs()` reads the
|
|
980
|
+
* `__AIT_PACE_METHOD_MS__` page global — see `method-pace.ts`). Composition
|
|
981
|
+
* order is pacing INSIDE the stub (`wrapSdkWithStub(wrapWithMethodPacing(sdk,
|
|
982
|
+
* gap), enabled)`): a stubbed name resolves instantly from
|
|
983
|
+
* `bridge-stub.ts`'s fixtures without ever reaching the paced wrapper, and
|
|
984
|
+
* only calls that fall through to the real bridge — the ones that can
|
|
985
|
+
* actually trigger `APP_BRIDGE_THROTTLED` — pay the pacing cost. `gapMs <= 0`
|
|
986
|
+
* (default, no `--pace-method` / non-2.x cell) is a no-op fast path — see
|
|
987
|
+
* `method-pace.ts#wrapWithMethodPacing`'s doc for the full contract.
|
|
909
988
|
*/
|
|
989
|
+
/**
|
|
990
|
+
* Builds the virtual CommonJS-style module contents the `sdk-redirect`
|
|
991
|
+
* esbuild plugin loads for every `@apps-in-toss/web-framework` import.
|
|
992
|
+
*
|
|
993
|
+
* Pulled out to a pure, exported function (rather than inlined at the
|
|
994
|
+
* `build.onLoad` call site) so unit tests can assert the exact composition
|
|
995
|
+
* order (pacing wraps the raw SDK; the stub wraps the paced result) as a
|
|
996
|
+
* string-level contract, independent of esbuild — esbuild cannot run inside
|
|
997
|
+
* this repo's jsdom vitest environment (see `bundleTestFile`'s module doc's
|
|
998
|
+
* lazy-import note), so a real bundling round-trip is out of unit-test reach
|
|
999
|
+
* here; `wrapSdkWithStub`/`wrapWithMethodPacing`'s own composed-call behavior
|
|
1000
|
+
* is covered directly (see `bundle.test.ts`).
|
|
1001
|
+
*
|
|
1002
|
+
* Generates a virtual CommonJS-style module so that esbuild does NOT perform
|
|
1003
|
+
* strict named-export matching. When `format:'iife'` bundles a CJS module, it
|
|
1004
|
+
* wraps it with its own `__toCommonJS` helper and satisfies named imports via
|
|
1005
|
+
* property access on the `module.exports` object — which is our Proxy. This
|
|
1006
|
+
* means `import { getPlatformOS } from '...'` becomes
|
|
1007
|
+
* `__proxy.getPlatformOS` at runtime, which correctly reads from
|
|
1008
|
+
* `window.__sdk`.
|
|
1009
|
+
*
|
|
1010
|
+
* The bridge-stub and method-pace imports are STATIC imports of the real
|
|
1011
|
+
* `bridge-stub.ts`/`method-pace.ts` modules (not inlined JS) so esbuild
|
|
1012
|
+
* bundles each once and the fixture registry / pacing registry have a single
|
|
1013
|
+
* source of truth shared with their own unit tests.
|
|
1014
|
+
*
|
|
1015
|
+
* Exported for unit testing.
|
|
1016
|
+
*/
|
|
1017
|
+
function buildSdkRedirectModuleContents() {
|
|
1018
|
+
return `
|
|
1019
|
+
import { wrapSdkWithStub, isStubBlockingEnabled } from ${JSON.stringify(getBridgeStubPath())};
|
|
1020
|
+
import { wrapWithMethodPacing, getPaceMethodMs } from ${JSON.stringify(getMethodPacePath())};
|
|
1021
|
+
var __rawSdk = (typeof window !== 'undefined' && window.__sdk)
|
|
1022
|
+
? window.__sdk
|
|
1023
|
+
: new Proxy({}, {
|
|
1024
|
+
get: function(_t, p) {
|
|
1025
|
+
throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
var __pacedSdk = wrapWithMethodPacing(__rawSdk, getPaceMethodMs());
|
|
1029
|
+
var __proxy = wrapSdkWithStub(__pacedSdk, isStubBlockingEnabled());
|
|
1030
|
+
module.exports = __proxy;
|
|
1031
|
+
`;
|
|
1032
|
+
}
|
|
910
1033
|
function sdkRedirectPlugin() {
|
|
911
1034
|
return {
|
|
912
1035
|
name: "sdk-redirect",
|
|
@@ -919,17 +1042,9 @@ function sdkRedirectPlugin() {
|
|
|
919
1042
|
filter: /.*/,
|
|
920
1043
|
namespace: "sdk-redirect"
|
|
921
1044
|
}, () => ({
|
|
922
|
-
contents:
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
: new Proxy({}, {
|
|
926
|
-
get: function(_t, p) {
|
|
927
|
-
throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));
|
|
928
|
-
}
|
|
929
|
-
});
|
|
930
|
-
module.exports = __proxy;
|
|
931
|
-
`,
|
|
932
|
-
loader: "js"
|
|
1045
|
+
contents: buildSdkRedirectModuleContents(),
|
|
1046
|
+
loader: "js",
|
|
1047
|
+
resolveDir: process.cwd()
|
|
933
1048
|
}));
|
|
934
1049
|
}
|
|
935
1050
|
};
|
|
@@ -1063,8 +1178,9 @@ function userFactoryPlugin(absPath) {
|
|
|
1063
1178
|
};
|
|
1064
1179
|
}
|
|
1065
1180
|
/**
|
|
1066
|
-
* Returns the absolute filesystem path to
|
|
1067
|
-
* (
|
|
1181
|
+
* Returns the absolute filesystem path to a page-side leaf module shipped
|
|
1182
|
+
* alongside `bundle.ts` in dist (e.g. `runtime.js`, `bridge-stub.js`) — a
|
|
1183
|
+
* fully self-contained module with no further page-side deps of its own.
|
|
1068
1184
|
*
|
|
1069
1185
|
* Rolldown code-splitting duplicates this bundling logic into shared chunks
|
|
1070
1186
|
* emitted at ARBITRARY dist depths: the `devtools-test` CLI pulls it from
|
|
@@ -1074,26 +1190,30 @@ function userFactoryPlugin(absPath) {
|
|
|
1074
1190
|
* is therefore wrong from at least one chunk — the live #697 regression.
|
|
1075
1191
|
*
|
|
1076
1192
|
* This resolves WITHOUT assuming chunk depth: from `import.meta.url`'s dir it
|
|
1077
|
-
* probes the co-located
|
|
1193
|
+
* probes the co-located `<name>.js` and the nested `test-runner/<name>.js`,
|
|
1078
1194
|
* then ascends one directory at a time (bounded) repeating both probes. The
|
|
1079
|
-
* nested probe catches dist/test-runner
|
|
1195
|
+
* nested probe catches dist/test-runner/<name>.js from the dist/ root level no
|
|
1080
1196
|
* matter which depth the chunk was hoisted to (root, dist/mcp/, or a future
|
|
1081
|
-
* relocation). The build always emits dist/test-runner
|
|
1082
|
-
* `'test-runner
|
|
1197
|
+
* relocation). The build always emits dist/test-runner/<name>.js (tsdown entry
|
|
1198
|
+
* `'test-runner/<name>'`; guarded by scripts/check-test-runner-dist.sh for
|
|
1199
|
+
* `runtime`).
|
|
1083
1200
|
*
|
|
1084
1201
|
* An ABSOLUTE path is returned deliberately: esbuild loads it as a literal file
|
|
1085
1202
|
* read, bypassing Node module resolution entirely, so this works identically in
|
|
1086
1203
|
* the npx-daemon context (its own dist tree) and the consumer-CLI context
|
|
1087
1204
|
* (the mini-app's installed @ait-co/devtools dist) — neither needs the package
|
|
1088
1205
|
* to be node-resolvable from the caller.
|
|
1206
|
+
*
|
|
1207
|
+
* @param moduleName - The leaf module's basename without extension, e.g.
|
|
1208
|
+
* `'runtime'` or `'bridge-stub'`.
|
|
1089
1209
|
*/
|
|
1090
|
-
function
|
|
1210
|
+
function getPageSideModulePath(moduleName) {
|
|
1091
1211
|
const startDir = path$1.dirname(fileURLToPath(import.meta.url));
|
|
1092
1212
|
const RELATIVE_PROBES = [
|
|
1093
|
-
[
|
|
1094
|
-
["test-runner",
|
|
1095
|
-
[
|
|
1096
|
-
["test-runner",
|
|
1213
|
+
[`${moduleName}.js`],
|
|
1214
|
+
["test-runner", `${moduleName}.js`],
|
|
1215
|
+
[`${moduleName}.ts`],
|
|
1216
|
+
["test-runner", `${moduleName}.ts`]
|
|
1097
1217
|
];
|
|
1098
1218
|
let dir = startDir;
|
|
1099
1219
|
for (let i = 0; i < 12; i++) {
|
|
@@ -1108,7 +1228,27 @@ function getRuntimePath() {
|
|
|
1108
1228
|
if (parent === dir) break;
|
|
1109
1229
|
dir = parent;
|
|
1110
1230
|
}
|
|
1111
|
-
return path$1.join(startDir,
|
|
1231
|
+
return path$1.join(startDir, `${moduleName}.js`);
|
|
1232
|
+
}
|
|
1233
|
+
/** Absolute path to the test-runner page-side runtime (describe/it/test/expect). */
|
|
1234
|
+
function getRuntimePath() {
|
|
1235
|
+
return getPageSideModulePath("runtime");
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Absolute path to the bridge-stub interceptor (devtools#740, DT-2) —
|
|
1239
|
+
* `wrapSdkWithStub`/`isStubBlockingEnabled`, statically imported by the
|
|
1240
|
+
* `sdk-redirect` virtual module.
|
|
1241
|
+
*/
|
|
1242
|
+
function getBridgeStubPath() {
|
|
1243
|
+
return getPageSideModulePath("bridge-stub");
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Absolute path to the per-method pacing wrapper (devtools#769) —
|
|
1247
|
+
* `wrapWithMethodPacing`/`getPaceMethodMs`, statically imported by the
|
|
1248
|
+
* `sdk-redirect` virtual module.
|
|
1249
|
+
*/
|
|
1250
|
+
function getMethodPacePath() {
|
|
1251
|
+
return getPageSideModulePath("method-pace");
|
|
1112
1252
|
}
|
|
1113
1253
|
/**
|
|
1114
1254
|
* Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.
|
|
@@ -1382,15 +1522,21 @@ async function runTestFilesOverRelay(connection, files, opts) {
|
|
|
1382
1522
|
const manualFiles = opts?.manualFiles;
|
|
1383
1523
|
const manualTotal = manualFiles?.size ?? 0;
|
|
1384
1524
|
let manualIndex = 0;
|
|
1525
|
+
const stubBlockingFiles = opts?.stubBlockingFiles;
|
|
1385
1526
|
let preflightPermissions;
|
|
1386
|
-
if (files.length > 0) preflightPermissions = await runPermissionPreflight(connection);
|
|
1527
|
+
if (files.length > 0) preflightPermissions = await runPermissionPreflight(connection, PERMISSION_PREFLIGHT_TIMEOUT_MS, opts?.preflightSdkLine !== "3.x");
|
|
1528
|
+
const paceMs = opts?.paceMs;
|
|
1529
|
+
let ranFirstFile = false;
|
|
1387
1530
|
try {
|
|
1388
1531
|
for (const file of files) {
|
|
1389
1532
|
if (pendingReconnectCheck) {
|
|
1390
1533
|
await attemptReconnect("next-file");
|
|
1391
1534
|
pendingReconnectCheck = false;
|
|
1392
1535
|
}
|
|
1393
|
-
|
|
1536
|
+
if (paceMs !== void 0 && paceMs > 0 && ranFirstFile) await new Promise((resolve) => setTimeout(resolve, paceMs));
|
|
1537
|
+
ranFirstFile = true;
|
|
1538
|
+
const isStubbed = stubBlockingFiles?.has(file) === true;
|
|
1539
|
+
const isManual = !isStubbed && manualFiles?.has(file) === true;
|
|
1394
1540
|
if (isManual) {
|
|
1395
1541
|
manualIndex += 1;
|
|
1396
1542
|
opts?.onManualFile?.(file, manualIndex, manualTotal);
|
|
@@ -1457,7 +1603,8 @@ async function runTestFilesOverRelay(connection, files, opts) {
|
|
|
1457
1603
|
};
|
|
1458
1604
|
}
|
|
1459
1605
|
if ("error" in fileEntry.result && isRelayDisconnectMessage(fileEntry.result.error)) pendingReconnectCheck = true;
|
|
1460
|
-
if (
|
|
1606
|
+
if (isStubbed) fileEntry.mode = "stubbed";
|
|
1607
|
+
else if (isManual) fileEntry.mode = "manual";
|
|
1461
1608
|
fileResults.push(fileEntry);
|
|
1462
1609
|
}
|
|
1463
1610
|
} finally {
|
|
@@ -1573,7 +1720,7 @@ function serializeRelayReport(report, meta) {
|
|
|
1573
1720
|
...report.preflight ? { preflight: report.preflight } : {},
|
|
1574
1721
|
files: report.files.map((f) => {
|
|
1575
1722
|
const file = relativise(meta.projectRoot, f.file);
|
|
1576
|
-
const modeField = f.mode === "manual" ? { mode:
|
|
1723
|
+
const modeField = f.mode === "manual" || f.mode === "stubbed" ? { mode: f.mode } : {};
|
|
1577
1724
|
if ("error" in f.result) return {
|
|
1578
1725
|
file,
|
|
1579
1726
|
error: f.result.error,
|
|
@@ -1605,11 +1752,21 @@ function serializeRelayReport(report, meta) {
|
|
|
1605
1752
|
* `<dir>/<sdkLine>.<platform>.manual.json` artifact instead of the standard
|
|
1606
1753
|
* one — the standard `<sdkLine>.<platform>.json` filename is reserved for the
|
|
1607
1754
|
* regular (unattended) files only, so a manual run's presence never mutates
|
|
1608
|
-
* what the unattended-baseline filename means.
|
|
1609
|
-
*
|
|
1610
|
-
*
|
|
1611
|
-
*
|
|
1612
|
-
*
|
|
1755
|
+
* what the unattended-baseline filename means.
|
|
1756
|
+
*
|
|
1757
|
+
* Stub-blocking provenance (devtools#740, DT-2): `mode: 'stubbed'` files are
|
|
1758
|
+
* written to a THIRD, separate `<dir>/<sdkLine>.<platform>.stubbed.json`
|
|
1759
|
+
* artifact — never merged into the standard file (it is unattended, like the
|
|
1760
|
+
* standard file, so a naive filename split could conflate the two) and never
|
|
1761
|
+
* merged into `.manual.json` (it did NOT have a human present). Its body is
|
|
1762
|
+
* ALSO stamped `cell.bridgeStub: true` — the mandatory HYBRID-cell provenance
|
|
1763
|
+
* from the issue: a stubbed result must never be silently mixed into the
|
|
1764
|
+
* real-device baseline report.
|
|
1765
|
+
*
|
|
1766
|
+
* If ALL files in the run are regular, only the standard artifact is written
|
|
1767
|
+
* (today's behavior, unchanged). Any combination of the three modes present
|
|
1768
|
+
* in a single run writes ONLY the corresponding artifacts — each is additive,
|
|
1769
|
+
* never replacing another.
|
|
1613
1770
|
*
|
|
1614
1771
|
* SECRET-HANDLING: the written body contains no relay/secret fields (the schema
|
|
1615
1772
|
* has none). `dir`/`projectRoot` are local filesystem paths, never logged here.
|
|
@@ -1618,14 +1775,16 @@ function serializeRelayReport(report, meta) {
|
|
|
1618
1775
|
* @param dir - Output directory (created recursively if missing).
|
|
1619
1776
|
* @param meta - Cell axes + projectRoot.
|
|
1620
1777
|
* @returns The absolute path(s) written, in order: standard first (if any
|
|
1621
|
-
* regular files ran), then manual (if any manual files ran)
|
|
1622
|
-
* path is always returned when
|
|
1778
|
+
* regular files ran), then manual (if any manual files ran), then stubbed
|
|
1779
|
+
* (if any stubbed files ran). At least one path is always returned when
|
|
1780
|
+
* `report.files` is non-empty.
|
|
1623
1781
|
*/
|
|
1624
1782
|
async function writeReportArtifact(report, dir, meta) {
|
|
1625
1783
|
const serialised = serializeRelayReport(report, meta);
|
|
1626
1784
|
await mkdir(dir, { recursive: true });
|
|
1627
|
-
const regularFiles = serialised.files.filter((f) => f.mode
|
|
1785
|
+
const regularFiles = serialised.files.filter((f) => f.mode === void 0);
|
|
1628
1786
|
const manualFiles = serialised.files.filter((f) => f.mode === "manual");
|
|
1787
|
+
const stubbedFiles = serialised.files.filter((f) => f.mode === "stubbed");
|
|
1629
1788
|
const written = [];
|
|
1630
1789
|
if (regularFiles.length > 0 || serialised.files.length === 0) {
|
|
1631
1790
|
const outFile = path.join(dir, `${meta.sdkLine}.${meta.platform}.json`);
|
|
@@ -1643,6 +1802,18 @@ async function writeReportArtifact(report, dir, meta) {
|
|
|
1643
1802
|
}, null, 2)}\n`, "utf8");
|
|
1644
1803
|
written.push(manualOutFile);
|
|
1645
1804
|
}
|
|
1805
|
+
if (stubbedFiles.length > 0) {
|
|
1806
|
+
const stubbedOutFile = path.join(dir, `${meta.sdkLine}.${meta.platform}.stubbed.json`);
|
|
1807
|
+
await writeFile(stubbedOutFile, `${JSON.stringify({
|
|
1808
|
+
...serialised,
|
|
1809
|
+
cell: {
|
|
1810
|
+
...serialised.cell,
|
|
1811
|
+
bridgeStub: true
|
|
1812
|
+
},
|
|
1813
|
+
files: stubbedFiles
|
|
1814
|
+
}, null, 2)}\n`, "utf8");
|
|
1815
|
+
written.push(stubbedOutFile);
|
|
1816
|
+
}
|
|
1646
1817
|
return written;
|
|
1647
1818
|
}
|
|
1648
1819
|
/**
|
|
@@ -1777,7 +1948,21 @@ USAGE
|
|
|
1777
1948
|
|
|
1778
1949
|
OPTIONS
|
|
1779
1950
|
--scheme-url <url> intoss-private:// URL from \`ait deploy --scheme-only\`
|
|
1780
|
-
(required for standalone relay attach / env3)
|
|
1951
|
+
(required for standalone relay attach / env3). Unused
|
|
1952
|
+
and ignored when --attach-launcher is set.
|
|
1953
|
+
--attach-launcher Attach over the env-2 AITC Sandbox PWA launcher
|
|
1954
|
+
(real-device WebKit) instead of the env-3 intoss
|
|
1955
|
+
scheme deep-link. The relay/QR/dashboard/attach-wait
|
|
1956
|
+
are identical; only the QR is a launcher deep-link.
|
|
1957
|
+
Requires --app-url. The SDK still hits the mock in
|
|
1958
|
+
env 2, so this measures the mock + standard Web API
|
|
1959
|
+
layer's engine-attributable behavior (env1↔env2
|
|
1960
|
+
equivalence), NOT native-bridge fidelity (that is env3).
|
|
1961
|
+
--app-url <url> The consumer dev server's HTTP tunnel URL (e.g. the
|
|
1962
|
+
*.trycloudflare.com URL from \`pnpm dev:phone:cdp\`)
|
|
1963
|
+
that the launcher PWA frames. REQUIRED with
|
|
1964
|
+
--attach-launcher; ignored otherwise. SECRET: a tunnel
|
|
1965
|
+
host — never printed; it rides only inside the QR.
|
|
1781
1966
|
--timeout <ms> Per-file evaluate timeout in ms (default: 60000).
|
|
1782
1967
|
Controls how long a single test file is allowed to run
|
|
1783
1968
|
before it is considered hung. Does NOT affect how long
|
|
@@ -1789,7 +1974,10 @@ OPTIONS
|
|
|
1789
1974
|
Pass a value to bound the wait for CI/headless runs.
|
|
1790
1975
|
--cell-sdk-line <line> SDK line to inject as __AIT_CELL__.sdkLine (2.x|3.x)
|
|
1791
1976
|
--cell-platform <plat> Platform to inject as __AIT_CELL__.platform
|
|
1792
|
-
(mock|ios|android, default: AIT_CELL_PLATFORM
|
|
1977
|
+
(mock|ios|android|ios-pwa, default: AIT_CELL_PLATFORM
|
|
1978
|
+
env). Use ios-pwa for env-2 (--attach-launcher) runs
|
|
1979
|
+
so env1(mock@desktop) and env2(mock@WebKit) captures
|
|
1980
|
+
stay distinguishable. An unknown value is rejected.
|
|
1793
1981
|
--report-dir <dir> Persist a runner-agnostic report + captures to <dir>
|
|
1794
1982
|
(report: <sdkLine>.<platform>.json; captures:
|
|
1795
1983
|
<dir>/.ait-capture/<category>.<sdkLine>.<platform>.json).
|
|
@@ -1804,6 +1992,28 @@ OPTIONS
|
|
|
1804
1992
|
--headless Disable browser auto-open (text QR only)
|
|
1805
1993
|
--project-root <dir> Project root for .ait_relay secret lookup
|
|
1806
1994
|
(default: current working directory)
|
|
1995
|
+
--pace <ms> Minimum delay in ms between test-to-test AND
|
|
1996
|
+
file-to-file bridge calls (default: 0, i.e. no
|
|
1997
|
+
added delay — today's behavior byte-for-byte).
|
|
1998
|
+
Falls back to the AIT_PACE env var when omitted
|
|
1999
|
+
(--pace takes precedence over the env var when both
|
|
2000
|
+
are given). Use on a 2.x cell scan when the native
|
|
2001
|
+
per-method bridge rate limit (APP_BRIDGE_THROTTLED,
|
|
2002
|
+
devtools#767) is rejecting rapid same-method calls —
|
|
2003
|
+
3.x cells are unaffected by that limiter and do not
|
|
2004
|
+
need this flag.
|
|
2005
|
+
--pace-method <ms> Minimum delay in ms BETWEEN calls to the SAME named
|
|
2006
|
+
SDK function — paces a same-method burst WITHIN a
|
|
2007
|
+
single test body (e.g. a clipboard happy-path loop
|
|
2008
|
+
calling setClipboardText/getClipboardText 8 times
|
|
2009
|
+
back to back), which --pace's test/file spacing
|
|
2010
|
+
cannot reach (devtools#769). Falls back to the
|
|
2011
|
+
AIT_PACE_METHOD env var when omitted (--pace-method
|
|
2012
|
+
takes precedence over the env var when both are
|
|
2013
|
+
given). Default: 250ms when --cell-sdk-line is 2.x
|
|
2014
|
+
(unset --cell-sdk-line also defaults to 2.x — see
|
|
2015
|
+
--cell-sdk-line), 0 (no added delay) otherwise. Pass
|
|
2016
|
+
--pace-method 0 to opt out even on a 2.x cell.
|
|
1807
2017
|
--manual-blocking Run manual-tagged test files (*.manual.ait.test.ts)
|
|
1808
2018
|
LAST, after all regular files, with a human present.
|
|
1809
2019
|
Before each manual file, the QR dashboard is pushed
|
|
@@ -1822,6 +2032,24 @@ OPTIONS
|
|
|
1822
2032
|
each manual file's report entry is stamped
|
|
1823
2033
|
mode: 'manual' — never diff a manual run against an
|
|
1824
2034
|
unattended baseline as if they were equivalent.
|
|
2035
|
+
--stub-blocking Run manual-tagged test files (*.manual.ait.test.ts)
|
|
2036
|
+
UNATTENDED (devtools#740, DT-2) by intercepting a
|
|
2037
|
+
fixed allowlist of blocking-UI SDK calls (ads
|
|
2038
|
+
show*, openPermissionDialog/requestPermission,
|
|
2039
|
+
saveBase64Data) in the page and answering them from
|
|
2040
|
+
fixtures captured by a real --manual-blocking run,
|
|
2041
|
+
instead of forwarding them to native UI. Implies
|
|
2042
|
+
--manual-blocking (manual files are included in the
|
|
2043
|
+
run); no human presence or QR-dashboard prompt is
|
|
2044
|
+
needed for them. HYBRID cell, not pure env3 — every
|
|
2045
|
+
other SDK call in the same run still hits the real
|
|
2046
|
+
native bridge. With --report-dir, files that ran
|
|
2047
|
+
under the stub are written to a SEPARATE
|
|
2048
|
+
<sdkLine>.<platform>.stubbed.json artifact (never
|
|
2049
|
+
merged into the standard or .manual.json report) and
|
|
2050
|
+
the report body is stamped cell.bridgeStub: true —
|
|
2051
|
+
never diff a stubbed run against a real device
|
|
2052
|
+
baseline (manual or unattended) as if equivalent.
|
|
1825
2053
|
--help, -h Show this help message
|
|
1826
2054
|
|
|
1827
2055
|
DESCRIPTION
|
|
@@ -1838,7 +2066,7 @@ DESCRIPTION
|
|
|
1838
2066
|
The test files run against the live relay connection started by this process;
|
|
1839
2067
|
no separate MCP daemon is required.
|
|
1840
2068
|
|
|
1841
|
-
EXAMPLE
|
|
2069
|
+
EXAMPLE (env 3 — intoss-private scheme)
|
|
1842
2070
|
devtools-test 'src/**/*.ait.test.ts' \\
|
|
1843
2071
|
--scheme-url "intoss-private://..." \\
|
|
1844
2072
|
--cell-sdk-line 3.x \\
|
|
@@ -1846,6 +2074,14 @@ EXAMPLE
|
|
|
1846
2074
|
--report-dir .ait-report \\
|
|
1847
2075
|
--timeout 60000
|
|
1848
2076
|
|
|
2077
|
+
EXAMPLE (env 2 — AITC Sandbox PWA launcher)
|
|
2078
|
+
devtools-test 'src/**/*.ait.test.ts' \\
|
|
2079
|
+
--attach-launcher \\
|
|
2080
|
+
--app-url "https://<subdomain>.trycloudflare.com" \\
|
|
2081
|
+
--cell-sdk-line 3.x \\
|
|
2082
|
+
--cell-platform ios-pwa \\
|
|
2083
|
+
--report-dir .ait-report
|
|
2084
|
+
|
|
1849
2085
|
`.trimStart();
|
|
1850
2086
|
/**
|
|
1851
2087
|
* Grace period (ms) for the exit backstop armed around Step 6 teardown
|
|
@@ -1894,6 +2130,144 @@ function resolveDashboardPort(raw) {
|
|
|
1894
2130
|
return port;
|
|
1895
2131
|
}
|
|
1896
2132
|
/**
|
|
2133
|
+
* Rewrites a lone `--pace <value>` or `--pace-method <value>` pair from space
|
|
2134
|
+
* syntax to `=` syntax (`--pace=<value>` / `--pace-method=<value>`) when
|
|
2135
|
+
* `<value>` starts with `-` (devtools#768 review; extended to `--pace-method`
|
|
2136
|
+
* in devtools#769).
|
|
2137
|
+
*
|
|
2138
|
+
* Node's `util.parseArgs` treats a `type: 'string'` option's value as
|
|
2139
|
+
* "ambiguous" and throws its OWN parser error whenever that value starts with
|
|
2140
|
+
* a dash and was passed via the space form (`--pace -1`) — it never reaches
|
|
2141
|
+
* this module's `resolvePace`/`resolvePaceMethod`, so the friendly
|
|
2142
|
+
* `'--pace must be a non-negative integer'` message (and its unit-tested path)
|
|
2143
|
+
* was unreachable from the real CLI entry point for the single most natural
|
|
2144
|
+
* way a user would try a negative value, even though every other flag in
|
|
2145
|
+
* {@link USAGE} is documented in space syntax.
|
|
2146
|
+
*
|
|
2147
|
+
* Scoped narrowly to `--pace`/`--pace=...` and `--pace-method`/
|
|
2148
|
+
* `--pace-method=...` tokens only — every other flag is untouched, and
|
|
2149
|
+
* positionals/other options keep flowing through `parseArgs` unchanged. Only
|
|
2150
|
+
* rewrites the space form; `--pace=-1`/`--pace-method=-1` already parse fine
|
|
2151
|
+
* and are passed through untouched.
|
|
2152
|
+
*
|
|
2153
|
+
* Exported for unit testing.
|
|
2154
|
+
*/
|
|
2155
|
+
function normalizePaceArgv(argv) {
|
|
2156
|
+
const out = [];
|
|
2157
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2158
|
+
const arg = argv[i];
|
|
2159
|
+
const next = argv[i + 1];
|
|
2160
|
+
if ((arg === "--pace" || arg === "--pace-method") && next !== void 0 && next.startsWith("-") && next !== "--") {
|
|
2161
|
+
out.push(`${arg}=${next}`);
|
|
2162
|
+
i++;
|
|
2163
|
+
continue;
|
|
2164
|
+
}
|
|
2165
|
+
out.push(arg);
|
|
2166
|
+
}
|
|
2167
|
+
return out;
|
|
2168
|
+
}
|
|
2169
|
+
/**
|
|
2170
|
+
* Parses `--pace` (falling back to the `AIT_PACE` env var when the flag is
|
|
2171
|
+
* omitted) into a validated millisecond delay (devtools#767).
|
|
2172
|
+
*
|
|
2173
|
+
* Opt-in, zero-diff-when-absent: BOTH omitted resolves to `0` — no pacing —
|
|
2174
|
+
* which is byte-for-byte today's behavior (`runtime.ts` treats `0`/absent
|
|
2175
|
+
* `__AIT_PACE_MS__` identically, and `relay-worker.ts`'s file-to-file gap is
|
|
2176
|
+
* skipped entirely when the resolved value is 0). `--pace` takes precedence
|
|
2177
|
+
* over `AIT_PACE` when both are present, mirroring `--cell-platform` /
|
|
2178
|
+
* `AIT_CELL_PLATFORM`'s existing flag-over-env precedent.
|
|
2179
|
+
*
|
|
2180
|
+
* Returns an error string on invalid input (non-integer or negative), or the
|
|
2181
|
+
* resolved delay (`>= 0`) on success. Exported for unit testing.
|
|
2182
|
+
*/
|
|
2183
|
+
function resolvePace(rawFlag, rawEnv) {
|
|
2184
|
+
const raw = rawFlag ?? rawEnv;
|
|
2185
|
+
if (raw === void 0) return 0;
|
|
2186
|
+
const ms = parseInt(raw, 10);
|
|
2187
|
+
if (Number.isNaN(ms) || ms < 0) return "--pace must be a non-negative integer";
|
|
2188
|
+
return ms;
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* Parses `--pace-method` (falling back to the `AIT_PACE_METHOD` env var when
|
|
2192
|
+
* the flag is omitted) into a validated millisecond per-method minimum
|
|
2193
|
+
* interval (devtools#769).
|
|
2194
|
+
*
|
|
2195
|
+
* Precedence, highest first: `--pace-method` flag > `AIT_PACE_METHOD` env >
|
|
2196
|
+
* sdkLine-aware default (`{@link DEFAULT_PACE_METHOD_MS_2X}` for a 2.x cell,
|
|
2197
|
+
* `0` otherwise) — an EXPLICIT flag or env value always wins, including
|
|
2198
|
+
* `--pace-method 0` to opt out on a 2.x cell. Mirrors `resolvePace`'s
|
|
2199
|
+
* flag-over-env precedent, with the sdkLine default spliced in only when
|
|
2200
|
+
* BOTH are absent.
|
|
2201
|
+
*
|
|
2202
|
+
* Returns an error string on invalid input (non-integer or negative), or the
|
|
2203
|
+
* resolved delay (`>= 0`) on success. Exported for unit testing.
|
|
2204
|
+
*
|
|
2205
|
+
* @param rawFlag - The raw `--pace-method` string value, if passed.
|
|
2206
|
+
* @param rawEnv - The raw `AIT_PACE_METHOD` env value, if set.
|
|
2207
|
+
* @param resolvedCellSdkLine - The effective `cell.sdkLine` value (already
|
|
2208
|
+
* defaulted to '2.x' by the caller when `--cell-sdk-line` was omitted — see
|
|
2209
|
+
* `main()`'s `cell` construction) — used ONLY to pick the sdkLine-aware
|
|
2210
|
+
* default when both `rawFlag` and `rawEnv` are absent.
|
|
2211
|
+
*/
|
|
2212
|
+
function resolvePaceMethod(rawFlag, rawEnv, resolvedCellSdkLine) {
|
|
2213
|
+
const raw = rawFlag ?? rawEnv;
|
|
2214
|
+
if (raw === void 0) return resolvedCellSdkLine === "2.x" ? 250 : 0;
|
|
2215
|
+
const ms = parseInt(raw, 10);
|
|
2216
|
+
if (Number.isNaN(ms) || ms < 0) return "--pace-method must be a non-negative integer";
|
|
2217
|
+
return ms;
|
|
2218
|
+
}
|
|
2219
|
+
/**
|
|
2220
|
+
* The set of `__AIT_CELL__.platform` values the runner accepts (devtools#774).
|
|
2221
|
+
*
|
|
2222
|
+
* `mock`/`ios`/`android` mirror sdk-example `aitCapture`'s existing `Platform`
|
|
2223
|
+
* union; `ios-pwa` is the new env-2 axis value (issue #774) — env 1 is
|
|
2224
|
+
* mock@desktop-Chromium and env 2 is mock@real-device-WebKit, so the two share
|
|
2225
|
+
* `sdkLine`/`ios` but must be DISTINGUISHABLE in the capture cell to diff
|
|
2226
|
+
* engine-attributable differences. `ios-pwa` names "mock SDK, real-device iOS
|
|
2227
|
+
* WebKit via the launcher PWA" — it is NOT `ios` (which is env 3's native
|
|
2228
|
+
* bridge). The consuming `aitCapture` type extension is a separate sdk-example
|
|
2229
|
+
* PR (out of scope here); the runner only needs to inject + validate the label.
|
|
2230
|
+
*
|
|
2231
|
+
* Kept as a validated allowlist (rather than a free string) purely as a
|
|
2232
|
+
* typo guard: `platform` flows into report/capture FILENAMES
|
|
2233
|
+
* (`<sdkLine>.<platform>.json`), so a silent typo would fork the artifact set
|
|
2234
|
+
* and break offline diffing. This is the first validation on `--cell-platform`
|
|
2235
|
+
* — before #774 it was an unvalidated free string defaulting to `mock`.
|
|
2236
|
+
*/
|
|
2237
|
+
const ALLOWED_CELL_PLATFORMS = [
|
|
2238
|
+
"mock",
|
|
2239
|
+
"ios",
|
|
2240
|
+
"android",
|
|
2241
|
+
"ios-pwa"
|
|
2242
|
+
];
|
|
2243
|
+
/**
|
|
2244
|
+
* Validates a resolved `--cell-platform` value against {@link ALLOWED_CELL_PLATFORMS}.
|
|
2245
|
+
*
|
|
2246
|
+
* `undefined` (flag + `AIT_CELL_PLATFORM` env both absent) passes through as
|
|
2247
|
+
* `{ ok: true, value: undefined }` — the caller applies its own `'mock'`
|
|
2248
|
+
* default, so an omitted platform is never an error. A present-but-unknown
|
|
2249
|
+
* value returns `{ ok: false, error }` naming the allowed set.
|
|
2250
|
+
*
|
|
2251
|
+
* Uses a tagged result (not the `string`-is-error convention of
|
|
2252
|
+
* `resolveDashboardPort`/`resolvePace`) because BOTH the success value and the
|
|
2253
|
+
* error message are strings here — a bare `string` return would be ambiguous.
|
|
2254
|
+
* Exported for unit testing.
|
|
2255
|
+
*/
|
|
2256
|
+
function resolveCellPlatform(raw) {
|
|
2257
|
+
if (raw === void 0) return {
|
|
2258
|
+
ok: true,
|
|
2259
|
+
value: void 0
|
|
2260
|
+
};
|
|
2261
|
+
if (ALLOWED_CELL_PLATFORMS.includes(raw)) return {
|
|
2262
|
+
ok: true,
|
|
2263
|
+
value: raw
|
|
2264
|
+
};
|
|
2265
|
+
return {
|
|
2266
|
+
ok: false,
|
|
2267
|
+
error: `--cell-platform must be one of ${ALLOWED_CELL_PLATFORMS.join("|")} (got "${raw}")`
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
/**
|
|
1897
2271
|
* Renders per-file result lines and the aggregate totals line to a string.
|
|
1898
2272
|
*
|
|
1899
2273
|
* Each file gets one line:
|
|
@@ -1962,8 +2336,8 @@ function shouldSuppressQr(noQrFlag) {
|
|
|
1962
2336
|
*
|
|
1963
2337
|
* 1. Parse args: globs, --timeout (per-file evaluate), --attach-timeout (QR
|
|
1964
2338
|
* scan wait), --cell-sdk-line, --cell-platform, --scheme-url (required for
|
|
1965
|
-
* env3)
|
|
1966
|
-
* --project-root.
|
|
2339
|
+
* env3) OR --attach-launcher + --app-url (env2, devtools#774), --report-dir,
|
|
2340
|
+
* --dashboard-port, --no-qr-stdout, --headless, --project-root.
|
|
1967
2341
|
* 2. Discover test files; exit 1 if none.
|
|
1968
2342
|
* 3. factory.open() — boot relay → render QR (suppressed on non-interactive
|
|
1969
2343
|
* stdout) → wait for phone (up to attachTimeoutMs) → inject cell →
|
|
@@ -1984,7 +2358,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1984
2358
|
let parsed;
|
|
1985
2359
|
try {
|
|
1986
2360
|
parsed = parseArgs({
|
|
1987
|
-
args: argv,
|
|
2361
|
+
args: normalizePaceArgv(argv),
|
|
1988
2362
|
options: {
|
|
1989
2363
|
help: {
|
|
1990
2364
|
type: "boolean",
|
|
@@ -1993,6 +2367,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1993
2367
|
timeout: { type: "string" },
|
|
1994
2368
|
"attach-timeout": { type: "string" },
|
|
1995
2369
|
"scheme-url": { type: "string" },
|
|
2370
|
+
"attach-launcher": { type: "boolean" },
|
|
2371
|
+
"app-url": { type: "string" },
|
|
1996
2372
|
"cell-sdk-line": { type: "string" },
|
|
1997
2373
|
"cell-platform": { type: "string" },
|
|
1998
2374
|
"report-dir": { type: "string" },
|
|
@@ -2000,7 +2376,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2000
2376
|
"no-qr-stdout": { type: "boolean" },
|
|
2001
2377
|
headless: { type: "boolean" },
|
|
2002
2378
|
"project-root": { type: "string" },
|
|
2003
|
-
|
|
2379
|
+
pace: { type: "string" },
|
|
2380
|
+
"pace-method": { type: "string" },
|
|
2381
|
+
"manual-blocking": { type: "boolean" },
|
|
2382
|
+
"stub-blocking": { type: "boolean" }
|
|
2004
2383
|
},
|
|
2005
2384
|
allowPositionals: true
|
|
2006
2385
|
});
|
|
@@ -2021,9 +2400,17 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2021
2400
|
return;
|
|
2022
2401
|
}
|
|
2023
2402
|
const { evaluateTimeoutMs, attachTimeoutMs } = timeouts;
|
|
2403
|
+
const attachLauncher = vals["attach-launcher"] === true;
|
|
2024
2404
|
const schemeUrl = typeof vals["scheme-url"] === "string" ? vals["scheme-url"] : "";
|
|
2025
|
-
|
|
2026
|
-
|
|
2405
|
+
const appUrl = typeof vals["app-url"] === "string" ? vals["app-url"] : "";
|
|
2406
|
+
if (attachLauncher) {
|
|
2407
|
+
if (appUrl === "") {
|
|
2408
|
+
process.stderr.write("devtools-test: --app-url is required with --attach-launcher (env 2).\n Pass the consumer dev server's tunnel URL (e.g. the HTTP *.trycloudflare.com\n URL printed by `pnpm dev:phone:cdp`). --scheme-url is not used in this mode.\n");
|
|
2409
|
+
process.exitCode = 1;
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
} else if (schemeUrl === "") {
|
|
2413
|
+
process.stderr.write("devtools-test: --scheme-url is required for standalone relay attach.\n Pass the intoss-private:// URL from `ait deploy --scheme-only`.\n (For env 2 / AITC Sandbox PWA, use --attach-launcher --app-url <tunnel-url> instead.)\n");
|
|
2027
2414
|
process.exitCode = 1;
|
|
2028
2415
|
return;
|
|
2029
2416
|
}
|
|
@@ -2036,15 +2423,34 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2036
2423
|
process.exitCode = 1;
|
|
2037
2424
|
return;
|
|
2038
2425
|
}
|
|
2426
|
+
const paceMs = resolvePace(typeof vals.pace === "string" ? vals.pace : void 0, process.env.AIT_PACE);
|
|
2427
|
+
if (typeof paceMs === "string") {
|
|
2428
|
+
process.stderr.write(`devtools-test: ${paceMs}\n`);
|
|
2429
|
+
process.exitCode = 1;
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2039
2432
|
const suppressQr = shouldSuppressQr(vals["no-qr-stdout"] === true);
|
|
2040
|
-
const
|
|
2433
|
+
const stubBlocking = vals["stub-blocking"] === true;
|
|
2434
|
+
const manualBlocking = vals["manual-blocking"] === true || stubBlocking;
|
|
2041
2435
|
const cellSdkLine = typeof vals["cell-sdk-line"] === "string" ? vals["cell-sdk-line"] : void 0;
|
|
2042
|
-
const
|
|
2436
|
+
const cellPlatformResult = resolveCellPlatform(typeof vals["cell-platform"] === "string" ? vals["cell-platform"] : process.env.AIT_CELL_PLATFORM);
|
|
2437
|
+
if (!cellPlatformResult.ok) {
|
|
2438
|
+
process.stderr.write(`devtools-test: ${cellPlatformResult.error}\n`);
|
|
2439
|
+
process.exitCode = 1;
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
const cellPlatform = cellPlatformResult.value;
|
|
2043
2443
|
const hasCell = cellSdkLine !== void 0 || cellPlatform !== void 0;
|
|
2044
2444
|
const cell = {
|
|
2045
2445
|
sdkLine: cellSdkLine ?? "2.x",
|
|
2046
2446
|
platform: cellPlatform ?? "mock"
|
|
2047
2447
|
};
|
|
2448
|
+
const paceMethodMs = resolvePaceMethod(typeof vals["pace-method"] === "string" ? vals["pace-method"] : void 0, process.env.AIT_PACE_METHOD, cell.sdkLine);
|
|
2449
|
+
if (typeof paceMethodMs === "string") {
|
|
2450
|
+
process.stderr.write(`devtools-test: ${paceMethodMs}\n`);
|
|
2451
|
+
process.exitCode = 1;
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2048
2454
|
const globs = parsed.positionals;
|
|
2049
2455
|
if (globs.length === 0) {
|
|
2050
2456
|
process.stderr.write(`devtools-test: at least one glob pattern is required\n`);
|
|
@@ -2063,13 +2469,23 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2063
2469
|
const manualFileSet = new Set(manual);
|
|
2064
2470
|
process.stderr.write(manualBlocking && manual.length > 0 ? `devtools-test: found ${regular.length} regular + ${manual.length} manual (${MANUAL_TEST_SUFFIX}) test file(s)\n` : `devtools-test: found ${files.length} test file(s)\n`);
|
|
2065
2471
|
if (hasCell) process.stderr.write(`devtools-test: injecting __AIT_CELL__ = ${JSON.stringify(cell)}\n`);
|
|
2472
|
+
if (stubBlocking) process.stderr.write("devtools-test: --stub-blocking enabled — blocking-UI calls in manual-tagged files will be answered from fixtures (devtools#740), not forwarded to native UI\n");
|
|
2473
|
+
if (paceMs > 0) process.stderr.write(`devtools-test: --pace ${paceMs}ms enabled (devtools#767)\n`);
|
|
2474
|
+
if (paceMethodMs > 0) process.stderr.write(`devtools-test: --pace-method ${paceMethodMs}ms enabled (devtools#769)\n`);
|
|
2066
2475
|
const factory = createRelayConnectionFactory({
|
|
2067
2476
|
schemeUrl,
|
|
2477
|
+
...attachLauncher ? {
|
|
2478
|
+
attachLauncher: true,
|
|
2479
|
+
appUrl
|
|
2480
|
+
} : {},
|
|
2068
2481
|
projectRoot,
|
|
2069
2482
|
...attachTimeoutMs !== void 0 ? { timeoutMs: attachTimeoutMs } : {},
|
|
2070
2483
|
...dashboardPort !== void 0 ? { dashboardPort } : {},
|
|
2071
2484
|
headless,
|
|
2072
2485
|
cell: hasCell ? cell : void 0,
|
|
2486
|
+
stubBlocking,
|
|
2487
|
+
paceMs,
|
|
2488
|
+
paceMethodMs,
|
|
2073
2489
|
onQrContent: (chunks) => {
|
|
2074
2490
|
if (suppressQr) {
|
|
2075
2491
|
process.stdout.write("QR suppressed (non-interactive)\n");
|
|
@@ -2093,7 +2509,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2093
2509
|
timeoutMs: evaluateTimeoutMs,
|
|
2094
2510
|
printSummary: true,
|
|
2095
2511
|
collectCaptures: reportDir !== void 0,
|
|
2512
|
+
paceMs,
|
|
2513
|
+
preflightSdkLine: cell.sdkLine,
|
|
2096
2514
|
manualFiles: manualBlocking && manualFileSet.size > 0 ? manualFileSet : void 0,
|
|
2515
|
+
stubBlockingFiles: stubBlocking && manualFileSet.size > 0 ? manualFileSet : void 0,
|
|
2097
2516
|
onManualFile: (file, index, total) => {
|
|
2098
2517
|
const name = basename(file);
|
|
2099
2518
|
process.stdout.write(`수동 단계: ${name} — 폰에서 네이티브 시트가 뜨면 안내에 따라 조작하세요 (${index}/${total})\n`);
|