@ait-co/devtools 0.1.117 → 0.1.119
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/attach-orchestrator-CFotueL3.js +1749 -0
- package/dist/attach-orchestrator-CFotueL3.js.map +1 -0
- package/dist/attach-orchestrator-CII9Gl5m.js +2797 -0
- package/dist/attach-orchestrator-CII9Gl5m.js.map +1 -0
- package/dist/attach-orchestrator-C_6xb_L5.js +3 -0
- package/dist/attach-orchestrator-DEJfLm7V.js +1749 -0
- package/dist/attach-orchestrator-DEJfLm7V.js.map +1 -0
- package/dist/{cli-CvUNQKqw.js → attach-orchestrator-qFEMBUwb.js} +3 -805
- package/dist/attach-orchestrator-qFEMBUwb.js.map +1 -0
- package/dist/{bundle-KFs4t-wc.d.ts → bundle-CA1TCXED.d.ts} +1 -1
- package/dist/bundle-CA1TCXED.d.ts.map +1 -0
- package/dist/capture-BmK_cBqQ.d.ts +58 -0
- package/dist/capture-BmK_cBqQ.d.ts.map +1 -0
- package/dist/{cdp-connection-C0AP0tH2.d.ts → cdp-connection-DB2zgthr.d.ts} +1 -1
- package/dist/{cdp-connection-C0AP0tH2.d.ts.map → cdp-connection-DB2zgthr.d.ts.map} +1 -1
- package/dist/cell-BJiaGlVu.js +3 -0
- package/dist/cell-BPWZUm_X.js +85 -0
- package/dist/cell-BPWZUm_X.js.map +1 -0
- package/dist/cell-Cv3e_qwn.js +84 -0
- package/dist/cell-Cv3e_qwn.js.map +1 -0
- package/dist/cell-bLwQ4gTq.js +85 -0
- package/dist/cell-bLwQ4gTq.js.map +1 -0
- package/dist/cell-nrWFeqvG.js +84 -0
- package/dist/cell-nrWFeqvG.js.map +1 -0
- package/dist/cli-RhzASQef.js +1057 -0
- package/dist/cli-RhzASQef.js.map +1 -0
- package/dist/debug-server-BhHVy3XT.js +1191 -0
- package/dist/debug-server-BhHVy3XT.js.map +1 -0
- package/dist/{debug-server-jrI9U_f4.js → debug-server-Bi6jLuY6.js} +467 -2989
- package/dist/debug-server-Bi6jLuY6.js.map +1 -0
- package/dist/debug-server-Buc8yoa7.js +1909 -0
- package/dist/debug-server-Buc8yoa7.js.map +1 -0
- package/dist/{debug-server-BvrESnaV.js → debug-server-C_5ag9jj.js} +5 -3
- package/dist/debug-server-C_5ag9jj.js.map +1 -0
- package/dist/mcp/cli.js +3 -2
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/panel/index.js +1 -1
- package/dist/{pool-htlVnEFl.d.ts → pool-U9FrxEuR.d.ts} +5 -5
- package/dist/{pool-htlVnEFl.d.ts.map → pool-U9FrxEuR.d.ts.map} +1 -1
- package/dist/relay-factory-DVI7TrX4.js +69 -0
- package/dist/relay-factory-DVI7TrX4.js.map +1 -0
- package/dist/relay-secret-store-BcVrWwTq.js +153 -0
- package/dist/relay-secret-store-BcVrWwTq.js.map +1 -0
- package/dist/{relay-secret-store-BHcOmaNK.js → relay-secret-store-BlFEhqwb.js} +1 -1
- package/dist/{relay-secret-store-CmqchhR5.js → relay-secret-store-CFc9n0OA.js} +1 -1
- package/dist/{relay-secret-store-CmqchhR5.js.map → relay-secret-store-CFc9n0OA.js.map} +1 -1
- package/dist/{relay-secret-store-CkA7KNUb.js → relay-secret-store-CmDDfZMh.js} +2 -2
- package/dist/{relay-secret-store-CkA7KNUb.js.map → relay-secret-store-CmDDfZMh.js.map} +1 -1
- package/dist/relay-secret-store-DNPJKHNs.js +153 -0
- package/dist/relay-secret-store-DNPJKHNs.js.map +1 -0
- package/dist/relay-url-store-CdA58fgw.js +122 -0
- package/dist/relay-url-store-CdA58fgw.js.map +1 -0
- package/dist/{relay-url-store-C0qukm3R.js → relay-url-store-CnL2zwbH.js} +2 -2
- package/dist/{relay-url-store-C0qukm3R.js.map → relay-url-store-CnL2zwbH.js.map} +1 -1
- package/dist/{relay-url-store-NDtEcOE-.js → relay-url-store-DGQ-HPQC.js} +2 -2
- package/dist/{relay-url-store-NDtEcOE-.js.map → relay-url-store-DGQ-HPQC.js.map} +1 -1
- package/dist/relay-url-store-Dv5HSzwb.js +122 -0
- package/dist/relay-url-store-Dv5HSzwb.js.map +1 -0
- package/dist/{relay-worker-DWW4vZxU.d.ts → relay-worker-D-3aAyDO.d.ts} +28 -4
- package/dist/relay-worker-D-3aAyDO.d.ts.map +1 -0
- package/dist/{runtime-BU-iMHz6.d.ts → runtime-D57Rtnsd.d.ts} +1 -1
- package/dist/{runtime-BU-iMHz6.d.ts.map → runtime-D57Rtnsd.d.ts.map} +1 -1
- package/dist/test-runner/bundle.d.ts +1 -1
- package/dist/test-runner/bundle.js +45 -27
- package/dist/test-runner/bundle.js.map +1 -1
- package/dist/test-runner/capture.d.ts +2 -0
- package/dist/test-runner/capture.js +44 -0
- package/dist/test-runner/capture.js.map +1 -0
- package/dist/test-runner/cli.d.ts +85 -12
- package/dist/test-runner/cli.d.ts.map +1 -1
- package/dist/test-runner/cli.js +2 -2
- package/dist/test-runner/config.d.ts +76 -2
- package/dist/test-runner/config.d.ts.map +1 -1
- package/dist/test-runner/config.js +4 -2
- package/dist/test-runner/config.js.map +1 -1
- package/dist/test-runner/pool.d.ts +1 -1
- package/dist/test-runner/relay-factory.d.ts +11136 -0
- package/dist/test-runner/relay-factory.d.ts.map +1 -0
- package/dist/test-runner/relay-factory.js +69 -0
- package/dist/test-runner/relay-factory.js.map +1 -0
- package/dist/test-runner/relay-worker.d.ts +1 -1
- package/dist/test-runner/relay-worker.js +66 -20
- package/dist/test-runner/relay-worker.js.map +1 -1
- package/dist/test-runner/report.d.ts +106 -0
- package/dist/test-runner/report.d.ts.map +1 -0
- package/dist/test-runner/report.js +139 -0
- package/dist/test-runner/report.js.map +1 -0
- package/dist/test-runner/rpc.d.ts +2 -2
- package/dist/test-runner/runtime.d.ts +1 -1
- package/dist/test-runner/task-graph.d.ts +1 -1
- package/dist/{totp-D70zD5tJ.js → totp-0bvlo9Yb.js} +1 -1
- package/dist/{totp-D70zD5tJ.js.map → totp-0bvlo9Yb.js.map} +1 -1
- package/dist/totp-DfekTBk3.js +211 -0
- package/dist/totp-DfekTBk3.js.map +1 -0
- package/dist/totp-zGIlDsZS.js +3 -0
- package/package.json +1 -1
- package/dist/bundle-KFs4t-wc.d.ts.map +0 -1
- package/dist/cli-CvUNQKqw.js.map +0 -1
- package/dist/debug-server-BvrESnaV.js.map +0 -1
- package/dist/debug-server-jrI9U_f4.js.map +0 -1
- package/dist/relay-worker-DWW4vZxU.d.ts.map +0 -1
- package/dist/totp-D104cJQN.js +0 -3
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { r as generateTotp } from "./totp-DvOYkYim.js";
|
|
3
|
-
import { parseArgs } from "node:util";
|
|
4
|
-
import { accessSync } from "node:fs";
|
|
5
|
-
import * as path from "node:path";
|
|
6
|
-
import { isAbsolute, resolve } from "node:path";
|
|
7
3
|
import { randomBytes } from "node:crypto";
|
|
8
4
|
import { Tunnel, bin, install } from "cloudflared";
|
|
9
|
-
import * as fs from "node:fs/promises";
|
|
10
|
-
import { glob } from "node:fs/promises";
|
|
11
|
-
import { fileURLToPath } from "node:url";
|
|
12
5
|
//#region src/mcp/deeplink.ts
|
|
13
6
|
/**
|
|
14
7
|
* URL of the AITC Sandbox launcher PWA.
|
|
@@ -1477,7 +1470,7 @@ async function prepareAttach(deps, env, args, conn) {
|
|
|
1477
1470
|
const buildProjectRoot = typeof rawProjectRoot === "string" ? rawProjectRoot : void 0;
|
|
1478
1471
|
let tunnelHttpUrl = process.env.AIT_TUNNEL_BASE_URL?.trim() ?? "";
|
|
1479
1472
|
if (tunnelHttpUrl === "" && buildProjectRoot !== void 0) {
|
|
1480
|
-
const { readRelayUrls } = await import("./relay-url-store-
|
|
1473
|
+
const { readRelayUrls } = await import("./relay-url-store-DGQ-HPQC.js");
|
|
1481
1474
|
tunnelHttpUrl = (await readRelayUrls({ projectRoot: buildProjectRoot }))?.tunnelBaseUrl ?? "";
|
|
1482
1475
|
}
|
|
1483
1476
|
if (tunnelHttpUrl === "") return {
|
|
@@ -1752,801 +1745,6 @@ function buildIndicatorExpression(opts) {
|
|
|
1752
1745
|
return `(() => {if (document.getElementById('__ait_debug_indicator')) return;const el = document.createElement('div');el.id = '__ait_debug_indicator';el.style.cssText = ['position:fixed','left:max(12px,calc(env(safe-area-inset-left,0px) + 8px))','bottom:max(12px,calc(env(safe-area-inset-bottom,0px) + 8px))','z-index:2147483647','background:#e5484d','color:#fff','font:bold 11px/1 system-ui,sans-serif','padding:5px 9px','border-radius:6px','pointer-events:auto','user-select:none',].join(';');el.textContent = ${JSON.stringify(label)};el.addEventListener('pointerdown', () => el.remove(), { once: true });document.body.appendChild(el);})()`;
|
|
1753
1746
|
}
|
|
1754
1747
|
//#endregion
|
|
1755
|
-
|
|
1756
|
-
/**
|
|
1757
|
-
* Cell injection utility for the `devtools-test` CLI (issue #684 §4.1).
|
|
1758
|
-
*
|
|
1759
|
-
* Injects arbitrary globals into the page via `Runtime.evaluate` BEFORE the
|
|
1760
|
-
* first test bundle is injected. The injected values are session-global — one
|
|
1761
|
-
* call covers all files in the run.
|
|
1762
|
-
*
|
|
1763
|
-
* The primary use-case is injecting `__AIT_CELL__` (sdkLine/platform) so
|
|
1764
|
-
* sdk-example's `aitCapture.ts` picks up the correct test-axis values instead
|
|
1765
|
-
* of falling back to `'2.x'`/`'mock'`.
|
|
1766
|
-
*
|
|
1767
|
-
* devtools does NOT know the shape of `__AIT_CELL__` — it only provides the
|
|
1768
|
-
* general injection mechanism. The caller (CLI or MCP auto-attach path) is
|
|
1769
|
-
* responsible for constructing the cell object.
|
|
1770
|
-
*
|
|
1771
|
-
* SECRET-HANDLING: cell values (sdkLine/platform) are not secrets and may be
|
|
1772
|
-
* logged at the caller's discretion. This module does NOT log them itself.
|
|
1773
|
-
*
|
|
1774
|
-
* Node-only. react-free. CdpConnection only.
|
|
1775
|
-
*/
|
|
1776
|
-
/**
|
|
1777
|
-
* Injects each key of `globals` into `globalThis` in the page via a single
|
|
1778
|
-
* `Runtime.evaluate` call. Must be called BEFORE the first `bundleTestFile`
|
|
1779
|
-
* inject — the cell is session-global and applies to all subsequent files.
|
|
1780
|
-
*
|
|
1781
|
-
* Throws if the CDP evaluate returns an exception.
|
|
1782
|
-
*
|
|
1783
|
-
* @param conn - The live CDP connection (relay-attached page).
|
|
1784
|
-
* @param globals - Plain-JSON-serialisable key→value map to assign onto `globalThis`.
|
|
1785
|
-
*/
|
|
1786
|
-
async function injectGlobals(conn, globals) {
|
|
1787
|
-
const expr = `(() => { Object.assign(globalThis, ${JSON.stringify(globals)}); return true; })()`;
|
|
1788
|
-
const result = await conn.send("Runtime.evaluate", {
|
|
1789
|
-
expression: expr,
|
|
1790
|
-
returnByValue: true
|
|
1791
|
-
});
|
|
1792
|
-
if (result.exceptionDetails) {
|
|
1793
|
-
const msg = result.exceptionDetails.exception?.description ?? result.exceptionDetails.text ?? "unknown error";
|
|
1794
|
-
throw new Error(`injectGlobals: Runtime.evaluate threw: ${msg}`);
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
/**
|
|
1798
|
-
* Injects the "Debugger Connected" on-phone indicator via `Runtime.evaluate`.
|
|
1799
|
-
*
|
|
1800
|
-
* Uses the same CDP mechanism as {@link injectGlobals} — a single
|
|
1801
|
-
* `Runtime.evaluate` round-trip with the expression built by
|
|
1802
|
-
* {@link buildIndicatorExpression}. The indicator is a dismissible red badge
|
|
1803
|
-
* rendered at the bottom-left of the page (position:fixed, safe-area-aware).
|
|
1804
|
-
*
|
|
1805
|
-
* **Isolation**: unlike {@link injectGlobals}, this function NEVER throws to
|
|
1806
|
-
* its caller. Injection failure (e.g. page detached during inject, CSS not
|
|
1807
|
-
* supported) is swallowed and logged as `console.debug` — the badge is
|
|
1808
|
-
* informational UI and must never block attach success or test execution.
|
|
1809
|
-
* This is the same fire-and-forget spirit as the eruda mount in
|
|
1810
|
-
* `src/in-app/attach.ts`.
|
|
1811
|
-
*
|
|
1812
|
-
* Call this ONLY on the manual debug paths (start_attach MCP, devtools-test
|
|
1813
|
-
* CLI). Do NOT call on the `run_tests` auto-attach path — the red badge
|
|
1814
|
-
* would contaminate screenshots, measure_safe_area probes, and DOM snapshots
|
|
1815
|
-
* taken during automated measurement runs.
|
|
1816
|
-
*
|
|
1817
|
-
* SECRET-HANDLING: the injected expression contains no secrets, relay URLs,
|
|
1818
|
-
* wss addresses, or TOTP codes — it is pure DOM UI text built by
|
|
1819
|
-
* {@link buildIndicatorExpression}.
|
|
1820
|
-
*
|
|
1821
|
-
* @param conn - The live CDP connection (relay-attached page).
|
|
1822
|
-
* @param opts - Optional overrides forwarded to {@link buildIndicatorExpression}.
|
|
1823
|
-
*/
|
|
1824
|
-
async function injectDebugIndicator(conn, opts) {
|
|
1825
|
-
try {
|
|
1826
|
-
await conn.send("Runtime.evaluate", {
|
|
1827
|
-
expression: buildIndicatorExpression(opts),
|
|
1828
|
-
returnByValue: true
|
|
1829
|
-
});
|
|
1830
|
-
} catch (err) {
|
|
1831
|
-
console.debug("[@ait-co/devtools] debug indicator inject skipped:", err);
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
//#endregion
|
|
1835
|
-
//#region src/test-runner/discover.ts
|
|
1836
|
-
/**
|
|
1837
|
-
* Test-file discovery shared by the `devtools-test` CLI and the `run_tests`
|
|
1838
|
-
* MCP tool, so both expand glob patterns with identical semantics.
|
|
1839
|
-
*
|
|
1840
|
-
* Uses Node's built-in `fs/promises` `glob` (Node 22+) — no extra dependency,
|
|
1841
|
-
* which keeps the MCP daemon install graph lean (a plain glob lib would land in
|
|
1842
|
-
* the `npx … devtools-mcp` path for no benefit).
|
|
1843
|
-
*
|
|
1844
|
-
* Pure Node IO only (`node:fs/promises` + `node:path`) — react-free, so it is
|
|
1845
|
-
* safe to import from the MCP daemon graph.
|
|
1846
|
-
*/
|
|
1847
|
-
/**
|
|
1848
|
-
* Expands `patterns` (globs or plain paths) into a sorted, de-duplicated list of
|
|
1849
|
-
* ABSOLUTE test file paths, resolved relative to `cwd`.
|
|
1850
|
-
*
|
|
1851
|
-
* A plain (non-glob) path passes through when it matches a real file; a glob
|
|
1852
|
-
* expands against `cwd`. Absolute matches are kept as-is; relative matches are
|
|
1853
|
-
* resolved against `cwd`. `bundleTestFile` requires an absolute path, so the
|
|
1854
|
-
* absolute output feeds it directly.
|
|
1855
|
-
*
|
|
1856
|
-
* @param patterns Glob patterns or file paths (e.g. `['src/**\/*.ait.test.ts']`).
|
|
1857
|
-
* @param cwd Base directory for relative patterns/results.
|
|
1858
|
-
* @returns Sorted, de-duplicated absolute file paths. Empty when nothing matches.
|
|
1859
|
-
*/
|
|
1860
|
-
async function discoverTestFiles(patterns, cwd) {
|
|
1861
|
-
const out = /* @__PURE__ */ new Set();
|
|
1862
|
-
for await (const match of glob(patterns, { cwd })) out.add(isAbsolute(match) ? match : resolve(cwd, match));
|
|
1863
|
-
return [...out].sort();
|
|
1864
|
-
}
|
|
1865
|
-
//#endregion
|
|
1866
|
-
//#region src/test-runner/bundle.ts
|
|
1867
|
-
/**
|
|
1868
|
-
* esbuild-based bundler for user test files.
|
|
1869
|
-
*
|
|
1870
|
-
* Bundles a single test file into a self-contained IIFE string that can be
|
|
1871
|
-
* injected into a WebView via `Runtime.evaluate`. The bundle includes the
|
|
1872
|
-
* test runtime (`runtime.ts`), which provides `describe/it/test/expect` and
|
|
1873
|
-
* the `runTestModule(factory)` entry point.
|
|
1874
|
-
*
|
|
1875
|
-
* ## How the wiring works
|
|
1876
|
-
*
|
|
1877
|
-
* The bundle exposes two exports on `globalThis.__testBundle`:
|
|
1878
|
-
* - `runTestModule` — the runtime's entry function.
|
|
1879
|
-
* - `__userFactory` — an async function whose body is the user's top-level
|
|
1880
|
-
* test registration code (describe/it/test calls).
|
|
1881
|
-
*
|
|
1882
|
-
* The Node-side RPC (`rpc.ts`) calls:
|
|
1883
|
-
* `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`
|
|
1884
|
-
*
|
|
1885
|
-
* `runTestModule` then installs `describe/it/test/expect` as globals, invokes
|
|
1886
|
-
* the factory (which registers all tests), runs them, and returns a `RunReport`.
|
|
1887
|
-
*
|
|
1888
|
-
* ## Why a factory wrapper is needed
|
|
1889
|
-
*
|
|
1890
|
-
* Naively adding the runtime to `entryPoints` and bundling the user file would
|
|
1891
|
-
* fail for two reasons:
|
|
1892
|
-
* 1. `describe/it/test/expect` from the runtime are module-local in the IIFE
|
|
1893
|
-
* scope. The user's top-level `describe(...)` calls expect them as globals —
|
|
1894
|
-
* they are not globals until `runTestModule` installs them.
|
|
1895
|
-
* 2. Even with globals pre-installed, the user file runs at IIFE-evaluation
|
|
1896
|
-
* time, before the RPC layer calls `runTestModule` to reset state and start
|
|
1897
|
-
* the test clock.
|
|
1898
|
-
*
|
|
1899
|
-
* The factory approach solves both: the user's registration code is deferred
|
|
1900
|
-
* into a function that `runTestModule` calls AFTER installing the globals.
|
|
1901
|
-
*
|
|
1902
|
-
* ## Factory extraction algorithm
|
|
1903
|
-
*
|
|
1904
|
-
* The `userFactoryPlugin` reads the user file and splits lines into:
|
|
1905
|
-
* - **top-level**: `import …` and re-export lines — kept at module scope
|
|
1906
|
-
* (the only valid position for static `import` in ESM).
|
|
1907
|
-
* - **body**: all other statements — moved into the body of the exported
|
|
1908
|
-
* `__userFactory` async function.
|
|
1909
|
-
*
|
|
1910
|
-
* esbuild processes the re-generated module, following each static import
|
|
1911
|
-
* through the normal dependency graph (including the SDK-redirect plugin).
|
|
1912
|
-
*
|
|
1913
|
-
* ## SDK redirect
|
|
1914
|
-
*
|
|
1915
|
-
* Imports of `@apps-in-toss/web-framework` (and sub-paths) are intercepted via
|
|
1916
|
-
* the `sdkRedirectPlugin` and replaced with a virtual `window.__sdk` proxy that
|
|
1917
|
-
* `src/in-app/auto.ts` installs at runtime. This works for both 2.x and 3.x SDK.
|
|
1918
|
-
*
|
|
1919
|
-
* SECRET-HANDLING: the returned bundle code is caller-managed; never log it.
|
|
1920
|
-
*/
|
|
1921
|
-
/** The SDK package name that mini-app test code imports from. */
|
|
1922
|
-
const SDK_PACKAGE = "@apps-in-toss/web-framework";
|
|
1923
|
-
/**
|
|
1924
|
-
* Names the runtime installs as globals before invoking the user factory.
|
|
1925
|
-
* The `vitest` virtual module re-exports each as a lazy getter that reads from
|
|
1926
|
-
* `globalThis` at access time. Keep in sync with the globals installed in
|
|
1927
|
-
* `runtime.ts#runTestModule`.
|
|
1928
|
-
*/
|
|
1929
|
-
const VITEST_GLOBAL_NAMES = [
|
|
1930
|
-
"describe",
|
|
1931
|
-
"it",
|
|
1932
|
-
"test",
|
|
1933
|
-
"expect",
|
|
1934
|
-
"beforeAll",
|
|
1935
|
-
"afterAll",
|
|
1936
|
-
"beforeEach",
|
|
1937
|
-
"afterEach",
|
|
1938
|
-
"vi"
|
|
1939
|
-
];
|
|
1940
|
-
/**
|
|
1941
|
-
* Matches the bare SDK package and any sub-path import
|
|
1942
|
-
* (`@apps-in-toss/web-framework`, `@apps-in-toss/web-framework/foo`).
|
|
1943
|
-
* Built from {@link SDK_PACKAGE} so the package name has a single source.
|
|
1944
|
-
*/
|
|
1945
|
-
const SDK_IMPORT_FILTER = new RegExp(`^${SDK_PACKAGE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
|
|
1946
|
-
/**
|
|
1947
|
-
* esbuild plugin that intercepts SDK imports and redirects them to the
|
|
1948
|
-
* `window.__sdk` proxy that `src/in-app/auto.ts` installs at runtime.
|
|
1949
|
-
*
|
|
1950
|
-
* Strategy: for every import of `@apps-in-toss/web-framework` (or sub-paths),
|
|
1951
|
-
* esbuild resolves it to a virtual module that re-exports all named exports
|
|
1952
|
-
* via `window.__sdk[name]`. This avoids bundling the real SDK (which may not
|
|
1953
|
-
* be available in the test environment) while still making named imports work.
|
|
1954
|
-
*
|
|
1955
|
-
* If `window.__sdk` is absent (non-dog-food build), every access throws a
|
|
1956
|
-
* descriptive error rather than returning `undefined` silently.
|
|
1957
|
-
*/
|
|
1958
|
-
function sdkRedirectPlugin() {
|
|
1959
|
-
return {
|
|
1960
|
-
name: "sdk-redirect",
|
|
1961
|
-
setup(build) {
|
|
1962
|
-
build.onResolve({ filter: SDK_IMPORT_FILTER }, (args) => ({
|
|
1963
|
-
path: args.path,
|
|
1964
|
-
namespace: "sdk-redirect"
|
|
1965
|
-
}));
|
|
1966
|
-
build.onLoad({
|
|
1967
|
-
filter: /.*/,
|
|
1968
|
-
namespace: "sdk-redirect"
|
|
1969
|
-
}, () => ({
|
|
1970
|
-
contents: `
|
|
1971
|
-
var __proxy = (typeof window !== 'undefined' && window.__sdk)
|
|
1972
|
-
? window.__sdk
|
|
1973
|
-
: new Proxy({}, {
|
|
1974
|
-
get: function(_t, p) {
|
|
1975
|
-
throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));
|
|
1976
|
-
}
|
|
1977
|
-
});
|
|
1978
|
-
module.exports = __proxy;
|
|
1979
|
-
`,
|
|
1980
|
-
loader: "js"
|
|
1981
|
-
}));
|
|
1982
|
-
}
|
|
1983
|
-
};
|
|
1984
|
-
}
|
|
1985
|
-
/**
|
|
1986
|
-
* esbuild plugin that intercepts `import … from 'vitest'` and replaces it with
|
|
1987
|
-
* a virtual module that delegates every named import to `globalThis` at ACCESS
|
|
1988
|
-
* time (not at bundle-evaluation time).
|
|
1989
|
-
*
|
|
1990
|
-
* The runtime installs `describe/it/test/expect/beforeAll/afterAll/beforeEach/
|
|
1991
|
-
* afterEach/vi` as globals inside `runTestModule`, which runs AFTER the bundle
|
|
1992
|
-
* IIFE is evaluated. A value-copy redirect (`export var describe =
|
|
1993
|
-
* globalThis.describe`) would therefore capture `undefined` at evaluation time
|
|
1994
|
-
* and the user's `describe(...)` calls would be no-ops — registering zero tests.
|
|
1995
|
-
*
|
|
1996
|
-
* The fix defers the lookup to call time using per-name **getter** exports.
|
|
1997
|
-
* We emit a CommonJS module that:
|
|
1998
|
-
* 1. sets `__esModule = true` so esbuild's `__toESM` interop maps each named
|
|
1999
|
-
* import directly to a property access on the module (NOT wrapped under a
|
|
2000
|
-
* `default` shim — which is what happens for a bare Proxy whose own-keys
|
|
2001
|
-
* are empty, leaving every named import `undefined`);
|
|
2002
|
-
* 2. defines each global name as a getter that reads `globalThis[name]` on
|
|
2003
|
-
* every access. So `import { describe } from 'vitest'` compiles to
|
|
2004
|
-
* `import_vitest.describe`, whose getter returns the real `describe` only
|
|
2005
|
-
* when the factory calls it — after `runTestModule` installs the globals.
|
|
2006
|
-
*
|
|
2007
|
-
* A plain `module.exports = new Proxy(...)` does NOT work here: esbuild routes
|
|
2008
|
-
* the virtual module through `__toESM`, which enumerates own-keys (none on an
|
|
2009
|
-
* empty Proxy target) and therefore exposes zero named exports. Explicit getter
|
|
2010
|
-
* properties give `__toESM` real keys to map while keeping access lazy.
|
|
2011
|
-
*/
|
|
2012
|
-
function vitestRedirectPlugin() {
|
|
2013
|
-
return {
|
|
2014
|
-
name: "vitest-redirect",
|
|
2015
|
-
setup(build) {
|
|
2016
|
-
build.onResolve({ filter: /^vitest$/ }, () => ({
|
|
2017
|
-
path: "vitest",
|
|
2018
|
-
namespace: "vitest-redirect"
|
|
2019
|
-
}));
|
|
2020
|
-
build.onLoad({
|
|
2021
|
-
filter: /^vitest$/,
|
|
2022
|
-
namespace: "vitest-redirect"
|
|
2023
|
-
}, () => {
|
|
2024
|
-
return {
|
|
2025
|
-
contents: `Object.defineProperty(exports, '__esModule', { value: true });\n${VITEST_GLOBAL_NAMES.map((name) => `Object.defineProperty(exports, ${JSON.stringify(name)}, { enumerable: true, get: function() { return globalThis[${JSON.stringify(name)}]; } });`).join("\n")}\n`,
|
|
2026
|
-
loader: "js"
|
|
2027
|
-
};
|
|
2028
|
-
});
|
|
2029
|
-
}
|
|
2030
|
-
};
|
|
2031
|
-
}
|
|
2032
|
-
/**
|
|
2033
|
-
* esbuild plugin that transforms the user test file into a module that exports
|
|
2034
|
-
* an async `__userFactory` function. The factory defers the user's top-level
|
|
2035
|
-
* test registration code (describe/it/test calls) so it only runs when
|
|
2036
|
-
* `runTestModule(__userFactory)` explicitly invokes it — AFTER the runtime has
|
|
2037
|
-
* installed describe/it/test/expect as globals.
|
|
2038
|
-
*
|
|
2039
|
-
* Algorithm:
|
|
2040
|
-
* - Import declarations and re-export statements are kept at module top-level
|
|
2041
|
-
* (the only valid ESM position for static `import`). A statement that spans
|
|
2042
|
-
* multiple lines — e.g. a named import with one member per line:
|
|
2043
|
-
* import {
|
|
2044
|
-
* appLogin,
|
|
2045
|
-
* getAnonymousKey,
|
|
2046
|
-
* } from '@apps-in-toss/web-framework';
|
|
2047
|
-
* is tracked as a single block: every line from the opening `import {` /
|
|
2048
|
-
* `export {` through the closing `from '…'` (or side-effect `'…'`) line is
|
|
2049
|
-
* kept together at top-level. This prevents the member lines and the
|
|
2050
|
-
* closing `} from '…'` line from leaking into the factory body, which would
|
|
2051
|
-
* leave an unterminated `import {` at module scope (the #678 env3 failure:
|
|
2052
|
-
* esbuild threw `Expected "as" but found "{"` on multi-line SDK imports).
|
|
2053
|
-
* - All other lines (describe/it/test calls, local declarations, etc.) are
|
|
2054
|
-
* moved into the body of the exported async factory function.
|
|
2055
|
-
*
|
|
2056
|
-
* This preserves SDK import resolution (the sdk-redirect plugin processes
|
|
2057
|
-
* top-level imports normally) while deferring test registration to the factory.
|
|
2058
|
-
*/
|
|
2059
|
-
function userFactoryPlugin(absPath) {
|
|
2060
|
-
const NAMESPACE = "user-test-factory";
|
|
2061
|
-
return {
|
|
2062
|
-
name: "user-test-factory",
|
|
2063
|
-
setup(build) {
|
|
2064
|
-
build.onResolve({ filter: /^user-test-factory$/ }, () => ({
|
|
2065
|
-
path: absPath,
|
|
2066
|
-
namespace: NAMESPACE
|
|
2067
|
-
}));
|
|
2068
|
-
build.onLoad({
|
|
2069
|
-
filter: /.*/,
|
|
2070
|
-
namespace: NAMESPACE
|
|
2071
|
-
}, async (args) => {
|
|
2072
|
-
const lines = (await fs.readFile(args.path, "utf8")).split("\n");
|
|
2073
|
-
const topLevelLines = [];
|
|
2074
|
-
const bodyLines = [];
|
|
2075
|
-
const EXPORT_DECLARATION_RE = /^(export\s+)(default\s+|async\s+function\s+|function\s+|class\s+|const\s+|let\s+|var\s+)/;
|
|
2076
|
-
const isImportStart = (trimmed) => trimmed.startsWith("import ") || trimmed.startsWith("import{") || trimmed.startsWith("import'") || trimmed.startsWith("import\"");
|
|
2077
|
-
const endsStatement = (trimmed) => /['"]\s*;?\s*$/.test(trimmed.replace(/\/\/.*$/, "").trimEnd());
|
|
2078
|
-
let inImportBlock = false;
|
|
2079
|
-
for (const line of lines) {
|
|
2080
|
-
const trimmed = line.trimStart();
|
|
2081
|
-
const indent = line.slice(0, line.length - trimmed.length);
|
|
2082
|
-
if (inImportBlock) {
|
|
2083
|
-
topLevelLines.push(line);
|
|
2084
|
-
if (endsStatement(trimmed)) inImportBlock = false;
|
|
2085
|
-
continue;
|
|
2086
|
-
}
|
|
2087
|
-
if (isImportStart(trimmed)) {
|
|
2088
|
-
topLevelLines.push(line);
|
|
2089
|
-
if (!endsStatement(trimmed)) inImportBlock = true;
|
|
2090
|
-
} else if (trimmed.startsWith("export ")) if (trimmed.match(EXPORT_DECLARATION_RE)) bodyLines.push(indent + trimmed.slice(7));
|
|
2091
|
-
else {
|
|
2092
|
-
topLevelLines.push(line);
|
|
2093
|
-
if (/\bfrom\b/.test(trimmed) ? !endsStatement(trimmed) : trimmed.endsWith("{")) inImportBlock = true;
|
|
2094
|
-
}
|
|
2095
|
-
else bodyLines.push(line);
|
|
2096
|
-
}
|
|
2097
|
-
return {
|
|
2098
|
-
contents: [
|
|
2099
|
-
...topLevelLines,
|
|
2100
|
-
"",
|
|
2101
|
-
"// biome-ignore lint: generated factory wrapper",
|
|
2102
|
-
"export default async function __userFactory(): Promise<void> {",
|
|
2103
|
-
...bodyLines.map((l) => ` ${l}`),
|
|
2104
|
-
"}"
|
|
2105
|
-
].join("\n"),
|
|
2106
|
-
loader: "ts",
|
|
2107
|
-
resolveDir: path.dirname(absPath)
|
|
2108
|
-
};
|
|
2109
|
-
});
|
|
2110
|
-
}
|
|
2111
|
-
};
|
|
2112
|
-
}
|
|
2113
|
-
/**
|
|
2114
|
-
* Returns the absolute path to the test-runner runtime module.
|
|
2115
|
-
*
|
|
2116
|
-
* Searches candidates in priority order:
|
|
2117
|
-
* 1. Co-located `runtime.ts` / `runtime.js` — covers the source tree
|
|
2118
|
-
* (tsx / ts-node) and the `dist/test-runner/` entry.
|
|
2119
|
-
* 2. `../test-runner/runtime.js` — covers the `dist/mcp/cli.js` entry,
|
|
2120
|
-
* where `import.meta.url` resolves to `dist/mcp/` (a sibling directory
|
|
2121
|
-
* of `dist/test-runner/`). Without this second candidate the MCP entry
|
|
2122
|
-
* point would look for `dist/mcp/runtime.js`, which does not exist, and
|
|
2123
|
-
* every `run_tests` call would fail with an esbuild "Could not resolve"
|
|
2124
|
-
* error (#678).
|
|
2125
|
-
*
|
|
2126
|
-
* Returns the first candidate that exists on disk. Falls back to the
|
|
2127
|
-
* co-located `runtime.js` path so esbuild produces a clear "file not found"
|
|
2128
|
-
* error rather than a cryptic failure.
|
|
2129
|
-
*/
|
|
2130
|
-
function getRuntimePath() {
|
|
2131
|
-
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
2132
|
-
const candidates = [
|
|
2133
|
-
path.join(dir, "runtime.ts"),
|
|
2134
|
-
path.join(dir, "runtime.js"),
|
|
2135
|
-
path.join(dir, "..", "test-runner", "runtime.js")
|
|
2136
|
-
];
|
|
2137
|
-
for (const candidate of candidates) try {
|
|
2138
|
-
accessSync(candidate);
|
|
2139
|
-
return candidate;
|
|
2140
|
-
} catch {}
|
|
2141
|
-
return path.join(dir, "runtime.js");
|
|
2142
|
-
}
|
|
2143
|
-
/**
|
|
2144
|
-
* Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.
|
|
2145
|
-
*
|
|
2146
|
-
* The IIFE installs `window.__testBundle` (or the custom `globalName`) with:
|
|
2147
|
-
* - `runTestModule` — the runtime entry (from `runtime.ts`).
|
|
2148
|
-
* - `__userFactory` — an async function wrapping the user's test registration
|
|
2149
|
-
* code so it runs AFTER `runTestModule` installs the globals.
|
|
2150
|
-
*
|
|
2151
|
-
* Callers (rpc.ts) invoke:
|
|
2152
|
-
* `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`
|
|
2153
|
-
*
|
|
2154
|
-
* @param absPath - Absolute path to the user test file.
|
|
2155
|
-
* @param opts - Optional bundling overrides.
|
|
2156
|
-
*/
|
|
2157
|
-
async function bundleTestFile(absPath, opts) {
|
|
2158
|
-
const globalName = opts?.globalName ?? "__testBundle";
|
|
2159
|
-
const extraExternals = opts?.extraExternals ?? [];
|
|
2160
|
-
const esbuild = await import("esbuild");
|
|
2161
|
-
const runtimePath = getRuntimePath();
|
|
2162
|
-
const wrapperContent = [
|
|
2163
|
-
`import { runTestModule } from ${JSON.stringify(runtimePath)};`,
|
|
2164
|
-
`import __userFactory from "user-test-factory";`,
|
|
2165
|
-
`export { runTestModule, __userFactory };`
|
|
2166
|
-
].join("\n");
|
|
2167
|
-
const result = await esbuild.build({
|
|
2168
|
-
stdin: {
|
|
2169
|
-
contents: wrapperContent,
|
|
2170
|
-
loader: "ts",
|
|
2171
|
-
resolveDir: path.dirname(absPath)
|
|
2172
|
-
},
|
|
2173
|
-
bundle: true,
|
|
2174
|
-
format: "iife",
|
|
2175
|
-
globalName,
|
|
2176
|
-
platform: "browser",
|
|
2177
|
-
target: "es2022",
|
|
2178
|
-
write: false,
|
|
2179
|
-
plugins: [
|
|
2180
|
-
userFactoryPlugin(absPath),
|
|
2181
|
-
vitestRedirectPlugin(),
|
|
2182
|
-
sdkRedirectPlugin()
|
|
2183
|
-
],
|
|
2184
|
-
external: extraExternals,
|
|
2185
|
-
treeShaking: true,
|
|
2186
|
-
footer: { js: `globalThis[${JSON.stringify(globalName)}] = ${globalName};` }
|
|
2187
|
-
});
|
|
2188
|
-
const warnings = result.warnings.map((w) => `${path.relative(process.cwd(), w.location?.file ?? "")}:${w.location?.line ?? "?"}: ${w.text}`);
|
|
2189
|
-
const outputFile = result.outputFiles?.[0];
|
|
2190
|
-
if (!outputFile) throw new Error("bundleTestFile: esbuild produced no output — check entryPoints");
|
|
2191
|
-
return {
|
|
2192
|
-
code: outputFile.text,
|
|
2193
|
-
warnings
|
|
2194
|
-
};
|
|
2195
|
-
}
|
|
2196
|
-
//#endregion
|
|
2197
|
-
//#region src/test-runner/rpc.ts
|
|
2198
|
-
/** Maximum milliseconds to wait for a single evaluate round-trip. */
|
|
2199
|
-
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
2200
|
-
/**
|
|
2201
|
-
* Wraps bundle code in a self-executing IIFE that:
|
|
2202
|
-
* 1. Evaluates the bundle (registering describe/it/test).
|
|
2203
|
-
* 2. Calls `__testBundle.runTestModule(...)` — the entry the runtime exports.
|
|
2204
|
-
* 3. Returns a JSON-serialised `RunReport` string.
|
|
2205
|
-
*
|
|
2206
|
-
* The double-serialisation (RunReport → JSON string → returnByValue string)
|
|
2207
|
-
* is intentional: CDP `returnByValue` reliably transports strings; deeply
|
|
2208
|
-
* nested objects can lose fidelity across the Chii relay.
|
|
2209
|
-
*
|
|
2210
|
-
* SECRET-HANDLING: `bundleCode` MUST NOT be logged by callers.
|
|
2211
|
-
*/
|
|
2212
|
-
function buildRunTestsExpression(bundleCode) {
|
|
2213
|
-
return `(async () => { try { ${bundleCode} } catch(e) { return JSON.stringify({ok:false,error:'bundle-eval: ' + String(e && e.message || e)}); } if (typeof globalThis.__testBundle !== 'object' || typeof globalThis.__testBundle.runTestModule !== 'function' || typeof globalThis.__testBundle.__userFactory !== 'function') { return JSON.stringify({ok:false,error:'bundle-missing-export: __testBundle.runTestModule or __userFactory is not a function'}); } try { const report = await globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory); return JSON.stringify({ok:true,value:report}); } catch(e) { return JSON.stringify({ok:false,error:'test-run: ' + String(e && e.message || e)}); }})()`;
|
|
2214
|
-
}
|
|
2215
|
-
/**
|
|
2216
|
-
* Parses the raw CDP `returnByValue` result from a `buildRunTestsExpression`
|
|
2217
|
-
* evaluate call into a typed `RpcRunResult`.
|
|
2218
|
-
*
|
|
2219
|
-
* Throws only on parse failure — an `ok:false` envelope is a normal result.
|
|
2220
|
-
*
|
|
2221
|
-
* SECRET-HANDLING: `rawValue` is not included in error messages.
|
|
2222
|
-
*/
|
|
2223
|
-
function parseRunTestsResult(rawValue) {
|
|
2224
|
-
if (typeof rawValue !== "string") throw new Error(`rpc.parseRunTestsResult: unexpected return type "${typeof rawValue}" — expected JSON string`);
|
|
2225
|
-
let parsed;
|
|
2226
|
-
try {
|
|
2227
|
-
parsed = JSON.parse(rawValue);
|
|
2228
|
-
} catch {
|
|
2229
|
-
throw new Error("rpc.parseRunTestsResult: bridge returned non-JSON string");
|
|
2230
|
-
}
|
|
2231
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("rpc.parseRunTestsResult: parsed result is not an object");
|
|
2232
|
-
const obj = parsed;
|
|
2233
|
-
if (obj.ok === true) return {
|
|
2234
|
-
ok: true,
|
|
2235
|
-
report: obj.value
|
|
2236
|
-
};
|
|
2237
|
-
if (obj.ok === false) return {
|
|
2238
|
-
ok: false,
|
|
2239
|
-
error: typeof obj.error === "string" ? obj.error : String(obj.error)
|
|
2240
|
-
};
|
|
2241
|
-
throw new Error("rpc.parseRunTestsResult: result missing \"ok\" field");
|
|
2242
|
-
}
|
|
2243
|
-
/**
|
|
2244
|
-
* Injects `bundleCode` into the attached page and awaits test execution.
|
|
2245
|
-
*
|
|
2246
|
-
* Uses `Runtime.evaluate` with `awaitPromise: true` to wait for the
|
|
2247
|
-
* async IIFE to settle. The 30-second CDP command timeout covers even
|
|
2248
|
-
* long-running test suites; split into smaller files if you hit it.
|
|
2249
|
-
*
|
|
2250
|
-
* @param connection - Active CDP connection (relay or local).
|
|
2251
|
-
* @param bundleCode - IIFE bundle string from `bundleTestFile`.
|
|
2252
|
-
* @param timeoutMs - Override the default 30 s timeout.
|
|
2253
|
-
*
|
|
2254
|
-
* SECRET-HANDLING: `bundleCode` and the raw CDP result value are never logged.
|
|
2255
|
-
*/
|
|
2256
|
-
async function injectAndRunBundle(connection, bundleCode, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
2257
|
-
const expression = buildRunTestsExpression(bundleCode);
|
|
2258
|
-
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error(`rpc: evaluate timed out after ${timeoutMs}ms`)), timeoutMs));
|
|
2259
|
-
const evalPromise = connection.send("Runtime.evaluate", {
|
|
2260
|
-
expression,
|
|
2261
|
-
returnByValue: true,
|
|
2262
|
-
awaitPromise: true
|
|
2263
|
-
});
|
|
2264
|
-
const cdpResult = await Promise.race([evalPromise, timeoutPromise]);
|
|
2265
|
-
if (cdpResult.exceptionDetails) {
|
|
2266
|
-
const msg = cdpResult.exceptionDetails.exception?.description ?? cdpResult.exceptionDetails.text ?? "Runtime.evaluate threw an exception";
|
|
2267
|
-
throw new Error(`rpc.injectAndRunBundle: ${msg}`);
|
|
2268
|
-
}
|
|
2269
|
-
return parseRunTestsResult(cdpResult.result.value);
|
|
2270
|
-
}
|
|
2271
|
-
//#endregion
|
|
2272
|
-
//#region src/test-runner/relay-worker.ts
|
|
2273
|
-
/**
|
|
2274
|
-
* Runs all `files` sequentially over the given CDP `connection`.
|
|
2275
|
-
*
|
|
2276
|
-
* For each file:
|
|
2277
|
-
* 1. Bundle with esbuild (includes SDK shim + runtime).
|
|
2278
|
-
* 2. Inject into the attached page via `Runtime.evaluate`.
|
|
2279
|
-
* 3. Await the `RunReport` JSON response.
|
|
2280
|
-
* 4. Accumulate results.
|
|
2281
|
-
*
|
|
2282
|
-
* Returns a `RelayRunReport` with per-file results and flattened totals.
|
|
2283
|
-
*
|
|
2284
|
-
* This function does NOT open or manage the relay connection — the caller
|
|
2285
|
-
* is responsible for attaching and closing it.
|
|
2286
|
-
*
|
|
2287
|
-
* TODO (#645): implement the Vitest `PoolRunnerInitializer` interface here
|
|
2288
|
-
* so that `runTestFilesOverRelay` can be used as a Vitest pool entry.
|
|
2289
|
-
*
|
|
2290
|
-
* @param connection - Active CDP connection (relay or local kind).
|
|
2291
|
-
* @param files - Absolute paths to test files, run in order.
|
|
2292
|
-
* @param opts - Optional per-run overrides.
|
|
2293
|
-
*/
|
|
2294
|
-
async function runTestFilesOverRelay(connection, files, opts) {
|
|
2295
|
-
const wallStart = Date.now();
|
|
2296
|
-
const startedAt = new Date(wallStart).toISOString();
|
|
2297
|
-
const fileResults = [];
|
|
2298
|
-
for (const file of files) {
|
|
2299
|
-
let fileEntry;
|
|
2300
|
-
try {
|
|
2301
|
-
const { code } = await bundleTestFile(file, opts?.bundleOptions);
|
|
2302
|
-
const rpcResult = await injectAndRunBundle(connection, code, opts?.timeoutMs);
|
|
2303
|
-
if (rpcResult.ok) fileEntry = {
|
|
2304
|
-
file,
|
|
2305
|
-
result: rpcResult.report
|
|
2306
|
-
};
|
|
2307
|
-
else fileEntry = {
|
|
2308
|
-
file,
|
|
2309
|
-
result: { error: rpcResult.error }
|
|
2310
|
-
};
|
|
2311
|
-
} catch (e) {
|
|
2312
|
-
fileEntry = {
|
|
2313
|
-
file,
|
|
2314
|
-
result: { error: e instanceof Error ? e.message : String(e) }
|
|
2315
|
-
};
|
|
2316
|
-
}
|
|
2317
|
-
fileResults.push(fileEntry);
|
|
2318
|
-
}
|
|
2319
|
-
const totals = fileResults.reduce((acc, { result }) => {
|
|
2320
|
-
if ("error" in result) {
|
|
2321
|
-
acc.failed += 1;
|
|
2322
|
-
acc.total += 1;
|
|
2323
|
-
} else {
|
|
2324
|
-
acc.passed += result.passed;
|
|
2325
|
-
acc.failed += result.failed;
|
|
2326
|
-
acc.skipped += result.skipped;
|
|
2327
|
-
acc.total += result.passed + result.failed + result.skipped;
|
|
2328
|
-
}
|
|
2329
|
-
return acc;
|
|
2330
|
-
}, {
|
|
2331
|
-
passed: 0,
|
|
2332
|
-
failed: 0,
|
|
2333
|
-
skipped: 0,
|
|
2334
|
-
total: 0
|
|
2335
|
-
});
|
|
2336
|
-
return {
|
|
2337
|
-
startedAt,
|
|
2338
|
-
duration: Date.now() - wallStart,
|
|
2339
|
-
files: fileResults,
|
|
2340
|
-
totals
|
|
2341
|
-
};
|
|
2342
|
-
}
|
|
2343
|
-
//#endregion
|
|
2344
|
-
//#region src/test-runner/cli.ts
|
|
2345
|
-
/**
|
|
2346
|
-
* `devtools-test` CLI.
|
|
2347
|
-
*
|
|
2348
|
-
* Shares test-file discovery with the `run_tests` MCP tool (`discoverTestFiles`)
|
|
2349
|
-
* and exposes `runWithConnection` — the pure run core that bundles, injects, and
|
|
2350
|
-
* collects each file over a CDP connection. The CLI's `main()` performs a
|
|
2351
|
-
* standalone relay attach (boot relay → QR → phone scan → cell inject → run).
|
|
2352
|
-
*
|
|
2353
|
-
* NOTE: no shebang in this source file — the tsdown entry's `banner` option
|
|
2354
|
-
* injects `#!/usr/bin/env node` into the compiled output (same pattern as
|
|
2355
|
-
* `src/mcp/cli.ts`).
|
|
2356
|
-
*/
|
|
2357
|
-
const USAGE = `
|
|
2358
|
-
devtools-test — run mini-app tests on a real device WebView over the CDP relay
|
|
2359
|
-
|
|
2360
|
-
USAGE
|
|
2361
|
-
devtools-test <glob> [<glob> ...] [options]
|
|
2362
|
-
|
|
2363
|
-
OPTIONS
|
|
2364
|
-
--scheme-url <url> intoss-private:// URL from \`ait deploy --scheme-only\`
|
|
2365
|
-
(required for standalone relay attach / env3)
|
|
2366
|
-
--timeout <ms> Per-file evaluate timeout in ms (default: 30000)
|
|
2367
|
-
--cell-sdk-line <line> SDK line to inject as __AIT_CELL__.sdkLine (2.x|3.x)
|
|
2368
|
-
--cell-platform <plat> Platform to inject as __AIT_CELL__.platform
|
|
2369
|
-
(mock|ios|android, default: AIT_CELL_PLATFORM env)
|
|
2370
|
-
--headless Disable browser auto-open (text QR only)
|
|
2371
|
-
--project-root <dir> Project root for .ait_relay secret lookup
|
|
2372
|
-
(default: current working directory)
|
|
2373
|
-
--help, -h Show this help message
|
|
2374
|
-
|
|
2375
|
-
DESCRIPTION
|
|
2376
|
-
Boots a Chii relay + cloudflared tunnel, renders a QR code, waits for a real
|
|
2377
|
-
device to scan and attach, injects the cell globals (__AIT_CELL__), bundles
|
|
2378
|
-
each matched test file with esbuild (SDK imports redirected to window.__sdk),
|
|
2379
|
-
injects the bundle into the attached WebView via Runtime.evaluate, and prints
|
|
2380
|
-
a summary.
|
|
2381
|
-
|
|
2382
|
-
The test files run against the live relay connection started by this process;
|
|
2383
|
-
no separate MCP daemon is required.
|
|
2384
|
-
|
|
2385
|
-
EXAMPLE
|
|
2386
|
-
devtools-test 'src/**/*.ait.test.ts' \\
|
|
2387
|
-
--scheme-url "intoss-private://..." \\
|
|
2388
|
-
--cell-platform ios \\
|
|
2389
|
-
--timeout 60000
|
|
2390
|
-
|
|
2391
|
-
`.trimStart();
|
|
2392
|
-
/**
|
|
2393
|
-
* Runs `files` over `connection` and returns the aggregate report.
|
|
2394
|
-
* This pure function is the testable core of the CLI (and is what the
|
|
2395
|
-
* `run_tests` MCP tool calls against the daemon's attached connection); it is
|
|
2396
|
-
* separate from `main()` so tests can call it without spawning a subprocess.
|
|
2397
|
-
*/
|
|
2398
|
-
async function runWithConnection(connection, files, opts) {
|
|
2399
|
-
const report = await runTestFilesOverRelay(connection, files, opts);
|
|
2400
|
-
if (opts?.printSummary) {
|
|
2401
|
-
const { totals } = report;
|
|
2402
|
-
process.stdout.write(`\ndevtools-test: ${totals.passed} passed, ${totals.failed} failed, ${totals.skipped} skipped (${report.duration}ms)\n`);
|
|
2403
|
-
}
|
|
2404
|
-
return report;
|
|
2405
|
-
}
|
|
2406
|
-
/**
|
|
2407
|
-
* CLI entry point.
|
|
2408
|
-
*
|
|
2409
|
-
* Performs a standalone relay attach → run lifecycle:
|
|
2410
|
-
*
|
|
2411
|
-
* 1. Parse args: globs, --timeout, --cell-sdk-line, --cell-platform,
|
|
2412
|
-
* --scheme-url (required for env3), --headless, --project-root.
|
|
2413
|
-
* 2. Discover test files; exit 1 if none.
|
|
2414
|
-
* 3. Load .ait_relay secret into AIT_DEBUG_TOTP_SECRET, then boot relay family.
|
|
2415
|
-
* 4. Assemble AttachDeps (no qrHttpServer → text QR).
|
|
2416
|
-
* 5. prepareAttach(deps, 'relay-dev', { scheme_url }, conn).
|
|
2417
|
-
* 6. renderAndMaybeWait(deps, prep, true, timeoutMs, conn) — text QR + wait.
|
|
2418
|
-
* 7. If cell flags present, injectGlobals({ __AIT_CELL__: cell }) before run.
|
|
2419
|
-
* 8. runWithConnection(conn, files, { timeoutMs, printSummary: true }).
|
|
2420
|
-
* 9. family.stop(); process.exitCode = failed > 0 ? 1 : 0.
|
|
2421
|
-
*
|
|
2422
|
-
* The CLI is not a daemon — no lock, router, SSE, or tools_list is needed.
|
|
2423
|
-
* Attach timeout exits with code 1; test failures exit with code 1.
|
|
2424
|
-
*
|
|
2425
|
-
* SECRET-HANDLING: scheme_url / relay wssUrl / TOTP codes are never written to
|
|
2426
|
-
* stdout/stderr directly. text QR renders via renderAndMaybeWait which encodes
|
|
2427
|
-
* the TOTP `at=` code inside the QR payload (not in plain log lines).
|
|
2428
|
-
*/
|
|
2429
|
-
async function main(argv = process.argv.slice(2)) {
|
|
2430
|
-
let parsed;
|
|
2431
|
-
try {
|
|
2432
|
-
parsed = parseArgs({
|
|
2433
|
-
args: argv,
|
|
2434
|
-
options: {
|
|
2435
|
-
help: {
|
|
2436
|
-
type: "boolean",
|
|
2437
|
-
short: "h"
|
|
2438
|
-
},
|
|
2439
|
-
timeout: { type: "string" },
|
|
2440
|
-
"scheme-url": { type: "string" },
|
|
2441
|
-
"cell-sdk-line": { type: "string" },
|
|
2442
|
-
"cell-platform": { type: "string" },
|
|
2443
|
-
headless: { type: "boolean" },
|
|
2444
|
-
"project-root": { type: "string" }
|
|
2445
|
-
},
|
|
2446
|
-
allowPositionals: true
|
|
2447
|
-
});
|
|
2448
|
-
} catch (e) {
|
|
2449
|
-
process.stderr.write(`devtools-test: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
2450
|
-
process.exitCode = 1;
|
|
2451
|
-
return;
|
|
2452
|
-
}
|
|
2453
|
-
if (parsed.values.help || argv.length === 0) {
|
|
2454
|
-
process.stdout.write(USAGE);
|
|
2455
|
-
return;
|
|
2456
|
-
}
|
|
2457
|
-
const vals = parsed.values;
|
|
2458
|
-
const rawTimeout = typeof vals.timeout === "string" ? vals.timeout : void 0;
|
|
2459
|
-
const timeoutMs = rawTimeout !== void 0 ? parseInt(rawTimeout, 10) : 3e4;
|
|
2460
|
-
if (Number.isNaN(timeoutMs) || timeoutMs <= 0) {
|
|
2461
|
-
process.stderr.write(`devtools-test: --timeout must be a positive integer\n`);
|
|
2462
|
-
process.exitCode = 1;
|
|
2463
|
-
return;
|
|
2464
|
-
}
|
|
2465
|
-
const schemeUrl = typeof vals["scheme-url"] === "string" ? vals["scheme-url"] : "";
|
|
2466
|
-
if (schemeUrl === "") {
|
|
2467
|
-
process.stderr.write("devtools-test: --scheme-url is required for standalone relay attach.\n Pass the intoss-private:// URL from `ait deploy --scheme-only`.\n");
|
|
2468
|
-
process.exitCode = 1;
|
|
2469
|
-
return;
|
|
2470
|
-
}
|
|
2471
|
-
const headless = vals.headless === true;
|
|
2472
|
-
const projectRoot = typeof vals["project-root"] === "string" ? vals["project-root"] : process.cwd();
|
|
2473
|
-
const cellSdkLine = typeof vals["cell-sdk-line"] === "string" ? vals["cell-sdk-line"] : void 0;
|
|
2474
|
-
const cellPlatform = typeof vals["cell-platform"] === "string" ? vals["cell-platform"] : process.env.AIT_CELL_PLATFORM;
|
|
2475
|
-
const hasCell = cellSdkLine !== void 0 || cellPlatform !== void 0;
|
|
2476
|
-
const globs = parsed.positionals;
|
|
2477
|
-
if (globs.length === 0) {
|
|
2478
|
-
process.stderr.write(`devtools-test: at least one glob pattern is required\n`);
|
|
2479
|
-
process.stdout.write(USAGE);
|
|
2480
|
-
process.exitCode = 1;
|
|
2481
|
-
return;
|
|
2482
|
-
}
|
|
2483
|
-
const files = await discoverTestFiles(globs, process.cwd());
|
|
2484
|
-
if (files.length === 0) {
|
|
2485
|
-
process.stderr.write(`devtools-test: no test files matched ${globs.join(", ")}\n`);
|
|
2486
|
-
process.exitCode = 1;
|
|
2487
|
-
return;
|
|
2488
|
-
}
|
|
2489
|
-
process.stderr.write(`devtools-test: found ${files.length} test file(s)\n`);
|
|
2490
|
-
const { loadRelaySecretReadOnly } = await import("./relay-secret-store-CmqchhR5.js");
|
|
2491
|
-
await loadRelaySecretReadOnly({ projectRoot });
|
|
2492
|
-
const { bootRelayFamily, buildRelayVerifyAuth } = await import("./debug-server-BvrESnaV.js");
|
|
2493
|
-
let family;
|
|
2494
|
-
try {
|
|
2495
|
-
family = await bootRelayFamily({ verifyAuth: buildRelayVerifyAuth() });
|
|
2496
|
-
} catch (e) {
|
|
2497
|
-
process.stderr.write(`devtools-test: failed to boot relay: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
2498
|
-
process.exitCode = 1;
|
|
2499
|
-
return;
|
|
2500
|
-
}
|
|
2501
|
-
let exitCode = 0;
|
|
2502
|
-
try {
|
|
2503
|
-
const attachDeps = {
|
|
2504
|
-
getTunnelStatus: family.getTunnelStatus ?? (() => ({
|
|
2505
|
-
up: false,
|
|
2506
|
-
wssUrl: null
|
|
2507
|
-
})),
|
|
2508
|
-
getTotpSecret: () => process.env.AIT_DEBUG_TOTP_SECRET,
|
|
2509
|
-
qrHttpServer: void 0,
|
|
2510
|
-
onAttachUrlBuilt: void 0,
|
|
2511
|
-
canOpenBrowser: () => !headless
|
|
2512
|
-
};
|
|
2513
|
-
const prep = await prepareAttach(attachDeps, "relay-dev", { scheme_url: schemeUrl }, family.connection);
|
|
2514
|
-
if (!prep.ok) {
|
|
2515
|
-
const errText = prep.error.content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
|
|
2516
|
-
process.stderr.write(`devtools-test: attach preparation failed:\n${errText}\n`);
|
|
2517
|
-
exitCode = 1;
|
|
2518
|
-
return;
|
|
2519
|
-
}
|
|
2520
|
-
const waitResult = await renderAndMaybeWait(attachDeps, prep, true, timeoutMs, family.connection);
|
|
2521
|
-
if (waitResult.isError) {
|
|
2522
|
-
const errText = waitResult.content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
|
|
2523
|
-
process.stderr.write(`devtools-test: attach timed out or failed:\n${errText}\n`);
|
|
2524
|
-
exitCode = 1;
|
|
2525
|
-
return;
|
|
2526
|
-
}
|
|
2527
|
-
for (const chunk of waitResult.content) if (chunk.type === "text") process.stdout.write(`${chunk.text}\n`);
|
|
2528
|
-
await injectDebugIndicator(family.connection);
|
|
2529
|
-
if (hasCell) {
|
|
2530
|
-
const cell = {};
|
|
2531
|
-
if (cellSdkLine !== void 0) cell.sdkLine = cellSdkLine;
|
|
2532
|
-
if (cellPlatform !== void 0) cell.platform = cellPlatform;
|
|
2533
|
-
process.stderr.write(`devtools-test: injecting __AIT_CELL__ = ${JSON.stringify(cell)}\n`);
|
|
2534
|
-
await injectGlobals(family.connection, { __AIT_CELL__: cell });
|
|
2535
|
-
}
|
|
2536
|
-
exitCode = (await runWithConnection(family.connection, files, {
|
|
2537
|
-
timeoutMs,
|
|
2538
|
-
printSummary: true
|
|
2539
|
-
})).totals.failed > 0 ? 1 : 0;
|
|
2540
|
-
} finally {
|
|
2541
|
-
family.stop();
|
|
2542
|
-
process.exitCode = exitCode;
|
|
2543
|
-
}
|
|
2544
|
-
}
|
|
2545
|
-
if (import.meta.url === new URL(process.argv[1], "file://").href) main().catch((e) => {
|
|
2546
|
-
process.stderr.write(`devtools-test: unexpected error: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
2547
|
-
process.exitCode = 1;
|
|
2548
|
-
});
|
|
2549
|
-
//#endregion
|
|
2550
|
-
export { START_ATTACH_SEGMENT_MS as a, generateAttachToken as c, startQuickTunnel as d, startTunnelHealthProbe as f, START_ATTACH_REMINT_THRESHOLD_MS as i, makeTunnelStatus as l, logInfo as m, runWithConnection as n, extractDeploymentId as o, logError as p, RELAY_SANDBOX_STALE_PAGE_MS as r, isSandboxPageFresh as s, main as t, printAttachBanner as u };
|
|
1748
|
+
export { RELAY_SANDBOX_STALE_PAGE_MS, START_ATTACH_REMINT_THRESHOLD_MS, START_ATTACH_SEGMENT_MS, startTunnelHealthProbe as a, buildIndicatorExpression, extractDeploymentId, startQuickTunnel as i, isSandboxPageFresh, makeTunnelStatus as n, logError as o, prepareAttach, printAttachBanner as r, renderAndMaybeWait, logInfo as s, generateAttachToken as t };
|
|
2551
1749
|
|
|
2552
|
-
//# sourceMappingURL=
|
|
1750
|
+
//# sourceMappingURL=attach-orchestrator-qFEMBUwb.js.map
|