@ait-co/devtools 0.1.141 → 0.1.143
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/README.en.md +4 -4
- package/README.md +4 -4
- package/dist/{attach-orchestrator-BG8TGJOZ.js → attach-orchestrator-0F0m_UqQ.js} +71 -9
- package/dist/{attach-orchestrator-BG8TGJOZ.js.map → attach-orchestrator-0F0m_UqQ.js.map} +1 -1
- package/dist/{attach-orchestrator-6kz_ZdWL.js → attach-orchestrator-D65KxFy_.js} +71 -9
- package/dist/{attach-orchestrator-6kz_ZdWL.js.map → attach-orchestrator-D65KxFy_.js.map} +1 -1
- package/dist/{attach-orchestrator-BEiz3wAx.js → attach-orchestrator-DL3NQ9ca.js} +71 -9
- package/dist/{attach-orchestrator-BEiz3wAx.js.map → attach-orchestrator-DL3NQ9ca.js.map} +1 -1
- package/dist/{cell-BMlFh08P.js → cell-BaLvusOl.js} +2 -2
- package/dist/{cell-BMlFh08P.js.map → cell-BaLvusOl.js.map} +1 -1
- package/dist/{cell-Dtgtusog.js → cell-CBUS3-nT.js} +2 -2
- package/dist/{cell-Dtgtusog.js.map → cell-CBUS3-nT.js.map} +1 -1
- package/dist/{cell-KsRXFdDs.js → cell-EBKKpAAT.js} +2 -2
- package/dist/{cell-KsRXFdDs.js.map → cell-EBKKpAAT.js.map} +1 -1
- package/dist/{debug-server-DWat7d-9.js → debug-server-B3ABDrRI.js} +4 -4
- package/dist/{debug-server-DWat7d-9.js.map → debug-server-B3ABDrRI.js.map} +1 -1
- package/dist/{debug-server-6c22qSWa.js → debug-server-BWhwrVXa.js} +4 -4
- package/dist/{debug-server-6c22qSWa.js.map → debug-server-BWhwrVXa.js.map} +1 -1
- package/dist/{debug-server-BQHbaKPT.js → debug-server-CfQNxxGW.js} +6 -6
- package/dist/{debug-server-BQHbaKPT.js.map → debug-server-CfQNxxGW.js.map} +1 -1
- package/dist/in-app/auto.js +299 -3
- package/dist/in-app/auto.js.map +1 -1
- package/dist/in-app/index.d.ts +161 -14
- package/dist/in-app/index.d.ts.map +1 -1
- package/dist/in-app/index.js +300 -4
- package/dist/in-app/index.js.map +1 -1
- package/dist/mcp/cli.js +82 -16
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/mock/index.d.ts +156 -98
- package/dist/mock/index.d.ts.map +1 -1
- package/dist/mock/index.js +52 -21
- package/dist/mock/index.js.map +1 -1
- package/dist/panel/index.js +9 -4
- package/dist/panel/index.js.map +1 -1
- package/dist/{pool-CrP5CPvU.d.ts → pool-DcaaOwUq.d.ts} +2 -2
- package/dist/{pool-CrP5CPvU.d.ts.map → pool-DcaaOwUq.d.ts.map} +1 -1
- package/dist/{qr-http-server-Cz1mA07b.cjs → qr-http-server--gl2-WKc.cjs} +9 -5
- package/dist/qr-http-server--gl2-WKc.cjs.map +1 -0
- package/dist/{qr-http-server-D4rGz8M7.js → qr-http-server-Bb7lMeqi.js} +9 -5
- package/dist/qr-http-server-Bb7lMeqi.js.map +1 -0
- package/dist/{qr-http-server-D1hpoyBF.js → qr-http-server-C_lqOrgc.js} +9 -5
- package/dist/qr-http-server-C_lqOrgc.js.map +1 -0
- package/dist/{qr-http-server-CTLdclg_.js → qr-http-server-CopuMbub.js} +9 -5
- package/dist/qr-http-server-CopuMbub.js.map +1 -0
- package/dist/{qr-http-server-D16Lq2ag.cjs → qr-http-server-D-Off6K1.cjs} +9 -5
- package/dist/qr-http-server-D-Off6K1.cjs.map +1 -0
- package/dist/{qr-http-server-DIjCV7h_.js → qr-http-server-DrbIVDjO.js} +9 -5
- package/dist/qr-http-server-DrbIVDjO.js.map +1 -0
- package/dist/qr-http-server-n1twN18z.js +1644 -0
- package/dist/qr-http-server-n1twN18z.js.map +1 -0
- package/dist/{relay-factory-C1X3G3WY.js → relay-factory-N9QobQxG.js} +6 -6
- package/dist/{relay-factory-C1X3G3WY.js.map → relay-factory-N9QobQxG.js.map} +1 -1
- package/dist/{relay-worker-BUXI0K0b.js → relay-worker-B5HKkGUY.js} +4 -4
- package/dist/{relay-worker-BUXI0K0b.js.map → relay-worker-B5HKkGUY.js.map} +1 -1
- package/dist/{relay-worker-DcyboK43.d.ts → relay-worker-YdlpZQl9.d.ts} +2 -2
- package/dist/{relay-worker-DcyboK43.d.ts.map → relay-worker-YdlpZQl9.d.ts.map} +1 -1
- package/dist/{runtime-BiigOuvb.d.ts → runtime-kn9DxOeg.d.ts} +2 -1
- package/dist/runtime-kn9DxOeg.d.ts.map +1 -0
- package/dist/test-runner/bin.js +8 -8
- package/dist/test-runner/bin.js.map +1 -1
- package/dist/test-runner/config.d.ts +1 -1
- package/dist/test-runner/config.js +1 -1
- package/dist/test-runner/pool.d.ts +1 -1
- package/dist/test-runner/pool.js +1 -1
- package/dist/test-runner/relay-factory.js +5 -5
- 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 +2 -2
- package/dist/test-runner/rpc.d.ts +1 -1
- package/dist/test-runner/runtime.d.ts +1 -1
- package/dist/test-runner/runtime.js +4 -0
- package/dist/test-runner/runtime.js.map +1 -1
- package/dist/test-runner/task-graph.d.ts +1 -1
- package/dist/{tunnel-DjtSENJU.cjs → tunnel-BVSMXctM.cjs} +2 -2
- package/dist/{tunnel-DjtSENJU.cjs.map → tunnel-BVSMXctM.cjs.map} +1 -1
- package/dist/{tunnel-5eBncITa.js → tunnel-D7xkimBu.js} +2 -2
- package/dist/{tunnel-5eBncITa.js.map → tunnel-D7xkimBu.js.map} +1 -1
- package/dist/unplugin/index.cjs +1 -1
- package/dist/unplugin/index.js +1 -1
- package/dist/unplugin/tunnel.cjs +1 -1
- package/dist/unplugin/tunnel.js +1 -1
- package/package.json +2 -2
- package/dist/qr-http-server-CTLdclg_.js.map +0 -1
- package/dist/qr-http-server-Cz1mA07b.cjs.map +0 -1
- package/dist/qr-http-server-D16Lq2ag.cjs.map +0 -1
- package/dist/qr-http-server-D1hpoyBF.js.map +0 -1
- package/dist/qr-http-server-D4rGz8M7.js.map +0 -1
- package/dist/qr-http-server-DIjCV7h_.js.map +0 -1
- package/dist/qr-http-server-wy3L7Yut.js +0 -1640
- package/dist/qr-http-server-wy3L7Yut.js.map +0 -1
- package/dist/runtime-BiigOuvb.d.ts.map +0 -1
|
@@ -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-DcaaOwUq.js";
|
|
2
2
|
|
|
3
3
|
//#region src/test-runner/relay-factory.d.ts
|
|
4
4
|
/** Options for {@link createRelayConnectionFactory}. */
|
|
@@ -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-N9QobQxG.js";
|
|
3
3
|
//#region src/test-runner/config.ts
|
|
4
4
|
const DEFAULT_CONFIG = {
|
|
5
5
|
include: ["**/*.ait.test.ts"],
|
|
@@ -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-DcaaOwUq.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-B5HKkGUY.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)`. */
|
|
@@ -28,10 +28,10 @@ function createRelayConnectionFactory(opts) {
|
|
|
28
28
|
let manualPrompt = null;
|
|
29
29
|
return {
|
|
30
30
|
async open() {
|
|
31
|
-
const { prepareAttach, renderAndMaybeWait, mintAttachUrl } = await import("../attach-orchestrator-
|
|
32
|
-
const { injectDebugIndicator, injectGlobals } = await import("../cell-
|
|
31
|
+
const { prepareAttach, renderAndMaybeWait, mintAttachUrl } = await import("../attach-orchestrator-D65KxFy_.js");
|
|
32
|
+
const { injectDebugIndicator, injectGlobals } = await import("../cell-BaLvusOl.js");
|
|
33
33
|
const { loadRelaySecretReadOnly } = await import("../relay-secret-store-DKxs7zwq.js");
|
|
34
|
-
const { bootRelayFamily, buildRelayVerifyAuth, startAttachWatcher } = await import("../debug-server-
|
|
34
|
+
const { bootRelayFamily, buildRelayVerifyAuth, startAttachWatcher } = await import("../debug-server-BWhwrVXa.js");
|
|
35
35
|
const { buildChiiInspectorUrl } = await import("../devtools-opener-3Drge_RJ.js");
|
|
36
36
|
const { generateTotp } = await import("../totp-DfekTBk3.js").then((n) => n.i);
|
|
37
37
|
await loadRelaySecretReadOnly({ projectRoot });
|
|
@@ -63,7 +63,7 @@ function createRelayConnectionFactory(opts) {
|
|
|
63
63
|
canOpenBrowser: () => !headless
|
|
64
64
|
};
|
|
65
65
|
try {
|
|
66
|
-
const { startQrHttpServer } = await import("../qr-http-server-
|
|
66
|
+
const { startQrHttpServer } = await import("../qr-http-server-C_lqOrgc.js");
|
|
67
67
|
const getDashboardState = () => ({
|
|
68
68
|
tunnel: attachDeps.getTunnelStatus(),
|
|
69
69
|
pages: booted.connection.listTargets().map((t) => ({
|
|
@@ -188,7 +188,7 @@ function createRelayConnectionFactory(opts) {
|
|
|
188
188
|
qrServer?.notifyStateChange();
|
|
189
189
|
}
|
|
190
190
|
try {
|
|
191
|
-
const { injectDebugIndicator } = await import("../cell-
|
|
191
|
+
const { injectDebugIndicator } = await import("../cell-BaLvusOl.js");
|
|
192
192
|
await injectDebugIndicator(connection, { state: "disconnected" });
|
|
193
193
|
} catch {}
|
|
194
194
|
family?.stop();
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as RelayRunReport, i as RelayRunOptions, n as FileResult, o as flattenResults, r as MANUAL_FILE_TIMEOUT_MS, s as runTestFilesOverRelay, t as EVALUATE_TIMEOUT_MARKER } from "../relay-worker-
|
|
1
|
+
import { a as RelayRunReport, i as RelayRunOptions, n as FileResult, o as flattenResults, r as MANUAL_FILE_TIMEOUT_MS, s as runTestFilesOverRelay, t as EVALUATE_TIMEOUT_MARKER } from "../relay-worker-YdlpZQl9.js";
|
|
2
2
|
export { EVALUATE_TIMEOUT_MARKER, FileResult, MANUAL_FILE_TIMEOUT_MS, RelayRunOptions, RelayRunReport, flattenResults, runTestFilesOverRelay };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as runTestFilesOverRelay, n as MANUAL_FILE_TIMEOUT_MS, r as flattenResults, t as EVALUATE_TIMEOUT_MARKER } from "../relay-worker-
|
|
1
|
+
import { i as runTestFilesOverRelay, n as MANUAL_FILE_TIMEOUT_MS, r as flattenResults, t as EVALUATE_TIMEOUT_MARKER } from "../relay-worker-B5HKkGUY.js";
|
|
2
2
|
export { EVALUATE_TIMEOUT_MARKER, MANUAL_FILE_TIMEOUT_MS, flattenResults, runTestFilesOverRelay };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as AitCaptureLine } from "../capture-DsP525OZ.js";
|
|
2
|
-
import { i as TestResult } from "../runtime-
|
|
3
|
-
import { a as RelayRunReport } from "../relay-worker-
|
|
2
|
+
import { i as TestResult } from "../runtime-kn9DxOeg.js";
|
|
3
|
+
import { a as RelayRunReport } from "../relay-worker-YdlpZQl9.js";
|
|
4
4
|
|
|
5
5
|
//#region src/test-runner/report.d.ts
|
|
6
6
|
/** The cell axes a report is stamped with — the test-matrix coordinates. */
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as runTestModule, i as TestResult, n as RunTestModuleOptions, o as runtimeGlobals, r as TestContext, t as RunReport } from "../runtime-
|
|
1
|
+
import { a as runTestModule, i as TestResult, n as RunTestModuleOptions, o as runtimeGlobals, r as TestContext, t as RunReport } from "../runtime-kn9DxOeg.js";
|
|
2
2
|
export { RunReport, RunTestModuleOptions, TestContext, TestResult, runTestModule, runtimeGlobals };
|
|
@@ -300,6 +300,10 @@ var Expectation = class Expectation {
|
|
|
300
300
|
toBeTypeOf(typeStr) {
|
|
301
301
|
this.#assert(typeof this.#received === typeStr, `Expected typeof "${typeStr}", received "${typeof this.#received}"`);
|
|
302
302
|
}
|
|
303
|
+
toHaveLength(n) {
|
|
304
|
+
const length = this.#received?.length;
|
|
305
|
+
this.#assert(typeof length === "number" && length === n, `Expected length ${n}, received ${typeof length === "number" ? length : String(this.#received)}`);
|
|
306
|
+
}
|
|
303
307
|
};
|
|
304
308
|
/** The `expect` function installed as a global. */
|
|
305
309
|
const expect = ((received) => new Expectation(received));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.js","names":["#received","#negated","#assert"],"sources":["../../src/test-runner/runtime.ts"],"sourcesContent":["/**\n * Thin test runtime for WebView execution.\n *\n * This file is bundled by `bundle.ts` together with the user's test file\n * into a single IIFE injected into the WebView via `Runtime.evaluate`.\n * It MUST stay browser-compatible — no Node.js APIs allowed.\n *\n * Design: rather than shipping @vitest/runner verbatim into a WebView (where\n * its Node-side internals cause issues), this runtime provides a minimal\n * compatible API surface — describe/it/test/expect globals — collects results\n * into a plain JSON-safe object, and exports `runTestModule` as the entry point.\n *\n * @vitest/runner and @vitest/expect are listed as dependencies so that the\n * package's type contracts are available and so the browser-compatible subsets\n * can be referenced. The Vitest custom pool that drives this runtime through\n * Vitest's `PoolRunnerInitializer` lives in `pool.ts`.\n *\n * NOTE: this file is imported by type from Node-side code (rpc.ts / relay-worker.ts)\n * for the RunReport / TestResult type shapes. The runtime ITSELF is not imported\n * at runtime on the Node side — only the types are used.\n */\n\nimport { isThrottledError } from './throttle.js';\n\n/* -------------------------------------------------------------------------- */\n/* Public result types (used by rpc.ts and relay-worker.ts) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Result of a single test case.\n * All fields are JSON-serialisable.\n */\nexport interface TestResult {\n /** Full dot-joined test name including nested suite names. */\n name: string;\n /** `'pass'` or `'fail'`. `'skip'` for skipped tests. */\n status: 'pass' | 'fail' | 'skip';\n /** Duration in milliseconds. */\n duration: number;\n /** Error message (fail only). Does NOT include the expression/secret. */\n error?: string;\n /**\n * Optional note attached to a skip — populated when the test body itself\n * calls `ctx.skip(cond, note)` (devtools#746), mirroring the static\n * `it.skip`/`it.skipIf` path which has no note today. `'skip'` status only.\n */\n note?: string;\n /**\n * Number of throttle-aware re-executions of the test BODY (devtools#767) —\n * present only when at least one retry was attempted (`>= 1`); omitted\n * (never `0`) for every test that never hit `APP_BRIDGE_THROTTLED`, so\n * existing consumers (sdk-example's diff script) see byte-identical reports\n * for the common case. A test that eventually PASSES after retrying still\n * carries this field — it is provenance, not a failure flag.\n */\n throttleRetries?: number;\n}\n\n/** Aggregate report returned by `runTestModule`. */\nexport interface RunReport {\n /** ISO timestamp of when `runTestModule` was called. */\n startedAt: string;\n /** Total elapsed milliseconds (wall-clock). */\n duration: number;\n passed: number;\n failed: number;\n skipped: number;\n tests: TestResult[];\n}\n\n/* -------------------------------------------------------------------------- */\n/* THROTTLED-aware test retry (devtools#767) */\n/* -------------------------------------------------------------------------- */\n\n/** Backoff delays (ms) between throttle-retry attempts: 1s, then 2s. */\nconst TEST_RETRY_BACKOFF_MS = [1_000, 2_000] as const;\n\n/** Max retry attempts per test after the initial run (devtools#767: \"최대 2회 재시도\"). */\nconst TEST_MAX_RETRIES = TEST_RETRY_BACKOFF_MS.length;\n\n/** Real `setTimeout`-backed sleep — the production default for {@link RunTestModuleOptions.sleep}. */\nconst DEFAULT_SLEEP = (ms: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, ms));\n\n/**\n * Test-only override surface for `runTestModule`. Never populated by the\n * generated bundle — see that function's doc.\n */\nexport interface RunTestModuleOptions {\n /**\n * Overrides the backoff/pacing sleep implementation. Defaults to a real\n * `setTimeout`-backed sleep. Unit tests pass a fake (e.g. an\n * immediately-resolving spy) so retry/pacing tests don't take real\n * wall-clock seconds.\n */\n sleep?: (ms: number) => Promise<void>;\n}\n\n/**\n * Renders a thrown value into a `TestResult.error` string. Handles three\n * shapes:\n * - a real `Error` (or subclass) — its `.message`;\n * - an error-LIKE plain object with a string `.message` (the 2.x native\n * bridge envelope this test-runner observes, e.g.\n * `{name, code, userInfo, moduleName, __isError}` — devtools#767's\n * `APP_BRIDGE_THROTTLED` rejections are exactly this shape, not an\n * `Error` instance) — its `.message` field;\n * - anything else — `String(e)` (the pre-#767 fallback, unchanged).\n *\n * Without the middle case, a native envelope's `.message` (the only useful\n * diagnostic text) was discarded in favor of the useless `\"[object Object]\"`.\n */\nfunction stringifyThrown(e: unknown): string {\n if (e instanceof Error) return e.message;\n if (typeof e === 'object' && e !== null) {\n const rec = e as { message?: unknown };\n if (typeof rec.message === 'string') return rec.message;\n }\n return String(e);\n}\n\n/* -------------------------------------------------------------------------- */\n/* Asymmetric matchers (devtools#692) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Brand property that tags asymmetric matcher markers (`expect.any(String)`,\n * `expect.objectContaining(...)`, etc). A reserved string key is used as the\n * brand so the guard stays a plain own-property check — markers are only ever\n * compared in-process by reference, so no cross-realm Symbol coordination is\n * needed.\n */\nconst ASYMMETRIC_BRAND = '$$asymmetricMatch' as const;\n\n/**\n * An asymmetric matcher: a placeholder that, when found on the `expected` side\n * of a deep comparison, runs its own `asymmetricMatch(actual)` predicate\n * instead of being structurally compared as a plain object. This is what makes\n * `toMatchObject({ hash: expect.any(String) })` match any string `hash`.\n */\ninterface AsymmetricMatcher {\n /** Brand discriminator — present only on asymmetric markers. */\n [ASYMMETRIC_BRAND]: true;\n /** Human-readable label used in failure messages. */\n toString(): string;\n /** Returns true when `actual` satisfies this matcher. */\n asymmetricMatch(actual: unknown): boolean;\n}\n\n/** Narrowing guard: is `v` an asymmetric matcher marker? */\nfunction isAsymmetric(v: unknown): v is AsymmetricMatcher {\n return (\n typeof v === 'object' &&\n v !== null &&\n (v as Record<string, unknown>)[ASYMMETRIC_BRAND] === true &&\n typeof (v as { asymmetricMatch?: unknown }).asymmetricMatch === 'function'\n );\n}\n\n/** The widest constructor type TypeScript allows without `any` (see toBeInstanceOf). */\ntype AnyCtor = abstract new (...args: never) => unknown;\n\n/**\n * `expect.any(Ctor)` — matches a value by its constructor. Primitive wrappers\n * (`String`/`Number`/`Boolean`) are matched by `typeof` (so the unboxed\n * primitive matches) AND by `instanceof` (so a boxed wrapper instance also\n * matches). `BigInt`/`Symbol` match by `typeof`. `Object` matches any non-null\n * object (NOT functions, matching vitest), `Array` uses `Array.isArray`,\n * `Function` uses `typeof === 'function'`, and any other constructor (user\n * classes) falls back to `instanceof`.\n */\nfunction makeAny(ctor: AnyCtor): AsymmetricMatcher {\n const name = (ctor as { name?: string }).name ?? String(ctor);\n const ctorAsFn = ctor as unknown as (new (...args: unknown[]) => unknown) | undefined;\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `Any<${name}>`,\n asymmetricMatch(actual: unknown): boolean {\n if (actual === null || actual === undefined) return false;\n switch (ctor as unknown) {\n case String:\n return typeof actual === 'string' || actual instanceof String;\n case Number:\n return typeof actual === 'number' || actual instanceof Number;\n case Boolean:\n return typeof actual === 'boolean' || actual instanceof Boolean;\n case BigInt:\n return typeof actual === 'bigint';\n case Symbol:\n return typeof actual === 'symbol';\n case Function:\n return typeof actual === 'function';\n case Object:\n // Match vitest: `expect.any(Object)` accepts objects only, NOT\n // functions (vitest checks `typeof other === 'object'`). `null` is\n // already excluded by the guard above.\n return typeof actual === 'object';\n case Array:\n return Array.isArray(actual);\n default:\n return ctorAsFn ? actual instanceof ctorAsFn : false;\n }\n },\n };\n}\n\n/** `expect.anything()` — matches any value except `null`/`undefined`. */\nfunction makeAnything(): AsymmetricMatcher {\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => 'Anything',\n asymmetricMatch: (actual: unknown): boolean => actual !== null && actual !== undefined,\n };\n}\n\n/** `expect.objectContaining(obj)` — partial-match every key of `obj` on `actual`. */\nfunction makeObjectContaining(expected: Record<string, unknown>): AsymmetricMatcher {\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `ObjectContaining<${safeStringify(expected)}>`,\n asymmetricMatch: (actual: unknown): boolean => deepMatchObject(actual, expected),\n };\n}\n\n/** `expect.arrayContaining(arr)` — `actual` is an array containing each element of `arr`. */\nfunction makeArrayContaining(expected: unknown[]): AsymmetricMatcher {\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `ArrayContaining<${safeStringify(expected)}>`,\n asymmetricMatch(actual: unknown): boolean {\n if (!Array.isArray(actual)) return false;\n return expected.every((exp) =>\n actual.some((act) => (isAsymmetric(exp) ? exp.asymmetricMatch(act) : deepEqual(act, exp))),\n );\n },\n };\n}\n\n/** `expect.stringContaining(sub)` — `actual` is a string containing `sub`. */\nfunction makeStringContaining(sub: string): AsymmetricMatcher {\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `StringContaining<${sub}>`,\n asymmetricMatch: (actual: unknown): boolean =>\n typeof actual === 'string' && actual.includes(sub),\n };\n}\n\n/** `expect.stringMatching(re)` — `actual` is a string matching `re` (string or RegExp). */\nfunction makeStringMatching(re: string | RegExp): AsymmetricMatcher {\n const pattern = typeof re === 'string' ? new RegExp(re) : re;\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `StringMatching<${String(pattern)}>`,\n asymmetricMatch: (actual: unknown): boolean =>\n typeof actual === 'string' && pattern.test(actual),\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Deep equality helpers */\n/* -------------------------------------------------------------------------- */\n\n/** JSON.stringify that never throws (cycles/BigInt) — for failure messages. */\nfunction safeStringify(v: unknown): string {\n try {\n return (\n JSON.stringify(v, (_k, val) =>\n isAsymmetric(val) ? val.toString() : typeof val === 'bigint' ? `${val}n` : val,\n ) ?? String(v)\n );\n } catch {\n return String(v);\n }\n}\n\n/**\n * Recursive structural equality — replaces JSON.stringify comparison to\n * handle key-order differences and undefined values correctly.\n *\n * When the `b` (expected) side is an asymmetric marker, its `asymmetricMatch`\n * predicate decides the result instead of structural comparison — markers are\n * placeholders, not plain objects, so deep-comparing them never matches.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (isAsymmetric(b)) return b.asymmetricMatch(a);\n if (Object.is(a, b)) return true;\n if (a === null || b === null) return false;\n if (typeof a !== 'object' || typeof b !== 'object') return false;\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!deepEqual(a[i], b[i])) return false;\n }\n return true;\n }\n if (Array.isArray(a) || Array.isArray(b)) return false;\n\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const aKeys = Object.keys(aObj);\n const bKeys = Object.keys(bObj);\n if (aKeys.length !== bKeys.length) return false;\n for (const key of aKeys) {\n if (!Object.hasOwn(bObj, key)) return false;\n if (!deepEqual(aObj[key], bObj[key])) return false;\n }\n return true;\n}\n\n/**\n * Partial-match: every key in `expected` must exist in `received` with a\n * recursively matching value. Extra keys in `received` are ignored.\n *\n * As in `deepEqual`, an asymmetric marker on the `expected` side delegates to\n * its `asymmetricMatch` predicate rather than being compared structurally.\n */\nfunction deepMatchObject(received: unknown, expected: unknown): boolean {\n if (isAsymmetric(expected)) return expected.asymmetricMatch(received);\n if (Object.is(received, expected)) return true;\n if (expected === null || received === null) return Object.is(received, expected);\n if (typeof expected !== 'object' || typeof received !== 'object')\n return Object.is(received, expected);\n\n if (Array.isArray(expected) && Array.isArray(received)) {\n if (expected.length !== received.length) return false;\n for (let i = 0; i < expected.length; i++) {\n if (!deepMatchObject(received[i], expected[i])) return false;\n }\n return true;\n }\n if (Array.isArray(expected) || Array.isArray(received)) return false;\n\n const expObj = expected as Record<string, unknown>;\n const recObj = received as Record<string, unknown>;\n for (const key of Object.keys(expObj)) {\n if (!Object.hasOwn(recObj, key)) return false;\n if (!deepMatchObject(recObj[key], expObj[key])) return false;\n }\n return true;\n}\n\n/**\n * Resolves a dot-separated property path on an object.\n * Returns `{ found: true, value }` or `{ found: false }`.\n */\nfunction resolvePath(obj: unknown, dotPath: string): { found: boolean; value?: unknown } {\n const parts = dotPath.split('.');\n let cur: unknown = obj;\n for (const part of parts) {\n if (cur === null || cur === undefined || typeof cur !== 'object') return { found: false };\n const record = cur as Record<string, unknown>;\n if (!Object.hasOwn(record, part)) return { found: false };\n cur = record[part];\n }\n return { found: true, value: cur };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Lightweight expect implementation */\n/* -------------------------------------------------------------------------- */\n\n/** Thrown by expect matchers on failure. */\nclass AssertionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'AssertionError';\n }\n}\n\n/** Minimal expect builder compatible with Vitest's jest-style API. */\nclass Expectation {\n #received: unknown;\n #negated = false;\n\n constructor(received: unknown) {\n this.#received = received;\n }\n\n get not(): this {\n const neg = new Expectation(this.#received) as this;\n neg.#negated = true;\n return neg;\n }\n\n #assert(pass: boolean, msg: string): void {\n const actual = this.#negated ? !pass : pass;\n if (!actual) {\n throw new AssertionError(this.#negated ? `Expected NOT: ${msg}` : msg);\n }\n }\n\n toBe(expected: unknown): void {\n this.#assert(\n Object.is(this.#received, expected),\n `Expected ${String(expected)}, received ${String(this.#received)}`,\n );\n }\n\n toEqual(expected: unknown): void {\n this.#assert(\n deepEqual(this.#received, expected),\n `Expected ${JSON.stringify(expected)}, received ${JSON.stringify(this.#received)}`,\n );\n }\n\n toBeTruthy(): void {\n this.#assert(Boolean(this.#received), `Expected truthy, received ${String(this.#received)}`);\n }\n\n toBeFalsy(): void {\n this.#assert(!this.#received, `Expected falsy, received ${String(this.#received)}`);\n }\n\n toBeNull(): void {\n this.#assert(this.#received === null, `Expected null, received ${String(this.#received)}`);\n }\n\n toBeUndefined(): void {\n this.#assert(\n this.#received === undefined,\n `Expected undefined, received ${String(this.#received)}`,\n );\n }\n\n toBeGreaterThan(n: number): void {\n this.#assert(\n typeof this.#received === 'number' && this.#received > n,\n `Expected > ${n}, received ${String(this.#received)}`,\n );\n }\n\n toBeLessThan(n: number): void {\n this.#assert(\n typeof this.#received === 'number' && this.#received < n,\n `Expected < ${n}, received ${String(this.#received)}`,\n );\n }\n\n toContain(sub: unknown): void {\n if (Array.isArray(this.#received)) {\n this.#assert(\n this.#received.includes(sub),\n `Expected array to contain ${JSON.stringify(sub)}, received ${JSON.stringify(this.#received)}`,\n );\n return;\n }\n this.#assert(\n typeof this.#received === 'string' && typeof sub === 'string' && this.#received.includes(sub),\n `Expected to contain \"${String(sub)}\", received \"${String(this.#received)}\"`,\n );\n }\n\n toThrow(msgFragment?: string): void {\n if (typeof this.#received !== 'function') {\n throw new AssertionError('toThrow: expected a function');\n }\n let threw = false;\n let errorMsg = '';\n try {\n (this.#received as () => unknown)();\n } catch (e) {\n threw = true;\n errorMsg = e instanceof Error ? e.message : String(e);\n }\n if (msgFragment !== undefined) {\n this.#assert(\n threw && errorMsg.includes(msgFragment),\n `Expected to throw containing \"${msgFragment}\", got \"${errorMsg}\"`,\n );\n } else {\n this.#assert(threw, 'Expected function to throw');\n }\n }\n\n // ---- New matchers (devtools#683) -----------------------------------------\n\n toMatchObject(expected: Record<string, unknown>): void {\n this.#assert(\n deepMatchObject(this.#received, expected),\n `Expected object to match ${JSON.stringify(expected)}, received ${JSON.stringify(this.#received)}`,\n );\n }\n\n toHaveProperty(dotPath: string, ...rest: unknown[]): void {\n const { found, value: actual } = resolvePath(this.#received, dotPath);\n if (rest.length > 0) {\n const value = rest[0];\n this.#assert(\n found && deepEqual(actual, value),\n `Expected property \"${dotPath}\" to equal ${JSON.stringify(value)}, got ${JSON.stringify(actual)}`,\n );\n } else {\n this.#assert(found, `Expected property \"${dotPath}\" to exist`);\n }\n }\n\n // The `abstract new (...args: never) => unknown` signature is the widest\n // constructor type that TypeScript allows without explicit `any`. Real-world\n // constructors like `ErrorConstructor` are assignable to it because `never`\n // in parameter position is contravariant (any arg list satisfies `never[]`).\n toBeInstanceOf(ctor: abstract new (...args: never) => unknown): void {\n this.#assert(\n this.#received instanceof (ctor as new (...args: unknown[]) => unknown),\n `Expected instance of ${(ctor as { name?: string }).name ?? String(ctor)}, received ${String(this.#received)}`,\n );\n }\n\n toBeTypeOf(typeStr: string): void {\n this.#assert(\n typeof this.#received === typeStr,\n `Expected typeof \"${typeStr}\", received \"${typeof this.#received}\"`,\n );\n }\n}\n\n/**\n * The `expect` callable plus its asymmetric-matcher static surface\n * (`expect.any`, `expect.objectContaining`, …). The user file accesses these as\n * `expect.any(String)`; the vitest redirect (`bundle.ts`) re-exports `expect` by\n * reading `globalThis.expect`, so attaching the statics to this function object\n * makes them reachable through the getter (a function's own properties survive\n * the getter that returns the function itself).\n */\ninterface ExpectStatic {\n (received: unknown): Expectation;\n any(ctor: AnyCtor): AsymmetricMatcher;\n anything(): AsymmetricMatcher;\n objectContaining(expected: Record<string, unknown>): AsymmetricMatcher;\n arrayContaining(expected: unknown[]): AsymmetricMatcher;\n stringContaining(sub: string): AsymmetricMatcher;\n stringMatching(re: string | RegExp): AsymmetricMatcher;\n}\n\n/** The `expect` function installed as a global. */\nconst expect: ExpectStatic = ((received: unknown): Expectation =>\n new Expectation(received)) as ExpectStatic;\n\n// Asymmetric matcher statics — vitest-compatible surface (devtools#692).\nexpect.any = makeAny;\nexpect.anything = makeAnything;\nexpect.objectContaining = makeObjectContaining;\nexpect.arrayContaining = makeArrayContaining;\nexpect.stringContaining = makeStringContaining;\nexpect.stringMatching = makeStringMatching;\n\n/* -------------------------------------------------------------------------- */\n/* Vitest-4-compatible task context (devtools#746) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Internal sentinel thrown by `ctx.skip()` to unwind the test body early.\n * Caught by the run loop (never surfaced as a failure) — see `runTestModule`'s\n * test-execution `catch` branch.\n */\nclass InPageSkipSentinel {\n constructor(readonly note: string | undefined) {}\n}\n\n/**\n * Minimal vitest-4-compatible task context passed as the first argument to a\n * test function, e.g. `it('...', async (ctx) => { ctx.skip(cond, note); })`.\n *\n * Real vitest's `TestContext` is much larger (`expect`, `onTestFailed`, …) —\n * this shim only implements what sdk-example's device-fixture suites actually\n * use: `ctx.skip` (vitest 4 semantics below) and a trivial `ctx.task.name`.\n */\nexport interface TestContext {\n /**\n * `ctx.skip()` (no args) unconditionally skips the remainder of the test\n * body. `ctx.skip(cond, note)` skips only when `cond` is truthy; when falsy,\n * this is a no-op and the body continues normally. Skipping throws an\n * internal sentinel — do not catch `InPageSkipSentinel` in user code.\n */\n skip(condition?: boolean, note?: string): void;\n /** Trivial task descriptor — nice-to-have, mirrors vitest's `ctx.task.name`. */\n task: { name: string };\n}\n\nfunction _makeTestContext(name: string): TestContext {\n return {\n skip(condition?: boolean, note?: string): void {\n if (condition === undefined || condition) {\n throw new InPageSkipSentinel(note);\n }\n },\n task: { name },\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* describe / it / test registry */\n/* -------------------------------------------------------------------------- */\n\n/** A test function as the user writes it — 0-arg or 1-arg (vitest task context). */\ntype TestFn = (ctx: TestContext) => void | Promise<void>;\n\ninterface PendingTest {\n suitePath: string[];\n name: string;\n fn: TestFn;\n skip: boolean;\n}\n\nconst _pendingTests: PendingTest[] = [];\nconst _suiteStack: string[] = [];\n\n/** Registers a suite scope; calls `fn` synchronously to collect inner tests. */\nfunction describe(name: string, fn: () => void): void {\n _suiteStack.push(name);\n fn();\n _suiteStack.pop();\n}\n\n/** Registers a test. */\nfunction it(name: string, fn: TestFn): void {\n _pendingTests.push({ suitePath: [..._suiteStack], name, fn, skip: false });\n}\n\n/** Alias for `it`. */\nconst test = it;\n\n/** Skipped test — registered but not executed. */\ndescribe.skip = (name: string, _fn: () => void): void => {\n void name;\n};\nit.skip = (name: string, _fn?: TestFn): void => {\n _pendingTests.push({ suitePath: [..._suiteStack], name, fn: () => {}, skip: true });\n};\ntest.skip = it.skip;\n\n/**\n * `it.skipIf(cond)(name, fn)` / `it.runIf(cond)(name, fn)` — conditional test\n * registration. `skipIf` skips when `cond` is truthy; `runIf` runs only when\n * `cond` is truthy (skips otherwise). sdk-example uses\n * `it.skipIf(cell.platform === 'mock')(...)` to skip real-SDK-only cases in env1.\n */\ntype ItRegistrar = (name: string, fn: TestFn) => void;\nfunction _conditionalIt(skip: boolean): ItRegistrar {\n return (name, fn) => {\n _pendingTests.push({ suitePath: [..._suiteStack], name, fn, skip });\n };\n}\nit.skipIf = (cond: unknown): ItRegistrar => _conditionalIt(Boolean(cond));\nit.runIf = (cond: unknown): ItRegistrar => _conditionalIt(!cond);\ntest.skipIf = it.skipIf;\ntest.runIf = it.runIf;\n\n/**\n * `describe.skipIf(cond)(name, fn)` / `describe.runIf(cond)(name, fn)`.\n * When skipped, the suite body still runs to register its tests but every test\n * inside is marked skipped (collected via a temporary skip flag is overkill —\n * a skipped describe simply does not invoke its body, mirroring `describe.skip`).\n */\ntype DescribeRegistrar = (name: string, fn: () => void) => void;\ndescribe.skipIf = (cond: unknown): DescribeRegistrar =>\n cond ? (name, _fn) => void name : (name, fn) => describe(name, fn);\ndescribe.runIf = (cond: unknown): DescribeRegistrar =>\n cond ? (name, fn) => describe(name, fn) : (name, _fn) => void name;\n\n/* -------------------------------------------------------------------------- */\n/* Lifecycle hooks (devtools#683) */\n/* -------------------------------------------------------------------------- */\n\ntype HookType = 'beforeAll' | 'afterAll' | 'beforeEach' | 'afterEach';\n\ninterface HookEntry {\n /** Suite path at the time of registration ([] = module scope). */\n suitePath: string[];\n type: HookType;\n fn: () => void | Promise<void>;\n}\n\nconst _hooks: HookEntry[] = [];\n\nfunction _registerHook(type: HookType, fn: () => void | Promise<void>): void {\n _hooks.push({ suitePath: [..._suiteStack], type, fn });\n}\n\n/**\n * Returns hooks whose suitePath is a prefix of `testSuitePath`\n * (i.e. the hook's scope contains the test).\n */\nfunction _hooksFor(type: HookType, testSuitePath: string[]): Array<() => void | Promise<void>> {\n return _hooks\n .filter((h) => {\n if (h.type !== type) return false;\n // Hook scope must be a prefix of the test's suite path\n if (h.suitePath.length > testSuitePath.length) return false;\n for (let i = 0; i < h.suitePath.length; i++) {\n if (h.suitePath[i] !== testSuitePath[i]) return false;\n }\n return true;\n })\n .map((h) => h.fn);\n}\n\n/** Runs an array of hook functions in order, awaiting each. */\nasync function _runHooks(fns: Array<() => void | Promise<void>>): Promise<Error | null> {\n for (const fn of fns) {\n try {\n await fn();\n } catch (e) {\n return e instanceof Error ? e : new Error(String(e));\n }\n }\n return null;\n}\n\nfunction beforeAll(fn: () => void | Promise<void>): void {\n _registerHook('beforeAll', fn);\n}\nfunction afterAll(fn: () => void | Promise<void>): void {\n _registerHook('afterAll', fn);\n}\nfunction beforeEach(fn: () => void | Promise<void>): void {\n _registerHook('beforeEach', fn);\n}\nfunction afterEach(fn: () => void | Promise<void>): void {\n _registerHook('afterEach', fn);\n}\n\n/* -------------------------------------------------------------------------- */\n/* vi shim (devtools#683) */\n/* -------------------------------------------------------------------------- */\n\ninterface SpyCall {\n args: unknown[];\n returnValue: unknown;\n}\n\ninterface MockFn<T extends unknown[], R> {\n (...args: T): R;\n mock: { calls: SpyCall[] };\n mockImplementation(fn: (...args: T) => R): this;\n mockReturnValue(value: R): this;\n mockRestore(): void;\n}\n\ninterface SpyRecord {\n obj: Record<string, unknown>;\n method: string;\n original: unknown;\n}\n\nconst _spyRegistry: SpyRecord[] = [];\n\nfunction _createMockFn<T extends unknown[], R>(impl?: (...args: T) => R): MockFn<T, R> {\n let currentImpl: ((...args: T) => R) | undefined = impl;\n const calls: SpyCall[] = [];\n\n const mockFn = ((...args: T): R => {\n const returnValue = currentImpl ? currentImpl(...args) : (undefined as unknown as R);\n calls.push({ args, returnValue });\n return returnValue;\n }) as MockFn<T, R>;\n\n mockFn.mock = { calls };\n\n mockFn.mockImplementation = function (fn: (...args: T) => R): typeof mockFn {\n currentImpl = fn;\n return this;\n };\n\n mockFn.mockReturnValue = function (value: R): typeof mockFn {\n currentImpl = () => value;\n return this;\n };\n\n // No-op restore for standalone fn (only spyOn-created fns have a real restore)\n mockFn.mockRestore = (): void => {};\n\n return mockFn;\n}\n\nconst vi = {\n /** Creates a spy on `obj[method]`, replacing it with a mock function. */\n spyOn<T extends Record<string, unknown>, K extends keyof T>(\n obj: T,\n method: K,\n ): MockFn<unknown[], unknown> {\n const original = obj[method];\n const spy = _createMockFn<unknown[], unknown>(\n typeof original === 'function' ? (original as (...args: unknown[]) => unknown) : undefined,\n );\n\n spy.mockRestore = (): void => {\n obj[method] = original as T[K];\n };\n\n _spyRegistry.push({ obj: obj as Record<string, unknown>, method: method as string, original });\n obj[method] = spy as unknown as T[K];\n return spy;\n },\n\n /** Creates a standalone mock function, optionally wrapping `impl`. */\n fn<T extends unknown[], R>(impl?: (...args: T) => R): MockFn<T, R> {\n return _createMockFn(impl);\n },\n\n /** Restores all spies created via `vi.spyOn` to their original values. */\n restoreAllMocks(): void {\n for (const { obj, method, original } of _spyRegistry) {\n obj[method] = original;\n }\n _spyRegistry.length = 0;\n },\n};\n\n/* -------------------------------------------------------------------------- */\n/* Runtime entry point */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Runtime globals object — exported for direct use in unit tests so that\n * test factories can reference runtime's own `it`/`expect`/`beforeAll`/etc.\n * without depending on `globalThis` injection.\n *\n * In a real WebView bundle these are accessed via globals installed by\n * `runTestModule`; in Node tests, import from here directly.\n */\nexport const runtimeGlobals = {\n describe,\n it,\n test,\n expect,\n beforeAll,\n afterAll,\n beforeEach,\n afterEach,\n vi,\n} as const;\n\n/**\n * Installs describe/it/test/expect/afterAll/afterEach/beforeAll/beforeEach/vi\n * as globals, invokes `moduleFactory` to register the user's tests, then\n * executes them and returns a RunReport.\n *\n * This function is exported as `__testBundle.runTestModule` by the IIFE wrapper\n * that bundle.ts generates. The Node-side rpc.ts calls it via `Runtime.evaluate`.\n *\n * @param moduleFactory - A zero-argument function that contains the user's\n * top-level test code (describe/it/test calls). The bundler wraps the entire\n * test module so that its top-level statements become the body of this factory.\n * @param opts - Test-only overrides (see {@link RunTestModuleOptions}). Never\n * passed by the generated bundle (`rpc.ts#buildRunTestsExpression` calls\n * `runTestModule(__userFactory)` with exactly one argument) — this second\n * parameter exists so unit tests can inject a fake sleep and assert the\n * THROTTLED-retry timing without real delays.\n */\nexport async function runTestModule(\n moduleFactory?: () => void | Promise<void>,\n opts?: RunTestModuleOptions,\n): Promise<RunReport> {\n const sleep = opts?.sleep ?? DEFAULT_SLEEP;\n // Reset state for re-entrant calls within the same page context.\n _pendingTests.length = 0;\n _suiteStack.length = 0;\n _hooks.length = 0;\n _spyRegistry.length = 0;\n\n // Install globals that user test code expects.\n type G = typeof globalThis & {\n describe: typeof describe;\n it: typeof it;\n test: typeof test;\n expect: typeof expect;\n beforeAll: typeof beforeAll;\n afterAll: typeof afterAll;\n beforeEach: typeof beforeEach;\n afterEach: typeof afterEach;\n vi: typeof vi;\n };\n const g = globalThis as G;\n g.describe = describe;\n g.it = it;\n g.test = test;\n g.expect = expect;\n g.beforeAll = beforeAll;\n g.afterAll = afterAll;\n g.beforeEach = beforeEach;\n g.afterEach = afterEach;\n g.vi = vi;\n\n // Run the factory (which registers describe/it/test blocks via globals).\n if (moduleFactory) {\n await moduleFactory();\n }\n\n const wallStart = Date.now();\n const startedAt = new Date(wallStart).toISOString();\n const results: TestResult[] = [];\n\n // Determine unique suite scopes from registered tests, in discovery order.\n // We need to fire beforeAll/afterAll once per scope (grouped by suitePath).\n //\n // Simplified model: we run beforeAll hooks before the first test in a scope\n // and afterAll hooks after the last test in a scope. Scope identity is the\n // entire suitePath string (e.g. \"Suite A > Suite B\").\n //\n // For module-scope hooks (suitePath=[]), they wrap the entire test run.\n\n // Group tests by their suite key to track beforeAll/afterAll per scope.\n // We keep a Set of scopes for which beforeAll has been fired.\n const firedBeforeAll = new Set<string>();\n // Map from scope key → last index in _pendingTests that belongs to that scope.\n const lastIndexForScope = new Map<string, number>();\n for (let i = 0; i < _pendingTests.length; i++) {\n const key = _pendingTests[i].suitePath.join('\\0');\n lastIndexForScope.set(key, i);\n // Also compute for parent scopes (module scope = '')\n const path = _pendingTests[i].suitePath;\n for (let depth = 0; depth < path.length; depth++) {\n const parentKey = path.slice(0, depth).join('\\0');\n const cur = lastIndexForScope.get(parentKey) ?? -1;\n if (i > cur) lastIndexForScope.set(parentKey, i);\n }\n }\n // Module scope key\n const MODULE_KEY = '';\n if (!lastIndexForScope.has(MODULE_KEY) && _pendingTests.length > 0) {\n lastIndexForScope.set(MODULE_KEY, _pendingTests.length - 1);\n }\n\n // Fire module-scope beforeAll before any tests\n if (_pendingTests.length > 0) {\n const baFns = _hooksFor('beforeAll', []);\n const baErr = await _runHooks(baFns);\n firedBeforeAll.add(MODULE_KEY);\n if (baErr) {\n // If module beforeAll fails, mark all tests as failed.\n for (const pending of _pendingTests) {\n const fullName = [...pending.suitePath, pending.name].join(' > ');\n results.push({\n name: fullName,\n status: 'fail',\n duration: 0,\n error: `beforeAll failed: ${baErr.message}`,\n });\n }\n const duration = Date.now() - wallStart;\n const passed = results.filter((r) => r.status === 'pass').length;\n const failed = results.filter((r) => r.status === 'fail').length;\n const skipped = results.filter((r) => r.status === 'skip').length;\n // Still run module afterAll for cleanup (e.g. flushCapture)\n const aaFns = _hooksFor('afterAll', []);\n await _runHooks(aaFns);\n return { startedAt, duration, passed, failed, skipped, tests: results };\n }\n }\n\n // devtools#767 --pace: read the page-global __AIT_PACE_MS__ (injected by\n // relay-factory.ts the same way __AIT_CELL__ is, via cell.ts#injectGlobals)\n // ONCE per run. Absent/0 (the default) means zero behavior change — the\n // wait below becomes a no-op sleep(0) that resolves on the next microtask\n // tick, same as today's unpaced loop for every existing caller.\n const paceMs = (() => {\n const raw = (globalThis as { __AIT_PACE_MS__?: unknown }).__AIT_PACE_MS__;\n return typeof raw === 'number' && raw > 0 ? raw : 0;\n })();\n let ranFirstTest = false;\n\n for (let i = 0; i < _pendingTests.length; i++) {\n const pending = _pendingTests[i];\n const fullName = [...pending.suitePath, pending.name].join(' > ');\n\n if (pending.skip) {\n results.push({ name: fullName, status: 'skip', duration: 0 });\n continue;\n }\n\n // --pace inter-test spacing: wait BEFORE every test after the first one\n // that actually runs (skipped tests above never reach here). Mirrors the\n // permission preflight's \"no wait before the first probe\" pacing model\n // (cell.ts#buildPermissionPreflightExpression).\n if (paceMs > 0 && ranFirstTest) {\n await sleep(paceMs);\n }\n ranFirstTest = true;\n\n // Fire suite-scoped beforeAll for any new scopes entered by this test\n for (let depth = 1; depth <= pending.suitePath.length; depth++) {\n const scopePath = pending.suitePath.slice(0, depth);\n const scopeKey = scopePath.join('\\0');\n if (!firedBeforeAll.has(scopeKey)) {\n // Only hooks registered exactly at this scope (not broader/narrower)\n const scopedFns = _hooks\n .filter((h) => h.type === 'beforeAll' && h.suitePath.join('\\0') === scopeKey)\n .map((h) => h.fn);\n const baErr = await _runHooks(scopedFns);\n firedBeforeAll.add(scopeKey);\n if (baErr) {\n // Mark remaining tests in this scope as failed\n results.push({\n name: fullName,\n status: 'fail',\n duration: 0,\n error: `beforeAll failed: ${baErr.message}`,\n });\n }\n }\n }\n\n // beforeEach\n const beFns = _hooksFor('beforeEach', pending.suitePath);\n const beErr = await _runHooks(beFns);\n\n const tStart = Date.now();\n let testErr: string | undefined;\n let inPageSkip: InPageSkipSentinel | undefined;\n let throttleRetries = 0;\n\n if (beErr) {\n testErr = `beforeEach failed: ${beErr.message}`;\n } else {\n // THROTTLED-aware retry (devtools#767): only the test BODY is retried —\n // beforeEach (above) and afterEach (below) each run exactly once\n // regardless of how many times the body is attempted. This keeps any\n // capture-emitting hook (e.g. sdk-example's flushCapture) firing exactly\n // once per test, so a retried body can never duplicate a capture record\n // (see relay-worker.ts's harvesting model — it's a passive listener over\n // the whole run, not a per-attempt flush/drain, so this invariant is what\n // keeps a retry from polluting the capture stream).\n for (let attempt = 0; ; attempt++) {\n try {\n await pending.fn(_makeTestContext(fullName));\n testErr = undefined;\n break;\n } catch (e) {\n if (e instanceof InPageSkipSentinel) {\n inPageSkip = e;\n break;\n }\n testErr = stringifyThrown(e);\n if (isThrottledError(e) && attempt < TEST_MAX_RETRIES) {\n throttleRetries += 1;\n await sleep(TEST_RETRY_BACKOFF_MS[attempt]);\n continue;\n }\n break;\n }\n }\n }\n\n // afterEach — always run even if test failed or was skipped mid-body.\n const aeFns = _hooksFor('afterEach', pending.suitePath);\n const aeErr = await _runHooks(aeFns);\n if (aeErr && !testErr && !inPageSkip) testErr = `afterEach failed: ${aeErr.message}`;\n\n if (inPageSkip) {\n // ctx.skip() unwound the body — record as skipped (not pass/fail), same\n // as the static it.skip path. The note (if any) rides on TestResult.note.\n results.push({\n name: fullName,\n status: 'skip',\n duration: Date.now() - tStart,\n ...(inPageSkip.note !== undefined ? { note: inPageSkip.note } : {}),\n });\n } else if (testErr !== undefined) {\n results.push({\n name: fullName,\n status: 'fail',\n duration: Date.now() - tStart,\n error: testErr,\n ...(throttleRetries > 0 ? { throttleRetries } : {}),\n });\n } else {\n results.push({\n name: fullName,\n status: 'pass',\n duration: Date.now() - tStart,\n ...(throttleRetries > 0 ? { throttleRetries } : {}),\n });\n }\n\n // Fire suite-scoped afterAll when this is the last test in each scope\n for (let depth = pending.suitePath.length; depth >= 1; depth--) {\n const scopePath = pending.suitePath.slice(0, depth);\n const scopeKey = scopePath.join('\\0');\n const lastIdx = lastIndexForScope.get(scopeKey) ?? -1;\n if (lastIdx === i) {\n const scopedFns = _hooks\n .filter((h) => h.type === 'afterAll' && h.suitePath.join('\\0') === scopeKey)\n .map((h) => h.fn);\n await _runHooks(scopedFns);\n }\n }\n }\n\n // Fire module-scope afterAll after all tests — critical for flushCapture\n const moduleAfterAllFns = _hooks\n .filter((h) => h.type === 'afterAll' && h.suitePath.length === 0)\n .map((h) => h.fn);\n await _runHooks(moduleAfterAllFns);\n\n const duration = Date.now() - wallStart;\n const passed = results.filter((r) => r.status === 'pass').length;\n const failed = results.filter((r) => r.status === 'fail').length;\n const skipped = results.filter((r) => r.status === 'skip').length;\n\n return { startedAt, duration, passed, failed, skipped, tests: results };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"runtime.js","names":["#received","#negated","#assert"],"sources":["../../src/test-runner/runtime.ts"],"sourcesContent":["/**\n * Thin test runtime for WebView execution.\n *\n * This file is bundled by `bundle.ts` together with the user's test file\n * into a single IIFE injected into the WebView via `Runtime.evaluate`.\n * It MUST stay browser-compatible — no Node.js APIs allowed.\n *\n * Design: rather than shipping @vitest/runner verbatim into a WebView (where\n * its Node-side internals cause issues), this runtime provides a minimal\n * compatible API surface — describe/it/test/expect globals — collects results\n * into a plain JSON-safe object, and exports `runTestModule` as the entry point.\n *\n * @vitest/runner and @vitest/expect are listed as dependencies so that the\n * package's type contracts are available and so the browser-compatible subsets\n * can be referenced. The Vitest custom pool that drives this runtime through\n * Vitest's `PoolRunnerInitializer` lives in `pool.ts`.\n *\n * NOTE: this file is imported by type from Node-side code (rpc.ts / relay-worker.ts)\n * for the RunReport / TestResult type shapes. The runtime ITSELF is not imported\n * at runtime on the Node side — only the types are used.\n */\n\nimport { isThrottledError } from './throttle.js';\n\n/* -------------------------------------------------------------------------- */\n/* Public result types (used by rpc.ts and relay-worker.ts) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Result of a single test case.\n * All fields are JSON-serialisable.\n */\nexport interface TestResult {\n /** Full dot-joined test name including nested suite names. */\n name: string;\n /** `'pass'` or `'fail'`. `'skip'` for skipped tests. */\n status: 'pass' | 'fail' | 'skip';\n /** Duration in milliseconds. */\n duration: number;\n /** Error message (fail only). Does NOT include the expression/secret. */\n error?: string;\n /**\n * Optional note attached to a skip — populated when the test body itself\n * calls `ctx.skip(cond, note)` (devtools#746), mirroring the static\n * `it.skip`/`it.skipIf` path which has no note today. `'skip'` status only.\n */\n note?: string;\n /**\n * Number of throttle-aware re-executions of the test BODY (devtools#767) —\n * present only when at least one retry was attempted (`>= 1`); omitted\n * (never `0`) for every test that never hit `APP_BRIDGE_THROTTLED`, so\n * existing consumers (sdk-example's diff script) see byte-identical reports\n * for the common case. A test that eventually PASSES after retrying still\n * carries this field — it is provenance, not a failure flag.\n */\n throttleRetries?: number;\n}\n\n/** Aggregate report returned by `runTestModule`. */\nexport interface RunReport {\n /** ISO timestamp of when `runTestModule` was called. */\n startedAt: string;\n /** Total elapsed milliseconds (wall-clock). */\n duration: number;\n passed: number;\n failed: number;\n skipped: number;\n tests: TestResult[];\n}\n\n/* -------------------------------------------------------------------------- */\n/* THROTTLED-aware test retry (devtools#767) */\n/* -------------------------------------------------------------------------- */\n\n/** Backoff delays (ms) between throttle-retry attempts: 1s, then 2s. */\nconst TEST_RETRY_BACKOFF_MS = [1_000, 2_000] as const;\n\n/** Max retry attempts per test after the initial run (devtools#767: \"최대 2회 재시도\"). */\nconst TEST_MAX_RETRIES = TEST_RETRY_BACKOFF_MS.length;\n\n/** Real `setTimeout`-backed sleep — the production default for {@link RunTestModuleOptions.sleep}. */\nconst DEFAULT_SLEEP = (ms: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, ms));\n\n/**\n * Test-only override surface for `runTestModule`. Never populated by the\n * generated bundle — see that function's doc.\n */\nexport interface RunTestModuleOptions {\n /**\n * Overrides the backoff/pacing sleep implementation. Defaults to a real\n * `setTimeout`-backed sleep. Unit tests pass a fake (e.g. an\n * immediately-resolving spy) so retry/pacing tests don't take real\n * wall-clock seconds.\n */\n sleep?: (ms: number) => Promise<void>;\n}\n\n/**\n * Renders a thrown value into a `TestResult.error` string. Handles three\n * shapes:\n * - a real `Error` (or subclass) — its `.message`;\n * - an error-LIKE plain object with a string `.message` (the 2.x native\n * bridge envelope this test-runner observes, e.g.\n * `{name, code, userInfo, moduleName, __isError}` — devtools#767's\n * `APP_BRIDGE_THROTTLED` rejections are exactly this shape, not an\n * `Error` instance) — its `.message` field;\n * - anything else — `String(e)` (the pre-#767 fallback, unchanged).\n *\n * Without the middle case, a native envelope's `.message` (the only useful\n * diagnostic text) was discarded in favor of the useless `\"[object Object]\"`.\n */\nfunction stringifyThrown(e: unknown): string {\n if (e instanceof Error) return e.message;\n if (typeof e === 'object' && e !== null) {\n const rec = e as { message?: unknown };\n if (typeof rec.message === 'string') return rec.message;\n }\n return String(e);\n}\n\n/* -------------------------------------------------------------------------- */\n/* Asymmetric matchers (devtools#692) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Brand property that tags asymmetric matcher markers (`expect.any(String)`,\n * `expect.objectContaining(...)`, etc). A reserved string key is used as the\n * brand so the guard stays a plain own-property check — markers are only ever\n * compared in-process by reference, so no cross-realm Symbol coordination is\n * needed.\n */\nconst ASYMMETRIC_BRAND = '$$asymmetricMatch' as const;\n\n/**\n * An asymmetric matcher: a placeholder that, when found on the `expected` side\n * of a deep comparison, runs its own `asymmetricMatch(actual)` predicate\n * instead of being structurally compared as a plain object. This is what makes\n * `toMatchObject({ hash: expect.any(String) })` match any string `hash`.\n */\ninterface AsymmetricMatcher {\n /** Brand discriminator — present only on asymmetric markers. */\n [ASYMMETRIC_BRAND]: true;\n /** Human-readable label used in failure messages. */\n toString(): string;\n /** Returns true when `actual` satisfies this matcher. */\n asymmetricMatch(actual: unknown): boolean;\n}\n\n/** Narrowing guard: is `v` an asymmetric matcher marker? */\nfunction isAsymmetric(v: unknown): v is AsymmetricMatcher {\n return (\n typeof v === 'object' &&\n v !== null &&\n (v as Record<string, unknown>)[ASYMMETRIC_BRAND] === true &&\n typeof (v as { asymmetricMatch?: unknown }).asymmetricMatch === 'function'\n );\n}\n\n/** The widest constructor type TypeScript allows without `any` (see toBeInstanceOf). */\ntype AnyCtor = abstract new (...args: never) => unknown;\n\n/**\n * `expect.any(Ctor)` — matches a value by its constructor. Primitive wrappers\n * (`String`/`Number`/`Boolean`) are matched by `typeof` (so the unboxed\n * primitive matches) AND by `instanceof` (so a boxed wrapper instance also\n * matches). `BigInt`/`Symbol` match by `typeof`. `Object` matches any non-null\n * object (NOT functions, matching vitest), `Array` uses `Array.isArray`,\n * `Function` uses `typeof === 'function'`, and any other constructor (user\n * classes) falls back to `instanceof`.\n */\nfunction makeAny(ctor: AnyCtor): AsymmetricMatcher {\n const name = (ctor as { name?: string }).name ?? String(ctor);\n const ctorAsFn = ctor as unknown as (new (...args: unknown[]) => unknown) | undefined;\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `Any<${name}>`,\n asymmetricMatch(actual: unknown): boolean {\n if (actual === null || actual === undefined) return false;\n switch (ctor as unknown) {\n case String:\n return typeof actual === 'string' || actual instanceof String;\n case Number:\n return typeof actual === 'number' || actual instanceof Number;\n case Boolean:\n return typeof actual === 'boolean' || actual instanceof Boolean;\n case BigInt:\n return typeof actual === 'bigint';\n case Symbol:\n return typeof actual === 'symbol';\n case Function:\n return typeof actual === 'function';\n case Object:\n // Match vitest: `expect.any(Object)` accepts objects only, NOT\n // functions (vitest checks `typeof other === 'object'`). `null` is\n // already excluded by the guard above.\n return typeof actual === 'object';\n case Array:\n return Array.isArray(actual);\n default:\n return ctorAsFn ? actual instanceof ctorAsFn : false;\n }\n },\n };\n}\n\n/** `expect.anything()` — matches any value except `null`/`undefined`. */\nfunction makeAnything(): AsymmetricMatcher {\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => 'Anything',\n asymmetricMatch: (actual: unknown): boolean => actual !== null && actual !== undefined,\n };\n}\n\n/** `expect.objectContaining(obj)` — partial-match every key of `obj` on `actual`. */\nfunction makeObjectContaining(expected: Record<string, unknown>): AsymmetricMatcher {\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `ObjectContaining<${safeStringify(expected)}>`,\n asymmetricMatch: (actual: unknown): boolean => deepMatchObject(actual, expected),\n };\n}\n\n/** `expect.arrayContaining(arr)` — `actual` is an array containing each element of `arr`. */\nfunction makeArrayContaining(expected: unknown[]): AsymmetricMatcher {\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `ArrayContaining<${safeStringify(expected)}>`,\n asymmetricMatch(actual: unknown): boolean {\n if (!Array.isArray(actual)) return false;\n return expected.every((exp) =>\n actual.some((act) => (isAsymmetric(exp) ? exp.asymmetricMatch(act) : deepEqual(act, exp))),\n );\n },\n };\n}\n\n/** `expect.stringContaining(sub)` — `actual` is a string containing `sub`. */\nfunction makeStringContaining(sub: string): AsymmetricMatcher {\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `StringContaining<${sub}>`,\n asymmetricMatch: (actual: unknown): boolean =>\n typeof actual === 'string' && actual.includes(sub),\n };\n}\n\n/** `expect.stringMatching(re)` — `actual` is a string matching `re` (string or RegExp). */\nfunction makeStringMatching(re: string | RegExp): AsymmetricMatcher {\n const pattern = typeof re === 'string' ? new RegExp(re) : re;\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `StringMatching<${String(pattern)}>`,\n asymmetricMatch: (actual: unknown): boolean =>\n typeof actual === 'string' && pattern.test(actual),\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Deep equality helpers */\n/* -------------------------------------------------------------------------- */\n\n/** JSON.stringify that never throws (cycles/BigInt) — for failure messages. */\nfunction safeStringify(v: unknown): string {\n try {\n return (\n JSON.stringify(v, (_k, val) =>\n isAsymmetric(val) ? val.toString() : typeof val === 'bigint' ? `${val}n` : val,\n ) ?? String(v)\n );\n } catch {\n return String(v);\n }\n}\n\n/**\n * Recursive structural equality — replaces JSON.stringify comparison to\n * handle key-order differences and undefined values correctly.\n *\n * When the `b` (expected) side is an asymmetric marker, its `asymmetricMatch`\n * predicate decides the result instead of structural comparison — markers are\n * placeholders, not plain objects, so deep-comparing them never matches.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (isAsymmetric(b)) return b.asymmetricMatch(a);\n if (Object.is(a, b)) return true;\n if (a === null || b === null) return false;\n if (typeof a !== 'object' || typeof b !== 'object') return false;\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!deepEqual(a[i], b[i])) return false;\n }\n return true;\n }\n if (Array.isArray(a) || Array.isArray(b)) return false;\n\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const aKeys = Object.keys(aObj);\n const bKeys = Object.keys(bObj);\n if (aKeys.length !== bKeys.length) return false;\n for (const key of aKeys) {\n if (!Object.hasOwn(bObj, key)) return false;\n if (!deepEqual(aObj[key], bObj[key])) return false;\n }\n return true;\n}\n\n/**\n * Partial-match: every key in `expected` must exist in `received` with a\n * recursively matching value. Extra keys in `received` are ignored.\n *\n * As in `deepEqual`, an asymmetric marker on the `expected` side delegates to\n * its `asymmetricMatch` predicate rather than being compared structurally.\n */\nfunction deepMatchObject(received: unknown, expected: unknown): boolean {\n if (isAsymmetric(expected)) return expected.asymmetricMatch(received);\n if (Object.is(received, expected)) return true;\n if (expected === null || received === null) return Object.is(received, expected);\n if (typeof expected !== 'object' || typeof received !== 'object')\n return Object.is(received, expected);\n\n if (Array.isArray(expected) && Array.isArray(received)) {\n if (expected.length !== received.length) return false;\n for (let i = 0; i < expected.length; i++) {\n if (!deepMatchObject(received[i], expected[i])) return false;\n }\n return true;\n }\n if (Array.isArray(expected) || Array.isArray(received)) return false;\n\n const expObj = expected as Record<string, unknown>;\n const recObj = received as Record<string, unknown>;\n for (const key of Object.keys(expObj)) {\n if (!Object.hasOwn(recObj, key)) return false;\n if (!deepMatchObject(recObj[key], expObj[key])) return false;\n }\n return true;\n}\n\n/**\n * Resolves a dot-separated property path on an object.\n * Returns `{ found: true, value }` or `{ found: false }`.\n */\nfunction resolvePath(obj: unknown, dotPath: string): { found: boolean; value?: unknown } {\n const parts = dotPath.split('.');\n let cur: unknown = obj;\n for (const part of parts) {\n if (cur === null || cur === undefined || typeof cur !== 'object') return { found: false };\n const record = cur as Record<string, unknown>;\n if (!Object.hasOwn(record, part)) return { found: false };\n cur = record[part];\n }\n return { found: true, value: cur };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Lightweight expect implementation */\n/* -------------------------------------------------------------------------- */\n\n/** Thrown by expect matchers on failure. */\nclass AssertionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'AssertionError';\n }\n}\n\n/** Minimal expect builder compatible with Vitest's jest-style API. */\nclass Expectation {\n #received: unknown;\n #negated = false;\n\n constructor(received: unknown) {\n this.#received = received;\n }\n\n get not(): this {\n const neg = new Expectation(this.#received) as this;\n neg.#negated = true;\n return neg;\n }\n\n #assert(pass: boolean, msg: string): void {\n const actual = this.#negated ? !pass : pass;\n if (!actual) {\n throw new AssertionError(this.#negated ? `Expected NOT: ${msg}` : msg);\n }\n }\n\n toBe(expected: unknown): void {\n this.#assert(\n Object.is(this.#received, expected),\n `Expected ${String(expected)}, received ${String(this.#received)}`,\n );\n }\n\n toEqual(expected: unknown): void {\n this.#assert(\n deepEqual(this.#received, expected),\n `Expected ${JSON.stringify(expected)}, received ${JSON.stringify(this.#received)}`,\n );\n }\n\n toBeTruthy(): void {\n this.#assert(Boolean(this.#received), `Expected truthy, received ${String(this.#received)}`);\n }\n\n toBeFalsy(): void {\n this.#assert(!this.#received, `Expected falsy, received ${String(this.#received)}`);\n }\n\n toBeNull(): void {\n this.#assert(this.#received === null, `Expected null, received ${String(this.#received)}`);\n }\n\n toBeUndefined(): void {\n this.#assert(\n this.#received === undefined,\n `Expected undefined, received ${String(this.#received)}`,\n );\n }\n\n toBeGreaterThan(n: number): void {\n this.#assert(\n typeof this.#received === 'number' && this.#received > n,\n `Expected > ${n}, received ${String(this.#received)}`,\n );\n }\n\n toBeLessThan(n: number): void {\n this.#assert(\n typeof this.#received === 'number' && this.#received < n,\n `Expected < ${n}, received ${String(this.#received)}`,\n );\n }\n\n toContain(sub: unknown): void {\n if (Array.isArray(this.#received)) {\n this.#assert(\n this.#received.includes(sub),\n `Expected array to contain ${JSON.stringify(sub)}, received ${JSON.stringify(this.#received)}`,\n );\n return;\n }\n this.#assert(\n typeof this.#received === 'string' && typeof sub === 'string' && this.#received.includes(sub),\n `Expected to contain \"${String(sub)}\", received \"${String(this.#received)}\"`,\n );\n }\n\n toThrow(msgFragment?: string): void {\n if (typeof this.#received !== 'function') {\n throw new AssertionError('toThrow: expected a function');\n }\n let threw = false;\n let errorMsg = '';\n try {\n (this.#received as () => unknown)();\n } catch (e) {\n threw = true;\n errorMsg = e instanceof Error ? e.message : String(e);\n }\n if (msgFragment !== undefined) {\n this.#assert(\n threw && errorMsg.includes(msgFragment),\n `Expected to throw containing \"${msgFragment}\", got \"${errorMsg}\"`,\n );\n } else {\n this.#assert(threw, 'Expected function to throw');\n }\n }\n\n // ---- New matchers (devtools#683) -----------------------------------------\n\n toMatchObject(expected: Record<string, unknown>): void {\n this.#assert(\n deepMatchObject(this.#received, expected),\n `Expected object to match ${JSON.stringify(expected)}, received ${JSON.stringify(this.#received)}`,\n );\n }\n\n toHaveProperty(dotPath: string, ...rest: unknown[]): void {\n const { found, value: actual } = resolvePath(this.#received, dotPath);\n if (rest.length > 0) {\n const value = rest[0];\n this.#assert(\n found && deepEqual(actual, value),\n `Expected property \"${dotPath}\" to equal ${JSON.stringify(value)}, got ${JSON.stringify(actual)}`,\n );\n } else {\n this.#assert(found, `Expected property \"${dotPath}\" to exist`);\n }\n }\n\n // The `abstract new (...args: never) => unknown` signature is the widest\n // constructor type that TypeScript allows without explicit `any`. Real-world\n // constructors like `ErrorConstructor` are assignable to it because `never`\n // in parameter position is contravariant (any arg list satisfies `never[]`).\n toBeInstanceOf(ctor: abstract new (...args: never) => unknown): void {\n this.#assert(\n this.#received instanceof (ctor as new (...args: unknown[]) => unknown),\n `Expected instance of ${(ctor as { name?: string }).name ?? String(ctor)}, received ${String(this.#received)}`,\n );\n }\n\n toBeTypeOf(typeStr: string): void {\n this.#assert(\n typeof this.#received === typeStr,\n `Expected typeof \"${typeStr}\", received \"${typeof this.#received}\"`,\n );\n }\n\n // ---- New matcher (devtools#807) -------------------------------------------\n\n toHaveLength(n: number): void {\n const length = (this.#received as { length?: unknown } | null | undefined)?.length;\n this.#assert(\n typeof length === 'number' && length === n,\n `Expected length ${n}, received ${typeof length === 'number' ? length : String(this.#received)}`,\n );\n }\n}\n\n/**\n * The `expect` callable plus its asymmetric-matcher static surface\n * (`expect.any`, `expect.objectContaining`, …). The user file accesses these as\n * `expect.any(String)`; the vitest redirect (`bundle.ts`) re-exports `expect` by\n * reading `globalThis.expect`, so attaching the statics to this function object\n * makes them reachable through the getter (a function's own properties survive\n * the getter that returns the function itself).\n */\ninterface ExpectStatic {\n (received: unknown): Expectation;\n any(ctor: AnyCtor): AsymmetricMatcher;\n anything(): AsymmetricMatcher;\n objectContaining(expected: Record<string, unknown>): AsymmetricMatcher;\n arrayContaining(expected: unknown[]): AsymmetricMatcher;\n stringContaining(sub: string): AsymmetricMatcher;\n stringMatching(re: string | RegExp): AsymmetricMatcher;\n}\n\n/** The `expect` function installed as a global. */\nconst expect: ExpectStatic = ((received: unknown): Expectation =>\n new Expectation(received)) as ExpectStatic;\n\n// Asymmetric matcher statics — vitest-compatible surface (devtools#692).\nexpect.any = makeAny;\nexpect.anything = makeAnything;\nexpect.objectContaining = makeObjectContaining;\nexpect.arrayContaining = makeArrayContaining;\nexpect.stringContaining = makeStringContaining;\nexpect.stringMatching = makeStringMatching;\n\n/* -------------------------------------------------------------------------- */\n/* Vitest-4-compatible task context (devtools#746) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Internal sentinel thrown by `ctx.skip()` to unwind the test body early.\n * Caught by the run loop (never surfaced as a failure) — see `runTestModule`'s\n * test-execution `catch` branch.\n */\nclass InPageSkipSentinel {\n constructor(readonly note: string | undefined) {}\n}\n\n/**\n * Minimal vitest-4-compatible task context passed as the first argument to a\n * test function, e.g. `it('...', async (ctx) => { ctx.skip(cond, note); })`.\n *\n * Real vitest's `TestContext` is much larger (`expect`, `onTestFailed`, …) —\n * this shim only implements what sdk-example's device-fixture suites actually\n * use: `ctx.skip` (vitest 4 semantics below) and a trivial `ctx.task.name`.\n */\nexport interface TestContext {\n /**\n * `ctx.skip()` (no args) unconditionally skips the remainder of the test\n * body. `ctx.skip(cond, note)` skips only when `cond` is truthy; when falsy,\n * this is a no-op and the body continues normally. Skipping throws an\n * internal sentinel — do not catch `InPageSkipSentinel` in user code.\n */\n skip(condition?: boolean, note?: string): void;\n /** Trivial task descriptor — nice-to-have, mirrors vitest's `ctx.task.name`. */\n task: { name: string };\n}\n\nfunction _makeTestContext(name: string): TestContext {\n return {\n skip(condition?: boolean, note?: string): void {\n if (condition === undefined || condition) {\n throw new InPageSkipSentinel(note);\n }\n },\n task: { name },\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* describe / it / test registry */\n/* -------------------------------------------------------------------------- */\n\n/** A test function as the user writes it — 0-arg or 1-arg (vitest task context). */\ntype TestFn = (ctx: TestContext) => void | Promise<void>;\n\ninterface PendingTest {\n suitePath: string[];\n name: string;\n fn: TestFn;\n skip: boolean;\n}\n\nconst _pendingTests: PendingTest[] = [];\nconst _suiteStack: string[] = [];\n\n/** Registers a suite scope; calls `fn` synchronously to collect inner tests. */\nfunction describe(name: string, fn: () => void): void {\n _suiteStack.push(name);\n fn();\n _suiteStack.pop();\n}\n\n/** Registers a test. */\nfunction it(name: string, fn: TestFn): void {\n _pendingTests.push({ suitePath: [..._suiteStack], name, fn, skip: false });\n}\n\n/** Alias for `it`. */\nconst test = it;\n\n/** Skipped test — registered but not executed. */\ndescribe.skip = (name: string, _fn: () => void): void => {\n void name;\n};\nit.skip = (name: string, _fn?: TestFn): void => {\n _pendingTests.push({ suitePath: [..._suiteStack], name, fn: () => {}, skip: true });\n};\ntest.skip = it.skip;\n\n/**\n * `it.skipIf(cond)(name, fn)` / `it.runIf(cond)(name, fn)` — conditional test\n * registration. `skipIf` skips when `cond` is truthy; `runIf` runs only when\n * `cond` is truthy (skips otherwise). sdk-example uses\n * `it.skipIf(cell.platform === 'mock')(...)` to skip real-SDK-only cases in env1.\n */\ntype ItRegistrar = (name: string, fn: TestFn) => void;\nfunction _conditionalIt(skip: boolean): ItRegistrar {\n return (name, fn) => {\n _pendingTests.push({ suitePath: [..._suiteStack], name, fn, skip });\n };\n}\nit.skipIf = (cond: unknown): ItRegistrar => _conditionalIt(Boolean(cond));\nit.runIf = (cond: unknown): ItRegistrar => _conditionalIt(!cond);\ntest.skipIf = it.skipIf;\ntest.runIf = it.runIf;\n\n/**\n * `describe.skipIf(cond)(name, fn)` / `describe.runIf(cond)(name, fn)`.\n * When skipped, the suite body still runs to register its tests but every test\n * inside is marked skipped (collected via a temporary skip flag is overkill —\n * a skipped describe simply does not invoke its body, mirroring `describe.skip`).\n */\ntype DescribeRegistrar = (name: string, fn: () => void) => void;\ndescribe.skipIf = (cond: unknown): DescribeRegistrar =>\n cond ? (name, _fn) => void name : (name, fn) => describe(name, fn);\ndescribe.runIf = (cond: unknown): DescribeRegistrar =>\n cond ? (name, fn) => describe(name, fn) : (name, _fn) => void name;\n\n/* -------------------------------------------------------------------------- */\n/* Lifecycle hooks (devtools#683) */\n/* -------------------------------------------------------------------------- */\n\ntype HookType = 'beforeAll' | 'afterAll' | 'beforeEach' | 'afterEach';\n\ninterface HookEntry {\n /** Suite path at the time of registration ([] = module scope). */\n suitePath: string[];\n type: HookType;\n fn: () => void | Promise<void>;\n}\n\nconst _hooks: HookEntry[] = [];\n\nfunction _registerHook(type: HookType, fn: () => void | Promise<void>): void {\n _hooks.push({ suitePath: [..._suiteStack], type, fn });\n}\n\n/**\n * Returns hooks whose suitePath is a prefix of `testSuitePath`\n * (i.e. the hook's scope contains the test).\n */\nfunction _hooksFor(type: HookType, testSuitePath: string[]): Array<() => void | Promise<void>> {\n return _hooks\n .filter((h) => {\n if (h.type !== type) return false;\n // Hook scope must be a prefix of the test's suite path\n if (h.suitePath.length > testSuitePath.length) return false;\n for (let i = 0; i < h.suitePath.length; i++) {\n if (h.suitePath[i] !== testSuitePath[i]) return false;\n }\n return true;\n })\n .map((h) => h.fn);\n}\n\n/** Runs an array of hook functions in order, awaiting each. */\nasync function _runHooks(fns: Array<() => void | Promise<void>>): Promise<Error | null> {\n for (const fn of fns) {\n try {\n await fn();\n } catch (e) {\n return e instanceof Error ? e : new Error(String(e));\n }\n }\n return null;\n}\n\nfunction beforeAll(fn: () => void | Promise<void>): void {\n _registerHook('beforeAll', fn);\n}\nfunction afterAll(fn: () => void | Promise<void>): void {\n _registerHook('afterAll', fn);\n}\nfunction beforeEach(fn: () => void | Promise<void>): void {\n _registerHook('beforeEach', fn);\n}\nfunction afterEach(fn: () => void | Promise<void>): void {\n _registerHook('afterEach', fn);\n}\n\n/* -------------------------------------------------------------------------- */\n/* vi shim (devtools#683) */\n/* -------------------------------------------------------------------------- */\n\ninterface SpyCall {\n args: unknown[];\n returnValue: unknown;\n}\n\ninterface MockFn<T extends unknown[], R> {\n (...args: T): R;\n mock: { calls: SpyCall[] };\n mockImplementation(fn: (...args: T) => R): this;\n mockReturnValue(value: R): this;\n mockRestore(): void;\n}\n\ninterface SpyRecord {\n obj: Record<string, unknown>;\n method: string;\n original: unknown;\n}\n\nconst _spyRegistry: SpyRecord[] = [];\n\nfunction _createMockFn<T extends unknown[], R>(impl?: (...args: T) => R): MockFn<T, R> {\n let currentImpl: ((...args: T) => R) | undefined = impl;\n const calls: SpyCall[] = [];\n\n const mockFn = ((...args: T): R => {\n const returnValue = currentImpl ? currentImpl(...args) : (undefined as unknown as R);\n calls.push({ args, returnValue });\n return returnValue;\n }) as MockFn<T, R>;\n\n mockFn.mock = { calls };\n\n mockFn.mockImplementation = function (fn: (...args: T) => R): typeof mockFn {\n currentImpl = fn;\n return this;\n };\n\n mockFn.mockReturnValue = function (value: R): typeof mockFn {\n currentImpl = () => value;\n return this;\n };\n\n // No-op restore for standalone fn (only spyOn-created fns have a real restore)\n mockFn.mockRestore = (): void => {};\n\n return mockFn;\n}\n\nconst vi = {\n /** Creates a spy on `obj[method]`, replacing it with a mock function. */\n spyOn<T extends Record<string, unknown>, K extends keyof T>(\n obj: T,\n method: K,\n ): MockFn<unknown[], unknown> {\n const original = obj[method];\n const spy = _createMockFn<unknown[], unknown>(\n typeof original === 'function' ? (original as (...args: unknown[]) => unknown) : undefined,\n );\n\n spy.mockRestore = (): void => {\n obj[method] = original as T[K];\n };\n\n _spyRegistry.push({ obj: obj as Record<string, unknown>, method: method as string, original });\n obj[method] = spy as unknown as T[K];\n return spy;\n },\n\n /** Creates a standalone mock function, optionally wrapping `impl`. */\n fn<T extends unknown[], R>(impl?: (...args: T) => R): MockFn<T, R> {\n return _createMockFn(impl);\n },\n\n /** Restores all spies created via `vi.spyOn` to their original values. */\n restoreAllMocks(): void {\n for (const { obj, method, original } of _spyRegistry) {\n obj[method] = original;\n }\n _spyRegistry.length = 0;\n },\n};\n\n/* -------------------------------------------------------------------------- */\n/* Runtime entry point */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Runtime globals object — exported for direct use in unit tests so that\n * test factories can reference runtime's own `it`/`expect`/`beforeAll`/etc.\n * without depending on `globalThis` injection.\n *\n * In a real WebView bundle these are accessed via globals installed by\n * `runTestModule`; in Node tests, import from here directly.\n */\nexport const runtimeGlobals = {\n describe,\n it,\n test,\n expect,\n beforeAll,\n afterAll,\n beforeEach,\n afterEach,\n vi,\n} as const;\n\n/**\n * Installs describe/it/test/expect/afterAll/afterEach/beforeAll/beforeEach/vi\n * as globals, invokes `moduleFactory` to register the user's tests, then\n * executes them and returns a RunReport.\n *\n * This function is exported as `__testBundle.runTestModule` by the IIFE wrapper\n * that bundle.ts generates. The Node-side rpc.ts calls it via `Runtime.evaluate`.\n *\n * @param moduleFactory - A zero-argument function that contains the user's\n * top-level test code (describe/it/test calls). The bundler wraps the entire\n * test module so that its top-level statements become the body of this factory.\n * @param opts - Test-only overrides (see {@link RunTestModuleOptions}). Never\n * passed by the generated bundle (`rpc.ts#buildRunTestsExpression` calls\n * `runTestModule(__userFactory)` with exactly one argument) — this second\n * parameter exists so unit tests can inject a fake sleep and assert the\n * THROTTLED-retry timing without real delays.\n */\nexport async function runTestModule(\n moduleFactory?: () => void | Promise<void>,\n opts?: RunTestModuleOptions,\n): Promise<RunReport> {\n const sleep = opts?.sleep ?? DEFAULT_SLEEP;\n // Reset state for re-entrant calls within the same page context.\n _pendingTests.length = 0;\n _suiteStack.length = 0;\n _hooks.length = 0;\n _spyRegistry.length = 0;\n\n // Install globals that user test code expects.\n type G = typeof globalThis & {\n describe: typeof describe;\n it: typeof it;\n test: typeof test;\n expect: typeof expect;\n beforeAll: typeof beforeAll;\n afterAll: typeof afterAll;\n beforeEach: typeof beforeEach;\n afterEach: typeof afterEach;\n vi: typeof vi;\n };\n const g = globalThis as G;\n g.describe = describe;\n g.it = it;\n g.test = test;\n g.expect = expect;\n g.beforeAll = beforeAll;\n g.afterAll = afterAll;\n g.beforeEach = beforeEach;\n g.afterEach = afterEach;\n g.vi = vi;\n\n // Run the factory (which registers describe/it/test blocks via globals).\n if (moduleFactory) {\n await moduleFactory();\n }\n\n const wallStart = Date.now();\n const startedAt = new Date(wallStart).toISOString();\n const results: TestResult[] = [];\n\n // Determine unique suite scopes from registered tests, in discovery order.\n // We need to fire beforeAll/afterAll once per scope (grouped by suitePath).\n //\n // Simplified model: we run beforeAll hooks before the first test in a scope\n // and afterAll hooks after the last test in a scope. Scope identity is the\n // entire suitePath string (e.g. \"Suite A > Suite B\").\n //\n // For module-scope hooks (suitePath=[]), they wrap the entire test run.\n\n // Group tests by their suite key to track beforeAll/afterAll per scope.\n // We keep a Set of scopes for which beforeAll has been fired.\n const firedBeforeAll = new Set<string>();\n // Map from scope key → last index in _pendingTests that belongs to that scope.\n const lastIndexForScope = new Map<string, number>();\n for (let i = 0; i < _pendingTests.length; i++) {\n const key = _pendingTests[i].suitePath.join('\\0');\n lastIndexForScope.set(key, i);\n // Also compute for parent scopes (module scope = '')\n const path = _pendingTests[i].suitePath;\n for (let depth = 0; depth < path.length; depth++) {\n const parentKey = path.slice(0, depth).join('\\0');\n const cur = lastIndexForScope.get(parentKey) ?? -1;\n if (i > cur) lastIndexForScope.set(parentKey, i);\n }\n }\n // Module scope key\n const MODULE_KEY = '';\n if (!lastIndexForScope.has(MODULE_KEY) && _pendingTests.length > 0) {\n lastIndexForScope.set(MODULE_KEY, _pendingTests.length - 1);\n }\n\n // Fire module-scope beforeAll before any tests\n if (_pendingTests.length > 0) {\n const baFns = _hooksFor('beforeAll', []);\n const baErr = await _runHooks(baFns);\n firedBeforeAll.add(MODULE_KEY);\n if (baErr) {\n // If module beforeAll fails, mark all tests as failed.\n for (const pending of _pendingTests) {\n const fullName = [...pending.suitePath, pending.name].join(' > ');\n results.push({\n name: fullName,\n status: 'fail',\n duration: 0,\n error: `beforeAll failed: ${baErr.message}`,\n });\n }\n const duration = Date.now() - wallStart;\n const passed = results.filter((r) => r.status === 'pass').length;\n const failed = results.filter((r) => r.status === 'fail').length;\n const skipped = results.filter((r) => r.status === 'skip').length;\n // Still run module afterAll for cleanup (e.g. flushCapture)\n const aaFns = _hooksFor('afterAll', []);\n await _runHooks(aaFns);\n return { startedAt, duration, passed, failed, skipped, tests: results };\n }\n }\n\n // devtools#767 --pace: read the page-global __AIT_PACE_MS__ (injected by\n // relay-factory.ts the same way __AIT_CELL__ is, via cell.ts#injectGlobals)\n // ONCE per run. Absent/0 (the default) means zero behavior change — the\n // wait below becomes a no-op sleep(0) that resolves on the next microtask\n // tick, same as today's unpaced loop for every existing caller.\n const paceMs = (() => {\n const raw = (globalThis as { __AIT_PACE_MS__?: unknown }).__AIT_PACE_MS__;\n return typeof raw === 'number' && raw > 0 ? raw : 0;\n })();\n let ranFirstTest = false;\n\n for (let i = 0; i < _pendingTests.length; i++) {\n const pending = _pendingTests[i];\n const fullName = [...pending.suitePath, pending.name].join(' > ');\n\n if (pending.skip) {\n results.push({ name: fullName, status: 'skip', duration: 0 });\n continue;\n }\n\n // --pace inter-test spacing: wait BEFORE every test after the first one\n // that actually runs (skipped tests above never reach here). Mirrors the\n // permission preflight's \"no wait before the first probe\" pacing model\n // (cell.ts#buildPermissionPreflightExpression).\n if (paceMs > 0 && ranFirstTest) {\n await sleep(paceMs);\n }\n ranFirstTest = true;\n\n // Fire suite-scoped beforeAll for any new scopes entered by this test\n for (let depth = 1; depth <= pending.suitePath.length; depth++) {\n const scopePath = pending.suitePath.slice(0, depth);\n const scopeKey = scopePath.join('\\0');\n if (!firedBeforeAll.has(scopeKey)) {\n // Only hooks registered exactly at this scope (not broader/narrower)\n const scopedFns = _hooks\n .filter((h) => h.type === 'beforeAll' && h.suitePath.join('\\0') === scopeKey)\n .map((h) => h.fn);\n const baErr = await _runHooks(scopedFns);\n firedBeforeAll.add(scopeKey);\n if (baErr) {\n // Mark remaining tests in this scope as failed\n results.push({\n name: fullName,\n status: 'fail',\n duration: 0,\n error: `beforeAll failed: ${baErr.message}`,\n });\n }\n }\n }\n\n // beforeEach\n const beFns = _hooksFor('beforeEach', pending.suitePath);\n const beErr = await _runHooks(beFns);\n\n const tStart = Date.now();\n let testErr: string | undefined;\n let inPageSkip: InPageSkipSentinel | undefined;\n let throttleRetries = 0;\n\n if (beErr) {\n testErr = `beforeEach failed: ${beErr.message}`;\n } else {\n // THROTTLED-aware retry (devtools#767): only the test BODY is retried —\n // beforeEach (above) and afterEach (below) each run exactly once\n // regardless of how many times the body is attempted. This keeps any\n // capture-emitting hook (e.g. sdk-example's flushCapture) firing exactly\n // once per test, so a retried body can never duplicate a capture record\n // (see relay-worker.ts's harvesting model — it's a passive listener over\n // the whole run, not a per-attempt flush/drain, so this invariant is what\n // keeps a retry from polluting the capture stream).\n for (let attempt = 0; ; attempt++) {\n try {\n await pending.fn(_makeTestContext(fullName));\n testErr = undefined;\n break;\n } catch (e) {\n if (e instanceof InPageSkipSentinel) {\n inPageSkip = e;\n break;\n }\n testErr = stringifyThrown(e);\n if (isThrottledError(e) && attempt < TEST_MAX_RETRIES) {\n throttleRetries += 1;\n await sleep(TEST_RETRY_BACKOFF_MS[attempt]);\n continue;\n }\n break;\n }\n }\n }\n\n // afterEach — always run even if test failed or was skipped mid-body.\n const aeFns = _hooksFor('afterEach', pending.suitePath);\n const aeErr = await _runHooks(aeFns);\n if (aeErr && !testErr && !inPageSkip) testErr = `afterEach failed: ${aeErr.message}`;\n\n if (inPageSkip) {\n // ctx.skip() unwound the body — record as skipped (not pass/fail), same\n // as the static it.skip path. The note (if any) rides on TestResult.note.\n results.push({\n name: fullName,\n status: 'skip',\n duration: Date.now() - tStart,\n ...(inPageSkip.note !== undefined ? { note: inPageSkip.note } : {}),\n });\n } else if (testErr !== undefined) {\n results.push({\n name: fullName,\n status: 'fail',\n duration: Date.now() - tStart,\n error: testErr,\n ...(throttleRetries > 0 ? { throttleRetries } : {}),\n });\n } else {\n results.push({\n name: fullName,\n status: 'pass',\n duration: Date.now() - tStart,\n ...(throttleRetries > 0 ? { throttleRetries } : {}),\n });\n }\n\n // Fire suite-scoped afterAll when this is the last test in each scope\n for (let depth = pending.suitePath.length; depth >= 1; depth--) {\n const scopePath = pending.suitePath.slice(0, depth);\n const scopeKey = scopePath.join('\\0');\n const lastIdx = lastIndexForScope.get(scopeKey) ?? -1;\n if (lastIdx === i) {\n const scopedFns = _hooks\n .filter((h) => h.type === 'afterAll' && h.suitePath.join('\\0') === scopeKey)\n .map((h) => h.fn);\n await _runHooks(scopedFns);\n }\n }\n }\n\n // Fire module-scope afterAll after all tests — critical for flushCapture\n const moduleAfterAllFns = _hooks\n .filter((h) => h.type === 'afterAll' && h.suitePath.length === 0)\n .map((h) => h.fn);\n await _runHooks(moduleAfterAllFns);\n\n const duration = Date.now() - wallStart;\n const passed = results.filter((r) => r.status === 'pass').length;\n const failed = results.filter((r) => r.status === 'fail').length;\n const skipped = results.filter((r) => r.status === 'skip').length;\n\n return { startedAt, duration, passed, failed, skipped, tests: results };\n}\n"],"mappings":"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"}
|
|
@@ -154,7 +154,7 @@ async function startTunnelDashboard(opts) {
|
|
|
154
154
|
if (opts.qr === false) return void 0;
|
|
155
155
|
const { isAutoDevtoolsDisabled } = await Promise.resolve().then(() => require("./devtools-opener-iv1OwfJN.cjs"));
|
|
156
156
|
if (!(opts.shouldOpen ?? (() => !isAutoDevtoolsDisabled() && canOpenBrowser()))()) return void 0;
|
|
157
|
-
const { startQrHttpServer } = await Promise.resolve().then(() => require("./qr-http-server-
|
|
157
|
+
const { startQrHttpServer } = await Promise.resolve().then(() => require("./qr-http-server--gl2-WKc.cjs"));
|
|
158
158
|
const { buildLauncherAttachUrl } = await Promise.resolve().then(() => require("./deeplink-BzdbA1gV.cjs"));
|
|
159
159
|
const { generateTotp } = await Promise.resolve().then(() => require("./totp-Df252ZdA.cjs"));
|
|
160
160
|
const getDashboardState = () => {
|
|
@@ -291,4 +291,4 @@ exports.printTunnelBanner = printTunnelBanner;
|
|
|
291
291
|
exports.startQuickTunnel = startQuickTunnel;
|
|
292
292
|
exports.startTunnelDashboard = startTunnelDashboard;
|
|
293
293
|
|
|
294
|
-
//# sourceMappingURL=tunnel-
|
|
294
|
+
//# sourceMappingURL=tunnel-BVSMXctM.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel-DjtSENJU.cjs","names":[],"sources":["../src/unplugin/tunnel.ts"],"sourcesContent":["/**\n * Cloudflare quick-tunnel helper for the devtools unplugin.\n *\n * Loaded lazily (`await import('./tunnel.js')`) only when the `tunnel` option is\n * on, so `cloudflared` / `qrcode-terminal` are never pulled in for the common\n * case. This is the one place in `@ait-co/devtools` that depends on Node-only\n * APIs (`child_process` via the `cloudflared` wrapper) — keep it thin and out of\n * jsdom unit tests; the spawn path is verified by hand / e2e (same spirit as the\n * \"web 모드는 e2e\" rule in CLAUDE.md). The pure helpers below\n * (`parseTrycloudflareUrl`, `printTunnelBanner`) are unit-tested.\n */\n\nimport { existsSync } from 'node:fs';\nimport { mkdir } from 'node:fs/promises';\nimport { dirname } from 'node:path';\n\n/** Matches the public URL cloudflared prints for an unauthenticated quick tunnel. */\nconst TRYCLOUDFLARE_RE = /https:\\/\\/[a-z0-9-]+\\.trycloudflare\\.com/i;\n\n/**\n * Extract the `https://<sub>.trycloudflare.com` URL from a line of cloudflared\n * output, or `null` if the line doesn't contain one. Pulled out as a pure\n * function so it can be unit-tested without spawning anything.\n */\nexport function parseTrycloudflareUrl(line: string): string | null {\n const m = line.match(TRYCLOUDFLARE_RE);\n return m ? m[0] : null;\n}\n\nexport interface PrintTunnelBannerOptions {\n /** Print an ASCII QR encoding the tunnel URL (default: true). */\n qr?: boolean;\n /** Sink for the banner text (default: `console.log`). Injected for testing. */\n log?: (msg: string) => void;\n /**\n * The `wss://` relay URL of the env-2 CDP tunnel, if `tunnel.cdp` is on. When\n * present the QR deep-link additionally carries `&debug=1&relay=<wss>` so the\n * framed PWA passes the in-app debug gate and attaches a Chii target — the\n * same single scan opens screen preview *and* CDP debugging.\n */\n relayWssUrl?: string;\n /**\n * Human-readable app name to embed as `name=` in the launcher deep-link (#498).\n * When provided (non-blank), the launcher partner bar shows this name instead of\n * the generic default.\n */\n name?: string;\n /**\n * The miniapp's webViewType. When `'game'`, the deep-link carries `&navBarType=game`\n * so the launcher enters game nav chrome automatically on scan (#584).\n * `'partner'` (the default) is the launcher's implicit default — not added to\n * keep the URL clean.\n */\n webViewType?: 'partner' | 'game';\n /**\n * Whether the miniapp's navigationBar has `transparentBackground: true`\n * (granite.config `navigationBar.transparentBackground`, SDK 2.8.0, #587).\n * When `true`, the deep-link carries `&navBarTransparent=1` so the launcher\n * partner bar renders with a transparent background (content shows through).\n * `false` / omitted → not added (URL clean, back-compat).\n */\n navBarTransparent?: boolean;\n /**\n * The miniapp's navigationBar theme (`granite.config `navigationBar.theme`,\n * SDK 2.8.0, #587). When `'light'` or `'dark'`, the deep-link carries\n * `&navBarTheme=<v>` so the launcher partner bar uses the matching foreground\n * colour. Omitted / other values → not added (URL clean, back-compat).\n */\n navBarTheme?: 'light' | 'dark';\n}\n\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Options for {@link buildLauncherDeepLink}.\n */\nexport interface BuildLauncherDeepLinkOptions {\n /**\n * `wss://` relay URL for env-2 CDP wiring. When present the deep-link carries\n * `&debug=1&relay=<wss>`.\n */\n relayWssUrl?: string;\n /**\n * Human-readable app name shown in the partner nav bar (`name=` param, #498).\n * Blank / whitespace-only values are not added.\n */\n name?: string;\n /**\n * The miniapp's webViewType. When `'game'`, adds `&navBarType=game` to the\n * deep-link so the launcher enters game nav chrome automatically on scan (#584).\n * `'partner'` (the launcher's implicit default) is not added to keep the URL\n * clean.\n */\n webViewType?: 'partner' | 'game';\n /**\n * Whether the miniapp's navigationBar has `transparentBackground: true`\n * (granite.config `navigationBar.transparentBackground`, SDK 2.8.0, #587).\n * When `true`, adds `&navBarTransparent=1` to the deep-link so the launcher\n * partner bar renders with a transparent background. Omitted when `false` /\n * undefined to keep the URL clean (back-compat).\n */\n navBarTransparent?: boolean;\n /**\n * The miniapp's navigationBar theme (granite.config `navigationBar.theme`,\n * SDK 2.8.0, #587). When `'light'` or `'dark'`, adds `&navBarTheme=<v>` to\n * the deep-link so the launcher partner bar uses the matching foreground colour.\n * Omitted when undefined / other values to keep the URL clean (back-compat).\n */\n navBarTheme?: 'light' | 'dark';\n}\n\n/**\n * Build the deep-link URL that QR codes encode: when the launcher PWA is\n * already on the phone's home screen, scanning this opens it directly into the\n * live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).\n * Plain-text raw URL is no longer enough — the launcher gates its setup UI to\n * the installed PWA, so a raw tunnel URL opened in a normal browser tab would\n * land on a \"please install\" screen.\n *\n * When `opts.relayWssUrl` is given (env-2 CDP wiring), the deep-link also carries\n * `&debug=1&relay=<wss>`; the launcher folds those onto the framed tunnel URL so\n * the in-app debug gate's Layer C (`debug=1` opt-in + `relay=<wss>`) is met and\n * a Chii target.js is injected into the live view.\n *\n * When `opts.name` is given (non-blank), it is added as `&name=` so the launcher\n * partner bar shows the app name instead of the generic default (#498).\n *\n * When `opts.webViewType` is `'game'`, `&navBarType=game` is appended so the\n * launcher enters game nav chrome (floating capsule, no full bar) automatically\n * on scan. `'partner'` is the launcher's implicit default and is not added to\n * keep the URL clean (#584).\n *\n * When `opts.navBarTransparent` is `true`, `&navBarTransparent=1` is appended\n * so the launcher partner bar renders with a transparent background (#587).\n *\n * When `opts.navBarTheme` is `'light'` or `'dark'`, `&navBarTheme=<v>` is\n * appended so the launcher partner bar uses the matching foreground colour (#587).\n *\n * Back-compat: the second argument may also be a plain string (`relayWssUrl`)\n * for callers that haven't migrated to the options object yet.\n */\nexport function buildLauncherDeepLink(\n tunnelUrl: string,\n optsOrRelay?: string | BuildLauncherDeepLinkOptions,\n): string {\n // Normalise the overloaded second argument.\n const opts: BuildLauncherDeepLinkOptions =\n typeof optsOrRelay === 'string' ? { relayWssUrl: optsOrRelay } : (optsOrRelay ?? {});\n\n const base = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;\n let url = base;\n if (opts.relayWssUrl) {\n url += `&debug=1&relay=${encodeURIComponent(opts.relayWssUrl)}`;\n }\n if (opts.name !== undefined && opts.name.trim() !== '') {\n url += `&name=${encodeURIComponent(opts.name.trim())}`;\n }\n if (opts.webViewType === 'game') {\n url += '&navBarType=game';\n }\n if (opts.navBarTransparent === true) {\n url += '&navBarTransparent=1';\n }\n if (opts.navBarTheme === 'light' || opts.navBarTheme === 'dark') {\n url += `&navBarTheme=${opts.navBarTheme}`;\n }\n return url;\n}\n\n/**\n * Print the terminal banner announcing the live tunnel: the public URL, an ASCII\n * QR encoding a launcher deep-link, and a one-line note that quick tunnels are\n * ephemeral, unauthenticated and not for production. Pure w.r.t. side effects\n * other than the injected `log` sink and `qrcode-terminal` — unit-tested.\n */\nexport async function printTunnelBanner(\n url: string,\n opts: PrintTunnelBannerOptions = {},\n): Promise<void> {\n const log = opts.log ?? ((m: string) => console.log(m));\n const deepLink = buildLauncherDeepLink(url, {\n relayWssUrl: opts.relayWssUrl,\n name: opts.name,\n webViewType: opts.webViewType,\n navBarTransparent: opts.navBarTransparent,\n navBarTheme: opts.navBarTheme,\n });\n const lines: string[] = [\n '',\n ' ┌─ @ait-co/devtools · live tunnel ────────────────────────────',\n ` │ ${url}`,\n ' │',\n ` │ Install the launcher PWA once: ${LAUNCHER_URL}`,\n ' │ Then scan the QR below — it opens the launcher directly',\n ' │ into this tunnel URL (no manual paste needed).',\n ...(opts.relayWssUrl\n ? [\n ' │ The same scan also attaches CDP — connect your AI host',\n ' │ to the relay and debug the live view on-device.',\n ]\n : []),\n ' │ Quick tunnels are unauthenticated, change every run, and are',\n ' │ not for production use.',\n ' └──────────────────────────────────────────────────────────────',\n '',\n ];\n log(lines.join('\\n'));\n\n if (opts.qr !== false) {\n // qrcode-terminal is only pulled in on this code path (ambient types live\n // in src/qrcode-terminal.d.ts).\n const qrcode = (await import('qrcode-terminal')).default;\n await new Promise<void>((resolve) => {\n qrcode.generate(deepLink, { small: true }, (out) => {\n log(out);\n resolve();\n });\n });\n }\n}\n\n/**\n * Heuristic: can this process open a GUI browser? Mirrors `canOpenBrowser` in\n * `src/mcp/tools.ts` but is re-declared here (not imported) so the tunnel path\n * does not statically pull the heavy MCP `tools.ts` module graph into the lazy\n * `import('./tunnel.js')` chunk. Kept in sync with the MCP copy.\n *\n * - macOS / Windows → assume yes (env-2 dev normally runs on the user's Mac).\n * - Linux → require `DISPLAY` or `WAYLAND_DISPLAY`.\n * - CI (`CI=true`/`CI=1`) → no.\n */\nfunction canOpenBrowser(): boolean {\n if (process.env.CI === 'true' || process.env.CI === '1') return false;\n const platform = process.platform;\n if (platform === 'darwin' || platform === 'win32') return true;\n if (platform === 'linux') {\n return Boolean(process.env.DISPLAY ?? process.env.WAYLAND_DISPLAY);\n }\n return false;\n}\n\n/** Handle returned by {@link startTunnelDashboard}. */\nexport interface TunnelDashboard {\n /** `http://127.0.0.1:<port>` — the local dashboard URL opened in the browser. */\n url: string;\n /** Tear down the local HTTP server. Idempotent via the underlying server. */\n close: () => Promise<void>;\n}\n\nexport interface StartTunnelDashboardOptions {\n /** The public `https://*.trycloudflare.com` app tunnel URL the launcher frames. */\n tunnelUrl: string;\n /** The `wss://` relay URL of the env-2 CDP tunnel. REQUIRED — the dashboard is a CDP-only UX. */\n relayWssUrl: string;\n /** Mirror of `tunnel.qr` — when `false` the dashboard is skipped (no browser open). */\n qr?: boolean;\n /**\n * Override the GUI/opt-out gate (testing only). When omitted the real\n * `canOpenBrowser()` + `AIT_AUTO_DEVTOOLS` checks decide.\n */\n shouldOpen?: () => boolean;\n /** Sink for the one-line \"opened in browser\" note (default: `console.log`). Injected for testing. */\n log?: (msg: string) => void;\n /**\n * Human-readable app name to embed as `name=` in the launcher deep-link (#498).\n * When provided (non-blank), the launcher partner bar shows this name instead of\n * the generic default.\n */\n name?: string;\n}\n\n/**\n * Env-2 UX parity with env 3/4 (issue #408): when CDP wiring is on and a GUI is\n * available, start the SAME `127.0.0.1` HTML dashboard (QR image + connect steps\n * + FAQ) that the MCP `start_attach` path serves, and auto-open it in the\n * browser. headless / opt-out falls back to the terminal ASCII QR (printed\n * separately by {@link printTunnelBanner}).\n *\n * Every part the install-graph invariant depends on (`qrcode`, the MCP HTTP\n * server, the opener) is reached only through dynamic `import()` here, inside\n * the already-lazy `tunnel.js` chunk — nothing is added to the common build\n * graph or the MCP-only install graph.\n *\n * TOTP encapsulation: the dashboard's `getDashboardState` closure mints a FRESH\n * TOTP `at=` code on every call via `generateTotp(secret, Date.now())` and folds\n * it into a fresh `buildLauncherAttachUrl(...)`. Because the QR is re-rendered on\n * each SSE push / page reload from this closure, the code a phone scans is always\n * within its 30 s window — no stale code is baked into static HTML.\n *\n * SECRET-HANDLING: the tunnel host, relay wssUrl, TOTP code, and `.ait_relay`\n * value/path are NEVER written to stdout/stderr/logs here. They live only inside\n * the attach URL (HTML body + `/qr.png` query, per qr-http-server's invariant).\n * The only thing opened/logged is `http://127.0.0.1:<port>` (local, safe).\n *\n * @returns the dashboard handle when it started (caller wires `close()` into the\n * tunnel cleanup), or `undefined` when skipped (no relay, `qr:false`, headless,\n * opt-out, or a start failure) — in which case ASCII QR fallback stands alone.\n */\nexport async function startTunnelDashboard(\n opts: StartTunnelDashboardOptions,\n): Promise<TunnelDashboard | undefined> {\n const log = opts.log ?? ((m: string) => console.log(m));\n\n // Gate: dashboard is a CDP-only UX (needs a relay to attach to).\n if (!opts.relayWssUrl) return undefined;\n // Opt-out via `tunnel.qr:false` (same toggle that suppresses the ASCII QR).\n if (opts.qr === false) return undefined;\n\n // GUI + AIT_AUTO_DEVTOOLS gate. Reuse the MCP opener's opt-out predicate so\n // the env-2 path honours the same `AIT_AUTO_DEVTOOLS=0` switch as env 3/4.\n const { isAutoDevtoolsDisabled } = await import('../mcp/devtools-opener.js');\n const gateOpen = opts.shouldOpen ?? (() => !isAutoDevtoolsDisabled() && canOpenBrowser());\n if (!gateOpen()) return undefined;\n\n const { startQrHttpServer } = await import('../mcp/qr-http-server.js');\n const { buildLauncherAttachUrl } = await import('../mcp/deeplink.js');\n const { generateTotp } = await import('../mcp/totp.js');\n\n // getDashboardState — mints a fresh TOTP + attach URL on every call so the QR\n // the dashboard renders (on load and on each SSE push) is never expired.\n // SECRET-HANDLING: the secret is read from env AT CALL TIME (it was injected\n // by ensureRelaySecret in the same CDP block) and is used only to compute the\n // at= code folded into attachUrl. tunnel.up is always true here — the relay\n // tunnel is already up by the time this runs.\n const getDashboardState = () => {\n const secret = process.env.AIT_DEBUG_TOTP_SECRET;\n const totpCode = secret ? generateTotp(secret, Date.now()) : undefined;\n const attachUrl = buildLauncherAttachUrl(opts.tunnelUrl, opts.relayWssUrl, totpCode, {\n name: opts.name,\n });\n // pages: null — env 2(unplugin)는 데몬이 아니라 vite 플러그인 안이라\n // startChiiRelay 핸들이 connected target을 노출하지 않는다. 라이브 page 목록을\n // 알 수 없으므로 거짓 빈 목록 대신 \"연결된 Pages\" 섹션 자체를 숨긴다(#411).\n // env 3/4(debug-server.ts)는 router.active.listTargets()로 실제 목록을 채운다.\n // mode: 'relay-mobile' — 이 대시보드는 항상 환경 2(AITC Sandbox PWA) 전용이므로\n // /attach 카피가 launcher PWA 절차(sandbox family)로 분기된다(#468).\n // inspectorUrl: null — env 2에서는 unplugin relay가 connected target ID를 노출하지\n // 않아 buildChiiInspectorUrl에 필요한 targetId를 알 수 없다. target attach 후\n // target ID가 필요하므로 env 3/4에서만 non-null이 된다(#503).\n return {\n tunnel: { up: true, wssUrl: opts.relayWssUrl },\n pages: null,\n attachUrl,\n inspectorUrl: null,\n mode: 'relay-mobile' as const,\n // phase (#730): this dev-tunnel dashboard has no CLI run lifecycle or\n // daemon shutdown signal to drive — it stays 'active' for its lifetime.\n phase: 'active' as const,\n };\n };\n\n let server: Awaited<ReturnType<typeof startQrHttpServer>>;\n try {\n server = await startQrHttpServer(getDashboardState);\n } catch {\n // SECRET-HANDLING: do not surface the error (could embed paths/hosts). The\n // ASCII QR printed by printTunnelBanner stays as the fallback.\n return undefined;\n }\n\n // TOTP periodic refresh timer — pushes a fresh at= code to SSE clients every\n // 20 s so a page left open never stales past the 90 s acceptance window (#448).\n // tunnel.ts always has relayWssUrl available here (gated above), so no\n // lastAttachParts guard is needed — getDashboardState mints a fresh TOTP on\n // every call unconditionally.\n // SECRET-HANDLING: callback is a plain trigger only — TOTP value and at= code\n // must never be logged or written to stdout.\n const TOTP_REFRESH_INTERVAL_MS = 20_000;\n let totpRefreshHandle: ReturnType<typeof setInterval> | null = setInterval(() => {\n server.notifyStateChange();\n }, TOTP_REFRESH_INTERVAL_MS);\n totpRefreshHandle.unref();\n\n const dashboardUrl = `http://127.0.0.1:${server.port}`;\n\n const { openUrlInBrowser } = await import('../mcp/devtools-opener.js');\n const opened = openUrlInBrowser(dashboardUrl);\n // SECRET-HANDLING: only the local 127.0.0.1 URL is logged — never the tunnel\n // host, relay wssUrl, or TOTP code.\n log(\n opened\n ? ` │ Opened a QR dashboard in your browser: ${dashboardUrl}`\n : ` │ Open this QR dashboard in your browser: ${dashboardUrl}`,\n );\n\n return {\n url: dashboardUrl,\n close: () => {\n if (totpRefreshHandle) {\n clearInterval(totpRefreshHandle);\n totpRefreshHandle = null;\n }\n return server.close();\n },\n };\n}\n\nexport interface QuickTunnel {\n /** The public `https://*.trycloudflare.com` URL. */\n url: string;\n /** Stop the underlying `cloudflared` process. Idempotent. */\n stop: () => void;\n}\n\n/**\n * Sanitize cloudflared stderr output for error diagnostics (#421).\n *\n * Masks `*.trycloudflare.com` hostnames and full `https://` / `wss://` URLs\n * that carry those hostnames so tunnel host values never appear in error\n * messages. Diagnostic content (error codes, reasons, JSON blobs) is preserved.\n *\n * SECRET-HANDLING: tunnel host is SECRET-class per harness policy — only\n * placeholder text is emitted.\n */\nexport function sanitizeCloudflaredOutput(line: string): string {\n // Full URL forms: https://xxx.trycloudflare.com/… and wss://xxx.trycloudflare.com/…\n let s = line.replace(/(?:https?|wss?):\\/\\/[a-z0-9-]+\\.trycloudflare\\.com(?:\\/[^\\s]*)*/gi, (m) =>\n m.replace(/[a-z0-9-]+\\.trycloudflare\\.com/i, '<HOST>.trycloudflare.com'),\n );\n // Bare hostname without scheme (e.g. printed in cloudflared JSON logs)\n s = s.replace(/[a-z0-9-]+\\.trycloudflare\\.com/gi, '<HOST>.trycloudflare.com');\n return s;\n}\n\nconst URL_TIMEOUT_MS = 20_000;\n\n/**\n * Start an unauthenticated Cloudflare quick tunnel to `http://localhost:<port>`\n * and resolve once the public URL is known. Downloads the `cloudflared` binary\n * on first use if it is not already installed. Rejects with a friendly error if\n * no URL appears within {@link URL_TIMEOUT_MS}.\n */\nexport async function startQuickTunnel(port: number): Promise<QuickTunnel> {\n const cloudflared = await import('cloudflared');\n const { bin, install, Tunnel } = cloudflared;\n\n if (!existsSync(bin)) {\n await mkdir(dirname(bin), { recursive: true });\n await install(bin);\n }\n\n const tunnel = Tunnel.quick(`http://localhost:${port}`);\n let stopped = false;\n const stop = () => {\n if (stopped) return;\n stopped = true;\n try {\n tunnel.stop();\n } catch {\n // process may already be gone\n }\n };\n\n return new Promise<QuickTunnel>((resolve, reject) => {\n // #421: accumulate stderr to attach as diagnostics on failure.\n // SECRET-HANDLING: lines are sanitized before inclusion in error messages.\n const stderrLines: string[] = [];\n\n /**\n * Format the last `n` sanitized stderr lines as a diagnostic appendix.\n * Returns an empty string when no lines have been collected.\n */\n const stderrTail = (n = 15): string => {\n if (stderrLines.length === 0) return '';\n const tail = stderrLines.slice(-n).map(sanitizeCloudflaredOutput).join('');\n return `\\ncloudflared 출력 (마지막 ${Math.min(n, stderrLines.length)}줄):\\n${tail}`;\n };\n\n const timer = setTimeout(() => {\n cleanup();\n stop();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared did not report a tunnel URL within ${\n URL_TIMEOUT_MS / 1000\n }s. Check your network connection, or run \\`cloudflared tunnel --url http://localhost:${port}\\` manually.${stderrTail()}`,\n ),\n );\n }, URL_TIMEOUT_MS);\n\n const onUrl = (line: string) => {\n const found = parseTrycloudflareUrl(line);\n if (!found) return;\n clearTimeout(timer);\n // Stop scanning further output once we have the URL.\n cleanup();\n resolve({ url: found, stop });\n };\n\n // Accumulate stderr lines for diagnostics (#421). Named so it can be\n // removed from the listener list when cleanup() runs.\n const pushStderr = (line: string) => {\n stderrLines.push(line);\n };\n\n const cleanup = () => {\n tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\n tunnel.off('stderr', pushStderr);\n };\n\n // The library emits a parsed `url` event; we also scan raw stdout/stderr in\n // case the output format shifts.\n tunnel.once('url', onUrl);\n tunnel.on('stdout', onUrl);\n tunnel.on('stderr', onUrl);\n // Second stderr listener: accumulate all lines for error diagnostics.\n tunnel.on('stderr', pushStderr);\n tunnel.once('error', (err: Error) => {\n clearTimeout(timer);\n cleanup();\n stop();\n reject(err);\n });\n tunnel.once('exit', (code: number | null) => {\n if (stopped) return;\n clearTimeout(timer);\n cleanup();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared exited (code ${code ?? 'null'}) before reporting a tunnel URL.${stderrTail()}`,\n ),\n );\n });\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tunnel-BVSMXctM.cjs","names":[],"sources":["../src/unplugin/tunnel.ts"],"sourcesContent":["/**\n * Cloudflare quick-tunnel helper for the devtools unplugin.\n *\n * Loaded lazily (`await import('./tunnel.js')`) only when the `tunnel` option is\n * on, so `cloudflared` / `qrcode-terminal` are never pulled in for the common\n * case. This is the one place in `@ait-co/devtools` that depends on Node-only\n * APIs (`child_process` via the `cloudflared` wrapper) — keep it thin and out of\n * jsdom unit tests; the spawn path is verified by hand / e2e (same spirit as the\n * \"web 모드는 e2e\" rule in CLAUDE.md). The pure helpers below\n * (`parseTrycloudflareUrl`, `printTunnelBanner`) are unit-tested.\n */\n\nimport { existsSync } from 'node:fs';\nimport { mkdir } from 'node:fs/promises';\nimport { dirname } from 'node:path';\n\n/** Matches the public URL cloudflared prints for an unauthenticated quick tunnel. */\nconst TRYCLOUDFLARE_RE = /https:\\/\\/[a-z0-9-]+\\.trycloudflare\\.com/i;\n\n/**\n * Extract the `https://<sub>.trycloudflare.com` URL from a line of cloudflared\n * output, or `null` if the line doesn't contain one. Pulled out as a pure\n * function so it can be unit-tested without spawning anything.\n */\nexport function parseTrycloudflareUrl(line: string): string | null {\n const m = line.match(TRYCLOUDFLARE_RE);\n return m ? m[0] : null;\n}\n\nexport interface PrintTunnelBannerOptions {\n /** Print an ASCII QR encoding the tunnel URL (default: true). */\n qr?: boolean;\n /** Sink for the banner text (default: `console.log`). Injected for testing. */\n log?: (msg: string) => void;\n /**\n * The `wss://` relay URL of the env-2 CDP tunnel, if `tunnel.cdp` is on. When\n * present the QR deep-link additionally carries `&debug=1&relay=<wss>` so the\n * framed PWA passes the in-app debug gate and attaches a Chii target — the\n * same single scan opens screen preview *and* CDP debugging.\n */\n relayWssUrl?: string;\n /**\n * Human-readable app name to embed as `name=` in the launcher deep-link (#498).\n * When provided (non-blank), the launcher partner bar shows this name instead of\n * the generic default.\n */\n name?: string;\n /**\n * The miniapp's webViewType. When `'game'`, the deep-link carries `&navBarType=game`\n * so the launcher enters game nav chrome automatically on scan (#584).\n * `'partner'` (the default) is the launcher's implicit default — not added to\n * keep the URL clean.\n */\n webViewType?: 'partner' | 'game';\n /**\n * Whether the miniapp's navigationBar has `transparentBackground: true`\n * (granite.config `navigationBar.transparentBackground`, SDK 2.8.0, #587).\n * When `true`, the deep-link carries `&navBarTransparent=1` so the launcher\n * partner bar renders with a transparent background (content shows through).\n * `false` / omitted → not added (URL clean, back-compat).\n */\n navBarTransparent?: boolean;\n /**\n * The miniapp's navigationBar theme (`granite.config `navigationBar.theme`,\n * SDK 2.8.0, #587). When `'light'` or `'dark'`, the deep-link carries\n * `&navBarTheme=<v>` so the launcher partner bar uses the matching foreground\n * colour. Omitted / other values → not added (URL clean, back-compat).\n */\n navBarTheme?: 'light' | 'dark';\n}\n\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Options for {@link buildLauncherDeepLink}.\n */\nexport interface BuildLauncherDeepLinkOptions {\n /**\n * `wss://` relay URL for env-2 CDP wiring. When present the deep-link carries\n * `&debug=1&relay=<wss>`.\n */\n relayWssUrl?: string;\n /**\n * Human-readable app name shown in the partner nav bar (`name=` param, #498).\n * Blank / whitespace-only values are not added.\n */\n name?: string;\n /**\n * The miniapp's webViewType. When `'game'`, adds `&navBarType=game` to the\n * deep-link so the launcher enters game nav chrome automatically on scan (#584).\n * `'partner'` (the launcher's implicit default) is not added to keep the URL\n * clean.\n */\n webViewType?: 'partner' | 'game';\n /**\n * Whether the miniapp's navigationBar has `transparentBackground: true`\n * (granite.config `navigationBar.transparentBackground`, SDK 2.8.0, #587).\n * When `true`, adds `&navBarTransparent=1` to the deep-link so the launcher\n * partner bar renders with a transparent background. Omitted when `false` /\n * undefined to keep the URL clean (back-compat).\n */\n navBarTransparent?: boolean;\n /**\n * The miniapp's navigationBar theme (granite.config `navigationBar.theme`,\n * SDK 2.8.0, #587). When `'light'` or `'dark'`, adds `&navBarTheme=<v>` to\n * the deep-link so the launcher partner bar uses the matching foreground colour.\n * Omitted when undefined / other values to keep the URL clean (back-compat).\n */\n navBarTheme?: 'light' | 'dark';\n}\n\n/**\n * Build the deep-link URL that QR codes encode: when the launcher PWA is\n * already on the phone's home screen, scanning this opens it directly into the\n * live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).\n * Plain-text raw URL is no longer enough — the launcher gates its setup UI to\n * the installed PWA, so a raw tunnel URL opened in a normal browser tab would\n * land on a \"please install\" screen.\n *\n * When `opts.relayWssUrl` is given (env-2 CDP wiring), the deep-link also carries\n * `&debug=1&relay=<wss>`; the launcher folds those onto the framed tunnel URL so\n * the in-app debug gate's Layer C (`debug=1` opt-in + `relay=<wss>`) is met and\n * a Chii target.js is injected into the live view.\n *\n * When `opts.name` is given (non-blank), it is added as `&name=` so the launcher\n * partner bar shows the app name instead of the generic default (#498).\n *\n * When `opts.webViewType` is `'game'`, `&navBarType=game` is appended so the\n * launcher enters game nav chrome (floating capsule, no full bar) automatically\n * on scan. `'partner'` is the launcher's implicit default and is not added to\n * keep the URL clean (#584).\n *\n * When `opts.navBarTransparent` is `true`, `&navBarTransparent=1` is appended\n * so the launcher partner bar renders with a transparent background (#587).\n *\n * When `opts.navBarTheme` is `'light'` or `'dark'`, `&navBarTheme=<v>` is\n * appended so the launcher partner bar uses the matching foreground colour (#587).\n *\n * Back-compat: the second argument may also be a plain string (`relayWssUrl`)\n * for callers that haven't migrated to the options object yet.\n */\nexport function buildLauncherDeepLink(\n tunnelUrl: string,\n optsOrRelay?: string | BuildLauncherDeepLinkOptions,\n): string {\n // Normalise the overloaded second argument.\n const opts: BuildLauncherDeepLinkOptions =\n typeof optsOrRelay === 'string' ? { relayWssUrl: optsOrRelay } : (optsOrRelay ?? {});\n\n const base = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;\n let url = base;\n if (opts.relayWssUrl) {\n url += `&debug=1&relay=${encodeURIComponent(opts.relayWssUrl)}`;\n }\n if (opts.name !== undefined && opts.name.trim() !== '') {\n url += `&name=${encodeURIComponent(opts.name.trim())}`;\n }\n if (opts.webViewType === 'game') {\n url += '&navBarType=game';\n }\n if (opts.navBarTransparent === true) {\n url += '&navBarTransparent=1';\n }\n if (opts.navBarTheme === 'light' || opts.navBarTheme === 'dark') {\n url += `&navBarTheme=${opts.navBarTheme}`;\n }\n return url;\n}\n\n/**\n * Print the terminal banner announcing the live tunnel: the public URL, an ASCII\n * QR encoding a launcher deep-link, and a one-line note that quick tunnels are\n * ephemeral, unauthenticated and not for production. Pure w.r.t. side effects\n * other than the injected `log` sink and `qrcode-terminal` — unit-tested.\n */\nexport async function printTunnelBanner(\n url: string,\n opts: PrintTunnelBannerOptions = {},\n): Promise<void> {\n const log = opts.log ?? ((m: string) => console.log(m));\n const deepLink = buildLauncherDeepLink(url, {\n relayWssUrl: opts.relayWssUrl,\n name: opts.name,\n webViewType: opts.webViewType,\n navBarTransparent: opts.navBarTransparent,\n navBarTheme: opts.navBarTheme,\n });\n const lines: string[] = [\n '',\n ' ┌─ @ait-co/devtools · live tunnel ────────────────────────────',\n ` │ ${url}`,\n ' │',\n ` │ Install the launcher PWA once: ${LAUNCHER_URL}`,\n ' │ Then scan the QR below — it opens the launcher directly',\n ' │ into this tunnel URL (no manual paste needed).',\n ...(opts.relayWssUrl\n ? [\n ' │ The same scan also attaches CDP — connect your AI host',\n ' │ to the relay and debug the live view on-device.',\n ]\n : []),\n ' │ Quick tunnels are unauthenticated, change every run, and are',\n ' │ not for production use.',\n ' └──────────────────────────────────────────────────────────────',\n '',\n ];\n log(lines.join('\\n'));\n\n if (opts.qr !== false) {\n // qrcode-terminal is only pulled in on this code path (ambient types live\n // in src/qrcode-terminal.d.ts).\n const qrcode = (await import('qrcode-terminal')).default;\n await new Promise<void>((resolve) => {\n qrcode.generate(deepLink, { small: true }, (out) => {\n log(out);\n resolve();\n });\n });\n }\n}\n\n/**\n * Heuristic: can this process open a GUI browser? Mirrors `canOpenBrowser` in\n * `src/mcp/tools.ts` but is re-declared here (not imported) so the tunnel path\n * does not statically pull the heavy MCP `tools.ts` module graph into the lazy\n * `import('./tunnel.js')` chunk. Kept in sync with the MCP copy.\n *\n * - macOS / Windows → assume yes (env-2 dev normally runs on the user's Mac).\n * - Linux → require `DISPLAY` or `WAYLAND_DISPLAY`.\n * - CI (`CI=true`/`CI=1`) → no.\n */\nfunction canOpenBrowser(): boolean {\n if (process.env.CI === 'true' || process.env.CI === '1') return false;\n const platform = process.platform;\n if (platform === 'darwin' || platform === 'win32') return true;\n if (platform === 'linux') {\n return Boolean(process.env.DISPLAY ?? process.env.WAYLAND_DISPLAY);\n }\n return false;\n}\n\n/** Handle returned by {@link startTunnelDashboard}. */\nexport interface TunnelDashboard {\n /** `http://127.0.0.1:<port>` — the local dashboard URL opened in the browser. */\n url: string;\n /** Tear down the local HTTP server. Idempotent via the underlying server. */\n close: () => Promise<void>;\n}\n\nexport interface StartTunnelDashboardOptions {\n /** The public `https://*.trycloudflare.com` app tunnel URL the launcher frames. */\n tunnelUrl: string;\n /** The `wss://` relay URL of the env-2 CDP tunnel. REQUIRED — the dashboard is a CDP-only UX. */\n relayWssUrl: string;\n /** Mirror of `tunnel.qr` — when `false` the dashboard is skipped (no browser open). */\n qr?: boolean;\n /**\n * Override the GUI/opt-out gate (testing only). When omitted the real\n * `canOpenBrowser()` + `AIT_AUTO_DEVTOOLS` checks decide.\n */\n shouldOpen?: () => boolean;\n /** Sink for the one-line \"opened in browser\" note (default: `console.log`). Injected for testing. */\n log?: (msg: string) => void;\n /**\n * Human-readable app name to embed as `name=` in the launcher deep-link (#498).\n * When provided (non-blank), the launcher partner bar shows this name instead of\n * the generic default.\n */\n name?: string;\n}\n\n/**\n * Env-2 UX parity with env 3/4 (issue #408): when CDP wiring is on and a GUI is\n * available, start the SAME `127.0.0.1` HTML dashboard (QR image + connect steps\n * + FAQ) that the MCP `start_attach` path serves, and auto-open it in the\n * browser. headless / opt-out falls back to the terminal ASCII QR (printed\n * separately by {@link printTunnelBanner}).\n *\n * Every part the install-graph invariant depends on (`qrcode`, the MCP HTTP\n * server, the opener) is reached only through dynamic `import()` here, inside\n * the already-lazy `tunnel.js` chunk — nothing is added to the common build\n * graph or the MCP-only install graph.\n *\n * TOTP encapsulation: the dashboard's `getDashboardState` closure mints a FRESH\n * TOTP `at=` code on every call via `generateTotp(secret, Date.now())` and folds\n * it into a fresh `buildLauncherAttachUrl(...)`. Because the QR is re-rendered on\n * each SSE push / page reload from this closure, the code a phone scans is always\n * within its 30 s window — no stale code is baked into static HTML.\n *\n * SECRET-HANDLING: the tunnel host, relay wssUrl, TOTP code, and `.ait_relay`\n * value/path are NEVER written to stdout/stderr/logs here. They live only inside\n * the attach URL (HTML body + `/qr.png` query, per qr-http-server's invariant).\n * The only thing opened/logged is `http://127.0.0.1:<port>` (local, safe).\n *\n * @returns the dashboard handle when it started (caller wires `close()` into the\n * tunnel cleanup), or `undefined` when skipped (no relay, `qr:false`, headless,\n * opt-out, or a start failure) — in which case ASCII QR fallback stands alone.\n */\nexport async function startTunnelDashboard(\n opts: StartTunnelDashboardOptions,\n): Promise<TunnelDashboard | undefined> {\n const log = opts.log ?? ((m: string) => console.log(m));\n\n // Gate: dashboard is a CDP-only UX (needs a relay to attach to).\n if (!opts.relayWssUrl) return undefined;\n // Opt-out via `tunnel.qr:false` (same toggle that suppresses the ASCII QR).\n if (opts.qr === false) return undefined;\n\n // GUI + AIT_AUTO_DEVTOOLS gate. Reuse the MCP opener's opt-out predicate so\n // the env-2 path honours the same `AIT_AUTO_DEVTOOLS=0` switch as env 3/4.\n const { isAutoDevtoolsDisabled } = await import('../mcp/devtools-opener.js');\n const gateOpen = opts.shouldOpen ?? (() => !isAutoDevtoolsDisabled() && canOpenBrowser());\n if (!gateOpen()) return undefined;\n\n const { startQrHttpServer } = await import('../mcp/qr-http-server.js');\n const { buildLauncherAttachUrl } = await import('../mcp/deeplink.js');\n const { generateTotp } = await import('../mcp/totp.js');\n\n // getDashboardState — mints a fresh TOTP + attach URL on every call so the QR\n // the dashboard renders (on load and on each SSE push) is never expired.\n // SECRET-HANDLING: the secret is read from env AT CALL TIME (it was injected\n // by ensureRelaySecret in the same CDP block) and is used only to compute the\n // at= code folded into attachUrl. tunnel.up is always true here — the relay\n // tunnel is already up by the time this runs.\n const getDashboardState = () => {\n const secret = process.env.AIT_DEBUG_TOTP_SECRET;\n const totpCode = secret ? generateTotp(secret, Date.now()) : undefined;\n const attachUrl = buildLauncherAttachUrl(opts.tunnelUrl, opts.relayWssUrl, totpCode, {\n name: opts.name,\n });\n // pages: null — env 2(unplugin)는 데몬이 아니라 vite 플러그인 안이라\n // startChiiRelay 핸들이 connected target을 노출하지 않는다. 라이브 page 목록을\n // 알 수 없으므로 거짓 빈 목록 대신 \"연결된 Pages\" 섹션 자체를 숨긴다(#411).\n // env 3/4(debug-server.ts)는 router.active.listTargets()로 실제 목록을 채운다.\n // mode: 'relay-mobile' — 이 대시보드는 항상 환경 2(AITC Sandbox PWA) 전용이므로\n // /attach 카피가 launcher PWA 절차(sandbox family)로 분기된다(#468).\n // inspectorUrl: null — env 2에서는 unplugin relay가 connected target ID를 노출하지\n // 않아 buildChiiInspectorUrl에 필요한 targetId를 알 수 없다. target attach 후\n // target ID가 필요하므로 env 3/4에서만 non-null이 된다(#503).\n return {\n tunnel: { up: true, wssUrl: opts.relayWssUrl },\n pages: null,\n attachUrl,\n inspectorUrl: null,\n mode: 'relay-mobile' as const,\n // phase (#730): this dev-tunnel dashboard has no CLI run lifecycle or\n // daemon shutdown signal to drive — it stays 'active' for its lifetime.\n phase: 'active' as const,\n };\n };\n\n let server: Awaited<ReturnType<typeof startQrHttpServer>>;\n try {\n server = await startQrHttpServer(getDashboardState);\n } catch {\n // SECRET-HANDLING: do not surface the error (could embed paths/hosts). The\n // ASCII QR printed by printTunnelBanner stays as the fallback.\n return undefined;\n }\n\n // TOTP periodic refresh timer — pushes a fresh at= code to SSE clients every\n // 20 s so a page left open never stales past the 90 s acceptance window (#448).\n // tunnel.ts always has relayWssUrl available here (gated above), so no\n // lastAttachParts guard is needed — getDashboardState mints a fresh TOTP on\n // every call unconditionally.\n // SECRET-HANDLING: callback is a plain trigger only — TOTP value and at= code\n // must never be logged or written to stdout.\n const TOTP_REFRESH_INTERVAL_MS = 20_000;\n let totpRefreshHandle: ReturnType<typeof setInterval> | null = setInterval(() => {\n server.notifyStateChange();\n }, TOTP_REFRESH_INTERVAL_MS);\n totpRefreshHandle.unref();\n\n const dashboardUrl = `http://127.0.0.1:${server.port}`;\n\n const { openUrlInBrowser } = await import('../mcp/devtools-opener.js');\n const opened = openUrlInBrowser(dashboardUrl);\n // SECRET-HANDLING: only the local 127.0.0.1 URL is logged — never the tunnel\n // host, relay wssUrl, or TOTP code.\n log(\n opened\n ? ` │ Opened a QR dashboard in your browser: ${dashboardUrl}`\n : ` │ Open this QR dashboard in your browser: ${dashboardUrl}`,\n );\n\n return {\n url: dashboardUrl,\n close: () => {\n if (totpRefreshHandle) {\n clearInterval(totpRefreshHandle);\n totpRefreshHandle = null;\n }\n return server.close();\n },\n };\n}\n\nexport interface QuickTunnel {\n /** The public `https://*.trycloudflare.com` URL. */\n url: string;\n /** Stop the underlying `cloudflared` process. Idempotent. */\n stop: () => void;\n}\n\n/**\n * Sanitize cloudflared stderr output for error diagnostics (#421).\n *\n * Masks `*.trycloudflare.com` hostnames and full `https://` / `wss://` URLs\n * that carry those hostnames so tunnel host values never appear in error\n * messages. Diagnostic content (error codes, reasons, JSON blobs) is preserved.\n *\n * SECRET-HANDLING: tunnel host is SECRET-class per harness policy — only\n * placeholder text is emitted.\n */\nexport function sanitizeCloudflaredOutput(line: string): string {\n // Full URL forms: https://xxx.trycloudflare.com/… and wss://xxx.trycloudflare.com/…\n let s = line.replace(/(?:https?|wss?):\\/\\/[a-z0-9-]+\\.trycloudflare\\.com(?:\\/[^\\s]*)*/gi, (m) =>\n m.replace(/[a-z0-9-]+\\.trycloudflare\\.com/i, '<HOST>.trycloudflare.com'),\n );\n // Bare hostname without scheme (e.g. printed in cloudflared JSON logs)\n s = s.replace(/[a-z0-9-]+\\.trycloudflare\\.com/gi, '<HOST>.trycloudflare.com');\n return s;\n}\n\nconst URL_TIMEOUT_MS = 20_000;\n\n/**\n * Start an unauthenticated Cloudflare quick tunnel to `http://localhost:<port>`\n * and resolve once the public URL is known. Downloads the `cloudflared` binary\n * on first use if it is not already installed. Rejects with a friendly error if\n * no URL appears within {@link URL_TIMEOUT_MS}.\n */\nexport async function startQuickTunnel(port: number): Promise<QuickTunnel> {\n const cloudflared = await import('cloudflared');\n const { bin, install, Tunnel } = cloudflared;\n\n if (!existsSync(bin)) {\n await mkdir(dirname(bin), { recursive: true });\n await install(bin);\n }\n\n const tunnel = Tunnel.quick(`http://localhost:${port}`);\n let stopped = false;\n const stop = () => {\n if (stopped) return;\n stopped = true;\n try {\n tunnel.stop();\n } catch {\n // process may already be gone\n }\n };\n\n return new Promise<QuickTunnel>((resolve, reject) => {\n // #421: accumulate stderr to attach as diagnostics on failure.\n // SECRET-HANDLING: lines are sanitized before inclusion in error messages.\n const stderrLines: string[] = [];\n\n /**\n * Format the last `n` sanitized stderr lines as a diagnostic appendix.\n * Returns an empty string when no lines have been collected.\n */\n const stderrTail = (n = 15): string => {\n if (stderrLines.length === 0) return '';\n const tail = stderrLines.slice(-n).map(sanitizeCloudflaredOutput).join('');\n return `\\ncloudflared 출력 (마지막 ${Math.min(n, stderrLines.length)}줄):\\n${tail}`;\n };\n\n const timer = setTimeout(() => {\n cleanup();\n stop();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared did not report a tunnel URL within ${\n URL_TIMEOUT_MS / 1000\n }s. Check your network connection, or run \\`cloudflared tunnel --url http://localhost:${port}\\` manually.${stderrTail()}`,\n ),\n );\n }, URL_TIMEOUT_MS);\n\n const onUrl = (line: string) => {\n const found = parseTrycloudflareUrl(line);\n if (!found) return;\n clearTimeout(timer);\n // Stop scanning further output once we have the URL.\n cleanup();\n resolve({ url: found, stop });\n };\n\n // Accumulate stderr lines for diagnostics (#421). Named so it can be\n // removed from the listener list when cleanup() runs.\n const pushStderr = (line: string) => {\n stderrLines.push(line);\n };\n\n const cleanup = () => {\n tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\n tunnel.off('stderr', pushStderr);\n };\n\n // The library emits a parsed `url` event; we also scan raw stdout/stderr in\n // case the output format shifts.\n tunnel.once('url', onUrl);\n tunnel.on('stdout', onUrl);\n tunnel.on('stderr', onUrl);\n // Second stderr listener: accumulate all lines for error diagnostics.\n tunnel.on('stderr', pushStderr);\n tunnel.once('error', (err: Error) => {\n clearTimeout(timer);\n cleanup();\n stop();\n reject(err);\n });\n tunnel.once('exit', (code: number | null) => {\n if (stopped) return;\n clearTimeout(timer);\n cleanup();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared exited (code ${code ?? 'null'}) before reporting a tunnel URL.${stderrTail()}`,\n ),\n );\n });\n });\n}\n"],"mappings":"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"}
|
|
@@ -154,7 +154,7 @@ async function startTunnelDashboard(opts) {
|
|
|
154
154
|
if (opts.qr === false) return void 0;
|
|
155
155
|
const { isAutoDevtoolsDisabled } = await import("./devtools-opener-B8nxrxqu.js");
|
|
156
156
|
if (!(opts.shouldOpen ?? (() => !isAutoDevtoolsDisabled() && canOpenBrowser()))()) return void 0;
|
|
157
|
-
const { startQrHttpServer } = await import("./qr-http-server-
|
|
157
|
+
const { startQrHttpServer } = await import("./qr-http-server-n1twN18z.js");
|
|
158
158
|
const { buildLauncherAttachUrl } = await import("./deeplink-B5-Hxu0Q.js");
|
|
159
159
|
const { generateTotp } = await import("./totp-DIbrZtI7.js");
|
|
160
160
|
const getDashboardState = () => {
|
|
@@ -289,4 +289,4 @@ async function startQuickTunnel(port) {
|
|
|
289
289
|
//#endregion
|
|
290
290
|
export { printTunnelBanner, startQuickTunnel, startTunnelDashboard };
|
|
291
291
|
|
|
292
|
-
//# sourceMappingURL=tunnel-
|
|
292
|
+
//# sourceMappingURL=tunnel-D7xkimBu.js.map
|