@ait-co/devtools 0.1.137 → 0.1.139
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-CBK7dn8O.js → debug-server-6c22qSWa.js} +132 -9
- package/dist/debug-server-6c22qSWa.js.map +1 -0
- package/dist/{debug-server-BQplCzf8.js → debug-server-BQHbaKPT.js} +134 -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 +299 -60
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/mock/index.d.ts +79 -1
- package/dist/mock/index.d.ts.map +1 -1
- package/dist/mock/index.js +218 -14
- package/dist/mock/index.js.map +1 -1
- package/dist/panel/index.js +171 -10
- package/dist/panel/index.js.map +1 -1
- package/dist/{pool-CmfvXnwh.d.ts → pool-CrP5CPvU.d.ts} +3 -3
- package/dist/{pool-CmfvXnwh.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-DVzpBRxz.js → relay-factory-C1X3G3WY.js} +71 -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-BcVrWwTq.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-DGduVJhs.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-6fy1eaIo.js → relay-worker-BUXI0K0b.js} +8 -4
- package/dist/{relay-worker-6fy1eaIo.js.map → relay-worker-BUXI0K0b.js.map} +1 -1
- package/dist/{relay-worker-DJnZkXza.d.ts → relay-worker-DcyboK43.d.ts} +30 -5
- 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 +376 -38
- package/dist/test-runner/bin.js.map +1 -1
- package/dist/test-runner/bundle.d.ts +2 -2
- package/dist/test-runner/bundle.js +66 -13
- 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 +58 -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 +57 -4
- package/dist/test-runner/relay-factory.d.ts.map +1 -1
- package/dist/test-runner/relay-factory.js +70 -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 +3 -3
- 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-BQplCzf8.js.map +0 -1
- package/dist/debug-server-CBK7dn8O.js.map +0 -1
- package/dist/debug-server-D2wpPK3_.js.map +0 -1
- package/dist/relay-factory-DVzpBRxz.js.map +0 -1
- package/dist/relay-worker-DJnZkXza.d.ts.map +0 -1
- package/dist/runtime-DfHHZms2.d.ts.map +0 -1
- package/dist/totp-D1pulXLa.js +0 -3
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as BundleResult, r as
|
|
2
|
-
export { BundleOptions, BundleResult, bundleTestFile };
|
|
1
|
+
import { i as bundleTestFile, n as BundleResult, r as buildSdkRedirectModuleContents, t as BundleOptions } from "../bundle-C796JIwG.js";
|
|
2
|
+
export { BundleOptions, BundleResult, buildSdkRedirectModuleContents, bundleTestFile };
|
|
@@ -103,7 +103,63 @@ const SDK_IMPORT_FILTER = new RegExp(`^${SDK_PACKAGE.replace(/[.*+?^${}()|[\]\\]
|
|
|
103
103
|
* `--stub-blocking` flag). Default off => zero behavior change: every existing
|
|
104
104
|
* (non-stub) test run resolves the exact same `window.__sdk` reference it did
|
|
105
105
|
* before this plugin learned about stubbing.
|
|
106
|
+
*
|
|
107
|
+
* Per-method pacing (devtools#769): BEFORE the stub wrap, `window.__sdk` is
|
|
108
|
+
* run through `wrapWithMethodPacing`, which enforces a minimum interval
|
|
109
|
+
* between calls to the SAME named SDK function (`getPaceMethodMs()` reads the
|
|
110
|
+
* `__AIT_PACE_METHOD_MS__` page global — see `method-pace.ts`). Composition
|
|
111
|
+
* order is pacing INSIDE the stub (`wrapSdkWithStub(wrapWithMethodPacing(sdk,
|
|
112
|
+
* gap), enabled)`): a stubbed name resolves instantly from
|
|
113
|
+
* `bridge-stub.ts`'s fixtures without ever reaching the paced wrapper, and
|
|
114
|
+
* only calls that fall through to the real bridge — the ones that can
|
|
115
|
+
* actually trigger `APP_BRIDGE_THROTTLED` — pay the pacing cost. `gapMs <= 0`
|
|
116
|
+
* (default, no `--pace-method` / non-2.x cell) is a no-op fast path — see
|
|
117
|
+
* `method-pace.ts#wrapWithMethodPacing`'s doc for the full contract.
|
|
118
|
+
*/
|
|
119
|
+
/**
|
|
120
|
+
* Builds the virtual CommonJS-style module contents the `sdk-redirect`
|
|
121
|
+
* esbuild plugin loads for every `@apps-in-toss/web-framework` import.
|
|
122
|
+
*
|
|
123
|
+
* Pulled out to a pure, exported function (rather than inlined at the
|
|
124
|
+
* `build.onLoad` call site) so unit tests can assert the exact composition
|
|
125
|
+
* order (pacing wraps the raw SDK; the stub wraps the paced result) as a
|
|
126
|
+
* string-level contract, independent of esbuild — esbuild cannot run inside
|
|
127
|
+
* this repo's jsdom vitest environment (see `bundleTestFile`'s module doc's
|
|
128
|
+
* lazy-import note), so a real bundling round-trip is out of unit-test reach
|
|
129
|
+
* here; `wrapSdkWithStub`/`wrapWithMethodPacing`'s own composed-call behavior
|
|
130
|
+
* is covered directly (see `bundle.test.ts`).
|
|
131
|
+
*
|
|
132
|
+
* Generates a virtual CommonJS-style module so that esbuild does NOT perform
|
|
133
|
+
* strict named-export matching. When `format:'iife'` bundles a CJS module, it
|
|
134
|
+
* wraps it with its own `__toCommonJS` helper and satisfies named imports via
|
|
135
|
+
* property access on the `module.exports` object — which is our Proxy. This
|
|
136
|
+
* means `import { getPlatformOS } from '...'` becomes
|
|
137
|
+
* `__proxy.getPlatformOS` at runtime, which correctly reads from
|
|
138
|
+
* `window.__sdk`.
|
|
139
|
+
*
|
|
140
|
+
* The bridge-stub and method-pace imports are STATIC imports of the real
|
|
141
|
+
* `bridge-stub.ts`/`method-pace.ts` modules (not inlined JS) so esbuild
|
|
142
|
+
* bundles each once and the fixture registry / pacing registry have a single
|
|
143
|
+
* source of truth shared with their own unit tests.
|
|
144
|
+
*
|
|
145
|
+
* Exported for unit testing.
|
|
106
146
|
*/
|
|
147
|
+
function buildSdkRedirectModuleContents() {
|
|
148
|
+
return `
|
|
149
|
+
import { wrapSdkWithStub, isStubBlockingEnabled } from ${JSON.stringify(getBridgeStubPath())};
|
|
150
|
+
import { wrapWithMethodPacing, getPaceMethodMs } from ${JSON.stringify(getMethodPacePath())};
|
|
151
|
+
var __rawSdk = (typeof window !== 'undefined' && window.__sdk)
|
|
152
|
+
? window.__sdk
|
|
153
|
+
: new Proxy({}, {
|
|
154
|
+
get: function(_t, p) {
|
|
155
|
+
throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
var __pacedSdk = wrapWithMethodPacing(__rawSdk, getPaceMethodMs());
|
|
159
|
+
var __proxy = wrapSdkWithStub(__pacedSdk, isStubBlockingEnabled());
|
|
160
|
+
module.exports = __proxy;
|
|
161
|
+
`;
|
|
162
|
+
}
|
|
107
163
|
function sdkRedirectPlugin() {
|
|
108
164
|
return {
|
|
109
165
|
name: "sdk-redirect",
|
|
@@ -116,18 +172,7 @@ function sdkRedirectPlugin() {
|
|
|
116
172
|
filter: /.*/,
|
|
117
173
|
namespace: "sdk-redirect"
|
|
118
174
|
}, () => ({
|
|
119
|
-
contents:
|
|
120
|
-
import { wrapSdkWithStub, isStubBlockingEnabled } from ${JSON.stringify(getBridgeStubPath())};
|
|
121
|
-
var __rawSdk = (typeof window !== 'undefined' && window.__sdk)
|
|
122
|
-
? window.__sdk
|
|
123
|
-
: new Proxy({}, {
|
|
124
|
-
get: function(_t, p) {
|
|
125
|
-
throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
var __proxy = wrapSdkWithStub(__rawSdk, isStubBlockingEnabled());
|
|
129
|
-
module.exports = __proxy;
|
|
130
|
-
`,
|
|
175
|
+
contents: buildSdkRedirectModuleContents(),
|
|
131
176
|
loader: "js",
|
|
132
177
|
resolveDir: process.cwd()
|
|
133
178
|
}));
|
|
@@ -328,6 +373,14 @@ function getBridgeStubPath() {
|
|
|
328
373
|
return getPageSideModulePath("bridge-stub");
|
|
329
374
|
}
|
|
330
375
|
/**
|
|
376
|
+
* Absolute path to the per-method pacing wrapper (devtools#769) —
|
|
377
|
+
* `wrapWithMethodPacing`/`getPaceMethodMs`, statically imported by the
|
|
378
|
+
* `sdk-redirect` virtual module.
|
|
379
|
+
*/
|
|
380
|
+
function getMethodPacePath() {
|
|
381
|
+
return getPageSideModulePath("method-pace");
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
331
384
|
* Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.
|
|
332
385
|
*
|
|
333
386
|
* The IIFE installs `window.__testBundle` (or the custom `globalName`) with:
|
|
@@ -381,6 +434,6 @@ async function bundleTestFile(absPath, opts) {
|
|
|
381
434
|
};
|
|
382
435
|
}
|
|
383
436
|
//#endregion
|
|
384
|
-
export { bundleTestFile };
|
|
437
|
+
export { buildSdkRedirectModuleContents, bundleTestFile };
|
|
385
438
|
|
|
386
439
|
//# sourceMappingURL=bundle.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle.js","names":["path"],"sources":["../../src/test-runner/bundle.ts"],"sourcesContent":["/**\n * esbuild-based bundler for user test files.\n *\n * Bundles a single test file into a self-contained IIFE string that can be\n * injected into a WebView via `Runtime.evaluate`. The bundle includes the\n * test runtime (`runtime.ts`), which provides `describe/it/test/expect` and\n * the `runTestModule(factory)` entry point.\n *\n * ## How the wiring works\n *\n * The bundle exposes two exports on `globalThis.__testBundle`:\n * - `runTestModule` — the runtime's entry function.\n * - `__userFactory` — an async function whose body is the user's top-level\n * test registration code (describe/it/test calls).\n *\n * The Node-side RPC (`rpc.ts`) calls:\n * `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`\n *\n * `runTestModule` then installs `describe/it/test/expect` as globals, invokes\n * the factory (which registers all tests), runs them, and returns a `RunReport`.\n *\n * ## Why a factory wrapper is needed\n *\n * Naively adding the runtime to `entryPoints` and bundling the user file would\n * fail for two reasons:\n * 1. `describe/it/test/expect` from the runtime are module-local in the IIFE\n * scope. The user's top-level `describe(...)` calls expect them as globals —\n * they are not globals until `runTestModule` installs them.\n * 2. Even with globals pre-installed, the user file runs at IIFE-evaluation\n * time, before the RPC layer calls `runTestModule` to reset state and start\n * the test clock.\n *\n * The factory approach solves both: the user's registration code is deferred\n * into a function that `runTestModule` calls AFTER installing the globals.\n *\n * ## Factory extraction algorithm\n *\n * The `userFactoryPlugin` reads the user file and splits lines into:\n * - **top-level**: `import …` and re-export lines — kept at module scope\n * (the only valid position for static `import` in ESM).\n * - **body**: all other statements — moved into the body of the exported\n * `__userFactory` async function.\n *\n * esbuild processes the re-generated module, following each static import\n * through the normal dependency graph (including the SDK-redirect plugin).\n *\n * ## SDK redirect\n *\n * Imports of `@apps-in-toss/web-framework` (and sub-paths) are intercepted via\n * the `sdkRedirectPlugin` and replaced with a virtual `window.__sdk` proxy that\n * `src/in-app/auto.ts` installs at runtime. This works for both 2.x and 3.x SDK.\n *\n * SECRET-HANDLING: the returned bundle code is caller-managed; never log it.\n */\n\nimport { accessSync } from 'node:fs';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n// esbuild is imported for TYPES only at module scope; the runtime module is\n// loaded lazily inside `bundleTestFile` via dynamic import. esbuild runs a\n// startup invariant check (`TextEncoder().encode('') instanceof Uint8Array`)\n// that fails in a jsdom realm — a static import would break every MCP/test\n// module that merely *imports* this file's transitive graph (e.g. debug-server →\n// run_tests). Lazy load keeps esbuild off the import graph until a bundle is\n// actually built, and mirrors the cloudflared/chii dynamic-import precedent.\nimport type * as esbuild from 'esbuild';\n\n/** Options accepted by `bundleTestFile`. */\nexport interface BundleOptions {\n /**\n * Additional esbuild `external` patterns. The SDK package\n * (`@apps-in-toss/web-framework` and `@apps-in-toss/web-framework/*`) is\n * always handled by the SDK redirect plugin — callers may add more patterns\n * to be left as globals.\n */\n extraExternals?: string[];\n /**\n * Global name for the IIFE output object. Defaults to `__testBundle`.\n * The runtime entry uses this to call `__testBundle.runTestModule(__userFactory)`.\n */\n globalName?: string;\n}\n\n/**\n * The result of bundling a test file.\n * `code` is a self-contained IIFE string ready for `Runtime.evaluate`.\n */\nexport interface BundleResult {\n code: string;\n warnings: string[];\n}\n\n/** The SDK package name that mini-app test code imports from. */\nconst SDK_PACKAGE = '@apps-in-toss/web-framework';\n\n/**\n * Names the runtime installs as globals before invoking the user factory.\n * The `vitest` virtual module re-exports each as a lazy getter that reads from\n * `globalThis` at access time. Keep in sync with the globals installed in\n * `runtime.ts#runTestModule`.\n */\nconst VITEST_GLOBAL_NAMES = [\n 'describe',\n 'it',\n 'test',\n 'expect',\n 'beforeAll',\n 'afterAll',\n 'beforeEach',\n 'afterEach',\n 'vi',\n] as const;\n\n/**\n * Matches the bare SDK package and any sub-path import\n * (`@apps-in-toss/web-framework`, `@apps-in-toss/web-framework/foo`).\n * Built from {@link SDK_PACKAGE} so the package name has a single source.\n */\nconst SDK_IMPORT_FILTER = new RegExp(`^${SDK_PACKAGE.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}`);\n\n/**\n * esbuild plugin that intercepts SDK imports and redirects them to the\n * `window.__sdk` proxy that `src/in-app/auto.ts` installs at runtime.\n *\n * Strategy: for every import of `@apps-in-toss/web-framework` (or sub-paths),\n * esbuild resolves it to a virtual module that re-exports all named exports\n * via `window.__sdk[name]`. This avoids bundling the real SDK (which may not\n * be available in the test environment) while still making named imports work.\n *\n * If `window.__sdk` is absent (non-dog-food build), every access throws a\n * descriptive error rather than returning `undefined` silently.\n *\n * Bridge-stub interception (devtools#740, DT-2): the virtual module runs the\n * resolved `window.__sdk` object through `wrapSdkWithStub` before exporting\n * it. `wrapSdkWithStub` is a pass-through (returns the same object,\n * unwrapped) unless `isStubBlockingEnabled()` reads `true` off\n * `globalThis.__AIT_STUB_BLOCKING__` — an opt-in flag injected the same way\n * `__AIT_CELL__` is (`cell.ts#injectGlobals`, wired from the CLI's\n * `--stub-blocking` flag). Default off => zero behavior change: every existing\n * (non-stub) test run resolves the exact same `window.__sdk` reference it did\n * before this plugin learned about stubbing.\n */\nfunction sdkRedirectPlugin(): esbuild.Plugin {\n return {\n name: 'sdk-redirect',\n setup(build) {\n // Match the bare package and any sub-path imports\n build.onResolve({ filter: SDK_IMPORT_FILTER }, (args) => ({\n path: args.path,\n namespace: 'sdk-redirect',\n }));\n\n build.onLoad({ filter: /.*/, namespace: 'sdk-redirect' }, () => ({\n // Generate a virtual CommonJS-style module so that esbuild does NOT perform\n // strict named-export matching. When `format:'iife'` bundles a CJS module,\n // it wraps it with its own __toCommonJS helper and satisfies named imports\n // via property access on the module.exports object — which is our Proxy.\n // This means `import { getPlatformOS } from '...'` becomes\n // `__proxy.getPlatformOS` at runtime, which correctly reads from window.__sdk.\n //\n // The bridge-stub import is a STATIC import of the real `bridge-stub.ts`\n // module (not inlined JS) so esbuild bundles it once and the fixture\n // registry has a single source of truth shared with its own unit tests.\n contents: `\nimport { wrapSdkWithStub, isStubBlockingEnabled } from ${JSON.stringify(getBridgeStubPath())};\nvar __rawSdk = (typeof window !== 'undefined' && window.__sdk)\n ? window.__sdk\n : new Proxy({}, {\n get: function(_t, p) {\n throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));\n }\n });\nvar __proxy = wrapSdkWithStub(__rawSdk, isStubBlockingEnabled());\nmodule.exports = __proxy;\n`,\n loader: 'js',\n // resolveDir is required so esbuild can resolve the absolute\n // getBridgeStubPath() import above from a filesystem base — a virtual\n // module has no implicit directory of its own. process.cwd() is a\n // safe base for an ABSOLUTE import specifier (it is never used to\n // resolve anything relative).\n resolveDir: process.cwd(),\n }));\n },\n };\n}\n\n/**\n * esbuild plugin that intercepts `import … from 'vitest'` and replaces it with\n * a virtual module that delegates every named import to `globalThis` at ACCESS\n * time (not at bundle-evaluation time).\n *\n * The runtime installs `describe/it/test/expect/beforeAll/afterAll/beforeEach/\n * afterEach/vi` as globals inside `runTestModule`, which runs AFTER the bundle\n * IIFE is evaluated. A value-copy redirect (`export var describe =\n * globalThis.describe`) would therefore capture `undefined` at evaluation time\n * and the user's `describe(...)` calls would be no-ops — registering zero tests.\n *\n * The fix defers the lookup to call time using per-name **getter** exports.\n * We emit a CommonJS module that:\n * 1. sets `__esModule = true` so esbuild's `__toESM` interop maps each named\n * import directly to a property access on the module (NOT wrapped under a\n * `default` shim — which is what happens for a bare Proxy whose own-keys\n * are empty, leaving every named import `undefined`);\n * 2. defines each global name as a getter that reads `globalThis[name]` on\n * every access. So `import { describe } from 'vitest'` compiles to\n * `import_vitest.describe`, whose getter returns the real `describe` only\n * when the factory calls it — after `runTestModule` installs the globals.\n *\n * A plain `module.exports = new Proxy(...)` does NOT work here: esbuild routes\n * the virtual module through `__toESM`, which enumerates own-keys (none on an\n * empty Proxy target) and therefore exposes zero named exports. Explicit getter\n * properties give `__toESM` real keys to map while keeping access lazy.\n */\nfunction vitestRedirectPlugin(): esbuild.Plugin {\n return {\n name: 'vitest-redirect',\n setup(build) {\n build.onResolve({ filter: /^vitest$/ }, () => ({\n path: 'vitest',\n namespace: 'vitest-redirect',\n }));\n\n build.onLoad({ filter: /^vitest$/, namespace: 'vitest-redirect' }, () => {\n const getters = VITEST_GLOBAL_NAMES.map(\n (name) =>\n `Object.defineProperty(exports, ${JSON.stringify(name)}, { enumerable: true, get: function() { return globalThis[${JSON.stringify(name)}]; } });`,\n ).join('\\n');\n // __esModule lets esbuild __toESM map named imports to these getters\n // directly. Each getter reads globalThis lazily so the value resolves at\n // call time — after runTestModule installs the globals.\n return {\n contents: `Object.defineProperty(exports, '__esModule', { value: true });\\n${getters}\\n`,\n loader: 'js',\n };\n });\n },\n };\n}\n\n/**\n * esbuild plugin that transforms the user test file into a module that exports\n * an async `__userFactory` function. The factory defers the user's top-level\n * test registration code (describe/it/test calls) so it only runs when\n * `runTestModule(__userFactory)` explicitly invokes it — AFTER the runtime has\n * installed describe/it/test/expect as globals.\n *\n * Algorithm:\n * - Import declarations and re-export statements are kept at module top-level\n * (the only valid ESM position for static `import`). A statement that spans\n * multiple lines — e.g. a named import with one member per line:\n * import {\n * appLogin,\n * getAnonymousKey,\n * } from '@apps-in-toss/web-framework';\n * is tracked as a single block: every line from the opening `import {` /\n * `export {` through the closing `from '…'` (or side-effect `'…'`) line is\n * kept together at top-level. This prevents the member lines and the\n * closing `} from '…'` line from leaking into the factory body, which would\n * leave an unterminated `import {` at module scope (the #678 env3 failure:\n * esbuild threw `Expected \"as\" but found \"{\"` on multi-line SDK imports).\n * - All other lines (describe/it/test calls, local declarations, etc.) are\n * moved into the body of the exported async factory function.\n *\n * This preserves SDK import resolution (the sdk-redirect plugin processes\n * top-level imports normally) while deferring test registration to the factory.\n */\nfunction userFactoryPlugin(absPath: string): esbuild.Plugin {\n const NAMESPACE = 'user-test-factory';\n return {\n name: 'user-test-factory',\n setup(build) {\n // Resolve the virtual \"user-test-factory\" specifier to our namespace.\n build.onResolve({ filter: /^user-test-factory$/ }, () => ({\n path: absPath,\n namespace: NAMESPACE,\n }));\n\n // Load the user file, split imports from body, wrap body in the factory.\n build.onLoad({ filter: /.*/, namespace: NAMESPACE }, async (args) => {\n const source = await fs.readFile(args.path, 'utf8');\n const lines = source.split('\\n');\n\n const topLevelLines: string[] = [];\n const bodyLines: string[] = [];\n\n // Matches `export` value declarations that cannot appear inside a\n // function body. We strip the `export` keyword so they become plain\n // declarations inside the factory.\n const EXPORT_DECLARATION_RE =\n /^(export\\s+)(default\\s+|async\\s+function\\s+|function\\s+|class\\s+|const\\s+|let\\s+|var\\s+)/;\n\n // True when `trimmed` begins a static `import` statement.\n const isImportStart = (trimmed: string): boolean =>\n trimmed.startsWith('import ') ||\n trimmed.startsWith('import{') ||\n trimmed.startsWith(\"import'\") ||\n trimmed.startsWith('import\"');\n\n // A module-scope `import`/`export … from` statement is \"complete\" on a\n // single line when it ends with a quoted module specifier (optionally\n // followed by `;`/whitespace), e.g. `… from '@x';` or side-effect\n // `import './x';`. A line ending in `{` or `,` (the common multi-line\n // named-import shape) is therefore NOT complete and must accumulate\n // further lines until the closing `from '…'` line.\n const endsStatement = (trimmed: string): boolean =>\n /['\"]\\s*;?\\s*$/.test(trimmed.replace(/\\/\\/.*$/, '').trimEnd());\n\n // When set, we are inside an unterminated multi-line import/re-export\n // block: every subsequent line stays at top level until the block ends.\n let inImportBlock = false;\n\n for (const line of lines) {\n const trimmed = line.trimStart();\n const indent = line.slice(0, line.length - trimmed.length);\n\n // Continuation of a multi-line import / re-export block — keep at\n // top level. The block ends on the line that terminates the\n // statement (closing `from '…'` / side-effect `'…'`).\n if (inImportBlock) {\n topLevelLines.push(line);\n if (endsStatement(trimmed)) {\n inImportBlock = false;\n }\n continue;\n }\n\n // Static import declarations must stay at module top level\n // (the ESM spec forbids `import` inside a function body). If the\n // statement does not terminate on this line, open an import block so\n // the member lines and closing `} from '…'` line stay top-level too.\n if (isImportStart(trimmed)) {\n topLevelLines.push(line);\n if (!endsStatement(trimmed)) {\n inImportBlock = true;\n }\n } else if (trimmed.startsWith('export ')) {\n // Determine whether this is a re-export (stays top-level) or a value\n // declaration (goes into the factory, export keyword stripped).\n const m = trimmed.match(EXPORT_DECLARATION_RE);\n if (m) {\n // Value declaration — strip `export ` and move into factory body.\n // e.g. `export function hello()` → `function hello()`\n // `export const x = 1` → `const x = 1`\n bodyLines.push(indent + trimmed.slice('export '.length));\n } else {\n // Re-export or `export type { … }` — stays at top level. A\n // multi-line `export { … } from '…'` opens an import block too.\n topLevelLines.push(line);\n if (/\\bfrom\\b/.test(trimmed) ? !endsStatement(trimmed) : trimmed.endsWith('{')) {\n inImportBlock = true;\n }\n }\n } else {\n bodyLines.push(line);\n }\n }\n\n const factoryContent = [\n ...topLevelLines,\n '',\n '// biome-ignore lint: generated factory wrapper',\n 'export default async function __userFactory(): Promise<void> {',\n ...bodyLines.map((l) => ` ${l}`),\n '}',\n ].join('\\n');\n\n return {\n contents: factoryContent,\n loader: 'ts',\n resolveDir: path.dirname(absPath),\n };\n });\n },\n };\n}\n\n/**\n * Returns the absolute filesystem path to a page-side leaf module shipped\n * alongside `bundle.ts` in dist (e.g. `runtime.js`, `bridge-stub.js`) — a\n * fully self-contained module with no further page-side deps of its own.\n *\n * Rolldown code-splitting duplicates this bundling logic into shared chunks\n * emitted at ARBITRARY dist depths: the `devtools-test` CLI pulls it from\n * dist/test-runner/bundle.js (dir = dist/test-runner/), while the `devtools-mcp`\n * daemon (dist/mcp/cli.js) pulls it through a ROOT chunk\n * (dist/debug-server-<hash>.js, dir = dist/). A fixed `..`-hop candidate list\n * is therefore wrong from at least one chunk — the live #697 regression.\n *\n * This resolves WITHOUT assuming chunk depth: from `import.meta.url`'s dir it\n * probes the co-located `<name>.js` and the nested `test-runner/<name>.js`,\n * then ascends one directory at a time (bounded) repeating both probes. The\n * nested probe catches dist/test-runner/<name>.js from the dist/ root level no\n * matter which depth the chunk was hoisted to (root, dist/mcp/, or a future\n * relocation). The build always emits dist/test-runner/<name>.js (tsdown entry\n * `'test-runner/<name>'`; guarded by scripts/check-test-runner-dist.sh for\n * `runtime`).\n *\n * An ABSOLUTE path is returned deliberately: esbuild loads it as a literal file\n * read, bypassing Node module resolution entirely, so this works identically in\n * the npx-daemon context (its own dist tree) and the consumer-CLI context\n * (the mini-app's installed @ait-co/devtools dist) — neither needs the package\n * to be node-resolvable from the caller.\n *\n * @param moduleName - The leaf module's basename without extension, e.g.\n * `'runtime'` or `'bridge-stub'`.\n */\nfunction getPageSideModulePath(moduleName: string): string {\n const startDir = path.dirname(fileURLToPath(import.meta.url));\n // .js first so a SHIPPED chunk never feeds esbuild raw TypeScript; the .ts\n // probe is last, for the source-tree (tsx) dev case where the module sits\n // beside bundle.ts and no dist exists.\n const RELATIVE_PROBES: string[][] = [\n [`${moduleName}.js`],\n ['test-runner', `${moduleName}.js`],\n [`${moduleName}.ts`],\n ['test-runner', `${moduleName}.ts`],\n ];\n // Bounded ascent: a package's dist is only a few levels deep. 12 is far more\n // than any real layout (root chunk → dist is 0 hops; dist/mcp → dist is 1)\n // and terminates well before the filesystem root in every case.\n let dir = startDir;\n for (let i = 0; i < 12; i++) {\n for (const segs of RELATIVE_PROBES) {\n const candidate = path.join(dir, ...segs);\n try {\n accessSync(candidate);\n return candidate;\n } catch {\n // try next probe\n }\n }\n const parent = path.dirname(dir);\n if (parent === dir) break; // reached filesystem root\n dir = parent;\n }\n // Nothing matched — return the co-located path so esbuild emits a clear\n // \"Could not resolve\" against a concrete path rather than failing cryptically.\n return path.join(startDir, `${moduleName}.js`);\n}\n\n/** Absolute path to the test-runner page-side runtime (describe/it/test/expect). */\nfunction getRuntimePath(): string {\n return getPageSideModulePath('runtime');\n}\n\n/**\n * Absolute path to the bridge-stub interceptor (devtools#740, DT-2) —\n * `wrapSdkWithStub`/`isStubBlockingEnabled`, statically imported by the\n * `sdk-redirect` virtual module.\n */\nfunction getBridgeStubPath(): string {\n return getPageSideModulePath('bridge-stub');\n}\n\n/**\n * Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.\n *\n * The IIFE installs `window.__testBundle` (or the custom `globalName`) with:\n * - `runTestModule` — the runtime entry (from `runtime.ts`).\n * - `__userFactory` — an async function wrapping the user's test registration\n * code so it runs AFTER `runTestModule` installs the globals.\n *\n * Callers (rpc.ts) invoke:\n * `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`\n *\n * @param absPath - Absolute path to the user test file.\n * @param opts - Optional bundling overrides.\n */\nexport async function bundleTestFile(absPath: string, opts?: BundleOptions): Promise<BundleResult> {\n const globalName = opts?.globalName ?? '__testBundle';\n const extraExternals = opts?.extraExternals ?? [];\n\n // Lazy load esbuild at call time (see the module-scope import note).\n const esbuild = await import('esbuild');\n const runtimePath = getRuntimePath();\n\n // Stdin wrapper: import the runtime and the user factory, re-export both.\n // esbuild follows the static imports to include runtime.ts and the user file\n // (via the userFactoryPlugin) in the single IIFE output.\n const wrapperContent = [\n `import { runTestModule } from ${JSON.stringify(runtimePath)};`,\n `import __userFactory from \"user-test-factory\";`,\n `export { runTestModule, __userFactory };`,\n ].join('\\n');\n\n const result = await esbuild.build({\n stdin: {\n contents: wrapperContent,\n loader: 'ts',\n // resolveDir is used for relative imports from the wrapper. Since the\n // wrapper only imports absolute paths (runtimePath) and the virtual\n // \"user-test-factory\" specifier (resolved by plugin), the directory\n // doesn't matter — but we still provide a sensible default.\n resolveDir: path.dirname(absPath),\n },\n bundle: true,\n format: 'iife',\n globalName,\n platform: 'browser',\n target: 'es2022',\n write: false,\n plugins: [userFactoryPlugin(absPath), vitestRedirectPlugin(), sdkRedirectPlugin()],\n external: extraExternals,\n treeShaking: true,\n // Ensure the IIFE result is always reachable via globalThis regardless of\n // the evaluation context. esbuild's `globalName` emits:\n // var __testBundle = (() => { ... })();\n // When `Runtime.evaluate` runs this bundle code inside an outer wrapper\n // (rpc.ts's async IIFE), `var` creates a local variable — NOT a global\n // property — so `globalThis.__testBundle` stays `undefined`. The footer\n // explicitly assigns the local variable to `globalThis` to close that gap.\n footer: {\n js: `globalThis[${JSON.stringify(globalName)}] = ${globalName};`,\n },\n });\n\n const warnings = result.warnings.map(\n (w) =>\n `${path.relative(process.cwd(), w.location?.file ?? '')}:${w.location?.line ?? '?'}: ${w.text}`,\n );\n\n const outputFile = result.outputFiles?.[0];\n if (!outputFile) {\n throw new Error('bundleTestFile: esbuild produced no output — check entryPoints');\n }\n\n return { code: outputFile.text, warnings };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"bundle.js","names":["path"],"sources":["../../src/test-runner/bundle.ts"],"sourcesContent":["/**\n * esbuild-based bundler for user test files.\n *\n * Bundles a single test file into a self-contained IIFE string that can be\n * injected into a WebView via `Runtime.evaluate`. The bundle includes the\n * test runtime (`runtime.ts`), which provides `describe/it/test/expect` and\n * the `runTestModule(factory)` entry point.\n *\n * ## How the wiring works\n *\n * The bundle exposes two exports on `globalThis.__testBundle`:\n * - `runTestModule` — the runtime's entry function.\n * - `__userFactory` — an async function whose body is the user's top-level\n * test registration code (describe/it/test calls).\n *\n * The Node-side RPC (`rpc.ts`) calls:\n * `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`\n *\n * `runTestModule` then installs `describe/it/test/expect` as globals, invokes\n * the factory (which registers all tests), runs them, and returns a `RunReport`.\n *\n * ## Why a factory wrapper is needed\n *\n * Naively adding the runtime to `entryPoints` and bundling the user file would\n * fail for two reasons:\n * 1. `describe/it/test/expect` from the runtime are module-local in the IIFE\n * scope. The user's top-level `describe(...)` calls expect them as globals —\n * they are not globals until `runTestModule` installs them.\n * 2. Even with globals pre-installed, the user file runs at IIFE-evaluation\n * time, before the RPC layer calls `runTestModule` to reset state and start\n * the test clock.\n *\n * The factory approach solves both: the user's registration code is deferred\n * into a function that `runTestModule` calls AFTER installing the globals.\n *\n * ## Factory extraction algorithm\n *\n * The `userFactoryPlugin` reads the user file and splits lines into:\n * - **top-level**: `import …` and re-export lines — kept at module scope\n * (the only valid position for static `import` in ESM).\n * - **body**: all other statements — moved into the body of the exported\n * `__userFactory` async function.\n *\n * esbuild processes the re-generated module, following each static import\n * through the normal dependency graph (including the SDK-redirect plugin).\n *\n * ## SDK redirect\n *\n * Imports of `@apps-in-toss/web-framework` (and sub-paths) are intercepted via\n * the `sdkRedirectPlugin` and replaced with a virtual `window.__sdk` proxy that\n * `src/in-app/auto.ts` installs at runtime. This works for both 2.x and 3.x SDK.\n *\n * SECRET-HANDLING: the returned bundle code is caller-managed; never log it.\n */\n\nimport { accessSync } from 'node:fs';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n// esbuild is imported for TYPES only at module scope; the runtime module is\n// loaded lazily inside `bundleTestFile` via dynamic import. esbuild runs a\n// startup invariant check (`TextEncoder().encode('') instanceof Uint8Array`)\n// that fails in a jsdom realm — a static import would break every MCP/test\n// module that merely *imports* this file's transitive graph (e.g. debug-server →\n// run_tests). Lazy load keeps esbuild off the import graph until a bundle is\n// actually built, and mirrors the cloudflared/chii dynamic-import precedent.\nimport type * as esbuild from 'esbuild';\n\n/** Options accepted by `bundleTestFile`. */\nexport interface BundleOptions {\n /**\n * Additional esbuild `external` patterns. The SDK package\n * (`@apps-in-toss/web-framework` and `@apps-in-toss/web-framework/*`) is\n * always handled by the SDK redirect plugin — callers may add more patterns\n * to be left as globals.\n */\n extraExternals?: string[];\n /**\n * Global name for the IIFE output object. Defaults to `__testBundle`.\n * The runtime entry uses this to call `__testBundle.runTestModule(__userFactory)`.\n */\n globalName?: string;\n}\n\n/**\n * The result of bundling a test file.\n * `code` is a self-contained IIFE string ready for `Runtime.evaluate`.\n */\nexport interface BundleResult {\n code: string;\n warnings: string[];\n}\n\n/** The SDK package name that mini-app test code imports from. */\nconst SDK_PACKAGE = '@apps-in-toss/web-framework';\n\n/**\n * Names the runtime installs as globals before invoking the user factory.\n * The `vitest` virtual module re-exports each as a lazy getter that reads from\n * `globalThis` at access time. Keep in sync with the globals installed in\n * `runtime.ts#runTestModule`.\n */\nconst VITEST_GLOBAL_NAMES = [\n 'describe',\n 'it',\n 'test',\n 'expect',\n 'beforeAll',\n 'afterAll',\n 'beforeEach',\n 'afterEach',\n 'vi',\n] as const;\n\n/**\n * Matches the bare SDK package and any sub-path import\n * (`@apps-in-toss/web-framework`, `@apps-in-toss/web-framework/foo`).\n * Built from {@link SDK_PACKAGE} so the package name has a single source.\n */\nconst SDK_IMPORT_FILTER = new RegExp(`^${SDK_PACKAGE.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}`);\n\n/**\n * esbuild plugin that intercepts SDK imports and redirects them to the\n * `window.__sdk` proxy that `src/in-app/auto.ts` installs at runtime.\n *\n * Strategy: for every import of `@apps-in-toss/web-framework` (or sub-paths),\n * esbuild resolves it to a virtual module that re-exports all named exports\n * via `window.__sdk[name]`. This avoids bundling the real SDK (which may not\n * be available in the test environment) while still making named imports work.\n *\n * If `window.__sdk` is absent (non-dog-food build), every access throws a\n * descriptive error rather than returning `undefined` silently.\n *\n * Bridge-stub interception (devtools#740, DT-2): the virtual module runs the\n * resolved `window.__sdk` object through `wrapSdkWithStub` before exporting\n * it. `wrapSdkWithStub` is a pass-through (returns the same object,\n * unwrapped) unless `isStubBlockingEnabled()` reads `true` off\n * `globalThis.__AIT_STUB_BLOCKING__` — an opt-in flag injected the same way\n * `__AIT_CELL__` is (`cell.ts#injectGlobals`, wired from the CLI's\n * `--stub-blocking` flag). Default off => zero behavior change: every existing\n * (non-stub) test run resolves the exact same `window.__sdk` reference it did\n * before this plugin learned about stubbing.\n *\n * Per-method pacing (devtools#769): BEFORE the stub wrap, `window.__sdk` is\n * run through `wrapWithMethodPacing`, which enforces a minimum interval\n * between calls to the SAME named SDK function (`getPaceMethodMs()` reads the\n * `__AIT_PACE_METHOD_MS__` page global — see `method-pace.ts`). Composition\n * order is pacing INSIDE the stub (`wrapSdkWithStub(wrapWithMethodPacing(sdk,\n * gap), enabled)`): a stubbed name resolves instantly from\n * `bridge-stub.ts`'s fixtures without ever reaching the paced wrapper, and\n * only calls that fall through to the real bridge — the ones that can\n * actually trigger `APP_BRIDGE_THROTTLED` — pay the pacing cost. `gapMs <= 0`\n * (default, no `--pace-method` / non-2.x cell) is a no-op fast path — see\n * `method-pace.ts#wrapWithMethodPacing`'s doc for the full contract.\n */\n/**\n * Builds the virtual CommonJS-style module contents the `sdk-redirect`\n * esbuild plugin loads for every `@apps-in-toss/web-framework` import.\n *\n * Pulled out to a pure, exported function (rather than inlined at the\n * `build.onLoad` call site) so unit tests can assert the exact composition\n * order (pacing wraps the raw SDK; the stub wraps the paced result) as a\n * string-level contract, independent of esbuild — esbuild cannot run inside\n * this repo's jsdom vitest environment (see `bundleTestFile`'s module doc's\n * lazy-import note), so a real bundling round-trip is out of unit-test reach\n * here; `wrapSdkWithStub`/`wrapWithMethodPacing`'s own composed-call behavior\n * is covered directly (see `bundle.test.ts`).\n *\n * Generates a virtual CommonJS-style module so that esbuild does NOT perform\n * strict named-export matching. When `format:'iife'` bundles a CJS module, it\n * wraps it with its own `__toCommonJS` helper and satisfies named imports via\n * property access on the `module.exports` object — which is our Proxy. This\n * means `import { getPlatformOS } from '...'` becomes\n * `__proxy.getPlatformOS` at runtime, which correctly reads from\n * `window.__sdk`.\n *\n * The bridge-stub and method-pace imports are STATIC imports of the real\n * `bridge-stub.ts`/`method-pace.ts` modules (not inlined JS) so esbuild\n * bundles each once and the fixture registry / pacing registry have a single\n * source of truth shared with their own unit tests.\n *\n * Exported for unit testing.\n */\nexport function buildSdkRedirectModuleContents(): string {\n return `\nimport { wrapSdkWithStub, isStubBlockingEnabled } from ${JSON.stringify(getBridgeStubPath())};\nimport { wrapWithMethodPacing, getPaceMethodMs } from ${JSON.stringify(getMethodPacePath())};\nvar __rawSdk = (typeof window !== 'undefined' && window.__sdk)\n ? window.__sdk\n : new Proxy({}, {\n get: function(_t, p) {\n throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));\n }\n });\nvar __pacedSdk = wrapWithMethodPacing(__rawSdk, getPaceMethodMs());\nvar __proxy = wrapSdkWithStub(__pacedSdk, isStubBlockingEnabled());\nmodule.exports = __proxy;\n`;\n}\n\nfunction sdkRedirectPlugin(): esbuild.Plugin {\n return {\n name: 'sdk-redirect',\n setup(build) {\n // Match the bare package and any sub-path imports\n build.onResolve({ filter: SDK_IMPORT_FILTER }, (args) => ({\n path: args.path,\n namespace: 'sdk-redirect',\n }));\n\n build.onLoad({ filter: /.*/, namespace: 'sdk-redirect' }, () => ({\n contents: buildSdkRedirectModuleContents(),\n loader: 'js',\n // resolveDir is required so esbuild can resolve the absolute\n // getBridgeStubPath()/getMethodPacePath() imports above from a\n // filesystem base — a virtual module has no implicit directory of\n // its own. process.cwd() is a safe base for an ABSOLUTE import\n // specifier (it is never used to resolve anything relative).\n resolveDir: process.cwd(),\n }));\n },\n };\n}\n\n/**\n * esbuild plugin that intercepts `import … from 'vitest'` and replaces it with\n * a virtual module that delegates every named import to `globalThis` at ACCESS\n * time (not at bundle-evaluation time).\n *\n * The runtime installs `describe/it/test/expect/beforeAll/afterAll/beforeEach/\n * afterEach/vi` as globals inside `runTestModule`, which runs AFTER the bundle\n * IIFE is evaluated. A value-copy redirect (`export var describe =\n * globalThis.describe`) would therefore capture `undefined` at evaluation time\n * and the user's `describe(...)` calls would be no-ops — registering zero tests.\n *\n * The fix defers the lookup to call time using per-name **getter** exports.\n * We emit a CommonJS module that:\n * 1. sets `__esModule = true` so esbuild's `__toESM` interop maps each named\n * import directly to a property access on the module (NOT wrapped under a\n * `default` shim — which is what happens for a bare Proxy whose own-keys\n * are empty, leaving every named import `undefined`);\n * 2. defines each global name as a getter that reads `globalThis[name]` on\n * every access. So `import { describe } from 'vitest'` compiles to\n * `import_vitest.describe`, whose getter returns the real `describe` only\n * when the factory calls it — after `runTestModule` installs the globals.\n *\n * A plain `module.exports = new Proxy(...)` does NOT work here: esbuild routes\n * the virtual module through `__toESM`, which enumerates own-keys (none on an\n * empty Proxy target) and therefore exposes zero named exports. Explicit getter\n * properties give `__toESM` real keys to map while keeping access lazy.\n */\nfunction vitestRedirectPlugin(): esbuild.Plugin {\n return {\n name: 'vitest-redirect',\n setup(build) {\n build.onResolve({ filter: /^vitest$/ }, () => ({\n path: 'vitest',\n namespace: 'vitest-redirect',\n }));\n\n build.onLoad({ filter: /^vitest$/, namespace: 'vitest-redirect' }, () => {\n const getters = VITEST_GLOBAL_NAMES.map(\n (name) =>\n `Object.defineProperty(exports, ${JSON.stringify(name)}, { enumerable: true, get: function() { return globalThis[${JSON.stringify(name)}]; } });`,\n ).join('\\n');\n // __esModule lets esbuild __toESM map named imports to these getters\n // directly. Each getter reads globalThis lazily so the value resolves at\n // call time — after runTestModule installs the globals.\n return {\n contents: `Object.defineProperty(exports, '__esModule', { value: true });\\n${getters}\\n`,\n loader: 'js',\n };\n });\n },\n };\n}\n\n/**\n * esbuild plugin that transforms the user test file into a module that exports\n * an async `__userFactory` function. The factory defers the user's top-level\n * test registration code (describe/it/test calls) so it only runs when\n * `runTestModule(__userFactory)` explicitly invokes it — AFTER the runtime has\n * installed describe/it/test/expect as globals.\n *\n * Algorithm:\n * - Import declarations and re-export statements are kept at module top-level\n * (the only valid ESM position for static `import`). A statement that spans\n * multiple lines — e.g. a named import with one member per line:\n * import {\n * appLogin,\n * getAnonymousKey,\n * } from '@apps-in-toss/web-framework';\n * is tracked as a single block: every line from the opening `import {` /\n * `export {` through the closing `from '…'` (or side-effect `'…'`) line is\n * kept together at top-level. This prevents the member lines and the\n * closing `} from '…'` line from leaking into the factory body, which would\n * leave an unterminated `import {` at module scope (the #678 env3 failure:\n * esbuild threw `Expected \"as\" but found \"{\"` on multi-line SDK imports).\n * - All other lines (describe/it/test calls, local declarations, etc.) are\n * moved into the body of the exported async factory function.\n *\n * This preserves SDK import resolution (the sdk-redirect plugin processes\n * top-level imports normally) while deferring test registration to the factory.\n */\nfunction userFactoryPlugin(absPath: string): esbuild.Plugin {\n const NAMESPACE = 'user-test-factory';\n return {\n name: 'user-test-factory',\n setup(build) {\n // Resolve the virtual \"user-test-factory\" specifier to our namespace.\n build.onResolve({ filter: /^user-test-factory$/ }, () => ({\n path: absPath,\n namespace: NAMESPACE,\n }));\n\n // Load the user file, split imports from body, wrap body in the factory.\n build.onLoad({ filter: /.*/, namespace: NAMESPACE }, async (args) => {\n const source = await fs.readFile(args.path, 'utf8');\n const lines = source.split('\\n');\n\n const topLevelLines: string[] = [];\n const bodyLines: string[] = [];\n\n // Matches `export` value declarations that cannot appear inside a\n // function body. We strip the `export` keyword so they become plain\n // declarations inside the factory.\n const EXPORT_DECLARATION_RE =\n /^(export\\s+)(default\\s+|async\\s+function\\s+|function\\s+|class\\s+|const\\s+|let\\s+|var\\s+)/;\n\n // True when `trimmed` begins a static `import` statement.\n const isImportStart = (trimmed: string): boolean =>\n trimmed.startsWith('import ') ||\n trimmed.startsWith('import{') ||\n trimmed.startsWith(\"import'\") ||\n trimmed.startsWith('import\"');\n\n // A module-scope `import`/`export … from` statement is \"complete\" on a\n // single line when it ends with a quoted module specifier (optionally\n // followed by `;`/whitespace), e.g. `… from '@x';` or side-effect\n // `import './x';`. A line ending in `{` or `,` (the common multi-line\n // named-import shape) is therefore NOT complete and must accumulate\n // further lines until the closing `from '…'` line.\n const endsStatement = (trimmed: string): boolean =>\n /['\"]\\s*;?\\s*$/.test(trimmed.replace(/\\/\\/.*$/, '').trimEnd());\n\n // When set, we are inside an unterminated multi-line import/re-export\n // block: every subsequent line stays at top level until the block ends.\n let inImportBlock = false;\n\n for (const line of lines) {\n const trimmed = line.trimStart();\n const indent = line.slice(0, line.length - trimmed.length);\n\n // Continuation of a multi-line import / re-export block — keep at\n // top level. The block ends on the line that terminates the\n // statement (closing `from '…'` / side-effect `'…'`).\n if (inImportBlock) {\n topLevelLines.push(line);\n if (endsStatement(trimmed)) {\n inImportBlock = false;\n }\n continue;\n }\n\n // Static import declarations must stay at module top level\n // (the ESM spec forbids `import` inside a function body). If the\n // statement does not terminate on this line, open an import block so\n // the member lines and closing `} from '…'` line stay top-level too.\n if (isImportStart(trimmed)) {\n topLevelLines.push(line);\n if (!endsStatement(trimmed)) {\n inImportBlock = true;\n }\n } else if (trimmed.startsWith('export ')) {\n // Determine whether this is a re-export (stays top-level) or a value\n // declaration (goes into the factory, export keyword stripped).\n const m = trimmed.match(EXPORT_DECLARATION_RE);\n if (m) {\n // Value declaration — strip `export ` and move into factory body.\n // e.g. `export function hello()` → `function hello()`\n // `export const x = 1` → `const x = 1`\n bodyLines.push(indent + trimmed.slice('export '.length));\n } else {\n // Re-export or `export type { … }` — stays at top level. A\n // multi-line `export { … } from '…'` opens an import block too.\n topLevelLines.push(line);\n if (/\\bfrom\\b/.test(trimmed) ? !endsStatement(trimmed) : trimmed.endsWith('{')) {\n inImportBlock = true;\n }\n }\n } else {\n bodyLines.push(line);\n }\n }\n\n const factoryContent = [\n ...topLevelLines,\n '',\n '// biome-ignore lint: generated factory wrapper',\n 'export default async function __userFactory(): Promise<void> {',\n ...bodyLines.map((l) => ` ${l}`),\n '}',\n ].join('\\n');\n\n return {\n contents: factoryContent,\n loader: 'ts',\n resolveDir: path.dirname(absPath),\n };\n });\n },\n };\n}\n\n/**\n * Returns the absolute filesystem path to a page-side leaf module shipped\n * alongside `bundle.ts` in dist (e.g. `runtime.js`, `bridge-stub.js`) — a\n * fully self-contained module with no further page-side deps of its own.\n *\n * Rolldown code-splitting duplicates this bundling logic into shared chunks\n * emitted at ARBITRARY dist depths: the `devtools-test` CLI pulls it from\n * dist/test-runner/bundle.js (dir = dist/test-runner/), while the `devtools-mcp`\n * daemon (dist/mcp/cli.js) pulls it through a ROOT chunk\n * (dist/debug-server-<hash>.js, dir = dist/). A fixed `..`-hop candidate list\n * is therefore wrong from at least one chunk — the live #697 regression.\n *\n * This resolves WITHOUT assuming chunk depth: from `import.meta.url`'s dir it\n * probes the co-located `<name>.js` and the nested `test-runner/<name>.js`,\n * then ascends one directory at a time (bounded) repeating both probes. The\n * nested probe catches dist/test-runner/<name>.js from the dist/ root level no\n * matter which depth the chunk was hoisted to (root, dist/mcp/, or a future\n * relocation). The build always emits dist/test-runner/<name>.js (tsdown entry\n * `'test-runner/<name>'`; guarded by scripts/check-test-runner-dist.sh for\n * `runtime`).\n *\n * An ABSOLUTE path is returned deliberately: esbuild loads it as a literal file\n * read, bypassing Node module resolution entirely, so this works identically in\n * the npx-daemon context (its own dist tree) and the consumer-CLI context\n * (the mini-app's installed @ait-co/devtools dist) — neither needs the package\n * to be node-resolvable from the caller.\n *\n * @param moduleName - The leaf module's basename without extension, e.g.\n * `'runtime'` or `'bridge-stub'`.\n */\nfunction getPageSideModulePath(moduleName: string): string {\n const startDir = path.dirname(fileURLToPath(import.meta.url));\n // .js first so a SHIPPED chunk never feeds esbuild raw TypeScript; the .ts\n // probe is last, for the source-tree (tsx) dev case where the module sits\n // beside bundle.ts and no dist exists.\n const RELATIVE_PROBES: string[][] = [\n [`${moduleName}.js`],\n ['test-runner', `${moduleName}.js`],\n [`${moduleName}.ts`],\n ['test-runner', `${moduleName}.ts`],\n ];\n // Bounded ascent: a package's dist is only a few levels deep. 12 is far more\n // than any real layout (root chunk → dist is 0 hops; dist/mcp → dist is 1)\n // and terminates well before the filesystem root in every case.\n let dir = startDir;\n for (let i = 0; i < 12; i++) {\n for (const segs of RELATIVE_PROBES) {\n const candidate = path.join(dir, ...segs);\n try {\n accessSync(candidate);\n return candidate;\n } catch {\n // try next probe\n }\n }\n const parent = path.dirname(dir);\n if (parent === dir) break; // reached filesystem root\n dir = parent;\n }\n // Nothing matched — return the co-located path so esbuild emits a clear\n // \"Could not resolve\" against a concrete path rather than failing cryptically.\n return path.join(startDir, `${moduleName}.js`);\n}\n\n/** Absolute path to the test-runner page-side runtime (describe/it/test/expect). */\nfunction getRuntimePath(): string {\n return getPageSideModulePath('runtime');\n}\n\n/**\n * Absolute path to the bridge-stub interceptor (devtools#740, DT-2) —\n * `wrapSdkWithStub`/`isStubBlockingEnabled`, statically imported by the\n * `sdk-redirect` virtual module.\n */\nfunction getBridgeStubPath(): string {\n return getPageSideModulePath('bridge-stub');\n}\n\n/**\n * Absolute path to the per-method pacing wrapper (devtools#769) —\n * `wrapWithMethodPacing`/`getPaceMethodMs`, statically imported by the\n * `sdk-redirect` virtual module.\n */\nfunction getMethodPacePath(): string {\n return getPageSideModulePath('method-pace');\n}\n\n/**\n * Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.\n *\n * The IIFE installs `window.__testBundle` (or the custom `globalName`) with:\n * - `runTestModule` — the runtime entry (from `runtime.ts`).\n * - `__userFactory` — an async function wrapping the user's test registration\n * code so it runs AFTER `runTestModule` installs the globals.\n *\n * Callers (rpc.ts) invoke:\n * `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`\n *\n * @param absPath - Absolute path to the user test file.\n * @param opts - Optional bundling overrides.\n */\nexport async function bundleTestFile(absPath: string, opts?: BundleOptions): Promise<BundleResult> {\n const globalName = opts?.globalName ?? '__testBundle';\n const extraExternals = opts?.extraExternals ?? [];\n\n // Lazy load esbuild at call time (see the module-scope import note).\n const esbuild = await import('esbuild');\n const runtimePath = getRuntimePath();\n\n // Stdin wrapper: import the runtime and the user factory, re-export both.\n // esbuild follows the static imports to include runtime.ts and the user file\n // (via the userFactoryPlugin) in the single IIFE output.\n const wrapperContent = [\n `import { runTestModule } from ${JSON.stringify(runtimePath)};`,\n `import __userFactory from \"user-test-factory\";`,\n `export { runTestModule, __userFactory };`,\n ].join('\\n');\n\n const result = await esbuild.build({\n stdin: {\n contents: wrapperContent,\n loader: 'ts',\n // resolveDir is used for relative imports from the wrapper. Since the\n // wrapper only imports absolute paths (runtimePath) and the virtual\n // \"user-test-factory\" specifier (resolved by plugin), the directory\n // doesn't matter — but we still provide a sensible default.\n resolveDir: path.dirname(absPath),\n },\n bundle: true,\n format: 'iife',\n globalName,\n platform: 'browser',\n target: 'es2022',\n write: false,\n plugins: [userFactoryPlugin(absPath), vitestRedirectPlugin(), sdkRedirectPlugin()],\n external: extraExternals,\n treeShaking: true,\n // Ensure the IIFE result is always reachable via globalThis regardless of\n // the evaluation context. esbuild's `globalName` emits:\n // var __testBundle = (() => { ... })();\n // When `Runtime.evaluate` runs this bundle code inside an outer wrapper\n // (rpc.ts's async IIFE), `var` creates a local variable — NOT a global\n // property — so `globalThis.__testBundle` stays `undefined`. The footer\n // explicitly assigns the local variable to `globalThis` to close that gap.\n footer: {\n js: `globalThis[${JSON.stringify(globalName)}] = ${globalName};`,\n },\n });\n\n const warnings = result.warnings.map(\n (w) =>\n `${path.relative(process.cwd(), w.location?.file ?? '')}:${w.location?.line ?? '?'}: ${w.text}`,\n );\n\n const outputFile = result.outputFiles?.[0];\n if (!outputFile) {\n throw new Error('bundleTestFile: esbuild produced no output — check entryPoints');\n }\n\n return { code: outputFile.text, warnings };\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as parseCaptureLines, t as AitCaptureLine } from "../capture-
|
|
1
|
+
import { n as parseCaptureLines, t as AitCaptureLine } from "../capture-DsP525OZ.js";
|
|
2
2
|
export { AitCaptureLine, parseCaptureLines };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as createRelayPool, n as RelayConnectionFactory, t as RELAY_POOL_NAME } from "../pool-
|
|
1
|
+
import { i as createRelayPool, n as RelayConnectionFactory, t as RELAY_POOL_NAME } from "../pool-CrP5CPvU.js";
|
|
2
2
|
|
|
3
3
|
//#region src/test-runner/relay-factory.d.ts
|
|
4
4
|
/** Options for {@link createRelayConnectionFactory}. */
|
|
@@ -7,8 +7,36 @@ interface RelayConnectionFactoryOptions {
|
|
|
7
7
|
* intoss-private:// scheme URL from `ait deploy --scheme-only` (env3). The
|
|
8
8
|
* phone cold-loads the candidate bundle this URL points at. SECRET-HANDLING:
|
|
9
9
|
* never logged.
|
|
10
|
+
*
|
|
11
|
+
* Ignored when {@link attachLauncher} is `true` — the env-2 launcher path
|
|
12
|
+
* builds its attach URL from {@link appUrl} + the relay wss, not a scheme URL.
|
|
13
|
+
* Pass `''` in that case.
|
|
10
14
|
*/
|
|
11
15
|
schemeUrl: string;
|
|
16
|
+
/**
|
|
17
|
+
* env-2 (AITC Sandbox PWA) attach mode (devtools#774). When `true`, `open()`
|
|
18
|
+
* builds the attach URL as a **launcher deep-link**
|
|
19
|
+
* (`buildLauncherAttachUrl(appUrl, wssUrl, totp)` — the same env-2 path the
|
|
20
|
+
* MCP daemon takes via `prepareAttach(deps, 'relay-mobile', …)`, issue #378)
|
|
21
|
+
* instead of an intoss-private scheme deep-link. The relay boot, QR
|
|
22
|
+
* rendering, dashboard, and attach-wait are byte-for-byte identical to env 3;
|
|
23
|
+
* only the URL family differs. Omitted/false keeps the env-3 scheme path
|
|
24
|
+
* unchanged (zero behavior change).
|
|
25
|
+
*/
|
|
26
|
+
attachLauncher?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* The consumer dev server's HTTP tunnel URL (e.g. an sdk-example
|
|
29
|
+
* `dev:phone:cdp` `*.trycloudflare.com` URL) that the launcher PWA frames.
|
|
30
|
+
* REQUIRED when {@link attachLauncher} is `true`; ignored otherwise.
|
|
31
|
+
*
|
|
32
|
+
* Threaded to the `relay-mobile` `prepareAttach` branch via the
|
|
33
|
+
* `AIT_TUNNEL_BASE_URL` env var (the same key the MCP daemon reads), so the
|
|
34
|
+
* exact same env-2 assembly runs — no divergent code path.
|
|
35
|
+
*
|
|
36
|
+
* SECRET-HANDLING: this is a tunnel host at the same sensitivity as the relay
|
|
37
|
+
* wss — NEVER logged. It rides only inside the QR/dashboard attach capsule.
|
|
38
|
+
*/
|
|
39
|
+
appUrl?: string;
|
|
12
40
|
/**
|
|
13
41
|
* Project root for the `.ait_relay` secret lookup (read-only). Defaults to
|
|
14
42
|
* `process.cwd()`.
|
|
@@ -45,6 +73,30 @@ interface RelayConnectionFactoryOptions {
|
|
|
45
73
|
* absence the same as `false`). Not a secret.
|
|
46
74
|
*/
|
|
47
75
|
stubBlocking?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Injects `__AIT_PACE_MS__ = paceMs` before the first test bundle runs
|
|
78
|
+
* (devtools#767) — `runtime.ts`'s test-execution loop reads this page
|
|
79
|
+
* global and waits `paceMs` before every test after the first one that
|
|
80
|
+
* actually runs. Optional — omitted/0 means no flag is injected, which is
|
|
81
|
+
* byte-for-byte the pre-#767 behavior (the loop treats an absent/non-
|
|
82
|
+
* positive value as "no pacing"). Not a secret. This is the PAGE-side half
|
|
83
|
+
* of `--pace`; the RUNNER-side (file-to-file) half is a plain `opts.paceMs`
|
|
84
|
+
* forwarded to `runTestFilesOverRelay` by the caller (cli.ts) — this
|
|
85
|
+
* factory only owns the page-global injection.
|
|
86
|
+
*/
|
|
87
|
+
paceMs?: number;
|
|
88
|
+
/**
|
|
89
|
+
* Injects `__AIT_PACE_METHOD_MS__ = paceMethodMs` before the first test
|
|
90
|
+
* bundle runs (devtools#769) — `bundle.ts`'s sdk-redirect virtual module
|
|
91
|
+
* reads this page global via `method-pace.ts#getPaceMethodMs()` and paces
|
|
92
|
+
* every SDK call to a minimum interval BETWEEN calls to the SAME named
|
|
93
|
+
* function, independent of `--pace`'s test-to-test/file-to-file spacing.
|
|
94
|
+
* Optional — omitted/0 means no flag is injected, which is byte-for-byte
|
|
95
|
+
* the pre-#769 behavior (`getPaceMethodMs()` treats an absent/non-positive
|
|
96
|
+
* value as "no pacing", and `wrapWithMethodPacing` returns the SDK object
|
|
97
|
+
* untouched in that case). Not a secret.
|
|
98
|
+
*/
|
|
99
|
+
paceMethodMs?: number;
|
|
48
100
|
/**
|
|
49
101
|
* Overrides the dashboard HTTP server's bind base port (devtools#752).
|
|
50
102
|
* Threaded straight through to `startQrHttpServer`'s `dashboardPort`
|
|
@@ -72,12 +124,13 @@ interface RelayConnectionFactoryOptions {
|
|
|
72
124
|
onQrContent: (textChunks: string[]) => void;
|
|
73
125
|
}
|
|
74
126
|
/**
|
|
75
|
-
* Builds a {@link RelayConnectionFactory} that opens a standalone
|
|
76
|
-
* connection
|
|
127
|
+
* Builds a {@link RelayConnectionFactory} that opens a standalone relay
|
|
128
|
+
* connection for env 3 (intoss-private scheme, default) or env 2 (AITC Sandbox
|
|
129
|
+
* PWA launcher deep-link, when `opts.attachLauncher` is set — devtools#774).
|
|
77
130
|
*
|
|
78
131
|
* `open()` performs the full attach lifecycle and BLOCKS while a human scans
|
|
79
|
-
* the rendered QR with their phone — there is no way around the manual scan
|
|
80
|
-
*
|
|
132
|
+
* the rendered QR with their phone — there is no way around the manual scan.
|
|
133
|
+
* By default the wait is UNBOUNDED (`opts.timeoutMs` omitted): the
|
|
81
134
|
* runner stays up until the user stops it (Ctrl-C/SIGTERM), since QR-scan is
|
|
82
135
|
* a human-paced action with no sound default bound (devtools#735). Passing an
|
|
83
136
|
* explicit `timeoutMs` opts into the old bounded behavior (CI/headless
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","names":[],"sources":["../../src/test-runner/relay-factory.ts","../../src/test-runner/config.ts"],"mappings":";;;;UAiDiB,6BAAA;
|
|
1
|
+
{"version":3,"file":"config.d.ts","names":[],"sources":["../../src/test-runner/relay-factory.ts","../../src/test-runner/config.ts"],"mappings":";;;;UAiDiB,6BAAA;ECrBe;;;;;;AAkBhC;;;EDaE,SAAA;ECZA;;;;;AAkBF;;;;;EDKE,cAAA;ECLuE;;;AAczE;;;;;;;;;EDIE,MAAA;ECEW;AAIb;;;EDDE,WAAA;ECC6C;;;;;;AAkC/C;;;EDzBE,SAAA;ECyBkD;EDvBlD,QAAA;ECuB0E;;;;;EDjB1E,IAAA;IAAS,OAAA;IAAiB,QAAA;EAAA;;;;;;;;;;EAU1B,YAAA;;;;;;;;;;;;EAYA,MAAA;;;;;;;;;;;;EAYA,YAAA;;;;;;;;EAQA,aAAA;;;;;;;;;;;;;;;;;EAiBA,WAAA,GAAc,UAAA;AAAA;;;;;;;;;;;;;;;;;;;iBAqBA,4BAAA,CACd,IAAA,EAAM,6BAAA,GACL,sBAAA;;;;;;UChKc,eAAA;ED8JD;;;;ECzJd,OAAA;ED0JA;;;ECtJA,SAAA;;;;AATF;EAcE,cAAA;AAAA;;UAIe,mBAAA;EACf,OAAA;EACA,SAAA;EACA,cAAA;AAAA;AAHF;;;;;;;AAAA,iBAmBgB,qBAAA,CAAsB,UAAA,GAAa,mBAAA,GAAsB,eAAA;;AAAzE;;;;;UAciB,qBAAA;EAdwD;EAgBvE,IAAA,EAAM,UAAA,QAAkB,eAAA;EAhB8D;EAkBtF,OAAA;EAJoC;EAMpC,WAAA;AAAA;;UAIe,qBAAA,SAA8B,mBAAA;EARrB;;;;;AAQ1B;;EAQE,UAAA,EAAY,sBAAA;EARoD;;;;;;;EAgBhE,eAAA;AAAA;;;;;;;;;;;;;;;;iBAkBc,uBAAA,CAAwB,UAAA,EAAY,qBAAA,GAAwB,qBAAA"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RELAY_POOL_NAME, createRelayPool } from "./pool.js";
|
|
2
|
-
import { t as createRelayConnectionFactory } from "../relay-factory-
|
|
2
|
+
import { t as createRelayConnectionFactory } from "../relay-factory-C1X3G3WY.js";
|
|
3
3
|
//#region src/test-runner/config.ts
|
|
4
4
|
const DEFAULT_CONFIG = {
|
|
5
5
|
include: ["**/*.ait.test.ts"],
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
//#region src/test-runner/method-pace.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Per-method minimum-interval pacing for SDK calls (devtools#769).
|
|
4
|
+
*
|
|
5
|
+
* #767's `--pace` inserts delays BETWEEN test files / test-to-test — it has no
|
|
6
|
+
* effect on a burst of same-method calls WITHIN a single test body (e.g. a
|
|
7
|
+
* clipboard happy-path loop calling `setClipboardText`/`getClipboardText`
|
|
8
|
+
* eight times back to back). Real-device observation (sdk-example#293,
|
|
9
|
+
* 2.x×iOS) showed that burst alone is enough to saturate the native
|
|
10
|
+
* per-method bridge rate limit (`APP_BRIDGE_THROTTLED`, devtools#767/throttle.ts)
|
|
11
|
+
* even though `--pace`'s file/test spacing was already in effect — the
|
|
12
|
+
* limiter reacts to burst cadence within a method name, not just across test
|
|
13
|
+
* boundaries.
|
|
14
|
+
*
|
|
15
|
+
* This module wraps `window.__sdk` (the same bridge object `bundle.ts`'s
|
|
16
|
+
* `sdkRedirectPlugin` redirects SDK imports to) so that every call to the
|
|
17
|
+
* SAME named function waits out a minimum gap since that function's PREVIOUS
|
|
18
|
+
* invocation before the real call proceeds. Preflight (`cell.ts`) proved the
|
|
19
|
+
* inverse case empirically: a steady 250ms sequential cadence passes cleanly
|
|
20
|
+
* where an unpaced burst does not — sequential-with-spacing is the fix this
|
|
21
|
+
* module generalizes to every SDK call, not just the six permission probes.
|
|
22
|
+
*
|
|
23
|
+
* Runs entirely on the PAGE side (browser-compatible, no Node APIs) — bundled
|
|
24
|
+
* into the injected IIFE by `bundle.ts`'s `sdkRedirectPlugin`, the same way
|
|
25
|
+
* `bridge-stub.ts` and `runtime.ts` are. Imported ONLY from the test-runner
|
|
26
|
+
* (never from `sdk-example/src` — boilerplate cleanliness, umbrella §1.4).
|
|
27
|
+
*
|
|
28
|
+
* SECRET-HANDLING: this module carries no secrets — only method names and
|
|
29
|
+
* millisecond timestamps/durations.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Reads the page-global `__AIT_PACE_METHOD_MS__` gap value (devtools#769),
|
|
33
|
+
* injected by `relay-factory.ts` the same way `__AIT_PACE_MS__`/`__AIT_CELL__`
|
|
34
|
+
* are (`cell.ts#injectGlobals`). Absent/non-positive resolves to `0` — the
|
|
35
|
+
* same "treat absence as zero" contract `runtime.ts`'s `__AIT_PACE_MS__` read
|
|
36
|
+
* uses.
|
|
37
|
+
*/
|
|
38
|
+
declare function getPaceMethodMs(): number;
|
|
39
|
+
/**
|
|
40
|
+
* Wraps an SDK-shaped bridge object (`window.__sdk`, or the already
|
|
41
|
+
* stub-wrapped result of `bridge-stub.ts#wrapSdkWithStub`) so that every
|
|
42
|
+
* function-valued export is paced to a minimum `gapMs` interval between
|
|
43
|
+
* calls to the SAME name (devtools#769). Non-function exports (constants,
|
|
44
|
+
* etc.) and classes (`PermissionError`, etc. — wrapping a class would break
|
|
45
|
+
* `new`, see `isClass`) pass through completely untouched.
|
|
46
|
+
*
|
|
47
|
+
* **Composition with bridge-stub (devtools#740)**: this wrapper is applied to
|
|
48
|
+
* the RAW `window.__sdk` object, and `wrapSdkWithStub` is applied on TOP of
|
|
49
|
+
* the paced result (`wrapSdkWithStub(wrapWithMethodPacing(rawSdk, gapMs),
|
|
50
|
+
* stubEnabled)` — see `bundle.ts`'s sdk-redirect virtual module). This means
|
|
51
|
+
* pacing sits BEHIND the stub interception: a stubbed name
|
|
52
|
+
* (`STUB_REGISTRY` — `openPermissionDialog`, `requestPermission`,
|
|
53
|
+
* `saveBase64Data`, `showFullScreenAd`) resolves instantly from the fixture
|
|
54
|
+
* with no added delay, because `wrapSdkWithStub`'s Proxy intercepts the
|
|
55
|
+
* property access before the paced function is ever reached. Only calls that
|
|
56
|
+
* fall through to the REAL bridge — the ones that actually risk
|
|
57
|
+
* `APP_BRIDGE_THROTTLED` — pay the pacing cost. Fixed by this ordering, not
|
|
58
|
+
* incidental: swapping it (pacing outside the stub) would pace fixture
|
|
59
|
+
* look-ups that can never hit the native rate limiter, for no benefit.
|
|
60
|
+
*
|
|
61
|
+
* `gapMs <= 0` (the default, no `--pace-method` / non-2.x cell) returns `sdk`
|
|
62
|
+
* itself, untouched — the exact object identity is preserved so the
|
|
63
|
+
* zero-diff-when-off contract holds under strict-equality checks, matching
|
|
64
|
+
* `wrapSdkWithStub`'s own `enabled === false` fast path.
|
|
65
|
+
*
|
|
66
|
+
* **Memoization**: wrapper functions are cached per name in a `Map` local to
|
|
67
|
+
* this call's `Proxy` (`wrapperCache` below), so repeated property access on
|
|
68
|
+
* the SAME `sdk` object within a single `wrapWithMethodPacing` call returns
|
|
69
|
+
* the SAME wrapper reference. This cache does NOT persist across separate
|
|
70
|
+
* `wrapWithMethodPacing` calls — each call (e.g. the sdk-redirect virtual
|
|
71
|
+
* module re-evaluated per test file bundle) gets its own fresh `Map` and
|
|
72
|
+
* therefore its own wrapper function instances, even for the same method
|
|
73
|
+
* name. That's fine for pacing correctness: the actual timing state lives in
|
|
74
|
+
* the page-global `getRegistry()` map (keyed by method name, not by wrapper
|
|
75
|
+
* identity), so cross-bundle pacing still works. But callers should NOT
|
|
76
|
+
* assume cross-call function identity — only "same wrapper within one call"
|
|
77
|
+
* is guaranteed.
|
|
78
|
+
*/
|
|
79
|
+
declare function wrapWithMethodPacing(sdk: Record<string, unknown>, gapMs: number): Record<string, unknown>;
|
|
80
|
+
//#endregion
|
|
81
|
+
export { getPaceMethodMs, wrapWithMethodPacing };
|
|
82
|
+
//# sourceMappingURL=method-pace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"method-pace.d.ts","names":[],"sources":["../../src/test-runner/method-pace.ts"],"mappings":";;AAmFA;;;;;AA4EA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA5EgB,eAAA,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA4EA,oBAAA,CACd,GAAA,EAAK,MAAA,mBACL,KAAA,WACC,MAAA"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
//#region src/test-runner/method-pace.ts
|
|
2
|
+
const REGISTRY_KEY = "__AIT_METHOD_PACE_STATE__";
|
|
3
|
+
/** Reads (or lazily creates) the page-global per-method pacing registry. */
|
|
4
|
+
function getRegistry() {
|
|
5
|
+
const g = globalThis;
|
|
6
|
+
let state = g[REGISTRY_KEY];
|
|
7
|
+
if (!(state instanceof Map)) {
|
|
8
|
+
state = /* @__PURE__ */ new Map();
|
|
9
|
+
g[REGISTRY_KEY] = state;
|
|
10
|
+
}
|
|
11
|
+
return state;
|
|
12
|
+
}
|
|
13
|
+
/** `setTimeout`-based sleep — no Node APIs, safe on the page side. */
|
|
14
|
+
function sleep(ms) {
|
|
15
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* True when `value` is a `class` declaration rather than a plain callable
|
|
19
|
+
* function. The SDK export surface includes at least one class (`PermissionError`,
|
|
20
|
+
* `src/__typecheck.ts`/`bridge-stub.ts` both reference it) — wrapping a class
|
|
21
|
+
* in an async arrow function would silently break `new PermissionError()`
|
|
22
|
+
* (arrow functions are not constructible), so classes must pass through
|
|
23
|
+
* `wrapWithMethodPacing` unwrapped exactly like any other non-callable
|
|
24
|
+
* export. Detected via `Function.prototype.toString` starting with
|
|
25
|
+
* `"class "` — the standard, spec-guaranteed way to distinguish a class from
|
|
26
|
+
* an ordinary function at runtime (no Babel/TS transform in this bundle
|
|
27
|
+
* target — `target: 'es2022'` in `bundle.ts` — down-compiles `class` to a
|
|
28
|
+
* function, so this check remains reliable for esbuild's es2022 output).
|
|
29
|
+
*/
|
|
30
|
+
function isClass(value) {
|
|
31
|
+
return Function.prototype.toString.call(value).startsWith("class ");
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Reads the page-global `__AIT_PACE_METHOD_MS__` gap value (devtools#769),
|
|
35
|
+
* injected by `relay-factory.ts` the same way `__AIT_PACE_MS__`/`__AIT_CELL__`
|
|
36
|
+
* are (`cell.ts#injectGlobals`). Absent/non-positive resolves to `0` — the
|
|
37
|
+
* same "treat absence as zero" contract `runtime.ts`'s `__AIT_PACE_MS__` read
|
|
38
|
+
* uses.
|
|
39
|
+
*/
|
|
40
|
+
function getPaceMethodMs() {
|
|
41
|
+
const raw = globalThis.__AIT_PACE_METHOD_MS__;
|
|
42
|
+
return typeof raw === "number" && raw > 0 ? raw : 0;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Wraps a single named SDK function so that calls to it wait out `gapMs`
|
|
46
|
+
* since the function's own previous invocation (tracked in the shared
|
|
47
|
+
* `getRegistry()` map, keyed by `name`) before the real `fn` runs. The
|
|
48
|
+
* result/rejection of `fn` passes through completely unchanged — pacing
|
|
49
|
+
* never alters contract-observed behavior, only its timing.
|
|
50
|
+
*/
|
|
51
|
+
function buildPacedFunction(name, fn, gapMs) {
|
|
52
|
+
return async (...args) => {
|
|
53
|
+
const registry = getRegistry();
|
|
54
|
+
const now = performance.now();
|
|
55
|
+
const lastInvokeAt = registry.get(name);
|
|
56
|
+
if (lastInvokeAt !== void 0) {
|
|
57
|
+
const elapsed = now - lastInvokeAt;
|
|
58
|
+
if (elapsed < gapMs) await sleep(gapMs - elapsed);
|
|
59
|
+
}
|
|
60
|
+
registry.set(name, performance.now());
|
|
61
|
+
return fn(...args);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Wraps an SDK-shaped bridge object (`window.__sdk`, or the already
|
|
66
|
+
* stub-wrapped result of `bridge-stub.ts#wrapSdkWithStub`) so that every
|
|
67
|
+
* function-valued export is paced to a minimum `gapMs` interval between
|
|
68
|
+
* calls to the SAME name (devtools#769). Non-function exports (constants,
|
|
69
|
+
* etc.) and classes (`PermissionError`, etc. — wrapping a class would break
|
|
70
|
+
* `new`, see `isClass`) pass through completely untouched.
|
|
71
|
+
*
|
|
72
|
+
* **Composition with bridge-stub (devtools#740)**: this wrapper is applied to
|
|
73
|
+
* the RAW `window.__sdk` object, and `wrapSdkWithStub` is applied on TOP of
|
|
74
|
+
* the paced result (`wrapSdkWithStub(wrapWithMethodPacing(rawSdk, gapMs),
|
|
75
|
+
* stubEnabled)` — see `bundle.ts`'s sdk-redirect virtual module). This means
|
|
76
|
+
* pacing sits BEHIND the stub interception: a stubbed name
|
|
77
|
+
* (`STUB_REGISTRY` — `openPermissionDialog`, `requestPermission`,
|
|
78
|
+
* `saveBase64Data`, `showFullScreenAd`) resolves instantly from the fixture
|
|
79
|
+
* with no added delay, because `wrapSdkWithStub`'s Proxy intercepts the
|
|
80
|
+
* property access before the paced function is ever reached. Only calls that
|
|
81
|
+
* fall through to the REAL bridge — the ones that actually risk
|
|
82
|
+
* `APP_BRIDGE_THROTTLED` — pay the pacing cost. Fixed by this ordering, not
|
|
83
|
+
* incidental: swapping it (pacing outside the stub) would pace fixture
|
|
84
|
+
* look-ups that can never hit the native rate limiter, for no benefit.
|
|
85
|
+
*
|
|
86
|
+
* `gapMs <= 0` (the default, no `--pace-method` / non-2.x cell) returns `sdk`
|
|
87
|
+
* itself, untouched — the exact object identity is preserved so the
|
|
88
|
+
* zero-diff-when-off contract holds under strict-equality checks, matching
|
|
89
|
+
* `wrapSdkWithStub`'s own `enabled === false` fast path.
|
|
90
|
+
*
|
|
91
|
+
* **Memoization**: wrapper functions are cached per name in a `Map` local to
|
|
92
|
+
* this call's `Proxy` (`wrapperCache` below), so repeated property access on
|
|
93
|
+
* the SAME `sdk` object within a single `wrapWithMethodPacing` call returns
|
|
94
|
+
* the SAME wrapper reference. This cache does NOT persist across separate
|
|
95
|
+
* `wrapWithMethodPacing` calls — each call (e.g. the sdk-redirect virtual
|
|
96
|
+
* module re-evaluated per test file bundle) gets its own fresh `Map` and
|
|
97
|
+
* therefore its own wrapper function instances, even for the same method
|
|
98
|
+
* name. That's fine for pacing correctness: the actual timing state lives in
|
|
99
|
+
* the page-global `getRegistry()` map (keyed by method name, not by wrapper
|
|
100
|
+
* identity), so cross-bundle pacing still works. But callers should NOT
|
|
101
|
+
* assume cross-call function identity — only "same wrapper within one call"
|
|
102
|
+
* is guaranteed.
|
|
103
|
+
*/
|
|
104
|
+
function wrapWithMethodPacing(sdk, gapMs) {
|
|
105
|
+
if (!(gapMs > 0)) return sdk;
|
|
106
|
+
const wrapperCache = /* @__PURE__ */ new Map();
|
|
107
|
+
return new Proxy(sdk, { get(target, prop, receiver) {
|
|
108
|
+
const value = Reflect.get(target, prop, receiver);
|
|
109
|
+
if (typeof prop !== "string" || typeof value !== "function" || isClass(value)) return value;
|
|
110
|
+
const cached = wrapperCache.get(prop);
|
|
111
|
+
if (cached !== void 0) return cached;
|
|
112
|
+
const wrapped = buildPacedFunction(prop, value, gapMs);
|
|
113
|
+
wrapperCache.set(prop, wrapped);
|
|
114
|
+
return wrapped;
|
|
115
|
+
} });
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
export { getPaceMethodMs, wrapWithMethodPacing };
|
|
119
|
+
|
|
120
|
+
//# sourceMappingURL=method-pace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"method-pace.js","names":[],"sources":["../../src/test-runner/method-pace.ts"],"sourcesContent":["/**\n * Per-method minimum-interval pacing for SDK calls (devtools#769).\n *\n * #767's `--pace` inserts delays BETWEEN test files / test-to-test — it has no\n * effect on a burst of same-method calls WITHIN a single test body (e.g. a\n * clipboard happy-path loop calling `setClipboardText`/`getClipboardText`\n * eight times back to back). Real-device observation (sdk-example#293,\n * 2.x×iOS) showed that burst alone is enough to saturate the native\n * per-method bridge rate limit (`APP_BRIDGE_THROTTLED`, devtools#767/throttle.ts)\n * even though `--pace`'s file/test spacing was already in effect — the\n * limiter reacts to burst cadence within a method name, not just across test\n * boundaries.\n *\n * This module wraps `window.__sdk` (the same bridge object `bundle.ts`'s\n * `sdkRedirectPlugin` redirects SDK imports to) so that every call to the\n * SAME named function waits out a minimum gap since that function's PREVIOUS\n * invocation before the real call proceeds. Preflight (`cell.ts`) proved the\n * inverse case empirically: a steady 250ms sequential cadence passes cleanly\n * where an unpaced burst does not — sequential-with-spacing is the fix this\n * module generalizes to every SDK call, not just the six permission probes.\n *\n * Runs entirely on the PAGE side (browser-compatible, no Node APIs) — bundled\n * into the injected IIFE by `bundle.ts`'s `sdkRedirectPlugin`, the same way\n * `bridge-stub.ts` and `runtime.ts` are. Imported ONLY from the test-runner\n * (never from `sdk-example/src` — boilerplate cleanliness, umbrella §1.4).\n *\n * SECRET-HANDLING: this module carries no secrets — only method names and\n * millisecond timestamps/durations.\n */\n\n/**\n * Page-global registry of per-method last-invocation timestamps\n * (`performance.now()`-relative, monotonic). One registry per page — shared\n * across every wrapped SDK object so repeated `wrapWithMethodPacing` calls\n * (e.g. the sdk-redirect virtual module re-evaluated per test file bundle)\n * still see the true last-invoke time of a given method name, not a fresh\n * per-bundle registry that would silently reset pacing at every file\n * boundary.\n */\ntype MethodPaceState = Map<string, number>;\n\nconst REGISTRY_KEY = '__AIT_METHOD_PACE_STATE__';\n\n/** Reads (or lazily creates) the page-global per-method pacing registry. */\nfunction getRegistry(): MethodPaceState {\n const g = globalThis as { [REGISTRY_KEY]?: MethodPaceState };\n let state = g[REGISTRY_KEY];\n if (!(state instanceof Map)) {\n state = new Map<string, number>();\n g[REGISTRY_KEY] = state;\n }\n return state;\n}\n\n/** `setTimeout`-based sleep — no Node APIs, safe on the page side. */\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * True when `value` is a `class` declaration rather than a plain callable\n * function. The SDK export surface includes at least one class (`PermissionError`,\n * `src/__typecheck.ts`/`bridge-stub.ts` both reference it) — wrapping a class\n * in an async arrow function would silently break `new PermissionError()`\n * (arrow functions are not constructible), so classes must pass through\n * `wrapWithMethodPacing` unwrapped exactly like any other non-callable\n * export. Detected via `Function.prototype.toString` starting with\n * `\"class \"` — the standard, spec-guaranteed way to distinguish a class from\n * an ordinary function at runtime (no Babel/TS transform in this bundle\n * target — `target: 'es2022'` in `bundle.ts` — down-compiles `class` to a\n * function, so this check remains reliable for esbuild's es2022 output).\n */\nfunction isClass(value: (...args: unknown[]) => unknown): boolean {\n return Function.prototype.toString.call(value).startsWith('class ');\n}\n\n/**\n * Reads the page-global `__AIT_PACE_METHOD_MS__` gap value (devtools#769),\n * injected by `relay-factory.ts` the same way `__AIT_PACE_MS__`/`__AIT_CELL__`\n * are (`cell.ts#injectGlobals`). Absent/non-positive resolves to `0` — the\n * same \"treat absence as zero\" contract `runtime.ts`'s `__AIT_PACE_MS__` read\n * uses.\n */\nexport function getPaceMethodMs(): number {\n const raw = (globalThis as { __AIT_PACE_METHOD_MS__?: unknown }).__AIT_PACE_METHOD_MS__;\n return typeof raw === 'number' && raw > 0 ? raw : 0;\n}\n\n/**\n * Wraps a single named SDK function so that calls to it wait out `gapMs`\n * since the function's own previous invocation (tracked in the shared\n * `getRegistry()` map, keyed by `name`) before the real `fn` runs. The\n * result/rejection of `fn` passes through completely unchanged — pacing\n * never alters contract-observed behavior, only its timing.\n */\nfunction buildPacedFunction(\n name: string,\n fn: (...args: unknown[]) => unknown,\n gapMs: number,\n): (...args: unknown[]) => unknown {\n return async (...args: unknown[]) => {\n const registry = getRegistry();\n const now = performance.now();\n const lastInvokeAt = registry.get(name);\n if (lastInvokeAt !== undefined) {\n const elapsed = now - lastInvokeAt;\n if (elapsed < gapMs) {\n await sleep(gapMs - elapsed);\n }\n }\n // Record the timestamp AFTER any wait but BEFORE the real call — this is\n // \"last invoke attempt\", matching the preflight's own \"sequential with\n // spacing before each call\" model rather than \"last completion\", so two\n // long-running calls in flight cannot both slip under the gap.\n registry.set(name, performance.now());\n return fn(...args);\n };\n}\n\n/**\n * Wraps an SDK-shaped bridge object (`window.__sdk`, or the already\n * stub-wrapped result of `bridge-stub.ts#wrapSdkWithStub`) so that every\n * function-valued export is paced to a minimum `gapMs` interval between\n * calls to the SAME name (devtools#769). Non-function exports (constants,\n * etc.) and classes (`PermissionError`, etc. — wrapping a class would break\n * `new`, see `isClass`) pass through completely untouched.\n *\n * **Composition with bridge-stub (devtools#740)**: this wrapper is applied to\n * the RAW `window.__sdk` object, and `wrapSdkWithStub` is applied on TOP of\n * the paced result (`wrapSdkWithStub(wrapWithMethodPacing(rawSdk, gapMs),\n * stubEnabled)` — see `bundle.ts`'s sdk-redirect virtual module). This means\n * pacing sits BEHIND the stub interception: a stubbed name\n * (`STUB_REGISTRY` — `openPermissionDialog`, `requestPermission`,\n * `saveBase64Data`, `showFullScreenAd`) resolves instantly from the fixture\n * with no added delay, because `wrapSdkWithStub`'s Proxy intercepts the\n * property access before the paced function is ever reached. Only calls that\n * fall through to the REAL bridge — the ones that actually risk\n * `APP_BRIDGE_THROTTLED` — pay the pacing cost. Fixed by this ordering, not\n * incidental: swapping it (pacing outside the stub) would pace fixture\n * look-ups that can never hit the native rate limiter, for no benefit.\n *\n * `gapMs <= 0` (the default, no `--pace-method` / non-2.x cell) returns `sdk`\n * itself, untouched — the exact object identity is preserved so the\n * zero-diff-when-off contract holds under strict-equality checks, matching\n * `wrapSdkWithStub`'s own `enabled === false` fast path.\n *\n * **Memoization**: wrapper functions are cached per name in a `Map` local to\n * this call's `Proxy` (`wrapperCache` below), so repeated property access on\n * the SAME `sdk` object within a single `wrapWithMethodPacing` call returns\n * the SAME wrapper reference. This cache does NOT persist across separate\n * `wrapWithMethodPacing` calls — each call (e.g. the sdk-redirect virtual\n * module re-evaluated per test file bundle) gets its own fresh `Map` and\n * therefore its own wrapper function instances, even for the same method\n * name. That's fine for pacing correctness: the actual timing state lives in\n * the page-global `getRegistry()` map (keyed by method name, not by wrapper\n * identity), so cross-bundle pacing still works. But callers should NOT\n * assume cross-call function identity — only \"same wrapper within one call\"\n * is guaranteed.\n */\nexport function wrapWithMethodPacing(\n sdk: Record<string, unknown>,\n gapMs: number,\n): Record<string, unknown> {\n if (!(gapMs > 0)) return sdk;\n\n const wrapperCache = new Map<string, (...args: unknown[]) => unknown>();\n\n return new Proxy(sdk, {\n get(target, prop, receiver) {\n const value = Reflect.get(target, prop, receiver);\n if (\n typeof prop !== 'string' ||\n typeof value !== 'function' ||\n isClass(value as (...args: unknown[]) => unknown)\n ) {\n // Non-function export (constants, symbols) OR a class (e.g.\n // PermissionError) — pass through unchanged. Wrapping a class would\n // break `new`, and per-name pacing only makes sense for callable SDK\n // methods anyway.\n return value;\n }\n const cached = wrapperCache.get(prop);\n if (cached !== undefined) return cached;\n const wrapped = buildPacedFunction(prop, value as (...args: unknown[]) => unknown, gapMs);\n wrapperCache.set(prop, wrapped);\n return wrapped;\n },\n });\n}\n"],"mappings":";AAyCA,MAAM,eAAe;;AAGrB,SAAS,cAA+B;CACtC,MAAM,IAAI;CACV,IAAI,QAAQ,EAAE;AACd,KAAI,EAAE,iBAAiB,MAAM;AAC3B,0BAAQ,IAAI,KAAqB;AACjC,IAAE,gBAAgB;;AAEpB,QAAO;;;AAIT,SAAS,MAAM,IAA2B;AACxC,QAAO,IAAI,SAAS,YAAY,WAAW,SAAS,GAAG,CAAC;;;;;;;;;;;;;;;AAgB1D,SAAS,QAAQ,OAAiD;AAChE,QAAO,SAAS,UAAU,SAAS,KAAK,MAAM,CAAC,WAAW,SAAS;;;;;;;;;AAUrE,SAAgB,kBAA0B;CACxC,MAAM,MAAO,WAAoD;AACjE,QAAO,OAAO,QAAQ,YAAY,MAAM,IAAI,MAAM;;;;;;;;;AAUpD,SAAS,mBACP,MACA,IACA,OACiC;AACjC,QAAO,OAAO,GAAG,SAAoB;EACnC,MAAM,WAAW,aAAa;EAC9B,MAAM,MAAM,YAAY,KAAK;EAC7B,MAAM,eAAe,SAAS,IAAI,KAAK;AACvC,MAAI,iBAAiB,KAAA,GAAW;GAC9B,MAAM,UAAU,MAAM;AACtB,OAAI,UAAU,MACZ,OAAM,MAAM,QAAQ,QAAQ;;AAOhC,WAAS,IAAI,MAAM,YAAY,KAAK,CAAC;AACrC,SAAO,GAAG,GAAG,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CtB,SAAgB,qBACd,KACA,OACyB;AACzB,KAAI,EAAE,QAAQ,GAAI,QAAO;CAEzB,MAAM,+BAAe,IAAI,KAA8C;AAEvE,QAAO,IAAI,MAAM,KAAK,EACpB,IAAI,QAAQ,MAAM,UAAU;EAC1B,MAAM,QAAQ,QAAQ,IAAI,QAAQ,MAAM,SAAS;AACjD,MACE,OAAO,SAAS,YAChB,OAAO,UAAU,cACjB,QAAQ,MAAyC,CAMjD,QAAO;EAET,MAAM,SAAS,aAAa,IAAI,KAAK;AACrC,MAAI,WAAW,KAAA,EAAW,QAAO;EACjC,MAAM,UAAU,mBAAmB,MAAM,OAA0C,MAAM;AACzF,eAAa,IAAI,MAAM,QAAQ;AAC/B,SAAO;IAEV,CAAC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as createRelayPool, n as RelayConnectionFactory, r as RelayPoolOptions, t as RELAY_POOL_NAME } from "../pool-
|
|
1
|
+
import { i as createRelayPool, n as RelayConnectionFactory, r as RelayPoolOptions, t as RELAY_POOL_NAME } from "../pool-CrP5CPvU.js";
|
|
2
2
|
export { RELAY_POOL_NAME, RelayConnectionFactory, RelayPoolOptions, createRelayPool };
|
package/dist/test-runner/pool.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as runTestFilesOverRelay } from "../relay-worker-
|
|
1
|
+
import { i as runTestFilesOverRelay } from "../relay-worker-BUXI0K0b.js";
|
|
2
2
|
import { synthesizeFileTask, toTaskEventPacks, toTaskResultPacks } from "./task-graph.js";
|
|
3
3
|
//#region src/test-runner/pool.ts
|
|
4
4
|
/** The pool name Vitest matches against `getFilePoolName(project)`. */
|