@ait-co/devtools 0.1.141 → 0.1.142
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 +124 -89
- package/dist/mock/index.d.ts.map +1 -1
- package/dist/mock/index.js +21 -2
- package/dist/mock/index.js.map +1 -1
- package/dist/panel/index.js +7 -2
- package/dist/panel/index.js.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.map +1 -1
- package/dist/test-runner/bin.js +8 -8
- package/dist/test-runner/bin.js.map +1 -1
- package/dist/test-runner/config.js +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.js +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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildIndicatorExpression } from "./attach-orchestrator-
|
|
1
|
+
import { buildIndicatorExpression } from "./attach-orchestrator-D65KxFy_.js";
|
|
2
2
|
//#region src/test-runner/cell.ts
|
|
3
3
|
/**
|
|
4
4
|
* Injects each key of `globals` into `globalThis` in the page via a single
|
|
@@ -65,4 +65,4 @@ async function injectDebugIndicator(conn, opts) {
|
|
|
65
65
|
//#endregion
|
|
66
66
|
export { injectDebugIndicator, injectGlobals };
|
|
67
67
|
|
|
68
|
-
//# sourceMappingURL=cell-
|
|
68
|
+
//# sourceMappingURL=cell-BaLvusOl.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell-BMlFh08P.js","names":[],"sources":["../src/test-runner/cell.ts"],"sourcesContent":["/**\n * Cell injection utility for the `devtools-test` CLI (issue #684 §4.1).\n *\n * Injects arbitrary globals into the page via `Runtime.evaluate` BEFORE the\n * first test bundle is injected. The injected values are session-global — one\n * call covers all files in the run.\n *\n * The primary use-case is injecting `__AIT_CELL__` (sdkLine/platform) so\n * sdk-example's `aitCapture.ts` picks up the correct test-axis values instead\n * of falling back to `'2.x'`/`'mock'`.\n *\n * devtools does NOT know the shape of `__AIT_CELL__` — it only provides the\n * general injection mechanism. The caller (CLI or MCP auto-attach path) is\n * responsible for constructing the cell object.\n *\n * SECRET-HANDLING: cell values (sdkLine/platform) are not secrets and may be\n * logged at the caller's discretion. This module does NOT log them itself.\n *\n * Node-only. react-free. CdpConnection only.\n */\n\nimport { THROTTLED_ERROR_CODE, THROTTLED_MESSAGE_SUBSTRING } from './throttle.js';\n\n/**\n * `__AIT_PERMS__` key → SDK function name whose `.getPermission()` is probed\n * (devtools#739). This is the exact contract sdk-example#265 consumes — do\n * not rename keys without a coordinated sdk-example update.\n *\n * Each SDK function is reached via `window.__sdk` — the SAME page-global\n * bridge `src/in-app/auto.ts` installs for `call_sdk`/`__sdkCall` (see\n * `src/test-runner/bundle.ts`'s `sdkRedirectPlugin`, which redirects bundled\n * `@apps-in-toss/web-framework` imports to this same object at runtime). The\n * test-runner bundle owns no independent SDK import — `window.__sdk` is a\n * page global regardless of whether a bundle has been injected yet, so this\n * preflight runs standalone via `injectGlobals`'s `Runtime.evaluate`\n * mechanism BEFORE the first bundle, rather than being prepended into\n * `rpc.ts#buildRunTestsExpression`. (Design choice documented in PR #739 —\n * bundle-prepend was the fallback plan if the SDK were only reachable from\n * inside bundle scope, which reality ruled out.)\n */\nconst PERMISSION_PROBE_MAP: Record<string, string> = {\n clipboardRead: 'getClipboardText',\n clipboardWrite: 'setClipboardText',\n album: 'fetchAlbumPhotos',\n camera: 'openCamera',\n contacts: 'fetchContacts',\n location: 'getCurrentLocation',\n};\n\n/**\n * Inter-probe spacing (ms) applied BEFORE every probe after the first\n * (devtools#767). The native per-method bridge rate limit observed\n * 2026-07-10 rejects a SECOND call to the same method within a short window\n * with `APP_BRIDGE_THROTTLED` — the permission preflight's un-paced burst\n * (6 probes fired back-to-back) was the FIRST trigger of that limiter in the\n * wild (only the first probe passed; the remaining 5 came back throttled →\n * `unavailable`, cascading into skipped `__AIT_PERMS__`-branching tests).\n * Sequential-with-spacing is cheap insurance even though each probe targets a\n * DIFFERENT method — the limiter's exact scope (per-method vs. a shared\n * bridge-wide window) was not reverse-engineered (explicitly out of scope,\n * devtools#767) so spacing every probe is the conservative choice.\n */\nconst PROBE_INTER_CALL_DELAY_MS = 250;\n\n/**\n * Backoff delays (ms) between retry attempts when a probe is throttled. Its\n * `.length` IS the max retry count (devtools#767: \"최대 2회 재시도\") — the\n * generated page-side expression below derives `attempt < backoff.length`\n * from this same array (via `backoffArray`), so there is a single source for\n * both the delay ladder and the retry count; no separate max-retries\n * constant is kept in this module (unlike `runtime.ts`'s `TEST_MAX_RETRIES`,\n * which IS consumed directly by a Node-side loop condition).\n */\nconst PROBE_RETRY_BACKOFF_MS = [500, 1000] as const;\n\n/**\n * Non-fatal bound for the permission preflight `Runtime.evaluate` round-trip\n * (devtools#767: raised from the pre-pacing 10s to cover the new sequential +\n * backoff time budget).\n *\n * Worst-case budget with 6 probes, each requiring the full retry ladder:\n * - inter-probe spacing: only BEFORE probes 2‑6 (none before the first) =\n * 5 × {@link PROBE_INTER_CALL_DELAY_MS} = 1 250ms\n * - per-probe backoff (both retries taken): 6 × (500 + 1000) = 9 000ms\n * - probe round-trips themselves (headroom, not just the sleeps): ~4 500ms\n * total ≈ 14 750ms, rounded up to a clean 20 000ms for headroom.\n */\nexport const PERMISSION_PREFLIGHT_TIMEOUT_MS = 20_000;\n\n/**\n * Builds the `Runtime.evaluate` expression for the permission-state preflight\n * (devtools#739). Evaluated ONCE per relay session, before the first test\n * bundle is injected (see `relay-worker.ts#runTestFilesOverRelay`).\n *\n * For each entry in {@link PERMISSION_PROBE_MAP}, probes\n * `window.__sdk.<fn>.getPermission()` — a NON-blocking query (never\n * `openPermissionDialog`/`requestPermission`, which open native UI) — and\n * assigns the resolved `'allowed'|'denied'|'notDetermined'` status to\n * `globalThis.__AIT_PERMS__[<key>]`. A probe that is absent, throws, or\n * rejects resolves to `'unavailable'` — this expression never throws to its\n * caller, and a single slow/broken permission must not fail the others (each\n * probe is awaited independently, not short-circuited).\n *\n * devtools#767: probes run SEQUENTIALLY (not a parallel burst) with a\n * {@link PROBE_INTER_CALL_DELAY_MS} gap before each probe after the first, and\n * a probe that comes back `APP_BRIDGE_THROTTLED` (native per-method bridge\n * rate limit) is retried up to {@link PROBE_RETRY_BACKOFF_MS}`.length` times\n * with the same {@link PROBE_RETRY_BACKOFF_MS} ladder before falling back to\n * `'unavailable'`.\n * The THROTTLED detection mirrors `throttle.ts#isThrottledError` inline (this\n * expression is raw JS text evaluated via CDP, not an esbuild-bundled module,\n * so it cannot statically `import` that helper) — kept in sync by checking\n * BOTH `e.code === 'APP_BRIDGE_THROTTLED'` and a message substring match, the\n * same two signals `isThrottledError` checks.\n *\n * devtools#767 acceptance criteria 2 (\"3.x run duration은 유의미하게 늘지 않음,\n * pacing은 opt-in 또는 throttle-adaptive\") requires this NOT be an unconditional\n * cost — `pace` (default `true`, matching the pre-existing sequential+spacing\n * behavior for 2.x/unknown cells) lets a 3.x caller skip BOTH the inter-probe\n * `sleep(250)` AND the retry backoff ladder, since `APP_BRIDGE_THROTTLED` is a\n * 2.x-bridge-only limiter (see `--pace`'s own doc in `cli.ts`: \"3.x cells are\n * unaffected by that limiter\"). When `pace` is `false`, probes still run\n * sequentially (never a parallel burst) but back-to-back with no added delay,\n * and a THROTTLED result resolves to `'unavailable'` immediately (no retry) —\n * this matches the 3.x runtime, which never emits `APP_BRIDGE_THROTTLED`.\n *\n * Tests (sdk-example#265) read `globalThis.__AIT_PERMS__` to branch\n * deterministically per permission state instead of blanket outcome-branching.\n *\n * Returns a JSON-serialised string (the same double-serialisation pattern\n * `rpc.ts#buildRunTestsExpression` uses) rather than relying on CDP\n * `returnByValue` structural fidelity for a plain object across the Chii\n * relay — the Node side re-parses via `JSON.parse`.\n *\n * Pure function — no I/O, no CDP call. Callers pass the returned string to\n * `Runtime.evaluate`.\n *\n * @param pace - Defaults to `true` (today's sequential+spacing+backoff\n * behavior). Pass `false` for a 3.x cell to skip the inter-probe delay and\n * the THROTTLED retry ladder entirely.\n */\nexport function buildPermissionPreflightExpression(pace = true): string {\n const probeKeys = Object.keys(PERMISSION_PROBE_MAP);\n const probeCalls = probeKeys\n .map((key, i) => {\n const fnName = PERMISSION_PROBE_MAP[key];\n // Every probe after the first waits PROBE_INTER_CALL_DELAY_MS first —\n // sequential execution with spacing, never a parallel burst. Skipped\n // entirely when `pace` is false (3.x cell — no native limiter to dodge).\n const spacing = i === 0 || !pace ? '' : ` await sleep(${PROBE_INTER_CALL_DELAY_MS});\\n`;\n return (\n `${spacing}` +\n ` result[${JSON.stringify(key)}] = await probeWithRetry(${JSON.stringify(fnName)});`\n );\n })\n .join('\\n');\n\n // 3.x cells never emit APP_BRIDGE_THROTTLED (it's a 2.x-bridge-only native\n // limiter) — an empty backoff ladder makes `probeWithRetry` resolve any\n // throw (throttled or not) to 'unavailable' on the FIRST attempt, with zero\n // retry sleeps, satisfying devtools#767 acceptance criteria 2.\n const backoffArray = pace ? `[${PROBE_RETRY_BACKOFF_MS.join(',')}]` : '[]';\n\n return (\n `(async () => {` +\n ` const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));` +\n ` const isThrottled = (e) => {` +\n ` if (!e) return false;` +\n ` if (e.code === ${JSON.stringify(THROTTLED_ERROR_CODE)}) return true;` +\n ` const msg = typeof e.message === 'string' ? e.message : '';` +\n ` return msg.indexOf(${JSON.stringify(THROTTLED_MESSAGE_SUBSTRING)}) !== -1;` +\n ` };` +\n ` const probe = async (fnName) => {` +\n ` const fn = globalThis.__sdk && globalThis.__sdk[fnName];` +\n ` if (!fn || typeof fn.getPermission !== 'function') return { ok: true, value: 'unavailable' };` +\n ` const status = await fn.getPermission();` +\n ` if (status === 'allowed' || status === 'denied' || status === 'notDetermined') return { ok: true, value: status };` +\n ` return { ok: true, value: 'unavailable' };` +\n ` };` +\n // probeWithRetry: up to PROBE_RETRY_BACKOFF_MS.length retries on THROTTLED,\n // backing off per PROBE_RETRY_BACKOFF_MS. Any OTHER throw (or a\n // non-throttled outcome) resolves to 'unavailable' immediately — only\n // THROTTLED gets a retry.\n ` const backoff = ${backoffArray};` +\n ` const probeWithRetry = async (fnName) => {` +\n ` for (let attempt = 0; attempt <= backoff.length; attempt++) {` +\n ` try {` +\n ` const r = await probe(fnName);` +\n ` return r.value;` +\n ` } catch (e) {` +\n ` if (isThrottled(e) && attempt < backoff.length) {` +\n ` await sleep(backoff[attempt]);` +\n ` continue;` +\n ` }` +\n ` return 'unavailable';` +\n ` }` +\n ` }` +\n ` return 'unavailable';` +\n ` };` +\n ` const result = {};` +\n `${probeCalls}\\n` +\n ` globalThis.__AIT_PERMS__ = result;` +\n ` return JSON.stringify(result);` +\n `})()`\n );\n}\n\n/**\n * Runs the permission-state preflight (devtools#739) via `Runtime.evaluate`\n * and returns the collected `__AIT_PERMS__` map, or `undefined` when the\n * preflight itself failed or timed out.\n *\n * NON-FATAL by design: any failure (CDP exception, timeout, page-side throw)\n * is caught here, logged as ONE stderr line, and swallowed — the whole test\n * run must proceed even when the preflight cannot complete (e.g. `window.__sdk`\n * not yet installed, or a page navigation mid-attach). Callers that want the\n * result for report provenance (`RelayRunReport.preflight`) get `undefined`\n * in that case, which downstream code treats the same as \"not run\".\n *\n * SECRET-HANDLING: no relay/wss/secret values are read or logged; only\n * permission-state strings cross this boundary.\n *\n * @param conn - The live CDP connection (relay-attached page).\n * @param timeoutMs - Round-trip bound. Defaults to {@link PERMISSION_PREFLIGHT_TIMEOUT_MS}.\n * @param pace - Forwarded to {@link buildPermissionPreflightExpression}.\n * Defaults to `true` (today's sequential+spacing+backoff behavior, correct\n * for 2.x/unknown cells). Callers that know the cell is 3.x should pass\n * `false` — devtools#767 acceptance criteria 2 requires 3.x run duration not\n * grow from this preflight, and 3.x never hits the 2.x-only\n * `APP_BRIDGE_THROTTLED` limiter this pacing exists to dodge.\n */\nexport async function runPermissionPreflight(\n conn: CdpConnection,\n timeoutMs = PERMISSION_PREFLIGHT_TIMEOUT_MS,\n pace = true,\n): Promise<Record<string, string> | undefined> {\n const TIMEOUT_SENTINEL = Symbol('permission-preflight-timeout');\n const timeoutPromise = new Promise<typeof TIMEOUT_SENTINEL>((resolve) =>\n setTimeout(() => resolve(TIMEOUT_SENTINEL), timeoutMs),\n );\n\n try {\n const evalPromise = conn.send('Runtime.evaluate', {\n expression: buildPermissionPreflightExpression(pace),\n returnByValue: true,\n awaitPromise: true,\n });\n\n const raceResult = await Promise.race([\n evalPromise.then((v) => ({ tag: 'eval' as const, v })),\n timeoutPromise.then(() => ({ tag: 'timeout' as const })),\n ]);\n\n if (raceResult.tag === 'timeout') {\n process.stderr.write(\n `test-runner: permission preflight timed out after ${timeoutMs}ms — proceeding with __AIT_PERMS__ unset\\n`,\n );\n return undefined;\n }\n\n if (raceResult.v.exceptionDetails) {\n process.stderr.write(\n 'test-runner: permission preflight threw — proceeding with __AIT_PERMS__ unset\\n',\n );\n return undefined;\n }\n\n const rawValue = raceResult.v.result.value;\n if (typeof rawValue !== 'string') return undefined;\n const parsed: unknown = JSON.parse(rawValue);\n if (typeof parsed !== 'object' || parsed === null) return undefined;\n return parsed as Record<string, string>;\n } catch (e) {\n process.stderr.write(\n `test-runner: permission preflight failed (${\n e instanceof Error ? e.message : String(e)\n }) — proceeding with __AIT_PERMS__ unset\\n`,\n );\n return undefined;\n }\n}\n\nimport { buildIndicatorExpression } from '../mcp/attach-orchestrator.js';\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\n\n/**\n * Injects each key of `globals` into `globalThis` in the page via a single\n * `Runtime.evaluate` call. Must be called BEFORE the first `bundleTestFile`\n * inject — the cell is session-global and applies to all subsequent files.\n *\n * Throws if the CDP evaluate returns an exception.\n *\n * @param conn - The live CDP connection (relay-attached page).\n * @param globals - Plain-JSON-serialisable key→value map to assign onto `globalThis`.\n */\nexport async function injectGlobals(\n conn: CdpConnection,\n globals: Record<string, unknown>,\n): Promise<void> {\n // JSON.stringify is safe here: globals values are plain data (sdkLine/platform\n // strings or simple objects). If a value is not JSON-serialisable this throws\n // at call time, which surfaces the error early and clearly.\n const expr = `(() => { Object.assign(globalThis, ${JSON.stringify(globals)}); return true; })()`;\n const result = await conn.send('Runtime.evaluate', { expression: expr, returnByValue: true });\n if (result.exceptionDetails) {\n const msg =\n result.exceptionDetails.exception?.description ??\n result.exceptionDetails.text ??\n 'unknown error';\n throw new Error(`injectGlobals: Runtime.evaluate threw: ${msg}`);\n }\n}\n\n/**\n * Injects (or updates) the \"Debugger Connected\"/\"Debugger Disconnected\"\n * on-phone indicator via `Runtime.evaluate` (#730).\n *\n * Uses the same CDP mechanism as {@link injectGlobals} — a single\n * `Runtime.evaluate` round-trip with the expression built by\n * {@link buildIndicatorExpression}. The indicator is a LIVE badge rendered at\n * the bottom-left of the page (position:fixed, safe-area-aware): the badge's\n * controller is idempotent, so calling this again (e.g. right before\n * `close()` with `{ state: 'disconnected' }`) UPDATES the existing badge in\n * place instead of injecting a duplicate.\n *\n * **Isolation**: unlike {@link injectGlobals}, this function NEVER throws to\n * its caller. Injection failure (e.g. page detached during inject, CSS not\n * supported) is swallowed and logged as `console.debug` — the badge is\n * informational UI and must never block attach success or test execution.\n * This is the same fire-and-forget spirit as the in-page console mount in\n * `src/in-app/attach.ts`.\n *\n * Call this ONLY on the manual debug paths (start_attach MCP, devtools-test\n * CLI). Do NOT call on the `run_tests` auto-attach path — the badge\n * would contaminate screenshots, measure_safe_area probes, and DOM snapshots\n * taken during automated measurement runs.\n *\n * SECRET-HANDLING: the injected expression contains no secrets, relay URLs,\n * wss addresses, or TOTP codes — it is pure DOM UI text + enum state built by\n * {@link buildIndicatorExpression}.\n *\n * @param conn - The live CDP connection (relay-attached page).\n * @param opts - Optional overrides forwarded to {@link buildIndicatorExpression}.\n */\nexport async function injectDebugIndicator(\n conn: CdpConnection,\n opts?: { label?: string; disconnectedLabel?: string; state?: 'attached' | 'disconnected' },\n): Promise<void> {\n try {\n await conn.send('Runtime.evaluate', {\n expression: buildIndicatorExpression(opts),\n returnByValue: true,\n });\n } catch (err) {\n // Badge injection is informational UI — swallow and log; never propagate.\n console.debug('[@ait-co/devtools] debug indicator inject skipped:', err);\n }\n}\n"],"mappings":";;;;;;;;;;;;AAuSA,eAAsB,cACpB,MACA,SACe;CAIf,MAAM,OAAO,sCAAsC,KAAK,UAAU,QAAQ,CAAC;CAC3E,MAAM,SAAS,MAAM,KAAK,KAAK,oBAAoB;EAAE,YAAY;EAAM,eAAe;EAAM,CAAC;AAC7F,KAAI,OAAO,kBAAkB;EAC3B,MAAM,MACJ,OAAO,iBAAiB,WAAW,eACnC,OAAO,iBAAiB,QACxB;AACF,QAAM,IAAI,MAAM,0CAA0C,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCpE,eAAsB,qBACpB,MACA,MACe;AACf,KAAI;AACF,QAAM,KAAK,KAAK,oBAAoB;GAClC,YAAY,yBAAyB,KAAK;GAC1C,eAAe;GAChB,CAAC;UACK,KAAK;AAEZ,UAAQ,MAAM,sDAAsD,IAAI"}
|
|
1
|
+
{"version":3,"file":"cell-BaLvusOl.js","names":[],"sources":["../src/test-runner/cell.ts"],"sourcesContent":["/**\n * Cell injection utility for the `devtools-test` CLI (issue #684 §4.1).\n *\n * Injects arbitrary globals into the page via `Runtime.evaluate` BEFORE the\n * first test bundle is injected. The injected values are session-global — one\n * call covers all files in the run.\n *\n * The primary use-case is injecting `__AIT_CELL__` (sdkLine/platform) so\n * sdk-example's `aitCapture.ts` picks up the correct test-axis values instead\n * of falling back to `'2.x'`/`'mock'`.\n *\n * devtools does NOT know the shape of `__AIT_CELL__` — it only provides the\n * general injection mechanism. The caller (CLI or MCP auto-attach path) is\n * responsible for constructing the cell object.\n *\n * SECRET-HANDLING: cell values (sdkLine/platform) are not secrets and may be\n * logged at the caller's discretion. This module does NOT log them itself.\n *\n * Node-only. react-free. CdpConnection only.\n */\n\nimport { THROTTLED_ERROR_CODE, THROTTLED_MESSAGE_SUBSTRING } from './throttle.js';\n\n/**\n * `__AIT_PERMS__` key → SDK function name whose `.getPermission()` is probed\n * (devtools#739). This is the exact contract sdk-example#265 consumes — do\n * not rename keys without a coordinated sdk-example update.\n *\n * Each SDK function is reached via `window.__sdk` — the SAME page-global\n * bridge `src/in-app/auto.ts` installs for `call_sdk`/`__sdkCall` (see\n * `src/test-runner/bundle.ts`'s `sdkRedirectPlugin`, which redirects bundled\n * `@apps-in-toss/web-framework` imports to this same object at runtime). The\n * test-runner bundle owns no independent SDK import — `window.__sdk` is a\n * page global regardless of whether a bundle has been injected yet, so this\n * preflight runs standalone via `injectGlobals`'s `Runtime.evaluate`\n * mechanism BEFORE the first bundle, rather than being prepended into\n * `rpc.ts#buildRunTestsExpression`. (Design choice documented in PR #739 —\n * bundle-prepend was the fallback plan if the SDK were only reachable from\n * inside bundle scope, which reality ruled out.)\n */\nconst PERMISSION_PROBE_MAP: Record<string, string> = {\n clipboardRead: 'getClipboardText',\n clipboardWrite: 'setClipboardText',\n album: 'fetchAlbumPhotos',\n camera: 'openCamera',\n contacts: 'fetchContacts',\n location: 'getCurrentLocation',\n};\n\n/**\n * Inter-probe spacing (ms) applied BEFORE every probe after the first\n * (devtools#767). The native per-method bridge rate limit observed\n * 2026-07-10 rejects a SECOND call to the same method within a short window\n * with `APP_BRIDGE_THROTTLED` — the permission preflight's un-paced burst\n * (6 probes fired back-to-back) was the FIRST trigger of that limiter in the\n * wild (only the first probe passed; the remaining 5 came back throttled →\n * `unavailable`, cascading into skipped `__AIT_PERMS__`-branching tests).\n * Sequential-with-spacing is cheap insurance even though each probe targets a\n * DIFFERENT method — the limiter's exact scope (per-method vs. a shared\n * bridge-wide window) was not reverse-engineered (explicitly out of scope,\n * devtools#767) so spacing every probe is the conservative choice.\n */\nconst PROBE_INTER_CALL_DELAY_MS = 250;\n\n/**\n * Backoff delays (ms) between retry attempts when a probe is throttled. Its\n * `.length` IS the max retry count (devtools#767: \"최대 2회 재시도\") — the\n * generated page-side expression below derives `attempt < backoff.length`\n * from this same array (via `backoffArray`), so there is a single source for\n * both the delay ladder and the retry count; no separate max-retries\n * constant is kept in this module (unlike `runtime.ts`'s `TEST_MAX_RETRIES`,\n * which IS consumed directly by a Node-side loop condition).\n */\nconst PROBE_RETRY_BACKOFF_MS = [500, 1000] as const;\n\n/**\n * Non-fatal bound for the permission preflight `Runtime.evaluate` round-trip\n * (devtools#767: raised from the pre-pacing 10s to cover the new sequential +\n * backoff time budget).\n *\n * Worst-case budget with 6 probes, each requiring the full retry ladder:\n * - inter-probe spacing: only BEFORE probes 2‑6 (none before the first) =\n * 5 × {@link PROBE_INTER_CALL_DELAY_MS} = 1 250ms\n * - per-probe backoff (both retries taken): 6 × (500 + 1000) = 9 000ms\n * - probe round-trips themselves (headroom, not just the sleeps): ~4 500ms\n * total ≈ 14 750ms, rounded up to a clean 20 000ms for headroom.\n */\nexport const PERMISSION_PREFLIGHT_TIMEOUT_MS = 20_000;\n\n/**\n * Builds the `Runtime.evaluate` expression for the permission-state preflight\n * (devtools#739). Evaluated ONCE per relay session, before the first test\n * bundle is injected (see `relay-worker.ts#runTestFilesOverRelay`).\n *\n * For each entry in {@link PERMISSION_PROBE_MAP}, probes\n * `window.__sdk.<fn>.getPermission()` — a NON-blocking query (never\n * `openPermissionDialog`/`requestPermission`, which open native UI) — and\n * assigns the resolved `'allowed'|'denied'|'notDetermined'` status to\n * `globalThis.__AIT_PERMS__[<key>]`. A probe that is absent, throws, or\n * rejects resolves to `'unavailable'` — this expression never throws to its\n * caller, and a single slow/broken permission must not fail the others (each\n * probe is awaited independently, not short-circuited).\n *\n * devtools#767: probes run SEQUENTIALLY (not a parallel burst) with a\n * {@link PROBE_INTER_CALL_DELAY_MS} gap before each probe after the first, and\n * a probe that comes back `APP_BRIDGE_THROTTLED` (native per-method bridge\n * rate limit) is retried up to {@link PROBE_RETRY_BACKOFF_MS}`.length` times\n * with the same {@link PROBE_RETRY_BACKOFF_MS} ladder before falling back to\n * `'unavailable'`.\n * The THROTTLED detection mirrors `throttle.ts#isThrottledError` inline (this\n * expression is raw JS text evaluated via CDP, not an esbuild-bundled module,\n * so it cannot statically `import` that helper) — kept in sync by checking\n * BOTH `e.code === 'APP_BRIDGE_THROTTLED'` and a message substring match, the\n * same two signals `isThrottledError` checks.\n *\n * devtools#767 acceptance criteria 2 (\"3.x run duration은 유의미하게 늘지 않음,\n * pacing은 opt-in 또는 throttle-adaptive\") requires this NOT be an unconditional\n * cost — `pace` (default `true`, matching the pre-existing sequential+spacing\n * behavior for 2.x/unknown cells) lets a 3.x caller skip BOTH the inter-probe\n * `sleep(250)` AND the retry backoff ladder, since `APP_BRIDGE_THROTTLED` is a\n * 2.x-bridge-only limiter (see `--pace`'s own doc in `cli.ts`: \"3.x cells are\n * unaffected by that limiter\"). When `pace` is `false`, probes still run\n * sequentially (never a parallel burst) but back-to-back with no added delay,\n * and a THROTTLED result resolves to `'unavailable'` immediately (no retry) —\n * this matches the 3.x runtime, which never emits `APP_BRIDGE_THROTTLED`.\n *\n * Tests (sdk-example#265) read `globalThis.__AIT_PERMS__` to branch\n * deterministically per permission state instead of blanket outcome-branching.\n *\n * Returns a JSON-serialised string (the same double-serialisation pattern\n * `rpc.ts#buildRunTestsExpression` uses) rather than relying on CDP\n * `returnByValue` structural fidelity for a plain object across the Chii\n * relay — the Node side re-parses via `JSON.parse`.\n *\n * Pure function — no I/O, no CDP call. Callers pass the returned string to\n * `Runtime.evaluate`.\n *\n * @param pace - Defaults to `true` (today's sequential+spacing+backoff\n * behavior). Pass `false` for a 3.x cell to skip the inter-probe delay and\n * the THROTTLED retry ladder entirely.\n */\nexport function buildPermissionPreflightExpression(pace = true): string {\n const probeKeys = Object.keys(PERMISSION_PROBE_MAP);\n const probeCalls = probeKeys\n .map((key, i) => {\n const fnName = PERMISSION_PROBE_MAP[key];\n // Every probe after the first waits PROBE_INTER_CALL_DELAY_MS first —\n // sequential execution with spacing, never a parallel burst. Skipped\n // entirely when `pace` is false (3.x cell — no native limiter to dodge).\n const spacing = i === 0 || !pace ? '' : ` await sleep(${PROBE_INTER_CALL_DELAY_MS});\\n`;\n return (\n `${spacing}` +\n ` result[${JSON.stringify(key)}] = await probeWithRetry(${JSON.stringify(fnName)});`\n );\n })\n .join('\\n');\n\n // 3.x cells never emit APP_BRIDGE_THROTTLED (it's a 2.x-bridge-only native\n // limiter) — an empty backoff ladder makes `probeWithRetry` resolve any\n // throw (throttled or not) to 'unavailable' on the FIRST attempt, with zero\n // retry sleeps, satisfying devtools#767 acceptance criteria 2.\n const backoffArray = pace ? `[${PROBE_RETRY_BACKOFF_MS.join(',')}]` : '[]';\n\n return (\n `(async () => {` +\n ` const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));` +\n ` const isThrottled = (e) => {` +\n ` if (!e) return false;` +\n ` if (e.code === ${JSON.stringify(THROTTLED_ERROR_CODE)}) return true;` +\n ` const msg = typeof e.message === 'string' ? e.message : '';` +\n ` return msg.indexOf(${JSON.stringify(THROTTLED_MESSAGE_SUBSTRING)}) !== -1;` +\n ` };` +\n ` const probe = async (fnName) => {` +\n ` const fn = globalThis.__sdk && globalThis.__sdk[fnName];` +\n ` if (!fn || typeof fn.getPermission !== 'function') return { ok: true, value: 'unavailable' };` +\n ` const status = await fn.getPermission();` +\n ` if (status === 'allowed' || status === 'denied' || status === 'notDetermined') return { ok: true, value: status };` +\n ` return { ok: true, value: 'unavailable' };` +\n ` };` +\n // probeWithRetry: up to PROBE_RETRY_BACKOFF_MS.length retries on THROTTLED,\n // backing off per PROBE_RETRY_BACKOFF_MS. Any OTHER throw (or a\n // non-throttled outcome) resolves to 'unavailable' immediately — only\n // THROTTLED gets a retry.\n ` const backoff = ${backoffArray};` +\n ` const probeWithRetry = async (fnName) => {` +\n ` for (let attempt = 0; attempt <= backoff.length; attempt++) {` +\n ` try {` +\n ` const r = await probe(fnName);` +\n ` return r.value;` +\n ` } catch (e) {` +\n ` if (isThrottled(e) && attempt < backoff.length) {` +\n ` await sleep(backoff[attempt]);` +\n ` continue;` +\n ` }` +\n ` return 'unavailable';` +\n ` }` +\n ` }` +\n ` return 'unavailable';` +\n ` };` +\n ` const result = {};` +\n `${probeCalls}\\n` +\n ` globalThis.__AIT_PERMS__ = result;` +\n ` return JSON.stringify(result);` +\n `})()`\n );\n}\n\n/**\n * Runs the permission-state preflight (devtools#739) via `Runtime.evaluate`\n * and returns the collected `__AIT_PERMS__` map, or `undefined` when the\n * preflight itself failed or timed out.\n *\n * NON-FATAL by design: any failure (CDP exception, timeout, page-side throw)\n * is caught here, logged as ONE stderr line, and swallowed — the whole test\n * run must proceed even when the preflight cannot complete (e.g. `window.__sdk`\n * not yet installed, or a page navigation mid-attach). Callers that want the\n * result for report provenance (`RelayRunReport.preflight`) get `undefined`\n * in that case, which downstream code treats the same as \"not run\".\n *\n * SECRET-HANDLING: no relay/wss/secret values are read or logged; only\n * permission-state strings cross this boundary.\n *\n * @param conn - The live CDP connection (relay-attached page).\n * @param timeoutMs - Round-trip bound. Defaults to {@link PERMISSION_PREFLIGHT_TIMEOUT_MS}.\n * @param pace - Forwarded to {@link buildPermissionPreflightExpression}.\n * Defaults to `true` (today's sequential+spacing+backoff behavior, correct\n * for 2.x/unknown cells). Callers that know the cell is 3.x should pass\n * `false` — devtools#767 acceptance criteria 2 requires 3.x run duration not\n * grow from this preflight, and 3.x never hits the 2.x-only\n * `APP_BRIDGE_THROTTLED` limiter this pacing exists to dodge.\n */\nexport async function runPermissionPreflight(\n conn: CdpConnection,\n timeoutMs = PERMISSION_PREFLIGHT_TIMEOUT_MS,\n pace = true,\n): Promise<Record<string, string> | undefined> {\n const TIMEOUT_SENTINEL = Symbol('permission-preflight-timeout');\n const timeoutPromise = new Promise<typeof TIMEOUT_SENTINEL>((resolve) =>\n setTimeout(() => resolve(TIMEOUT_SENTINEL), timeoutMs),\n );\n\n try {\n const evalPromise = conn.send('Runtime.evaluate', {\n expression: buildPermissionPreflightExpression(pace),\n returnByValue: true,\n awaitPromise: true,\n });\n\n const raceResult = await Promise.race([\n evalPromise.then((v) => ({ tag: 'eval' as const, v })),\n timeoutPromise.then(() => ({ tag: 'timeout' as const })),\n ]);\n\n if (raceResult.tag === 'timeout') {\n process.stderr.write(\n `test-runner: permission preflight timed out after ${timeoutMs}ms — proceeding with __AIT_PERMS__ unset\\n`,\n );\n return undefined;\n }\n\n if (raceResult.v.exceptionDetails) {\n process.stderr.write(\n 'test-runner: permission preflight threw — proceeding with __AIT_PERMS__ unset\\n',\n );\n return undefined;\n }\n\n const rawValue = raceResult.v.result.value;\n if (typeof rawValue !== 'string') return undefined;\n const parsed: unknown = JSON.parse(rawValue);\n if (typeof parsed !== 'object' || parsed === null) return undefined;\n return parsed as Record<string, string>;\n } catch (e) {\n process.stderr.write(\n `test-runner: permission preflight failed (${\n e instanceof Error ? e.message : String(e)\n }) — proceeding with __AIT_PERMS__ unset\\n`,\n );\n return undefined;\n }\n}\n\nimport { buildIndicatorExpression } from '../mcp/attach-orchestrator.js';\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\n\n/**\n * Injects each key of `globals` into `globalThis` in the page via a single\n * `Runtime.evaluate` call. Must be called BEFORE the first `bundleTestFile`\n * inject — the cell is session-global and applies to all subsequent files.\n *\n * Throws if the CDP evaluate returns an exception.\n *\n * @param conn - The live CDP connection (relay-attached page).\n * @param globals - Plain-JSON-serialisable key→value map to assign onto `globalThis`.\n */\nexport async function injectGlobals(\n conn: CdpConnection,\n globals: Record<string, unknown>,\n): Promise<void> {\n // JSON.stringify is safe here: globals values are plain data (sdkLine/platform\n // strings or simple objects). If a value is not JSON-serialisable this throws\n // at call time, which surfaces the error early and clearly.\n const expr = `(() => { Object.assign(globalThis, ${JSON.stringify(globals)}); return true; })()`;\n const result = await conn.send('Runtime.evaluate', { expression: expr, returnByValue: true });\n if (result.exceptionDetails) {\n const msg =\n result.exceptionDetails.exception?.description ??\n result.exceptionDetails.text ??\n 'unknown error';\n throw new Error(`injectGlobals: Runtime.evaluate threw: ${msg}`);\n }\n}\n\n/**\n * Injects (or updates) the \"Debugger Connected\"/\"Debugger Disconnected\"\n * on-phone indicator via `Runtime.evaluate` (#730).\n *\n * Uses the same CDP mechanism as {@link injectGlobals} — a single\n * `Runtime.evaluate` round-trip with the expression built by\n * {@link buildIndicatorExpression}. The indicator is a LIVE badge rendered at\n * the bottom-left of the page (position:fixed, safe-area-aware): the badge's\n * controller is idempotent, so calling this again (e.g. right before\n * `close()` with `{ state: 'disconnected' }`) UPDATES the existing badge in\n * place instead of injecting a duplicate.\n *\n * **Isolation**: unlike {@link injectGlobals}, this function NEVER throws to\n * its caller. Injection failure (e.g. page detached during inject, CSS not\n * supported) is swallowed and logged as `console.debug` — the badge is\n * informational UI and must never block attach success or test execution.\n * This is the same fire-and-forget spirit as the in-page console mount in\n * `src/in-app/attach.ts`.\n *\n * Call this ONLY on the manual debug paths (start_attach MCP, devtools-test\n * CLI). Do NOT call on the `run_tests` auto-attach path — the badge\n * would contaminate screenshots, measure_safe_area probes, and DOM snapshots\n * taken during automated measurement runs.\n *\n * SECRET-HANDLING: the injected expression contains no secrets, relay URLs,\n * wss addresses, or TOTP codes — it is pure DOM UI text + enum state built by\n * {@link buildIndicatorExpression}.\n *\n * @param conn - The live CDP connection (relay-attached page).\n * @param opts - Optional overrides forwarded to {@link buildIndicatorExpression}.\n */\nexport async function injectDebugIndicator(\n conn: CdpConnection,\n opts?: { label?: string; disconnectedLabel?: string; state?: 'attached' | 'disconnected' },\n): Promise<void> {\n try {\n await conn.send('Runtime.evaluate', {\n expression: buildIndicatorExpression(opts),\n returnByValue: true,\n });\n } catch (err) {\n // Badge injection is informational UI — swallow and log; never propagate.\n console.debug('[@ait-co/devtools] debug indicator inject skipped:', err);\n }\n}\n"],"mappings":";;;;;;;;;;;;AAuSA,eAAsB,cACpB,MACA,SACe;CAIf,MAAM,OAAO,sCAAsC,KAAK,UAAU,QAAQ,CAAC;CAC3E,MAAM,SAAS,MAAM,KAAK,KAAK,oBAAoB;EAAE,YAAY;EAAM,eAAe;EAAM,CAAC;AAC7F,KAAI,OAAO,kBAAkB;EAC3B,MAAM,MACJ,OAAO,iBAAiB,WAAW,eACnC,OAAO,iBAAiB,QACxB;AACF,QAAM,IAAI,MAAM,0CAA0C,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCpE,eAAsB,qBACpB,MACA,MACe;AACf,KAAI;AACF,QAAM,KAAK,KAAK,oBAAoB;GAClC,YAAY,yBAAyB,KAAK;GAC1C,eAAe;GAChB,CAAC;UACK,KAAK;AAEZ,UAAQ,MAAM,sDAAsD,IAAI"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-DUslC3ob.js";
|
|
2
|
-
import { a as buildIndicatorExpression } from "./attach-orchestrator-
|
|
2
|
+
import { a as buildIndicatorExpression } from "./attach-orchestrator-0F0m_UqQ.js";
|
|
3
3
|
import { n as THROTTLED_MESSAGE_SUBSTRING, t as THROTTLED_ERROR_CODE } from "./throttle-DKKzX1qC.js";
|
|
4
4
|
//#region src/test-runner/cell.ts
|
|
5
5
|
/**
|
|
@@ -271,4 +271,4 @@ async function injectDebugIndicator(conn, opts) {
|
|
|
271
271
|
//#endregion
|
|
272
272
|
export { cell_exports as n, runPermissionPreflight as r, PERMISSION_PREFLIGHT_TIMEOUT_MS as t };
|
|
273
273
|
|
|
274
|
-
//# sourceMappingURL=cell-
|
|
274
|
+
//# sourceMappingURL=cell-CBUS3-nT.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell-Dtgtusog.js","names":[],"sources":["../src/test-runner/cell.ts"],"sourcesContent":["/**\n * Cell injection utility for the `devtools-test` CLI (issue #684 §4.1).\n *\n * Injects arbitrary globals into the page via `Runtime.evaluate` BEFORE the\n * first test bundle is injected. The injected values are session-global — one\n * call covers all files in the run.\n *\n * The primary use-case is injecting `__AIT_CELL__` (sdkLine/platform) so\n * sdk-example's `aitCapture.ts` picks up the correct test-axis values instead\n * of falling back to `'2.x'`/`'mock'`.\n *\n * devtools does NOT know the shape of `__AIT_CELL__` — it only provides the\n * general injection mechanism. The caller (CLI or MCP auto-attach path) is\n * responsible for constructing the cell object.\n *\n * SECRET-HANDLING: cell values (sdkLine/platform) are not secrets and may be\n * logged at the caller's discretion. This module does NOT log them itself.\n *\n * Node-only. react-free. CdpConnection only.\n */\n\nimport { THROTTLED_ERROR_CODE, THROTTLED_MESSAGE_SUBSTRING } from './throttle.js';\n\n/**\n * `__AIT_PERMS__` key → SDK function name whose `.getPermission()` is probed\n * (devtools#739). This is the exact contract sdk-example#265 consumes — do\n * not rename keys without a coordinated sdk-example update.\n *\n * Each SDK function is reached via `window.__sdk` — the SAME page-global\n * bridge `src/in-app/auto.ts` installs for `call_sdk`/`__sdkCall` (see\n * `src/test-runner/bundle.ts`'s `sdkRedirectPlugin`, which redirects bundled\n * `@apps-in-toss/web-framework` imports to this same object at runtime). The\n * test-runner bundle owns no independent SDK import — `window.__sdk` is a\n * page global regardless of whether a bundle has been injected yet, so this\n * preflight runs standalone via `injectGlobals`'s `Runtime.evaluate`\n * mechanism BEFORE the first bundle, rather than being prepended into\n * `rpc.ts#buildRunTestsExpression`. (Design choice documented in PR #739 —\n * bundle-prepend was the fallback plan if the SDK were only reachable from\n * inside bundle scope, which reality ruled out.)\n */\nconst PERMISSION_PROBE_MAP: Record<string, string> = {\n clipboardRead: 'getClipboardText',\n clipboardWrite: 'setClipboardText',\n album: 'fetchAlbumPhotos',\n camera: 'openCamera',\n contacts: 'fetchContacts',\n location: 'getCurrentLocation',\n};\n\n/**\n * Inter-probe spacing (ms) applied BEFORE every probe after the first\n * (devtools#767). The native per-method bridge rate limit observed\n * 2026-07-10 rejects a SECOND call to the same method within a short window\n * with `APP_BRIDGE_THROTTLED` — the permission preflight's un-paced burst\n * (6 probes fired back-to-back) was the FIRST trigger of that limiter in the\n * wild (only the first probe passed; the remaining 5 came back throttled →\n * `unavailable`, cascading into skipped `__AIT_PERMS__`-branching tests).\n * Sequential-with-spacing is cheap insurance even though each probe targets a\n * DIFFERENT method — the limiter's exact scope (per-method vs. a shared\n * bridge-wide window) was not reverse-engineered (explicitly out of scope,\n * devtools#767) so spacing every probe is the conservative choice.\n */\nconst PROBE_INTER_CALL_DELAY_MS = 250;\n\n/**\n * Backoff delays (ms) between retry attempts when a probe is throttled. Its\n * `.length` IS the max retry count (devtools#767: \"최대 2회 재시도\") — the\n * generated page-side expression below derives `attempt < backoff.length`\n * from this same array (via `backoffArray`), so there is a single source for\n * both the delay ladder and the retry count; no separate max-retries\n * constant is kept in this module (unlike `runtime.ts`'s `TEST_MAX_RETRIES`,\n * which IS consumed directly by a Node-side loop condition).\n */\nconst PROBE_RETRY_BACKOFF_MS = [500, 1000] as const;\n\n/**\n * Non-fatal bound for the permission preflight `Runtime.evaluate` round-trip\n * (devtools#767: raised from the pre-pacing 10s to cover the new sequential +\n * backoff time budget).\n *\n * Worst-case budget with 6 probes, each requiring the full retry ladder:\n * - inter-probe spacing: only BEFORE probes 2‑6 (none before the first) =\n * 5 × {@link PROBE_INTER_CALL_DELAY_MS} = 1 250ms\n * - per-probe backoff (both retries taken): 6 × (500 + 1000) = 9 000ms\n * - probe round-trips themselves (headroom, not just the sleeps): ~4 500ms\n * total ≈ 14 750ms, rounded up to a clean 20 000ms for headroom.\n */\nexport const PERMISSION_PREFLIGHT_TIMEOUT_MS = 20_000;\n\n/**\n * Builds the `Runtime.evaluate` expression for the permission-state preflight\n * (devtools#739). Evaluated ONCE per relay session, before the first test\n * bundle is injected (see `relay-worker.ts#runTestFilesOverRelay`).\n *\n * For each entry in {@link PERMISSION_PROBE_MAP}, probes\n * `window.__sdk.<fn>.getPermission()` — a NON-blocking query (never\n * `openPermissionDialog`/`requestPermission`, which open native UI) — and\n * assigns the resolved `'allowed'|'denied'|'notDetermined'` status to\n * `globalThis.__AIT_PERMS__[<key>]`. A probe that is absent, throws, or\n * rejects resolves to `'unavailable'` — this expression never throws to its\n * caller, and a single slow/broken permission must not fail the others (each\n * probe is awaited independently, not short-circuited).\n *\n * devtools#767: probes run SEQUENTIALLY (not a parallel burst) with a\n * {@link PROBE_INTER_CALL_DELAY_MS} gap before each probe after the first, and\n * a probe that comes back `APP_BRIDGE_THROTTLED` (native per-method bridge\n * rate limit) is retried up to {@link PROBE_RETRY_BACKOFF_MS}`.length` times\n * with the same {@link PROBE_RETRY_BACKOFF_MS} ladder before falling back to\n * `'unavailable'`.\n * The THROTTLED detection mirrors `throttle.ts#isThrottledError` inline (this\n * expression is raw JS text evaluated via CDP, not an esbuild-bundled module,\n * so it cannot statically `import` that helper) — kept in sync by checking\n * BOTH `e.code === 'APP_BRIDGE_THROTTLED'` and a message substring match, the\n * same two signals `isThrottledError` checks.\n *\n * devtools#767 acceptance criteria 2 (\"3.x run duration은 유의미하게 늘지 않음,\n * pacing은 opt-in 또는 throttle-adaptive\") requires this NOT be an unconditional\n * cost — `pace` (default `true`, matching the pre-existing sequential+spacing\n * behavior for 2.x/unknown cells) lets a 3.x caller skip BOTH the inter-probe\n * `sleep(250)` AND the retry backoff ladder, since `APP_BRIDGE_THROTTLED` is a\n * 2.x-bridge-only limiter (see `--pace`'s own doc in `cli.ts`: \"3.x cells are\n * unaffected by that limiter\"). When `pace` is `false`, probes still run\n * sequentially (never a parallel burst) but back-to-back with no added delay,\n * and a THROTTLED result resolves to `'unavailable'` immediately (no retry) —\n * this matches the 3.x runtime, which never emits `APP_BRIDGE_THROTTLED`.\n *\n * Tests (sdk-example#265) read `globalThis.__AIT_PERMS__` to branch\n * deterministically per permission state instead of blanket outcome-branching.\n *\n * Returns a JSON-serialised string (the same double-serialisation pattern\n * `rpc.ts#buildRunTestsExpression` uses) rather than relying on CDP\n * `returnByValue` structural fidelity for a plain object across the Chii\n * relay — the Node side re-parses via `JSON.parse`.\n *\n * Pure function — no I/O, no CDP call. Callers pass the returned string to\n * `Runtime.evaluate`.\n *\n * @param pace - Defaults to `true` (today's sequential+spacing+backoff\n * behavior). Pass `false` for a 3.x cell to skip the inter-probe delay and\n * the THROTTLED retry ladder entirely.\n */\nexport function buildPermissionPreflightExpression(pace = true): string {\n const probeKeys = Object.keys(PERMISSION_PROBE_MAP);\n const probeCalls = probeKeys\n .map((key, i) => {\n const fnName = PERMISSION_PROBE_MAP[key];\n // Every probe after the first waits PROBE_INTER_CALL_DELAY_MS first —\n // sequential execution with spacing, never a parallel burst. Skipped\n // entirely when `pace` is false (3.x cell — no native limiter to dodge).\n const spacing = i === 0 || !pace ? '' : ` await sleep(${PROBE_INTER_CALL_DELAY_MS});\\n`;\n return (\n `${spacing}` +\n ` result[${JSON.stringify(key)}] = await probeWithRetry(${JSON.stringify(fnName)});`\n );\n })\n .join('\\n');\n\n // 3.x cells never emit APP_BRIDGE_THROTTLED (it's a 2.x-bridge-only native\n // limiter) — an empty backoff ladder makes `probeWithRetry` resolve any\n // throw (throttled or not) to 'unavailable' on the FIRST attempt, with zero\n // retry sleeps, satisfying devtools#767 acceptance criteria 2.\n const backoffArray = pace ? `[${PROBE_RETRY_BACKOFF_MS.join(',')}]` : '[]';\n\n return (\n `(async () => {` +\n ` const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));` +\n ` const isThrottled = (e) => {` +\n ` if (!e) return false;` +\n ` if (e.code === ${JSON.stringify(THROTTLED_ERROR_CODE)}) return true;` +\n ` const msg = typeof e.message === 'string' ? e.message : '';` +\n ` return msg.indexOf(${JSON.stringify(THROTTLED_MESSAGE_SUBSTRING)}) !== -1;` +\n ` };` +\n ` const probe = async (fnName) => {` +\n ` const fn = globalThis.__sdk && globalThis.__sdk[fnName];` +\n ` if (!fn || typeof fn.getPermission !== 'function') return { ok: true, value: 'unavailable' };` +\n ` const status = await fn.getPermission();` +\n ` if (status === 'allowed' || status === 'denied' || status === 'notDetermined') return { ok: true, value: status };` +\n ` return { ok: true, value: 'unavailable' };` +\n ` };` +\n // probeWithRetry: up to PROBE_RETRY_BACKOFF_MS.length retries on THROTTLED,\n // backing off per PROBE_RETRY_BACKOFF_MS. Any OTHER throw (or a\n // non-throttled outcome) resolves to 'unavailable' immediately — only\n // THROTTLED gets a retry.\n ` const backoff = ${backoffArray};` +\n ` const probeWithRetry = async (fnName) => {` +\n ` for (let attempt = 0; attempt <= backoff.length; attempt++) {` +\n ` try {` +\n ` const r = await probe(fnName);` +\n ` return r.value;` +\n ` } catch (e) {` +\n ` if (isThrottled(e) && attempt < backoff.length) {` +\n ` await sleep(backoff[attempt]);` +\n ` continue;` +\n ` }` +\n ` return 'unavailable';` +\n ` }` +\n ` }` +\n ` return 'unavailable';` +\n ` };` +\n ` const result = {};` +\n `${probeCalls}\\n` +\n ` globalThis.__AIT_PERMS__ = result;` +\n ` return JSON.stringify(result);` +\n `})()`\n );\n}\n\n/**\n * Runs the permission-state preflight (devtools#739) via `Runtime.evaluate`\n * and returns the collected `__AIT_PERMS__` map, or `undefined` when the\n * preflight itself failed or timed out.\n *\n * NON-FATAL by design: any failure (CDP exception, timeout, page-side throw)\n * is caught here, logged as ONE stderr line, and swallowed — the whole test\n * run must proceed even when the preflight cannot complete (e.g. `window.__sdk`\n * not yet installed, or a page navigation mid-attach). Callers that want the\n * result for report provenance (`RelayRunReport.preflight`) get `undefined`\n * in that case, which downstream code treats the same as \"not run\".\n *\n * SECRET-HANDLING: no relay/wss/secret values are read or logged; only\n * permission-state strings cross this boundary.\n *\n * @param conn - The live CDP connection (relay-attached page).\n * @param timeoutMs - Round-trip bound. Defaults to {@link PERMISSION_PREFLIGHT_TIMEOUT_MS}.\n * @param pace - Forwarded to {@link buildPermissionPreflightExpression}.\n * Defaults to `true` (today's sequential+spacing+backoff behavior, correct\n * for 2.x/unknown cells). Callers that know the cell is 3.x should pass\n * `false` — devtools#767 acceptance criteria 2 requires 3.x run duration not\n * grow from this preflight, and 3.x never hits the 2.x-only\n * `APP_BRIDGE_THROTTLED` limiter this pacing exists to dodge.\n */\nexport async function runPermissionPreflight(\n conn: CdpConnection,\n timeoutMs = PERMISSION_PREFLIGHT_TIMEOUT_MS,\n pace = true,\n): Promise<Record<string, string> | undefined> {\n const TIMEOUT_SENTINEL = Symbol('permission-preflight-timeout');\n const timeoutPromise = new Promise<typeof TIMEOUT_SENTINEL>((resolve) =>\n setTimeout(() => resolve(TIMEOUT_SENTINEL), timeoutMs),\n );\n\n try {\n const evalPromise = conn.send('Runtime.evaluate', {\n expression: buildPermissionPreflightExpression(pace),\n returnByValue: true,\n awaitPromise: true,\n });\n\n const raceResult = await Promise.race([\n evalPromise.then((v) => ({ tag: 'eval' as const, v })),\n timeoutPromise.then(() => ({ tag: 'timeout' as const })),\n ]);\n\n if (raceResult.tag === 'timeout') {\n process.stderr.write(\n `test-runner: permission preflight timed out after ${timeoutMs}ms — proceeding with __AIT_PERMS__ unset\\n`,\n );\n return undefined;\n }\n\n if (raceResult.v.exceptionDetails) {\n process.stderr.write(\n 'test-runner: permission preflight threw — proceeding with __AIT_PERMS__ unset\\n',\n );\n return undefined;\n }\n\n const rawValue = raceResult.v.result.value;\n if (typeof rawValue !== 'string') return undefined;\n const parsed: unknown = JSON.parse(rawValue);\n if (typeof parsed !== 'object' || parsed === null) return undefined;\n return parsed as Record<string, string>;\n } catch (e) {\n process.stderr.write(\n `test-runner: permission preflight failed (${\n e instanceof Error ? e.message : String(e)\n }) — proceeding with __AIT_PERMS__ unset\\n`,\n );\n return undefined;\n }\n}\n\nimport { buildIndicatorExpression } from '../mcp/attach-orchestrator.js';\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\n\n/**\n * Injects each key of `globals` into `globalThis` in the page via a single\n * `Runtime.evaluate` call. Must be called BEFORE the first `bundleTestFile`\n * inject — the cell is session-global and applies to all subsequent files.\n *\n * Throws if the CDP evaluate returns an exception.\n *\n * @param conn - The live CDP connection (relay-attached page).\n * @param globals - Plain-JSON-serialisable key→value map to assign onto `globalThis`.\n */\nexport async function injectGlobals(\n conn: CdpConnection,\n globals: Record<string, unknown>,\n): Promise<void> {\n // JSON.stringify is safe here: globals values are plain data (sdkLine/platform\n // strings or simple objects). If a value is not JSON-serialisable this throws\n // at call time, which surfaces the error early and clearly.\n const expr = `(() => { Object.assign(globalThis, ${JSON.stringify(globals)}); return true; })()`;\n const result = await conn.send('Runtime.evaluate', { expression: expr, returnByValue: true });\n if (result.exceptionDetails) {\n const msg =\n result.exceptionDetails.exception?.description ??\n result.exceptionDetails.text ??\n 'unknown error';\n throw new Error(`injectGlobals: Runtime.evaluate threw: ${msg}`);\n }\n}\n\n/**\n * Injects (or updates) the \"Debugger Connected\"/\"Debugger Disconnected\"\n * on-phone indicator via `Runtime.evaluate` (#730).\n *\n * Uses the same CDP mechanism as {@link injectGlobals} — a single\n * `Runtime.evaluate` round-trip with the expression built by\n * {@link buildIndicatorExpression}. The indicator is a LIVE badge rendered at\n * the bottom-left of the page (position:fixed, safe-area-aware): the badge's\n * controller is idempotent, so calling this again (e.g. right before\n * `close()` with `{ state: 'disconnected' }`) UPDATES the existing badge in\n * place instead of injecting a duplicate.\n *\n * **Isolation**: unlike {@link injectGlobals}, this function NEVER throws to\n * its caller. Injection failure (e.g. page detached during inject, CSS not\n * supported) is swallowed and logged as `console.debug` — the badge is\n * informational UI and must never block attach success or test execution.\n * This is the same fire-and-forget spirit as the in-page console mount in\n * `src/in-app/attach.ts`.\n *\n * Call this ONLY on the manual debug paths (start_attach MCP, devtools-test\n * CLI). Do NOT call on the `run_tests` auto-attach path — the badge\n * would contaminate screenshots, measure_safe_area probes, and DOM snapshots\n * taken during automated measurement runs.\n *\n * SECRET-HANDLING: the injected expression contains no secrets, relay URLs,\n * wss addresses, or TOTP codes — it is pure DOM UI text + enum state built by\n * {@link buildIndicatorExpression}.\n *\n * @param conn - The live CDP connection (relay-attached page).\n * @param opts - Optional overrides forwarded to {@link buildIndicatorExpression}.\n */\nexport async function injectDebugIndicator(\n conn: CdpConnection,\n opts?: { label?: string; disconnectedLabel?: string; state?: 'attached' | 'disconnected' },\n): Promise<void> {\n try {\n await conn.send('Runtime.evaluate', {\n expression: buildIndicatorExpression(opts),\n returnByValue: true,\n });\n } catch (err) {\n // Badge injection is informational UI — swallow and log; never propagate.\n console.debug('[@ait-co/devtools] debug indicator inject skipped:', err);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,MAAM,uBAA+C;CACnD,eAAe;CACf,gBAAgB;CAChB,OAAO;CACP,QAAQ;CACR,UAAU;CACV,UAAU;CACX;;;;;;;;;;;;;;AAeD,MAAM,4BAA4B;;;;;;;;;;AAWlC,MAAM,yBAAyB,CAAC,KAAK,IAAK;;;;;;;;;;;;;AAc1C,MAAa,kCAAkC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsD/C,SAAgB,mCAAmC,OAAO,MAAc;CAEtE,MAAM,aADY,OAAO,KAAK,qBAAqB,CAEhD,KAAK,KAAK,MAAM;EACf,MAAM,SAAS,qBAAqB;AAKpC,SACE,GAFc,MAAM,KAAK,CAAC,OAAO,KAAK,mBAAmB,0BAA0B,MAAA,aAGrE,KAAK,UAAU,IAAI,CAAC,2BAA2B,KAAK,UAAU,OAAO,CAAC;GAEtF,CACD,KAAK,KAAK;CAMb,MAAM,eAAe,OAAO,IAAI,uBAAuB,KAAK,IAAI,CAAC,KAAK;AAEtE,QACE,qKAIsB,KAAK,UAAU,qBAAqB,CAAC,sGAEjC,KAAK,UAAU,4BAA4B,CAAC,qbAajD,aAAa,kaAiB/B,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BlB,eAAsB,uBACpB,MACA,YAAY,iCACZ,OAAO,MACsC;CAC7C,MAAM,mBAAmB,OAAO,+BAA+B;CAC/D,MAAM,iBAAiB,IAAI,SAAkC,YAC3D,iBAAiB,QAAQ,iBAAiB,EAAE,UAAU,CACvD;AAED,KAAI;EACF,MAAM,cAAc,KAAK,KAAK,oBAAoB;GAChD,YAAY,mCAAmC,KAAK;GACpD,eAAe;GACf,cAAc;GACf,CAAC;EAEF,MAAM,aAAa,MAAM,QAAQ,KAAK,CACpC,YAAY,MAAM,OAAO;GAAE,KAAK;GAAiB;GAAG,EAAE,EACtD,eAAe,YAAY,EAAE,KAAK,WAAoB,EAAE,CACzD,CAAC;AAEF,MAAI,WAAW,QAAQ,WAAW;AAChC,WAAQ,OAAO,MACb,qDAAqD,UAAU,4CAChE;AACD;;AAGF,MAAI,WAAW,EAAE,kBAAkB;AACjC,WAAQ,OAAO,MACb,kFACD;AACD;;EAGF,MAAM,WAAW,WAAW,EAAE,OAAO;AACrC,MAAI,OAAO,aAAa,SAAU,QAAO,KAAA;EACzC,MAAM,SAAkB,KAAK,MAAM,SAAS;AAC5C,MAAI,OAAO,WAAW,YAAY,WAAW,KAAM,QAAO,KAAA;AAC1D,SAAO;UACA,GAAG;AACV,UAAQ,OAAO,MACb,6CACE,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,CAC3C,2CACF;AACD;;;;;;;;;;;;;AAiBJ,eAAsB,cACpB,MACA,SACe;CAIf,MAAM,OAAO,sCAAsC,KAAK,UAAU,QAAQ,CAAC;CAC3E,MAAM,SAAS,MAAM,KAAK,KAAK,oBAAoB;EAAE,YAAY;EAAM,eAAe;EAAM,CAAC;AAC7F,KAAI,OAAO,kBAAkB;EAC3B,MAAM,MACJ,OAAO,iBAAiB,WAAW,eACnC,OAAO,iBAAiB,QACxB;AACF,QAAM,IAAI,MAAM,0CAA0C,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCpE,eAAsB,qBACpB,MACA,MACe;AACf,KAAI;AACF,QAAM,KAAK,KAAK,oBAAoB;GAClC,YAAY,yBAAyB,KAAK;GAC1C,eAAe;GAChB,CAAC;UACK,KAAK;AAEZ,UAAQ,MAAM,sDAAsD,IAAI"}
|
|
1
|
+
{"version":3,"file":"cell-CBUS3-nT.js","names":[],"sources":["../src/test-runner/cell.ts"],"sourcesContent":["/**\n * Cell injection utility for the `devtools-test` CLI (issue #684 §4.1).\n *\n * Injects arbitrary globals into the page via `Runtime.evaluate` BEFORE the\n * first test bundle is injected. The injected values are session-global — one\n * call covers all files in the run.\n *\n * The primary use-case is injecting `__AIT_CELL__` (sdkLine/platform) so\n * sdk-example's `aitCapture.ts` picks up the correct test-axis values instead\n * of falling back to `'2.x'`/`'mock'`.\n *\n * devtools does NOT know the shape of `__AIT_CELL__` — it only provides the\n * general injection mechanism. The caller (CLI or MCP auto-attach path) is\n * responsible for constructing the cell object.\n *\n * SECRET-HANDLING: cell values (sdkLine/platform) are not secrets and may be\n * logged at the caller's discretion. This module does NOT log them itself.\n *\n * Node-only. react-free. CdpConnection only.\n */\n\nimport { THROTTLED_ERROR_CODE, THROTTLED_MESSAGE_SUBSTRING } from './throttle.js';\n\n/**\n * `__AIT_PERMS__` key → SDK function name whose `.getPermission()` is probed\n * (devtools#739). This is the exact contract sdk-example#265 consumes — do\n * not rename keys without a coordinated sdk-example update.\n *\n * Each SDK function is reached via `window.__sdk` — the SAME page-global\n * bridge `src/in-app/auto.ts` installs for `call_sdk`/`__sdkCall` (see\n * `src/test-runner/bundle.ts`'s `sdkRedirectPlugin`, which redirects bundled\n * `@apps-in-toss/web-framework` imports to this same object at runtime). The\n * test-runner bundle owns no independent SDK import — `window.__sdk` is a\n * page global regardless of whether a bundle has been injected yet, so this\n * preflight runs standalone via `injectGlobals`'s `Runtime.evaluate`\n * mechanism BEFORE the first bundle, rather than being prepended into\n * `rpc.ts#buildRunTestsExpression`. (Design choice documented in PR #739 —\n * bundle-prepend was the fallback plan if the SDK were only reachable from\n * inside bundle scope, which reality ruled out.)\n */\nconst PERMISSION_PROBE_MAP: Record<string, string> = {\n clipboardRead: 'getClipboardText',\n clipboardWrite: 'setClipboardText',\n album: 'fetchAlbumPhotos',\n camera: 'openCamera',\n contacts: 'fetchContacts',\n location: 'getCurrentLocation',\n};\n\n/**\n * Inter-probe spacing (ms) applied BEFORE every probe after the first\n * (devtools#767). The native per-method bridge rate limit observed\n * 2026-07-10 rejects a SECOND call to the same method within a short window\n * with `APP_BRIDGE_THROTTLED` — the permission preflight's un-paced burst\n * (6 probes fired back-to-back) was the FIRST trigger of that limiter in the\n * wild (only the first probe passed; the remaining 5 came back throttled →\n * `unavailable`, cascading into skipped `__AIT_PERMS__`-branching tests).\n * Sequential-with-spacing is cheap insurance even though each probe targets a\n * DIFFERENT method — the limiter's exact scope (per-method vs. a shared\n * bridge-wide window) was not reverse-engineered (explicitly out of scope,\n * devtools#767) so spacing every probe is the conservative choice.\n */\nconst PROBE_INTER_CALL_DELAY_MS = 250;\n\n/**\n * Backoff delays (ms) between retry attempts when a probe is throttled. Its\n * `.length` IS the max retry count (devtools#767: \"최대 2회 재시도\") — the\n * generated page-side expression below derives `attempt < backoff.length`\n * from this same array (via `backoffArray`), so there is a single source for\n * both the delay ladder and the retry count; no separate max-retries\n * constant is kept in this module (unlike `runtime.ts`'s `TEST_MAX_RETRIES`,\n * which IS consumed directly by a Node-side loop condition).\n */\nconst PROBE_RETRY_BACKOFF_MS = [500, 1000] as const;\n\n/**\n * Non-fatal bound for the permission preflight `Runtime.evaluate` round-trip\n * (devtools#767: raised from the pre-pacing 10s to cover the new sequential +\n * backoff time budget).\n *\n * Worst-case budget with 6 probes, each requiring the full retry ladder:\n * - inter-probe spacing: only BEFORE probes 2‑6 (none before the first) =\n * 5 × {@link PROBE_INTER_CALL_DELAY_MS} = 1 250ms\n * - per-probe backoff (both retries taken): 6 × (500 + 1000) = 9 000ms\n * - probe round-trips themselves (headroom, not just the sleeps): ~4 500ms\n * total ≈ 14 750ms, rounded up to a clean 20 000ms for headroom.\n */\nexport const PERMISSION_PREFLIGHT_TIMEOUT_MS = 20_000;\n\n/**\n * Builds the `Runtime.evaluate` expression for the permission-state preflight\n * (devtools#739). Evaluated ONCE per relay session, before the first test\n * bundle is injected (see `relay-worker.ts#runTestFilesOverRelay`).\n *\n * For each entry in {@link PERMISSION_PROBE_MAP}, probes\n * `window.__sdk.<fn>.getPermission()` — a NON-blocking query (never\n * `openPermissionDialog`/`requestPermission`, which open native UI) — and\n * assigns the resolved `'allowed'|'denied'|'notDetermined'` status to\n * `globalThis.__AIT_PERMS__[<key>]`. A probe that is absent, throws, or\n * rejects resolves to `'unavailable'` — this expression never throws to its\n * caller, and a single slow/broken permission must not fail the others (each\n * probe is awaited independently, not short-circuited).\n *\n * devtools#767: probes run SEQUENTIALLY (not a parallel burst) with a\n * {@link PROBE_INTER_CALL_DELAY_MS} gap before each probe after the first, and\n * a probe that comes back `APP_BRIDGE_THROTTLED` (native per-method bridge\n * rate limit) is retried up to {@link PROBE_RETRY_BACKOFF_MS}`.length` times\n * with the same {@link PROBE_RETRY_BACKOFF_MS} ladder before falling back to\n * `'unavailable'`.\n * The THROTTLED detection mirrors `throttle.ts#isThrottledError` inline (this\n * expression is raw JS text evaluated via CDP, not an esbuild-bundled module,\n * so it cannot statically `import` that helper) — kept in sync by checking\n * BOTH `e.code === 'APP_BRIDGE_THROTTLED'` and a message substring match, the\n * same two signals `isThrottledError` checks.\n *\n * devtools#767 acceptance criteria 2 (\"3.x run duration은 유의미하게 늘지 않음,\n * pacing은 opt-in 또는 throttle-adaptive\") requires this NOT be an unconditional\n * cost — `pace` (default `true`, matching the pre-existing sequential+spacing\n * behavior for 2.x/unknown cells) lets a 3.x caller skip BOTH the inter-probe\n * `sleep(250)` AND the retry backoff ladder, since `APP_BRIDGE_THROTTLED` is a\n * 2.x-bridge-only limiter (see `--pace`'s own doc in `cli.ts`: \"3.x cells are\n * unaffected by that limiter\"). When `pace` is `false`, probes still run\n * sequentially (never a parallel burst) but back-to-back with no added delay,\n * and a THROTTLED result resolves to `'unavailable'` immediately (no retry) —\n * this matches the 3.x runtime, which never emits `APP_BRIDGE_THROTTLED`.\n *\n * Tests (sdk-example#265) read `globalThis.__AIT_PERMS__` to branch\n * deterministically per permission state instead of blanket outcome-branching.\n *\n * Returns a JSON-serialised string (the same double-serialisation pattern\n * `rpc.ts#buildRunTestsExpression` uses) rather than relying on CDP\n * `returnByValue` structural fidelity for a plain object across the Chii\n * relay — the Node side re-parses via `JSON.parse`.\n *\n * Pure function — no I/O, no CDP call. Callers pass the returned string to\n * `Runtime.evaluate`.\n *\n * @param pace - Defaults to `true` (today's sequential+spacing+backoff\n * behavior). Pass `false` for a 3.x cell to skip the inter-probe delay and\n * the THROTTLED retry ladder entirely.\n */\nexport function buildPermissionPreflightExpression(pace = true): string {\n const probeKeys = Object.keys(PERMISSION_PROBE_MAP);\n const probeCalls = probeKeys\n .map((key, i) => {\n const fnName = PERMISSION_PROBE_MAP[key];\n // Every probe after the first waits PROBE_INTER_CALL_DELAY_MS first —\n // sequential execution with spacing, never a parallel burst. Skipped\n // entirely when `pace` is false (3.x cell — no native limiter to dodge).\n const spacing = i === 0 || !pace ? '' : ` await sleep(${PROBE_INTER_CALL_DELAY_MS});\\n`;\n return (\n `${spacing}` +\n ` result[${JSON.stringify(key)}] = await probeWithRetry(${JSON.stringify(fnName)});`\n );\n })\n .join('\\n');\n\n // 3.x cells never emit APP_BRIDGE_THROTTLED (it's a 2.x-bridge-only native\n // limiter) — an empty backoff ladder makes `probeWithRetry` resolve any\n // throw (throttled or not) to 'unavailable' on the FIRST attempt, with zero\n // retry sleeps, satisfying devtools#767 acceptance criteria 2.\n const backoffArray = pace ? `[${PROBE_RETRY_BACKOFF_MS.join(',')}]` : '[]';\n\n return (\n `(async () => {` +\n ` const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));` +\n ` const isThrottled = (e) => {` +\n ` if (!e) return false;` +\n ` if (e.code === ${JSON.stringify(THROTTLED_ERROR_CODE)}) return true;` +\n ` const msg = typeof e.message === 'string' ? e.message : '';` +\n ` return msg.indexOf(${JSON.stringify(THROTTLED_MESSAGE_SUBSTRING)}) !== -1;` +\n ` };` +\n ` const probe = async (fnName) => {` +\n ` const fn = globalThis.__sdk && globalThis.__sdk[fnName];` +\n ` if (!fn || typeof fn.getPermission !== 'function') return { ok: true, value: 'unavailable' };` +\n ` const status = await fn.getPermission();` +\n ` if (status === 'allowed' || status === 'denied' || status === 'notDetermined') return { ok: true, value: status };` +\n ` return { ok: true, value: 'unavailable' };` +\n ` };` +\n // probeWithRetry: up to PROBE_RETRY_BACKOFF_MS.length retries on THROTTLED,\n // backing off per PROBE_RETRY_BACKOFF_MS. Any OTHER throw (or a\n // non-throttled outcome) resolves to 'unavailable' immediately — only\n // THROTTLED gets a retry.\n ` const backoff = ${backoffArray};` +\n ` const probeWithRetry = async (fnName) => {` +\n ` for (let attempt = 0; attempt <= backoff.length; attempt++) {` +\n ` try {` +\n ` const r = await probe(fnName);` +\n ` return r.value;` +\n ` } catch (e) {` +\n ` if (isThrottled(e) && attempt < backoff.length) {` +\n ` await sleep(backoff[attempt]);` +\n ` continue;` +\n ` }` +\n ` return 'unavailable';` +\n ` }` +\n ` }` +\n ` return 'unavailable';` +\n ` };` +\n ` const result = {};` +\n `${probeCalls}\\n` +\n ` globalThis.__AIT_PERMS__ = result;` +\n ` return JSON.stringify(result);` +\n `})()`\n );\n}\n\n/**\n * Runs the permission-state preflight (devtools#739) via `Runtime.evaluate`\n * and returns the collected `__AIT_PERMS__` map, or `undefined` when the\n * preflight itself failed or timed out.\n *\n * NON-FATAL by design: any failure (CDP exception, timeout, page-side throw)\n * is caught here, logged as ONE stderr line, and swallowed — the whole test\n * run must proceed even when the preflight cannot complete (e.g. `window.__sdk`\n * not yet installed, or a page navigation mid-attach). Callers that want the\n * result for report provenance (`RelayRunReport.preflight`) get `undefined`\n * in that case, which downstream code treats the same as \"not run\".\n *\n * SECRET-HANDLING: no relay/wss/secret values are read or logged; only\n * permission-state strings cross this boundary.\n *\n * @param conn - The live CDP connection (relay-attached page).\n * @param timeoutMs - Round-trip bound. Defaults to {@link PERMISSION_PREFLIGHT_TIMEOUT_MS}.\n * @param pace - Forwarded to {@link buildPermissionPreflightExpression}.\n * Defaults to `true` (today's sequential+spacing+backoff behavior, correct\n * for 2.x/unknown cells). Callers that know the cell is 3.x should pass\n * `false` — devtools#767 acceptance criteria 2 requires 3.x run duration not\n * grow from this preflight, and 3.x never hits the 2.x-only\n * `APP_BRIDGE_THROTTLED` limiter this pacing exists to dodge.\n */\nexport async function runPermissionPreflight(\n conn: CdpConnection,\n timeoutMs = PERMISSION_PREFLIGHT_TIMEOUT_MS,\n pace = true,\n): Promise<Record<string, string> | undefined> {\n const TIMEOUT_SENTINEL = Symbol('permission-preflight-timeout');\n const timeoutPromise = new Promise<typeof TIMEOUT_SENTINEL>((resolve) =>\n setTimeout(() => resolve(TIMEOUT_SENTINEL), timeoutMs),\n );\n\n try {\n const evalPromise = conn.send('Runtime.evaluate', {\n expression: buildPermissionPreflightExpression(pace),\n returnByValue: true,\n awaitPromise: true,\n });\n\n const raceResult = await Promise.race([\n evalPromise.then((v) => ({ tag: 'eval' as const, v })),\n timeoutPromise.then(() => ({ tag: 'timeout' as const })),\n ]);\n\n if (raceResult.tag === 'timeout') {\n process.stderr.write(\n `test-runner: permission preflight timed out after ${timeoutMs}ms — proceeding with __AIT_PERMS__ unset\\n`,\n );\n return undefined;\n }\n\n if (raceResult.v.exceptionDetails) {\n process.stderr.write(\n 'test-runner: permission preflight threw — proceeding with __AIT_PERMS__ unset\\n',\n );\n return undefined;\n }\n\n const rawValue = raceResult.v.result.value;\n if (typeof rawValue !== 'string') return undefined;\n const parsed: unknown = JSON.parse(rawValue);\n if (typeof parsed !== 'object' || parsed === null) return undefined;\n return parsed as Record<string, string>;\n } catch (e) {\n process.stderr.write(\n `test-runner: permission preflight failed (${\n e instanceof Error ? e.message : String(e)\n }) — proceeding with __AIT_PERMS__ unset\\n`,\n );\n return undefined;\n }\n}\n\nimport { buildIndicatorExpression } from '../mcp/attach-orchestrator.js';\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\n\n/**\n * Injects each key of `globals` into `globalThis` in the page via a single\n * `Runtime.evaluate` call. Must be called BEFORE the first `bundleTestFile`\n * inject — the cell is session-global and applies to all subsequent files.\n *\n * Throws if the CDP evaluate returns an exception.\n *\n * @param conn - The live CDP connection (relay-attached page).\n * @param globals - Plain-JSON-serialisable key→value map to assign onto `globalThis`.\n */\nexport async function injectGlobals(\n conn: CdpConnection,\n globals: Record<string, unknown>,\n): Promise<void> {\n // JSON.stringify is safe here: globals values are plain data (sdkLine/platform\n // strings or simple objects). If a value is not JSON-serialisable this throws\n // at call time, which surfaces the error early and clearly.\n const expr = `(() => { Object.assign(globalThis, ${JSON.stringify(globals)}); return true; })()`;\n const result = await conn.send('Runtime.evaluate', { expression: expr, returnByValue: true });\n if (result.exceptionDetails) {\n const msg =\n result.exceptionDetails.exception?.description ??\n result.exceptionDetails.text ??\n 'unknown error';\n throw new Error(`injectGlobals: Runtime.evaluate threw: ${msg}`);\n }\n}\n\n/**\n * Injects (or updates) the \"Debugger Connected\"/\"Debugger Disconnected\"\n * on-phone indicator via `Runtime.evaluate` (#730).\n *\n * Uses the same CDP mechanism as {@link injectGlobals} — a single\n * `Runtime.evaluate` round-trip with the expression built by\n * {@link buildIndicatorExpression}. The indicator is a LIVE badge rendered at\n * the bottom-left of the page (position:fixed, safe-area-aware): the badge's\n * controller is idempotent, so calling this again (e.g. right before\n * `close()` with `{ state: 'disconnected' }`) UPDATES the existing badge in\n * place instead of injecting a duplicate.\n *\n * **Isolation**: unlike {@link injectGlobals}, this function NEVER throws to\n * its caller. Injection failure (e.g. page detached during inject, CSS not\n * supported) is swallowed and logged as `console.debug` — the badge is\n * informational UI and must never block attach success or test execution.\n * This is the same fire-and-forget spirit as the in-page console mount in\n * `src/in-app/attach.ts`.\n *\n * Call this ONLY on the manual debug paths (start_attach MCP, devtools-test\n * CLI). Do NOT call on the `run_tests` auto-attach path — the badge\n * would contaminate screenshots, measure_safe_area probes, and DOM snapshots\n * taken during automated measurement runs.\n *\n * SECRET-HANDLING: the injected expression contains no secrets, relay URLs,\n * wss addresses, or TOTP codes — it is pure DOM UI text + enum state built by\n * {@link buildIndicatorExpression}.\n *\n * @param conn - The live CDP connection (relay-attached page).\n * @param opts - Optional overrides forwarded to {@link buildIndicatorExpression}.\n */\nexport async function injectDebugIndicator(\n conn: CdpConnection,\n opts?: { label?: string; disconnectedLabel?: string; state?: 'attached' | 'disconnected' },\n): Promise<void> {\n try {\n await conn.send('Runtime.evaluate', {\n expression: buildIndicatorExpression(opts),\n returnByValue: true,\n });\n } catch (err) {\n // Badge injection is informational UI — swallow and log; never propagate.\n console.debug('[@ait-co/devtools] debug indicator inject skipped:', err);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,MAAM,uBAA+C;CACnD,eAAe;CACf,gBAAgB;CAChB,OAAO;CACP,QAAQ;CACR,UAAU;CACV,UAAU;CACX;;;;;;;;;;;;;;AAeD,MAAM,4BAA4B;;;;;;;;;;AAWlC,MAAM,yBAAyB,CAAC,KAAK,IAAK;;;;;;;;;;;;;AAc1C,MAAa,kCAAkC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsD/C,SAAgB,mCAAmC,OAAO,MAAc;CAEtE,MAAM,aADY,OAAO,KAAK,qBAAqB,CAEhD,KAAK,KAAK,MAAM;EACf,MAAM,SAAS,qBAAqB;AAKpC,SACE,GAFc,MAAM,KAAK,CAAC,OAAO,KAAK,mBAAmB,0BAA0B,MAAA,aAGrE,KAAK,UAAU,IAAI,CAAC,2BAA2B,KAAK,UAAU,OAAO,CAAC;GAEtF,CACD,KAAK,KAAK;CAMb,MAAM,eAAe,OAAO,IAAI,uBAAuB,KAAK,IAAI,CAAC,KAAK;AAEtE,QACE,qKAIsB,KAAK,UAAU,qBAAqB,CAAC,sGAEjC,KAAK,UAAU,4BAA4B,CAAC,qbAajD,aAAa,kaAiB/B,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BlB,eAAsB,uBACpB,MACA,YAAY,iCACZ,OAAO,MACsC;CAC7C,MAAM,mBAAmB,OAAO,+BAA+B;CAC/D,MAAM,iBAAiB,IAAI,SAAkC,YAC3D,iBAAiB,QAAQ,iBAAiB,EAAE,UAAU,CACvD;AAED,KAAI;EACF,MAAM,cAAc,KAAK,KAAK,oBAAoB;GAChD,YAAY,mCAAmC,KAAK;GACpD,eAAe;GACf,cAAc;GACf,CAAC;EAEF,MAAM,aAAa,MAAM,QAAQ,KAAK,CACpC,YAAY,MAAM,OAAO;GAAE,KAAK;GAAiB;GAAG,EAAE,EACtD,eAAe,YAAY,EAAE,KAAK,WAAoB,EAAE,CACzD,CAAC;AAEF,MAAI,WAAW,QAAQ,WAAW;AAChC,WAAQ,OAAO,MACb,qDAAqD,UAAU,4CAChE;AACD;;AAGF,MAAI,WAAW,EAAE,kBAAkB;AACjC,WAAQ,OAAO,MACb,kFACD;AACD;;EAGF,MAAM,WAAW,WAAW,EAAE,OAAO;AACrC,MAAI,OAAO,aAAa,SAAU,QAAO,KAAA;EACzC,MAAM,SAAkB,KAAK,MAAM,SAAS;AAC5C,MAAI,OAAO,WAAW,YAAY,WAAW,KAAM,QAAO,KAAA;AAC1D,SAAO;UACA,GAAG;AACV,UAAQ,OAAO,MACb,6CACE,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,CAC3C,2CACF;AACD;;;;;;;;;;;;;AAiBJ,eAAsB,cACpB,MACA,SACe;CAIf,MAAM,OAAO,sCAAsC,KAAK,UAAU,QAAQ,CAAC;CAC3E,MAAM,SAAS,MAAM,KAAK,KAAK,oBAAoB;EAAE,YAAY;EAAM,eAAe;EAAM,CAAC;AAC7F,KAAI,OAAO,kBAAkB;EAC3B,MAAM,MACJ,OAAO,iBAAiB,WAAW,eACnC,OAAO,iBAAiB,QACxB;AACF,QAAM,IAAI,MAAM,0CAA0C,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCpE,eAAsB,qBACpB,MACA,MACe;AACf,KAAI;AACF,QAAM,KAAK,KAAK,oBAAoB;GAClC,YAAY,yBAAyB,KAAK;GAC1C,eAAe;GAChB,CAAC;UACK,KAAK;AAEZ,UAAQ,MAAM,sDAAsD,IAAI"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as __exportAll } from "./rolldown-runtime-DGkTqVfb.js";
|
|
3
|
-
import { a as buildIndicatorExpression } from "./attach-orchestrator-
|
|
3
|
+
import { a as buildIndicatorExpression } from "./attach-orchestrator-DL3NQ9ca.js";
|
|
4
4
|
//#region src/test-runner/throttle.ts
|
|
5
5
|
/**
|
|
6
6
|
* Shared `APP_BRIDGE_THROTTLED` detection (devtools#767).
|
|
@@ -304,4 +304,4 @@ async function injectDebugIndicator(conn, opts) {
|
|
|
304
304
|
//#endregion
|
|
305
305
|
export { cell_exports as n, runPermissionPreflight as r, PERMISSION_PREFLIGHT_TIMEOUT_MS as t };
|
|
306
306
|
|
|
307
|
-
//# sourceMappingURL=cell-
|
|
307
|
+
//# sourceMappingURL=cell-EBKKpAAT.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell-KsRXFdDs.js","names":[],"sources":["../src/test-runner/throttle.ts","../src/test-runner/cell.ts"],"sourcesContent":["/**\n * Shared `APP_BRIDGE_THROTTLED` detection (devtools#767).\n *\n * 2026-07-10 real-device observation: the Toss app native side added a\n * per-method rate limit to the 2.x bridge — calling the SAME bridge method\n * twice within a short window rejects the second call immediately with a\n * 2.x native envelope (`{name, code, userInfo, moduleName, __isError}`) whose\n * `code` is `'APP_BRIDGE_THROTTLED'` and whose `message` reads\n * `\"Too many app bridge calls from <method>.\"`. The 3.x bridge path is\n * unaffected (devtools#767 issue body, sdk-example#284 2026-07-10 comment).\n *\n * This module is the SINGLE source of the detection predicate. Today's only\n * consumer is `runtime.ts` (in-page, per-test retry loop) — a plain static\n * import; esbuild inlines this file into the same IIFE bundle (same\n * mechanism as `bridge-stub.ts`, see `bundle.ts#getPageSideModulePath`). It\n * is exported from this standalone, dependency-free module (rather than\n * defined inline in `runtime.ts`) so a Node-side consumer could import the\n * exact same predicate in future if whole-FILE throttle-aware retry is ever\n * added on top of the per-test retry `runtime.ts` already performs — no such\n * Node-side caller exists yet (whole-file retry today only triggers on an\n * `injectAndRunBundle`-level timeout/exception, a distinct failure mode from\n * a `RunReport` that completed normally but contains a THROTTLED-failed\n * test — see `relay-worker.ts`'s `EVALUATE_TIMEOUT_MARKER` gate).\n *\n * Browser-compatible, dependency-free (no Node APIs) — safe to import from\n * either side of the CDP boundary, mirroring `runtime.ts`'s own constraint.\n */\n\n/** The native error code the Toss app bridge returns when rate-limited. */\nexport const THROTTLED_ERROR_CODE = 'APP_BRIDGE_THROTTLED';\n\n/** The message substring the native envelope carries when rate-limited. */\nexport const THROTTLED_MESSAGE_SUBSTRING = 'Too many app bridge calls';\n\n/**\n * True when `err` looks like a native `APP_BRIDGE_THROTTLED` rejection —\n * either the 2.x native envelope's `code` field equals\n * {@link THROTTLED_ERROR_CODE}, or the error's message contains\n * {@link THROTTLED_MESSAGE_SUBSTRING}. Either signal alone is sufficient: the\n * `code` field is the precise native signal, but a caller may only have a\n * stringified message (e.g. `Error#message` after the native envelope was\n * flattened) — checking both keeps detection robust across both shapes.\n *\n * Accepts `unknown` because callers may pass a caught page-side exception\n * (whose shape is not statically known), a `TestResult.error` string, or a\n * `FileResult` error string — never throws.\n */\nexport function isThrottledError(err: unknown): boolean {\n if (err === null || err === undefined) return false;\n\n if (typeof err === 'string') {\n return err.includes(THROTTLED_MESSAGE_SUBSTRING);\n }\n\n if (typeof err === 'object') {\n const rec = err as { code?: unknown; message?: unknown };\n if (rec.code === THROTTLED_ERROR_CODE) return true;\n if (typeof rec.message === 'string' && rec.message.includes(THROTTLED_MESSAGE_SUBSTRING)) {\n return true;\n }\n }\n\n return false;\n}\n","/**\n * Cell injection utility for the `devtools-test` CLI (issue #684 §4.1).\n *\n * Injects arbitrary globals into the page via `Runtime.evaluate` BEFORE the\n * first test bundle is injected. The injected values are session-global — one\n * call covers all files in the run.\n *\n * The primary use-case is injecting `__AIT_CELL__` (sdkLine/platform) so\n * sdk-example's `aitCapture.ts` picks up the correct test-axis values instead\n * of falling back to `'2.x'`/`'mock'`.\n *\n * devtools does NOT know the shape of `__AIT_CELL__` — it only provides the\n * general injection mechanism. The caller (CLI or MCP auto-attach path) is\n * responsible for constructing the cell object.\n *\n * SECRET-HANDLING: cell values (sdkLine/platform) are not secrets and may be\n * logged at the caller's discretion. This module does NOT log them itself.\n *\n * Node-only. react-free. CdpConnection only.\n */\n\nimport { THROTTLED_ERROR_CODE, THROTTLED_MESSAGE_SUBSTRING } from './throttle.js';\n\n/**\n * `__AIT_PERMS__` key → SDK function name whose `.getPermission()` is probed\n * (devtools#739). This is the exact contract sdk-example#265 consumes — do\n * not rename keys without a coordinated sdk-example update.\n *\n * Each SDK function is reached via `window.__sdk` — the SAME page-global\n * bridge `src/in-app/auto.ts` installs for `call_sdk`/`__sdkCall` (see\n * `src/test-runner/bundle.ts`'s `sdkRedirectPlugin`, which redirects bundled\n * `@apps-in-toss/web-framework` imports to this same object at runtime). The\n * test-runner bundle owns no independent SDK import — `window.__sdk` is a\n * page global regardless of whether a bundle has been injected yet, so this\n * preflight runs standalone via `injectGlobals`'s `Runtime.evaluate`\n * mechanism BEFORE the first bundle, rather than being prepended into\n * `rpc.ts#buildRunTestsExpression`. (Design choice documented in PR #739 —\n * bundle-prepend was the fallback plan if the SDK were only reachable from\n * inside bundle scope, which reality ruled out.)\n */\nconst PERMISSION_PROBE_MAP: Record<string, string> = {\n clipboardRead: 'getClipboardText',\n clipboardWrite: 'setClipboardText',\n album: 'fetchAlbumPhotos',\n camera: 'openCamera',\n contacts: 'fetchContacts',\n location: 'getCurrentLocation',\n};\n\n/**\n * Inter-probe spacing (ms) applied BEFORE every probe after the first\n * (devtools#767). The native per-method bridge rate limit observed\n * 2026-07-10 rejects a SECOND call to the same method within a short window\n * with `APP_BRIDGE_THROTTLED` — the permission preflight's un-paced burst\n * (6 probes fired back-to-back) was the FIRST trigger of that limiter in the\n * wild (only the first probe passed; the remaining 5 came back throttled →\n * `unavailable`, cascading into skipped `__AIT_PERMS__`-branching tests).\n * Sequential-with-spacing is cheap insurance even though each probe targets a\n * DIFFERENT method — the limiter's exact scope (per-method vs. a shared\n * bridge-wide window) was not reverse-engineered (explicitly out of scope,\n * devtools#767) so spacing every probe is the conservative choice.\n */\nconst PROBE_INTER_CALL_DELAY_MS = 250;\n\n/**\n * Backoff delays (ms) between retry attempts when a probe is throttled. Its\n * `.length` IS the max retry count (devtools#767: \"최대 2회 재시도\") — the\n * generated page-side expression below derives `attempt < backoff.length`\n * from this same array (via `backoffArray`), so there is a single source for\n * both the delay ladder and the retry count; no separate max-retries\n * constant is kept in this module (unlike `runtime.ts`'s `TEST_MAX_RETRIES`,\n * which IS consumed directly by a Node-side loop condition).\n */\nconst PROBE_RETRY_BACKOFF_MS = [500, 1000] as const;\n\n/**\n * Non-fatal bound for the permission preflight `Runtime.evaluate` round-trip\n * (devtools#767: raised from the pre-pacing 10s to cover the new sequential +\n * backoff time budget).\n *\n * Worst-case budget with 6 probes, each requiring the full retry ladder:\n * - inter-probe spacing: only BEFORE probes 2‑6 (none before the first) =\n * 5 × {@link PROBE_INTER_CALL_DELAY_MS} = 1 250ms\n * - per-probe backoff (both retries taken): 6 × (500 + 1000) = 9 000ms\n * - probe round-trips themselves (headroom, not just the sleeps): ~4 500ms\n * total ≈ 14 750ms, rounded up to a clean 20 000ms for headroom.\n */\nexport const PERMISSION_PREFLIGHT_TIMEOUT_MS = 20_000;\n\n/**\n * Builds the `Runtime.evaluate` expression for the permission-state preflight\n * (devtools#739). Evaluated ONCE per relay session, before the first test\n * bundle is injected (see `relay-worker.ts#runTestFilesOverRelay`).\n *\n * For each entry in {@link PERMISSION_PROBE_MAP}, probes\n * `window.__sdk.<fn>.getPermission()` — a NON-blocking query (never\n * `openPermissionDialog`/`requestPermission`, which open native UI) — and\n * assigns the resolved `'allowed'|'denied'|'notDetermined'` status to\n * `globalThis.__AIT_PERMS__[<key>]`. A probe that is absent, throws, or\n * rejects resolves to `'unavailable'` — this expression never throws to its\n * caller, and a single slow/broken permission must not fail the others (each\n * probe is awaited independently, not short-circuited).\n *\n * devtools#767: probes run SEQUENTIALLY (not a parallel burst) with a\n * {@link PROBE_INTER_CALL_DELAY_MS} gap before each probe after the first, and\n * a probe that comes back `APP_BRIDGE_THROTTLED` (native per-method bridge\n * rate limit) is retried up to {@link PROBE_RETRY_BACKOFF_MS}`.length` times\n * with the same {@link PROBE_RETRY_BACKOFF_MS} ladder before falling back to\n * `'unavailable'`.\n * The THROTTLED detection mirrors `throttle.ts#isThrottledError` inline (this\n * expression is raw JS text evaluated via CDP, not an esbuild-bundled module,\n * so it cannot statically `import` that helper) — kept in sync by checking\n * BOTH `e.code === 'APP_BRIDGE_THROTTLED'` and a message substring match, the\n * same two signals `isThrottledError` checks.\n *\n * devtools#767 acceptance criteria 2 (\"3.x run duration은 유의미하게 늘지 않음,\n * pacing은 opt-in 또는 throttle-adaptive\") requires this NOT be an unconditional\n * cost — `pace` (default `true`, matching the pre-existing sequential+spacing\n * behavior for 2.x/unknown cells) lets a 3.x caller skip BOTH the inter-probe\n * `sleep(250)` AND the retry backoff ladder, since `APP_BRIDGE_THROTTLED` is a\n * 2.x-bridge-only limiter (see `--pace`'s own doc in `cli.ts`: \"3.x cells are\n * unaffected by that limiter\"). When `pace` is `false`, probes still run\n * sequentially (never a parallel burst) but back-to-back with no added delay,\n * and a THROTTLED result resolves to `'unavailable'` immediately (no retry) —\n * this matches the 3.x runtime, which never emits `APP_BRIDGE_THROTTLED`.\n *\n * Tests (sdk-example#265) read `globalThis.__AIT_PERMS__` to branch\n * deterministically per permission state instead of blanket outcome-branching.\n *\n * Returns a JSON-serialised string (the same double-serialisation pattern\n * `rpc.ts#buildRunTestsExpression` uses) rather than relying on CDP\n * `returnByValue` structural fidelity for a plain object across the Chii\n * relay — the Node side re-parses via `JSON.parse`.\n *\n * Pure function — no I/O, no CDP call. Callers pass the returned string to\n * `Runtime.evaluate`.\n *\n * @param pace - Defaults to `true` (today's sequential+spacing+backoff\n * behavior). Pass `false` for a 3.x cell to skip the inter-probe delay and\n * the THROTTLED retry ladder entirely.\n */\nexport function buildPermissionPreflightExpression(pace = true): string {\n const probeKeys = Object.keys(PERMISSION_PROBE_MAP);\n const probeCalls = probeKeys\n .map((key, i) => {\n const fnName = PERMISSION_PROBE_MAP[key];\n // Every probe after the first waits PROBE_INTER_CALL_DELAY_MS first —\n // sequential execution with spacing, never a parallel burst. Skipped\n // entirely when `pace` is false (3.x cell — no native limiter to dodge).\n const spacing = i === 0 || !pace ? '' : ` await sleep(${PROBE_INTER_CALL_DELAY_MS});\\n`;\n return (\n `${spacing}` +\n ` result[${JSON.stringify(key)}] = await probeWithRetry(${JSON.stringify(fnName)});`\n );\n })\n .join('\\n');\n\n // 3.x cells never emit APP_BRIDGE_THROTTLED (it's a 2.x-bridge-only native\n // limiter) — an empty backoff ladder makes `probeWithRetry` resolve any\n // throw (throttled or not) to 'unavailable' on the FIRST attempt, with zero\n // retry sleeps, satisfying devtools#767 acceptance criteria 2.\n const backoffArray = pace ? `[${PROBE_RETRY_BACKOFF_MS.join(',')}]` : '[]';\n\n return (\n `(async () => {` +\n ` const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));` +\n ` const isThrottled = (e) => {` +\n ` if (!e) return false;` +\n ` if (e.code === ${JSON.stringify(THROTTLED_ERROR_CODE)}) return true;` +\n ` const msg = typeof e.message === 'string' ? e.message : '';` +\n ` return msg.indexOf(${JSON.stringify(THROTTLED_MESSAGE_SUBSTRING)}) !== -1;` +\n ` };` +\n ` const probe = async (fnName) => {` +\n ` const fn = globalThis.__sdk && globalThis.__sdk[fnName];` +\n ` if (!fn || typeof fn.getPermission !== 'function') return { ok: true, value: 'unavailable' };` +\n ` const status = await fn.getPermission();` +\n ` if (status === 'allowed' || status === 'denied' || status === 'notDetermined') return { ok: true, value: status };` +\n ` return { ok: true, value: 'unavailable' };` +\n ` };` +\n // probeWithRetry: up to PROBE_RETRY_BACKOFF_MS.length retries on THROTTLED,\n // backing off per PROBE_RETRY_BACKOFF_MS. Any OTHER throw (or a\n // non-throttled outcome) resolves to 'unavailable' immediately — only\n // THROTTLED gets a retry.\n ` const backoff = ${backoffArray};` +\n ` const probeWithRetry = async (fnName) => {` +\n ` for (let attempt = 0; attempt <= backoff.length; attempt++) {` +\n ` try {` +\n ` const r = await probe(fnName);` +\n ` return r.value;` +\n ` } catch (e) {` +\n ` if (isThrottled(e) && attempt < backoff.length) {` +\n ` await sleep(backoff[attempt]);` +\n ` continue;` +\n ` }` +\n ` return 'unavailable';` +\n ` }` +\n ` }` +\n ` return 'unavailable';` +\n ` };` +\n ` const result = {};` +\n `${probeCalls}\\n` +\n ` globalThis.__AIT_PERMS__ = result;` +\n ` return JSON.stringify(result);` +\n `})()`\n );\n}\n\n/**\n * Runs the permission-state preflight (devtools#739) via `Runtime.evaluate`\n * and returns the collected `__AIT_PERMS__` map, or `undefined` when the\n * preflight itself failed or timed out.\n *\n * NON-FATAL by design: any failure (CDP exception, timeout, page-side throw)\n * is caught here, logged as ONE stderr line, and swallowed — the whole test\n * run must proceed even when the preflight cannot complete (e.g. `window.__sdk`\n * not yet installed, or a page navigation mid-attach). Callers that want the\n * result for report provenance (`RelayRunReport.preflight`) get `undefined`\n * in that case, which downstream code treats the same as \"not run\".\n *\n * SECRET-HANDLING: no relay/wss/secret values are read or logged; only\n * permission-state strings cross this boundary.\n *\n * @param conn - The live CDP connection (relay-attached page).\n * @param timeoutMs - Round-trip bound. Defaults to {@link PERMISSION_PREFLIGHT_TIMEOUT_MS}.\n * @param pace - Forwarded to {@link buildPermissionPreflightExpression}.\n * Defaults to `true` (today's sequential+spacing+backoff behavior, correct\n * for 2.x/unknown cells). Callers that know the cell is 3.x should pass\n * `false` — devtools#767 acceptance criteria 2 requires 3.x run duration not\n * grow from this preflight, and 3.x never hits the 2.x-only\n * `APP_BRIDGE_THROTTLED` limiter this pacing exists to dodge.\n */\nexport async function runPermissionPreflight(\n conn: CdpConnection,\n timeoutMs = PERMISSION_PREFLIGHT_TIMEOUT_MS,\n pace = true,\n): Promise<Record<string, string> | undefined> {\n const TIMEOUT_SENTINEL = Symbol('permission-preflight-timeout');\n const timeoutPromise = new Promise<typeof TIMEOUT_SENTINEL>((resolve) =>\n setTimeout(() => resolve(TIMEOUT_SENTINEL), timeoutMs),\n );\n\n try {\n const evalPromise = conn.send('Runtime.evaluate', {\n expression: buildPermissionPreflightExpression(pace),\n returnByValue: true,\n awaitPromise: true,\n });\n\n const raceResult = await Promise.race([\n evalPromise.then((v) => ({ tag: 'eval' as const, v })),\n timeoutPromise.then(() => ({ tag: 'timeout' as const })),\n ]);\n\n if (raceResult.tag === 'timeout') {\n process.stderr.write(\n `test-runner: permission preflight timed out after ${timeoutMs}ms — proceeding with __AIT_PERMS__ unset\\n`,\n );\n return undefined;\n }\n\n if (raceResult.v.exceptionDetails) {\n process.stderr.write(\n 'test-runner: permission preflight threw — proceeding with __AIT_PERMS__ unset\\n',\n );\n return undefined;\n }\n\n const rawValue = raceResult.v.result.value;\n if (typeof rawValue !== 'string') return undefined;\n const parsed: unknown = JSON.parse(rawValue);\n if (typeof parsed !== 'object' || parsed === null) return undefined;\n return parsed as Record<string, string>;\n } catch (e) {\n process.stderr.write(\n `test-runner: permission preflight failed (${\n e instanceof Error ? e.message : String(e)\n }) — proceeding with __AIT_PERMS__ unset\\n`,\n );\n return undefined;\n }\n}\n\nimport { buildIndicatorExpression } from '../mcp/attach-orchestrator.js';\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\n\n/**\n * Injects each key of `globals` into `globalThis` in the page via a single\n * `Runtime.evaluate` call. Must be called BEFORE the first `bundleTestFile`\n * inject — the cell is session-global and applies to all subsequent files.\n *\n * Throws if the CDP evaluate returns an exception.\n *\n * @param conn - The live CDP connection (relay-attached page).\n * @param globals - Plain-JSON-serialisable key→value map to assign onto `globalThis`.\n */\nexport async function injectGlobals(\n conn: CdpConnection,\n globals: Record<string, unknown>,\n): Promise<void> {\n // JSON.stringify is safe here: globals values are plain data (sdkLine/platform\n // strings or simple objects). If a value is not JSON-serialisable this throws\n // at call time, which surfaces the error early and clearly.\n const expr = `(() => { Object.assign(globalThis, ${JSON.stringify(globals)}); return true; })()`;\n const result = await conn.send('Runtime.evaluate', { expression: expr, returnByValue: true });\n if (result.exceptionDetails) {\n const msg =\n result.exceptionDetails.exception?.description ??\n result.exceptionDetails.text ??\n 'unknown error';\n throw new Error(`injectGlobals: Runtime.evaluate threw: ${msg}`);\n }\n}\n\n/**\n * Injects (or updates) the \"Debugger Connected\"/\"Debugger Disconnected\"\n * on-phone indicator via `Runtime.evaluate` (#730).\n *\n * Uses the same CDP mechanism as {@link injectGlobals} — a single\n * `Runtime.evaluate` round-trip with the expression built by\n * {@link buildIndicatorExpression}. The indicator is a LIVE badge rendered at\n * the bottom-left of the page (position:fixed, safe-area-aware): the badge's\n * controller is idempotent, so calling this again (e.g. right before\n * `close()` with `{ state: 'disconnected' }`) UPDATES the existing badge in\n * place instead of injecting a duplicate.\n *\n * **Isolation**: unlike {@link injectGlobals}, this function NEVER throws to\n * its caller. Injection failure (e.g. page detached during inject, CSS not\n * supported) is swallowed and logged as `console.debug` — the badge is\n * informational UI and must never block attach success or test execution.\n * This is the same fire-and-forget spirit as the in-page console mount in\n * `src/in-app/attach.ts`.\n *\n * Call this ONLY on the manual debug paths (start_attach MCP, devtools-test\n * CLI). Do NOT call on the `run_tests` auto-attach path — the badge\n * would contaminate screenshots, measure_safe_area probes, and DOM snapshots\n * taken during automated measurement runs.\n *\n * SECRET-HANDLING: the injected expression contains no secrets, relay URLs,\n * wss addresses, or TOTP codes — it is pure DOM UI text + enum state built by\n * {@link buildIndicatorExpression}.\n *\n * @param conn - The live CDP connection (relay-attached page).\n * @param opts - Optional overrides forwarded to {@link buildIndicatorExpression}.\n */\nexport async function injectDebugIndicator(\n conn: CdpConnection,\n opts?: { label?: string; disconnectedLabel?: string; state?: 'attached' | 'disconnected' },\n): Promise<void> {\n try {\n await conn.send('Runtime.evaluate', {\n expression: buildIndicatorExpression(opts),\n returnByValue: true,\n });\n } catch (err) {\n // Badge injection is informational UI — swallow and log; never propagate.\n console.debug('[@ait-co/devtools] debug indicator inject skipped:', err);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAa,uBAAuB;;AAGpC,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACQ3C,MAAM,uBAA+C;CACnD,eAAe;CACf,gBAAgB;CAChB,OAAO;CACP,QAAQ;CACR,UAAU;CACV,UAAU;CACX;;;;;;;;;;;;;;AAeD,MAAM,4BAA4B;;;;;;;;;;AAWlC,MAAM,yBAAyB,CAAC,KAAK,IAAK;;;;;;;;;;;;;AAc1C,MAAa,kCAAkC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsD/C,SAAgB,mCAAmC,OAAO,MAAc;CAEtE,MAAM,aADY,OAAO,KAAK,qBAAqB,CAEhD,KAAK,KAAK,MAAM;EACf,MAAM,SAAS,qBAAqB;AAKpC,SACE,GAFc,MAAM,KAAK,CAAC,OAAO,KAAK,mBAAmB,0BAA0B,MAAA,aAGrE,KAAK,UAAU,IAAI,CAAC,2BAA2B,KAAK,UAAU,OAAO,CAAC;GAEtF,CACD,KAAK,KAAK;CAMb,MAAM,eAAe,OAAO,IAAI,uBAAuB,KAAK,IAAI,CAAC,KAAK;AAEtE,QACE,qKAIsB,KAAK,UAAU,qBAAqB,CAAC,sGAEjC,KAAK,UAAU,4BAA4B,CAAC,qbAajD,aAAa,kaAiB/B,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BlB,eAAsB,uBACpB,MACA,YAAY,iCACZ,OAAO,MACsC;CAC7C,MAAM,mBAAmB,OAAO,+BAA+B;CAC/D,MAAM,iBAAiB,IAAI,SAAkC,YAC3D,iBAAiB,QAAQ,iBAAiB,EAAE,UAAU,CACvD;AAED,KAAI;EACF,MAAM,cAAc,KAAK,KAAK,oBAAoB;GAChD,YAAY,mCAAmC,KAAK;GACpD,eAAe;GACf,cAAc;GACf,CAAC;EAEF,MAAM,aAAa,MAAM,QAAQ,KAAK,CACpC,YAAY,MAAM,OAAO;GAAE,KAAK;GAAiB;GAAG,EAAE,EACtD,eAAe,YAAY,EAAE,KAAK,WAAoB,EAAE,CACzD,CAAC;AAEF,MAAI,WAAW,QAAQ,WAAW;AAChC,WAAQ,OAAO,MACb,qDAAqD,UAAU,4CAChE;AACD;;AAGF,MAAI,WAAW,EAAE,kBAAkB;AACjC,WAAQ,OAAO,MACb,kFACD;AACD;;EAGF,MAAM,WAAW,WAAW,EAAE,OAAO;AACrC,MAAI,OAAO,aAAa,SAAU,QAAO,KAAA;EACzC,MAAM,SAAkB,KAAK,MAAM,SAAS;AAC5C,MAAI,OAAO,WAAW,YAAY,WAAW,KAAM,QAAO,KAAA;AAC1D,SAAO;UACA,GAAG;AACV,UAAQ,OAAO,MACb,6CACE,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,CAC3C,2CACF;AACD;;;;;;;;;;;;;AAiBJ,eAAsB,cACpB,MACA,SACe;CAIf,MAAM,OAAO,sCAAsC,KAAK,UAAU,QAAQ,CAAC;CAC3E,MAAM,SAAS,MAAM,KAAK,KAAK,oBAAoB;EAAE,YAAY;EAAM,eAAe;EAAM,CAAC;AAC7F,KAAI,OAAO,kBAAkB;EAC3B,MAAM,MACJ,OAAO,iBAAiB,WAAW,eACnC,OAAO,iBAAiB,QACxB;AACF,QAAM,IAAI,MAAM,0CAA0C,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCpE,eAAsB,qBACpB,MACA,MACe;AACf,KAAI;AACF,QAAM,KAAK,KAAK,oBAAoB;GAClC,YAAY,yBAAyB,KAAK;GAC1C,eAAe;GAChB,CAAC;UACK,KAAK;AAEZ,UAAQ,MAAM,sDAAsD,IAAI"}
|
|
1
|
+
{"version":3,"file":"cell-EBKKpAAT.js","names":[],"sources":["../src/test-runner/throttle.ts","../src/test-runner/cell.ts"],"sourcesContent":["/**\n * Shared `APP_BRIDGE_THROTTLED` detection (devtools#767).\n *\n * 2026-07-10 real-device observation: the Toss app native side added a\n * per-method rate limit to the 2.x bridge — calling the SAME bridge method\n * twice within a short window rejects the second call immediately with a\n * 2.x native envelope (`{name, code, userInfo, moduleName, __isError}`) whose\n * `code` is `'APP_BRIDGE_THROTTLED'` and whose `message` reads\n * `\"Too many app bridge calls from <method>.\"`. The 3.x bridge path is\n * unaffected (devtools#767 issue body, sdk-example#284 2026-07-10 comment).\n *\n * This module is the SINGLE source of the detection predicate. Today's only\n * consumer is `runtime.ts` (in-page, per-test retry loop) — a plain static\n * import; esbuild inlines this file into the same IIFE bundle (same\n * mechanism as `bridge-stub.ts`, see `bundle.ts#getPageSideModulePath`). It\n * is exported from this standalone, dependency-free module (rather than\n * defined inline in `runtime.ts`) so a Node-side consumer could import the\n * exact same predicate in future if whole-FILE throttle-aware retry is ever\n * added on top of the per-test retry `runtime.ts` already performs — no such\n * Node-side caller exists yet (whole-file retry today only triggers on an\n * `injectAndRunBundle`-level timeout/exception, a distinct failure mode from\n * a `RunReport` that completed normally but contains a THROTTLED-failed\n * test — see `relay-worker.ts`'s `EVALUATE_TIMEOUT_MARKER` gate).\n *\n * Browser-compatible, dependency-free (no Node APIs) — safe to import from\n * either side of the CDP boundary, mirroring `runtime.ts`'s own constraint.\n */\n\n/** The native error code the Toss app bridge returns when rate-limited. */\nexport const THROTTLED_ERROR_CODE = 'APP_BRIDGE_THROTTLED';\n\n/** The message substring the native envelope carries when rate-limited. */\nexport const THROTTLED_MESSAGE_SUBSTRING = 'Too many app bridge calls';\n\n/**\n * True when `err` looks like a native `APP_BRIDGE_THROTTLED` rejection —\n * either the 2.x native envelope's `code` field equals\n * {@link THROTTLED_ERROR_CODE}, or the error's message contains\n * {@link THROTTLED_MESSAGE_SUBSTRING}. Either signal alone is sufficient: the\n * `code` field is the precise native signal, but a caller may only have a\n * stringified message (e.g. `Error#message` after the native envelope was\n * flattened) — checking both keeps detection robust across both shapes.\n *\n * Accepts `unknown` because callers may pass a caught page-side exception\n * (whose shape is not statically known), a `TestResult.error` string, or a\n * `FileResult` error string — never throws.\n */\nexport function isThrottledError(err: unknown): boolean {\n if (err === null || err === undefined) return false;\n\n if (typeof err === 'string') {\n return err.includes(THROTTLED_MESSAGE_SUBSTRING);\n }\n\n if (typeof err === 'object') {\n const rec = err as { code?: unknown; message?: unknown };\n if (rec.code === THROTTLED_ERROR_CODE) return true;\n if (typeof rec.message === 'string' && rec.message.includes(THROTTLED_MESSAGE_SUBSTRING)) {\n return true;\n }\n }\n\n return false;\n}\n","/**\n * Cell injection utility for the `devtools-test` CLI (issue #684 §4.1).\n *\n * Injects arbitrary globals into the page via `Runtime.evaluate` BEFORE the\n * first test bundle is injected. The injected values are session-global — one\n * call covers all files in the run.\n *\n * The primary use-case is injecting `__AIT_CELL__` (sdkLine/platform) so\n * sdk-example's `aitCapture.ts` picks up the correct test-axis values instead\n * of falling back to `'2.x'`/`'mock'`.\n *\n * devtools does NOT know the shape of `__AIT_CELL__` — it only provides the\n * general injection mechanism. The caller (CLI or MCP auto-attach path) is\n * responsible for constructing the cell object.\n *\n * SECRET-HANDLING: cell values (sdkLine/platform) are not secrets and may be\n * logged at the caller's discretion. This module does NOT log them itself.\n *\n * Node-only. react-free. CdpConnection only.\n */\n\nimport { THROTTLED_ERROR_CODE, THROTTLED_MESSAGE_SUBSTRING } from './throttle.js';\n\n/**\n * `__AIT_PERMS__` key → SDK function name whose `.getPermission()` is probed\n * (devtools#739). This is the exact contract sdk-example#265 consumes — do\n * not rename keys without a coordinated sdk-example update.\n *\n * Each SDK function is reached via `window.__sdk` — the SAME page-global\n * bridge `src/in-app/auto.ts` installs for `call_sdk`/`__sdkCall` (see\n * `src/test-runner/bundle.ts`'s `sdkRedirectPlugin`, which redirects bundled\n * `@apps-in-toss/web-framework` imports to this same object at runtime). The\n * test-runner bundle owns no independent SDK import — `window.__sdk` is a\n * page global regardless of whether a bundle has been injected yet, so this\n * preflight runs standalone via `injectGlobals`'s `Runtime.evaluate`\n * mechanism BEFORE the first bundle, rather than being prepended into\n * `rpc.ts#buildRunTestsExpression`. (Design choice documented in PR #739 —\n * bundle-prepend was the fallback plan if the SDK were only reachable from\n * inside bundle scope, which reality ruled out.)\n */\nconst PERMISSION_PROBE_MAP: Record<string, string> = {\n clipboardRead: 'getClipboardText',\n clipboardWrite: 'setClipboardText',\n album: 'fetchAlbumPhotos',\n camera: 'openCamera',\n contacts: 'fetchContacts',\n location: 'getCurrentLocation',\n};\n\n/**\n * Inter-probe spacing (ms) applied BEFORE every probe after the first\n * (devtools#767). The native per-method bridge rate limit observed\n * 2026-07-10 rejects a SECOND call to the same method within a short window\n * with `APP_BRIDGE_THROTTLED` — the permission preflight's un-paced burst\n * (6 probes fired back-to-back) was the FIRST trigger of that limiter in the\n * wild (only the first probe passed; the remaining 5 came back throttled →\n * `unavailable`, cascading into skipped `__AIT_PERMS__`-branching tests).\n * Sequential-with-spacing is cheap insurance even though each probe targets a\n * DIFFERENT method — the limiter's exact scope (per-method vs. a shared\n * bridge-wide window) was not reverse-engineered (explicitly out of scope,\n * devtools#767) so spacing every probe is the conservative choice.\n */\nconst PROBE_INTER_CALL_DELAY_MS = 250;\n\n/**\n * Backoff delays (ms) between retry attempts when a probe is throttled. Its\n * `.length` IS the max retry count (devtools#767: \"최대 2회 재시도\") — the\n * generated page-side expression below derives `attempt < backoff.length`\n * from this same array (via `backoffArray`), so there is a single source for\n * both the delay ladder and the retry count; no separate max-retries\n * constant is kept in this module (unlike `runtime.ts`'s `TEST_MAX_RETRIES`,\n * which IS consumed directly by a Node-side loop condition).\n */\nconst PROBE_RETRY_BACKOFF_MS = [500, 1000] as const;\n\n/**\n * Non-fatal bound for the permission preflight `Runtime.evaluate` round-trip\n * (devtools#767: raised from the pre-pacing 10s to cover the new sequential +\n * backoff time budget).\n *\n * Worst-case budget with 6 probes, each requiring the full retry ladder:\n * - inter-probe spacing: only BEFORE probes 2‑6 (none before the first) =\n * 5 × {@link PROBE_INTER_CALL_DELAY_MS} = 1 250ms\n * - per-probe backoff (both retries taken): 6 × (500 + 1000) = 9 000ms\n * - probe round-trips themselves (headroom, not just the sleeps): ~4 500ms\n * total ≈ 14 750ms, rounded up to a clean 20 000ms for headroom.\n */\nexport const PERMISSION_PREFLIGHT_TIMEOUT_MS = 20_000;\n\n/**\n * Builds the `Runtime.evaluate` expression for the permission-state preflight\n * (devtools#739). Evaluated ONCE per relay session, before the first test\n * bundle is injected (see `relay-worker.ts#runTestFilesOverRelay`).\n *\n * For each entry in {@link PERMISSION_PROBE_MAP}, probes\n * `window.__sdk.<fn>.getPermission()` — a NON-blocking query (never\n * `openPermissionDialog`/`requestPermission`, which open native UI) — and\n * assigns the resolved `'allowed'|'denied'|'notDetermined'` status to\n * `globalThis.__AIT_PERMS__[<key>]`. A probe that is absent, throws, or\n * rejects resolves to `'unavailable'` — this expression never throws to its\n * caller, and a single slow/broken permission must not fail the others (each\n * probe is awaited independently, not short-circuited).\n *\n * devtools#767: probes run SEQUENTIALLY (not a parallel burst) with a\n * {@link PROBE_INTER_CALL_DELAY_MS} gap before each probe after the first, and\n * a probe that comes back `APP_BRIDGE_THROTTLED` (native per-method bridge\n * rate limit) is retried up to {@link PROBE_RETRY_BACKOFF_MS}`.length` times\n * with the same {@link PROBE_RETRY_BACKOFF_MS} ladder before falling back to\n * `'unavailable'`.\n * The THROTTLED detection mirrors `throttle.ts#isThrottledError` inline (this\n * expression is raw JS text evaluated via CDP, not an esbuild-bundled module,\n * so it cannot statically `import` that helper) — kept in sync by checking\n * BOTH `e.code === 'APP_BRIDGE_THROTTLED'` and a message substring match, the\n * same two signals `isThrottledError` checks.\n *\n * devtools#767 acceptance criteria 2 (\"3.x run duration은 유의미하게 늘지 않음,\n * pacing은 opt-in 또는 throttle-adaptive\") requires this NOT be an unconditional\n * cost — `pace` (default `true`, matching the pre-existing sequential+spacing\n * behavior for 2.x/unknown cells) lets a 3.x caller skip BOTH the inter-probe\n * `sleep(250)` AND the retry backoff ladder, since `APP_BRIDGE_THROTTLED` is a\n * 2.x-bridge-only limiter (see `--pace`'s own doc in `cli.ts`: \"3.x cells are\n * unaffected by that limiter\"). When `pace` is `false`, probes still run\n * sequentially (never a parallel burst) but back-to-back with no added delay,\n * and a THROTTLED result resolves to `'unavailable'` immediately (no retry) —\n * this matches the 3.x runtime, which never emits `APP_BRIDGE_THROTTLED`.\n *\n * Tests (sdk-example#265) read `globalThis.__AIT_PERMS__` to branch\n * deterministically per permission state instead of blanket outcome-branching.\n *\n * Returns a JSON-serialised string (the same double-serialisation pattern\n * `rpc.ts#buildRunTestsExpression` uses) rather than relying on CDP\n * `returnByValue` structural fidelity for a plain object across the Chii\n * relay — the Node side re-parses via `JSON.parse`.\n *\n * Pure function — no I/O, no CDP call. Callers pass the returned string to\n * `Runtime.evaluate`.\n *\n * @param pace - Defaults to `true` (today's sequential+spacing+backoff\n * behavior). Pass `false` for a 3.x cell to skip the inter-probe delay and\n * the THROTTLED retry ladder entirely.\n */\nexport function buildPermissionPreflightExpression(pace = true): string {\n const probeKeys = Object.keys(PERMISSION_PROBE_MAP);\n const probeCalls = probeKeys\n .map((key, i) => {\n const fnName = PERMISSION_PROBE_MAP[key];\n // Every probe after the first waits PROBE_INTER_CALL_DELAY_MS first —\n // sequential execution with spacing, never a parallel burst. Skipped\n // entirely when `pace` is false (3.x cell — no native limiter to dodge).\n const spacing = i === 0 || !pace ? '' : ` await sleep(${PROBE_INTER_CALL_DELAY_MS});\\n`;\n return (\n `${spacing}` +\n ` result[${JSON.stringify(key)}] = await probeWithRetry(${JSON.stringify(fnName)});`\n );\n })\n .join('\\n');\n\n // 3.x cells never emit APP_BRIDGE_THROTTLED (it's a 2.x-bridge-only native\n // limiter) — an empty backoff ladder makes `probeWithRetry` resolve any\n // throw (throttled or not) to 'unavailable' on the FIRST attempt, with zero\n // retry sleeps, satisfying devtools#767 acceptance criteria 2.\n const backoffArray = pace ? `[${PROBE_RETRY_BACKOFF_MS.join(',')}]` : '[]';\n\n return (\n `(async () => {` +\n ` const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));` +\n ` const isThrottled = (e) => {` +\n ` if (!e) return false;` +\n ` if (e.code === ${JSON.stringify(THROTTLED_ERROR_CODE)}) return true;` +\n ` const msg = typeof e.message === 'string' ? e.message : '';` +\n ` return msg.indexOf(${JSON.stringify(THROTTLED_MESSAGE_SUBSTRING)}) !== -1;` +\n ` };` +\n ` const probe = async (fnName) => {` +\n ` const fn = globalThis.__sdk && globalThis.__sdk[fnName];` +\n ` if (!fn || typeof fn.getPermission !== 'function') return { ok: true, value: 'unavailable' };` +\n ` const status = await fn.getPermission();` +\n ` if (status === 'allowed' || status === 'denied' || status === 'notDetermined') return { ok: true, value: status };` +\n ` return { ok: true, value: 'unavailable' };` +\n ` };` +\n // probeWithRetry: up to PROBE_RETRY_BACKOFF_MS.length retries on THROTTLED,\n // backing off per PROBE_RETRY_BACKOFF_MS. Any OTHER throw (or a\n // non-throttled outcome) resolves to 'unavailable' immediately — only\n // THROTTLED gets a retry.\n ` const backoff = ${backoffArray};` +\n ` const probeWithRetry = async (fnName) => {` +\n ` for (let attempt = 0; attempt <= backoff.length; attempt++) {` +\n ` try {` +\n ` const r = await probe(fnName);` +\n ` return r.value;` +\n ` } catch (e) {` +\n ` if (isThrottled(e) && attempt < backoff.length) {` +\n ` await sleep(backoff[attempt]);` +\n ` continue;` +\n ` }` +\n ` return 'unavailable';` +\n ` }` +\n ` }` +\n ` return 'unavailable';` +\n ` };` +\n ` const result = {};` +\n `${probeCalls}\\n` +\n ` globalThis.__AIT_PERMS__ = result;` +\n ` return JSON.stringify(result);` +\n `})()`\n );\n}\n\n/**\n * Runs the permission-state preflight (devtools#739) via `Runtime.evaluate`\n * and returns the collected `__AIT_PERMS__` map, or `undefined` when the\n * preflight itself failed or timed out.\n *\n * NON-FATAL by design: any failure (CDP exception, timeout, page-side throw)\n * is caught here, logged as ONE stderr line, and swallowed — the whole test\n * run must proceed even when the preflight cannot complete (e.g. `window.__sdk`\n * not yet installed, or a page navigation mid-attach). Callers that want the\n * result for report provenance (`RelayRunReport.preflight`) get `undefined`\n * in that case, which downstream code treats the same as \"not run\".\n *\n * SECRET-HANDLING: no relay/wss/secret values are read or logged; only\n * permission-state strings cross this boundary.\n *\n * @param conn - The live CDP connection (relay-attached page).\n * @param timeoutMs - Round-trip bound. Defaults to {@link PERMISSION_PREFLIGHT_TIMEOUT_MS}.\n * @param pace - Forwarded to {@link buildPermissionPreflightExpression}.\n * Defaults to `true` (today's sequential+spacing+backoff behavior, correct\n * for 2.x/unknown cells). Callers that know the cell is 3.x should pass\n * `false` — devtools#767 acceptance criteria 2 requires 3.x run duration not\n * grow from this preflight, and 3.x never hits the 2.x-only\n * `APP_BRIDGE_THROTTLED` limiter this pacing exists to dodge.\n */\nexport async function runPermissionPreflight(\n conn: CdpConnection,\n timeoutMs = PERMISSION_PREFLIGHT_TIMEOUT_MS,\n pace = true,\n): Promise<Record<string, string> | undefined> {\n const TIMEOUT_SENTINEL = Symbol('permission-preflight-timeout');\n const timeoutPromise = new Promise<typeof TIMEOUT_SENTINEL>((resolve) =>\n setTimeout(() => resolve(TIMEOUT_SENTINEL), timeoutMs),\n );\n\n try {\n const evalPromise = conn.send('Runtime.evaluate', {\n expression: buildPermissionPreflightExpression(pace),\n returnByValue: true,\n awaitPromise: true,\n });\n\n const raceResult = await Promise.race([\n evalPromise.then((v) => ({ tag: 'eval' as const, v })),\n timeoutPromise.then(() => ({ tag: 'timeout' as const })),\n ]);\n\n if (raceResult.tag === 'timeout') {\n process.stderr.write(\n `test-runner: permission preflight timed out after ${timeoutMs}ms — proceeding with __AIT_PERMS__ unset\\n`,\n );\n return undefined;\n }\n\n if (raceResult.v.exceptionDetails) {\n process.stderr.write(\n 'test-runner: permission preflight threw — proceeding with __AIT_PERMS__ unset\\n',\n );\n return undefined;\n }\n\n const rawValue = raceResult.v.result.value;\n if (typeof rawValue !== 'string') return undefined;\n const parsed: unknown = JSON.parse(rawValue);\n if (typeof parsed !== 'object' || parsed === null) return undefined;\n return parsed as Record<string, string>;\n } catch (e) {\n process.stderr.write(\n `test-runner: permission preflight failed (${\n e instanceof Error ? e.message : String(e)\n }) — proceeding with __AIT_PERMS__ unset\\n`,\n );\n return undefined;\n }\n}\n\nimport { buildIndicatorExpression } from '../mcp/attach-orchestrator.js';\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\n\n/**\n * Injects each key of `globals` into `globalThis` in the page via a single\n * `Runtime.evaluate` call. Must be called BEFORE the first `bundleTestFile`\n * inject — the cell is session-global and applies to all subsequent files.\n *\n * Throws if the CDP evaluate returns an exception.\n *\n * @param conn - The live CDP connection (relay-attached page).\n * @param globals - Plain-JSON-serialisable key→value map to assign onto `globalThis`.\n */\nexport async function injectGlobals(\n conn: CdpConnection,\n globals: Record<string, unknown>,\n): Promise<void> {\n // JSON.stringify is safe here: globals values are plain data (sdkLine/platform\n // strings or simple objects). If a value is not JSON-serialisable this throws\n // at call time, which surfaces the error early and clearly.\n const expr = `(() => { Object.assign(globalThis, ${JSON.stringify(globals)}); return true; })()`;\n const result = await conn.send('Runtime.evaluate', { expression: expr, returnByValue: true });\n if (result.exceptionDetails) {\n const msg =\n result.exceptionDetails.exception?.description ??\n result.exceptionDetails.text ??\n 'unknown error';\n throw new Error(`injectGlobals: Runtime.evaluate threw: ${msg}`);\n }\n}\n\n/**\n * Injects (or updates) the \"Debugger Connected\"/\"Debugger Disconnected\"\n * on-phone indicator via `Runtime.evaluate` (#730).\n *\n * Uses the same CDP mechanism as {@link injectGlobals} — a single\n * `Runtime.evaluate` round-trip with the expression built by\n * {@link buildIndicatorExpression}. The indicator is a LIVE badge rendered at\n * the bottom-left of the page (position:fixed, safe-area-aware): the badge's\n * controller is idempotent, so calling this again (e.g. right before\n * `close()` with `{ state: 'disconnected' }`) UPDATES the existing badge in\n * place instead of injecting a duplicate.\n *\n * **Isolation**: unlike {@link injectGlobals}, this function NEVER throws to\n * its caller. Injection failure (e.g. page detached during inject, CSS not\n * supported) is swallowed and logged as `console.debug` — the badge is\n * informational UI and must never block attach success or test execution.\n * This is the same fire-and-forget spirit as the in-page console mount in\n * `src/in-app/attach.ts`.\n *\n * Call this ONLY on the manual debug paths (start_attach MCP, devtools-test\n * CLI). Do NOT call on the `run_tests` auto-attach path — the badge\n * would contaminate screenshots, measure_safe_area probes, and DOM snapshots\n * taken during automated measurement runs.\n *\n * SECRET-HANDLING: the injected expression contains no secrets, relay URLs,\n * wss addresses, or TOTP codes — it is pure DOM UI text + enum state built by\n * {@link buildIndicatorExpression}.\n *\n * @param conn - The live CDP connection (relay-attached page).\n * @param opts - Optional overrides forwarded to {@link buildIndicatorExpression}.\n */\nexport async function injectDebugIndicator(\n conn: CdpConnection,\n opts?: { label?: string; disconnectedLabel?: string; state?: 'attached' | 'disconnected' },\n): Promise<void> {\n try {\n await conn.send('Runtime.evaluate', {\n expression: buildIndicatorExpression(opts),\n returnByValue: true,\n });\n } catch (err) {\n // Badge injection is informational UI — swallow and log; never propagate.\n console.debug('[@ait-co/devtools] debug indicator inject skipped:', err);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAa,uBAAuB;;AAGpC,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACQ3C,MAAM,uBAA+C;CACnD,eAAe;CACf,gBAAgB;CAChB,OAAO;CACP,QAAQ;CACR,UAAU;CACV,UAAU;CACX;;;;;;;;;;;;;;AAeD,MAAM,4BAA4B;;;;;;;;;;AAWlC,MAAM,yBAAyB,CAAC,KAAK,IAAK;;;;;;;;;;;;;AAc1C,MAAa,kCAAkC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsD/C,SAAgB,mCAAmC,OAAO,MAAc;CAEtE,MAAM,aADY,OAAO,KAAK,qBAAqB,CAEhD,KAAK,KAAK,MAAM;EACf,MAAM,SAAS,qBAAqB;AAKpC,SACE,GAFc,MAAM,KAAK,CAAC,OAAO,KAAK,mBAAmB,0BAA0B,MAAA,aAGrE,KAAK,UAAU,IAAI,CAAC,2BAA2B,KAAK,UAAU,OAAO,CAAC;GAEtF,CACD,KAAK,KAAK;CAMb,MAAM,eAAe,OAAO,IAAI,uBAAuB,KAAK,IAAI,CAAC,KAAK;AAEtE,QACE,qKAIsB,KAAK,UAAU,qBAAqB,CAAC,sGAEjC,KAAK,UAAU,4BAA4B,CAAC,qbAajD,aAAa,kaAiB/B,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BlB,eAAsB,uBACpB,MACA,YAAY,iCACZ,OAAO,MACsC;CAC7C,MAAM,mBAAmB,OAAO,+BAA+B;CAC/D,MAAM,iBAAiB,IAAI,SAAkC,YAC3D,iBAAiB,QAAQ,iBAAiB,EAAE,UAAU,CACvD;AAED,KAAI;EACF,MAAM,cAAc,KAAK,KAAK,oBAAoB;GAChD,YAAY,mCAAmC,KAAK;GACpD,eAAe;GACf,cAAc;GACf,CAAC;EAEF,MAAM,aAAa,MAAM,QAAQ,KAAK,CACpC,YAAY,MAAM,OAAO;GAAE,KAAK;GAAiB;GAAG,EAAE,EACtD,eAAe,YAAY,EAAE,KAAK,WAAoB,EAAE,CACzD,CAAC;AAEF,MAAI,WAAW,QAAQ,WAAW;AAChC,WAAQ,OAAO,MACb,qDAAqD,UAAU,4CAChE;AACD;;AAGF,MAAI,WAAW,EAAE,kBAAkB;AACjC,WAAQ,OAAO,MACb,kFACD;AACD;;EAGF,MAAM,WAAW,WAAW,EAAE,OAAO;AACrC,MAAI,OAAO,aAAa,SAAU,QAAO,KAAA;EACzC,MAAM,SAAkB,KAAK,MAAM,SAAS;AAC5C,MAAI,OAAO,WAAW,YAAY,WAAW,KAAM,QAAO,KAAA;AAC1D,SAAO;UACA,GAAG;AACV,UAAQ,OAAO,MACb,6CACE,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,CAC3C,2CACF;AACD;;;;;;;;;;;;;AAiBJ,eAAsB,cACpB,MACA,SACe;CAIf,MAAM,OAAO,sCAAsC,KAAK,UAAU,QAAQ,CAAC;CAC3E,MAAM,SAAS,MAAM,KAAK,KAAK,oBAAoB;EAAE,YAAY;EAAM,eAAe;EAAM,CAAC;AAC7F,KAAI,OAAO,kBAAkB;EAC3B,MAAM,MACJ,OAAO,iBAAiB,WAAW,eACnC,OAAO,iBAAiB,QACxB;AACF,QAAM,IAAI,MAAM,0CAA0C,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCpE,eAAsB,qBACpB,MACA,MACe;AACf,KAAI;AACF,QAAM,KAAK,KAAK,oBAAoB;GAClC,YAAY,yBAAyB,KAAK;GAC1C,eAAe;GAChB,CAAC;UACK,KAAK;AAEZ,UAAQ,MAAM,sDAAsD,IAAI"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { c as generateAttachToken, d as startQuickTunnel, f as startTunnelHealthProbe, l as makeTunnelStatus, m as logInfo, p as logError, u as printAttachBanner } from "./attach-orchestrator-
|
|
2
|
+
import { c as generateAttachToken, d as startQuickTunnel, f as startTunnelHealthProbe, l as makeTunnelStatus, m as logInfo, p as logError, u as printAttachBanner } from "./attach-orchestrator-DL3NQ9ca.js";
|
|
3
3
|
import { n as buildRelayVerifyAuth, t as assertRelayAuthConfigured } from "./totp-CZLLKfOC.js";
|
|
4
|
-
import "./cell-
|
|
4
|
+
import "./cell-EBKKpAAT.js";
|
|
5
5
|
import { n as RELAY_AUTH_REJECT_CLOSE_CODE, r as RELAY_AUTH_REJECT_REASON, t as ChiiCdpConnection } from "./test-runner/bin.js";
|
|
6
|
-
import "./qr-http-server-
|
|
6
|
+
import "./qr-http-server-DrbIVDjO.js";
|
|
7
7
|
import "./relay-secret-store-Bmyleu0A.js";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
9
|
import "node:events";
|
|
@@ -453,4 +453,4 @@ async function bootRelayFamily(options = {}) {
|
|
|
453
453
|
//#endregion
|
|
454
454
|
export { bootRelayFamily, buildRelayVerifyAuth, startAttachWatcher };
|
|
455
455
|
|
|
456
|
-
//# sourceMappingURL=debug-server-
|
|
456
|
+
//# sourceMappingURL=debug-server-B3ABDrRI.js.map
|