@ait-co/devtools 0.1.136 → 0.1.137

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.
Files changed (45) hide show
  1. package/dist/bundle-BKqyhEK9.d.ts.map +1 -1
  2. package/dist/{debug-server-DApo0o3t.js → debug-server-BQplCzf8.js} +21 -3
  3. package/dist/debug-server-BQplCzf8.js.map +1 -0
  4. package/dist/{debug-server-BZtH2Z5b.js → debug-server-CBK7dn8O.js} +19 -1
  5. package/dist/debug-server-CBK7dn8O.js.map +1 -0
  6. package/dist/mcp/cli.js +70 -19
  7. package/dist/mcp/cli.js.map +1 -1
  8. package/dist/mcp/server.js +1 -1
  9. package/dist/panel/index.js +1 -1
  10. package/dist/{pool-C6TgrcyW.d.ts → pool-CmfvXnwh.d.ts} +2 -2
  11. package/dist/{pool-C6TgrcyW.d.ts.map → pool-CmfvXnwh.d.ts.map} +1 -1
  12. package/dist/{relay-factory-DnuTH4v-.js → relay-factory-DVzpBRxz.js} +3 -2
  13. package/dist/relay-factory-DVzpBRxz.js.map +1 -0
  14. package/dist/{relay-worker-UK0nBfDX.js → relay-worker-6fy1eaIo.js} +6 -3
  15. package/dist/{relay-worker-UK0nBfDX.js.map → relay-worker-6fy1eaIo.js.map} +1 -1
  16. package/dist/{relay-worker-Dw3aIzIK.d.ts → relay-worker-DJnZkXza.d.ts} +41 -7
  17. package/dist/relay-worker-DJnZkXza.d.ts.map +1 -0
  18. package/dist/test-runner/bin.js +108 -27
  19. package/dist/test-runner/bin.js.map +1 -1
  20. package/dist/test-runner/bridge-stub.d.ts +125 -0
  21. package/dist/test-runner/bridge-stub.d.ts.map +1 -0
  22. package/dist/test-runner/bridge-stub.js +92 -0
  23. package/dist/test-runner/bridge-stub.js.map +1 -0
  24. package/dist/test-runner/bundle.js +44 -14
  25. package/dist/test-runner/bundle.js.map +1 -1
  26. package/dist/test-runner/config.d.ts +11 -1
  27. package/dist/test-runner/config.d.ts.map +1 -1
  28. package/dist/test-runner/config.js +1 -1
  29. package/dist/test-runner/pool.d.ts +1 -1
  30. package/dist/test-runner/pool.js +1 -1
  31. package/dist/test-runner/relay-factory.d.ts +10 -0
  32. package/dist/test-runner/relay-factory.d.ts.map +1 -1
  33. package/dist/test-runner/relay-factory.js +2 -1
  34. package/dist/test-runner/relay-factory.js.map +1 -1
  35. package/dist/test-runner/relay-worker.d.ts +1 -1
  36. package/dist/test-runner/relay-worker.js +1 -1
  37. package/dist/test-runner/report.d.ts +41 -15
  38. package/dist/test-runner/report.d.ts.map +1 -1
  39. package/dist/test-runner/report.js +33 -9
  40. package/dist/test-runner/report.js.map +1 -1
  41. package/package.json +1 -1
  42. package/dist/debug-server-BZtH2Z5b.js.map +0 -1
  43. package/dist/debug-server-DApo0o3t.js.map +0 -1
  44. package/dist/relay-factory-DnuTH4v-.js.map +0 -1
  45. package/dist/relay-worker-Dw3aIzIK.d.ts.map +0 -1
@@ -210,6 +210,7 @@ function createRelayConnectionFactory(opts) {
210
210
  }
211
211
  await injectDebugIndicator(booted.connection);
212
212
  if (opts.cell !== void 0) await injectGlobals(booted.connection, { __AIT_CELL__: opts.cell });
213
+ if (opts.stubBlocking === true) await injectGlobals(booted.connection, { __AIT_STUB_BLOCKING__: true });
213
214
  return booted.connection;
214
215
  },
215
216
  onSessionPhase(next) {
@@ -906,6 +907,16 @@ const SDK_IMPORT_FILTER = new RegExp(`^${SDK_PACKAGE.replace(/[.*+?^${}()|[\]\\]
906
907
  *
907
908
  * If `window.__sdk` is absent (non-dog-food build), every access throws a
908
909
  * descriptive error rather than returning `undefined` silently.
910
+ *
911
+ * Bridge-stub interception (devtools#740, DT-2): the virtual module runs the
912
+ * resolved `window.__sdk` object through `wrapSdkWithStub` before exporting
913
+ * it. `wrapSdkWithStub` is a pass-through (returns the same object,
914
+ * unwrapped) unless `isStubBlockingEnabled()` reads `true` off
915
+ * `globalThis.__AIT_STUB_BLOCKING__` — an opt-in flag injected the same way
916
+ * `__AIT_CELL__` is (`cell.ts#injectGlobals`, wired from the CLI's
917
+ * `--stub-blocking` flag). Default off => zero behavior change: every existing
918
+ * (non-stub) test run resolves the exact same `window.__sdk` reference it did
919
+ * before this plugin learned about stubbing.
909
920
  */
910
921
  function sdkRedirectPlugin() {
911
922
  return {
@@ -920,16 +931,19 @@ function sdkRedirectPlugin() {
920
931
  namespace: "sdk-redirect"
921
932
  }, () => ({
922
933
  contents: `
923
- var __proxy = (typeof window !== 'undefined' && window.__sdk)
934
+ import { wrapSdkWithStub, isStubBlockingEnabled } from ${JSON.stringify(getBridgeStubPath())};
935
+ var __rawSdk = (typeof window !== 'undefined' && window.__sdk)
924
936
  ? window.__sdk
925
937
  : new Proxy({}, {
926
938
  get: function(_t, p) {
927
939
  throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));
928
940
  }
929
941
  });
942
+ var __proxy = wrapSdkWithStub(__rawSdk, isStubBlockingEnabled());
930
943
  module.exports = __proxy;
931
944
  `,
932
- loader: "js"
945
+ loader: "js",
946
+ resolveDir: process.cwd()
933
947
  }));
934
948
  }
935
949
  };
@@ -1063,8 +1077,9 @@ function userFactoryPlugin(absPath) {
1063
1077
  };
1064
1078
  }
1065
1079
  /**
1066
- * Returns the absolute filesystem path to the test-runner runtime module
1067
- * (dist/test-runner/runtime.js — a fully self-contained page-side bundle).
1080
+ * Returns the absolute filesystem path to a page-side leaf module shipped
1081
+ * alongside `bundle.ts` in dist (e.g. `runtime.js`, `bridge-stub.js`) — a
1082
+ * fully self-contained module with no further page-side deps of its own.
1068
1083
  *
1069
1084
  * Rolldown code-splitting duplicates this bundling logic into shared chunks
1070
1085
  * emitted at ARBITRARY dist depths: the `devtools-test` CLI pulls it from
@@ -1074,26 +1089,30 @@ function userFactoryPlugin(absPath) {
1074
1089
  * is therefore wrong from at least one chunk — the live #697 regression.
1075
1090
  *
1076
1091
  * This resolves WITHOUT assuming chunk depth: from `import.meta.url`'s dir it
1077
- * probes the co-located `runtime.js` and the nested `test-runner/runtime.js`,
1092
+ * probes the co-located `<name>.js` and the nested `test-runner/<name>.js`,
1078
1093
  * then ascends one directory at a time (bounded) repeating both probes. The
1079
- * nested probe catches dist/test-runner/runtime.js from the dist/ root level no
1094
+ * nested probe catches dist/test-runner/<name>.js from the dist/ root level no
1080
1095
  * matter which depth the chunk was hoisted to (root, dist/mcp/, or a future
1081
- * relocation). The build always emits dist/test-runner/runtime.js (tsdown entry
1082
- * `'test-runner/runtime'`; guarded by scripts/check-test-runner-dist.sh).
1096
+ * relocation). The build always emits dist/test-runner/<name>.js (tsdown entry
1097
+ * `'test-runner/<name>'`; guarded by scripts/check-test-runner-dist.sh for
1098
+ * `runtime`).
1083
1099
  *
1084
1100
  * An ABSOLUTE path is returned deliberately: esbuild loads it as a literal file
1085
1101
  * read, bypassing Node module resolution entirely, so this works identically in
1086
1102
  * the npx-daemon context (its own dist tree) and the consumer-CLI context
1087
1103
  * (the mini-app's installed @ait-co/devtools dist) — neither needs the package
1088
1104
  * to be node-resolvable from the caller.
1105
+ *
1106
+ * @param moduleName - The leaf module's basename without extension, e.g.
1107
+ * `'runtime'` or `'bridge-stub'`.
1089
1108
  */
1090
- function getRuntimePath() {
1109
+ function getPageSideModulePath(moduleName) {
1091
1110
  const startDir = path$1.dirname(fileURLToPath(import.meta.url));
1092
1111
  const RELATIVE_PROBES = [
1093
- ["runtime.js"],
1094
- ["test-runner", "runtime.js"],
1095
- ["runtime.ts"],
1096
- ["test-runner", "runtime.ts"]
1112
+ [`${moduleName}.js`],
1113
+ ["test-runner", `${moduleName}.js`],
1114
+ [`${moduleName}.ts`],
1115
+ ["test-runner", `${moduleName}.ts`]
1097
1116
  ];
1098
1117
  let dir = startDir;
1099
1118
  for (let i = 0; i < 12; i++) {
@@ -1108,7 +1127,19 @@ function getRuntimePath() {
1108
1127
  if (parent === dir) break;
1109
1128
  dir = parent;
1110
1129
  }
1111
- return path$1.join(startDir, "runtime.js");
1130
+ return path$1.join(startDir, `${moduleName}.js`);
1131
+ }
1132
+ /** Absolute path to the test-runner page-side runtime (describe/it/test/expect). */
1133
+ function getRuntimePath() {
1134
+ return getPageSideModulePath("runtime");
1135
+ }
1136
+ /**
1137
+ * Absolute path to the bridge-stub interceptor (devtools#740, DT-2) —
1138
+ * `wrapSdkWithStub`/`isStubBlockingEnabled`, statically imported by the
1139
+ * `sdk-redirect` virtual module.
1140
+ */
1141
+ function getBridgeStubPath() {
1142
+ return getPageSideModulePath("bridge-stub");
1112
1143
  }
1113
1144
  /**
1114
1145
  * Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.
@@ -1382,6 +1413,7 @@ async function runTestFilesOverRelay(connection, files, opts) {
1382
1413
  const manualFiles = opts?.manualFiles;
1383
1414
  const manualTotal = manualFiles?.size ?? 0;
1384
1415
  let manualIndex = 0;
1416
+ const stubBlockingFiles = opts?.stubBlockingFiles;
1385
1417
  let preflightPermissions;
1386
1418
  if (files.length > 0) preflightPermissions = await runPermissionPreflight(connection);
1387
1419
  try {
@@ -1390,7 +1422,8 @@ async function runTestFilesOverRelay(connection, files, opts) {
1390
1422
  await attemptReconnect("next-file");
1391
1423
  pendingReconnectCheck = false;
1392
1424
  }
1393
- const isManual = manualFiles?.has(file) === true;
1425
+ const isStubbed = stubBlockingFiles?.has(file) === true;
1426
+ const isManual = !isStubbed && manualFiles?.has(file) === true;
1394
1427
  if (isManual) {
1395
1428
  manualIndex += 1;
1396
1429
  opts?.onManualFile?.(file, manualIndex, manualTotal);
@@ -1457,7 +1490,8 @@ async function runTestFilesOverRelay(connection, files, opts) {
1457
1490
  };
1458
1491
  }
1459
1492
  if ("error" in fileEntry.result && isRelayDisconnectMessage(fileEntry.result.error)) pendingReconnectCheck = true;
1460
- if (isManual) fileEntry.mode = "manual";
1493
+ if (isStubbed) fileEntry.mode = "stubbed";
1494
+ else if (isManual) fileEntry.mode = "manual";
1461
1495
  fileResults.push(fileEntry);
1462
1496
  }
1463
1497
  } finally {
@@ -1573,7 +1607,7 @@ function serializeRelayReport(report, meta) {
1573
1607
  ...report.preflight ? { preflight: report.preflight } : {},
1574
1608
  files: report.files.map((f) => {
1575
1609
  const file = relativise(meta.projectRoot, f.file);
1576
- const modeField = f.mode === "manual" ? { mode: "manual" } : {};
1610
+ const modeField = f.mode === "manual" || f.mode === "stubbed" ? { mode: f.mode } : {};
1577
1611
  if ("error" in f.result) return {
1578
1612
  file,
1579
1613
  error: f.result.error,
@@ -1605,11 +1639,21 @@ function serializeRelayReport(report, meta) {
1605
1639
  * `<dir>/<sdkLine>.<platform>.manual.json` artifact instead of the standard
1606
1640
  * one — the standard `<sdkLine>.<platform>.json` filename is reserved for the
1607
1641
  * regular (unattended) files only, so a manual run's presence never mutates
1608
- * what the unattended-baseline filename means. If ALL files in the run are
1609
- * regular, only the standard artifact is written (today's behavior,
1610
- * unchanged). If a run mixes both (regular files + `--manual-blocking`
1611
- * scheduled last), BOTH artifacts are written the manual one ALONGSIDE, not
1612
- * replacing, the standard one.
1642
+ * what the unattended-baseline filename means.
1643
+ *
1644
+ * Stub-blocking provenance (devtools#740, DT-2): `mode: 'stubbed'` files are
1645
+ * written to a THIRD, separate `<dir>/<sdkLine>.<platform>.stubbed.json`
1646
+ * artifact — never merged into the standard file (it is unattended, like the
1647
+ * standard file, so a naive filename split could conflate the two) and never
1648
+ * merged into `.manual.json` (it did NOT have a human present). Its body is
1649
+ * ALSO stamped `cell.bridgeStub: true` — the mandatory HYBRID-cell provenance
1650
+ * from the issue: a stubbed result must never be silently mixed into the
1651
+ * real-device baseline report.
1652
+ *
1653
+ * If ALL files in the run are regular, only the standard artifact is written
1654
+ * (today's behavior, unchanged). Any combination of the three modes present
1655
+ * in a single run writes ONLY the corresponding artifacts — each is additive,
1656
+ * never replacing another.
1613
1657
  *
1614
1658
  * SECRET-HANDLING: the written body contains no relay/secret fields (the schema
1615
1659
  * has none). `dir`/`projectRoot` are local filesystem paths, never logged here.
@@ -1618,14 +1662,16 @@ function serializeRelayReport(report, meta) {
1618
1662
  * @param dir - Output directory (created recursively if missing).
1619
1663
  * @param meta - Cell axes + projectRoot.
1620
1664
  * @returns The absolute path(s) written, in order: standard first (if any
1621
- * regular files ran), then manual (if any manual files ran). At least one
1622
- * path is always returned when `report.files` is non-empty.
1665
+ * regular files ran), then manual (if any manual files ran), then stubbed
1666
+ * (if any stubbed files ran). At least one path is always returned when
1667
+ * `report.files` is non-empty.
1623
1668
  */
1624
1669
  async function writeReportArtifact(report, dir, meta) {
1625
1670
  const serialised = serializeRelayReport(report, meta);
1626
1671
  await mkdir(dir, { recursive: true });
1627
- const regularFiles = serialised.files.filter((f) => f.mode !== "manual");
1672
+ const regularFiles = serialised.files.filter((f) => f.mode === void 0);
1628
1673
  const manualFiles = serialised.files.filter((f) => f.mode === "manual");
1674
+ const stubbedFiles = serialised.files.filter((f) => f.mode === "stubbed");
1629
1675
  const written = [];
1630
1676
  if (regularFiles.length > 0 || serialised.files.length === 0) {
1631
1677
  const outFile = path.join(dir, `${meta.sdkLine}.${meta.platform}.json`);
@@ -1643,6 +1689,18 @@ async function writeReportArtifact(report, dir, meta) {
1643
1689
  }, null, 2)}\n`, "utf8");
1644
1690
  written.push(manualOutFile);
1645
1691
  }
1692
+ if (stubbedFiles.length > 0) {
1693
+ const stubbedOutFile = path.join(dir, `${meta.sdkLine}.${meta.platform}.stubbed.json`);
1694
+ await writeFile(stubbedOutFile, `${JSON.stringify({
1695
+ ...serialised,
1696
+ cell: {
1697
+ ...serialised.cell,
1698
+ bridgeStub: true
1699
+ },
1700
+ files: stubbedFiles
1701
+ }, null, 2)}\n`, "utf8");
1702
+ written.push(stubbedOutFile);
1703
+ }
1646
1704
  return written;
1647
1705
  }
1648
1706
  /**
@@ -1822,6 +1880,24 @@ OPTIONS
1822
1880
  each manual file's report entry is stamped
1823
1881
  mode: 'manual' — never diff a manual run against an
1824
1882
  unattended baseline as if they were equivalent.
1883
+ --stub-blocking Run manual-tagged test files (*.manual.ait.test.ts)
1884
+ UNATTENDED (devtools#740, DT-2) by intercepting a
1885
+ fixed allowlist of blocking-UI SDK calls (ads
1886
+ show*, openPermissionDialog/requestPermission,
1887
+ saveBase64Data) in the page and answering them from
1888
+ fixtures captured by a real --manual-blocking run,
1889
+ instead of forwarding them to native UI. Implies
1890
+ --manual-blocking (manual files are included in the
1891
+ run); no human presence or QR-dashboard prompt is
1892
+ needed for them. HYBRID cell, not pure env3 — every
1893
+ other SDK call in the same run still hits the real
1894
+ native bridge. With --report-dir, files that ran
1895
+ under the stub are written to a SEPARATE
1896
+ <sdkLine>.<platform>.stubbed.json artifact (never
1897
+ merged into the standard or .manual.json report) and
1898
+ the report body is stamped cell.bridgeStub: true —
1899
+ never diff a stubbed run against a real device
1900
+ baseline (manual or unattended) as if equivalent.
1825
1901
  --help, -h Show this help message
1826
1902
 
1827
1903
  DESCRIPTION
@@ -2000,7 +2076,8 @@ async function main(argv = process.argv.slice(2)) {
2000
2076
  "no-qr-stdout": { type: "boolean" },
2001
2077
  headless: { type: "boolean" },
2002
2078
  "project-root": { type: "string" },
2003
- "manual-blocking": { type: "boolean" }
2079
+ "manual-blocking": { type: "boolean" },
2080
+ "stub-blocking": { type: "boolean" }
2004
2081
  },
2005
2082
  allowPositionals: true
2006
2083
  });
@@ -2037,7 +2114,8 @@ async function main(argv = process.argv.slice(2)) {
2037
2114
  return;
2038
2115
  }
2039
2116
  const suppressQr = shouldSuppressQr(vals["no-qr-stdout"] === true);
2040
- const manualBlocking = vals["manual-blocking"] === true;
2117
+ const stubBlocking = vals["stub-blocking"] === true;
2118
+ const manualBlocking = vals["manual-blocking"] === true || stubBlocking;
2041
2119
  const cellSdkLine = typeof vals["cell-sdk-line"] === "string" ? vals["cell-sdk-line"] : void 0;
2042
2120
  const cellPlatform = typeof vals["cell-platform"] === "string" ? vals["cell-platform"] : process.env.AIT_CELL_PLATFORM;
2043
2121
  const hasCell = cellSdkLine !== void 0 || cellPlatform !== void 0;
@@ -2063,6 +2141,7 @@ async function main(argv = process.argv.slice(2)) {
2063
2141
  const manualFileSet = new Set(manual);
2064
2142
  process.stderr.write(manualBlocking && manual.length > 0 ? `devtools-test: found ${regular.length} regular + ${manual.length} manual (${MANUAL_TEST_SUFFIX}) test file(s)\n` : `devtools-test: found ${files.length} test file(s)\n`);
2065
2143
  if (hasCell) process.stderr.write(`devtools-test: injecting __AIT_CELL__ = ${JSON.stringify(cell)}\n`);
2144
+ if (stubBlocking) process.stderr.write("devtools-test: --stub-blocking enabled — blocking-UI calls in manual-tagged files will be answered from fixtures (devtools#740), not forwarded to native UI\n");
2066
2145
  const factory = createRelayConnectionFactory({
2067
2146
  schemeUrl,
2068
2147
  projectRoot,
@@ -2070,6 +2149,7 @@ async function main(argv = process.argv.slice(2)) {
2070
2149
  ...dashboardPort !== void 0 ? { dashboardPort } : {},
2071
2150
  headless,
2072
2151
  cell: hasCell ? cell : void 0,
2152
+ stubBlocking,
2073
2153
  onQrContent: (chunks) => {
2074
2154
  if (suppressQr) {
2075
2155
  process.stdout.write("QR suppressed (non-interactive)\n");
@@ -2094,6 +2174,7 @@ async function main(argv = process.argv.slice(2)) {
2094
2174
  printSummary: true,
2095
2175
  collectCaptures: reportDir !== void 0,
2096
2176
  manualFiles: manualBlocking && manualFileSet.size > 0 ? manualFileSet : void 0,
2177
+ stubBlockingFiles: stubBlocking && manualFileSet.size > 0 ? manualFileSet : void 0,
2097
2178
  onManualFile: (file, index, total) => {
2098
2179
  const name = basename(file);
2099
2180
  process.stdout.write(`수동 단계: ${name} — 폰에서 네이티브 시트가 뜨면 안내에 따라 조작하세요 (${index}/${total})\n`);