@ait-co/devtools 0.1.131 → 0.1.132

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 (62) hide show
  1. package/dist/{debug-server-CD4zYBq4.js → debug-server-BBUzxZSf.js} +2 -2
  2. package/dist/{debug-server-CD4zYBq4.js.map → debug-server-BBUzxZSf.js.map} +1 -1
  3. package/dist/{debug-server-qRSRG7YZ.js → debug-server-Ch5p0ryv.js} +20 -2
  4. package/dist/debug-server-Ch5p0ryv.js.map +1 -0
  5. package/dist/{debug-server-BSbfnFR5.js → debug-server-DoWAtTNj.js} +22 -4
  6. package/dist/debug-server-DoWAtTNj.js.map +1 -0
  7. package/dist/mcp/cli.js +72 -7
  8. package/dist/mcp/cli.js.map +1 -1
  9. package/dist/mcp/server.js +1 -1
  10. package/dist/panel/index.js +1 -1
  11. package/dist/{pool-BaSCZALv.d.ts → pool-BtLh7U1y.d.ts} +16 -2
  12. package/dist/{pool-BaSCZALv.d.ts.map → pool-BtLh7U1y.d.ts.map} +1 -1
  13. package/dist/{qr-http-server-BxoRp2w7.cjs → qr-http-server-DX_9UA-p.cjs} +3 -2
  14. package/dist/qr-http-server-DX_9UA-p.cjs.map +1 -0
  15. package/dist/{qr-http-server-BdNGPRoL.js → qr-http-server-DeRrKp_R.js} +3 -2
  16. package/dist/{qr-http-server-cKbciY5k.js.map → qr-http-server-DeRrKp_R.js.map} +1 -1
  17. package/dist/{qr-http-server-DCbjORFa.js → qr-http-server-FVn61xAq.js} +3 -3
  18. package/dist/{qr-http-server-BdNGPRoL.js.map → qr-http-server-FVn61xAq.js.map} +1 -1
  19. package/dist/{qr-http-server-cKbciY5k.js → qr-http-server-KB147M-S.js} +4 -2
  20. package/dist/{qr-http-server-DCbjORFa.js.map → qr-http-server-KB147M-S.js.map} +1 -1
  21. package/dist/{qr-http-server-Bq9zR7-z.js → qr-http-server-V0xuU5TJ.js} +3 -2
  22. package/dist/{qr-http-server-Bq9zR7-z.js.map → qr-http-server-V0xuU5TJ.js.map} +1 -1
  23. package/dist/{qr-http-server-CV6TA9ZN.cjs → qr-http-server-iH_Hh_si.cjs} +3 -2
  24. package/dist/qr-http-server-iH_Hh_si.cjs.map +1 -0
  25. package/dist/{relay-factory-Cloog1jb.js → relay-factory-cNgPfrWg.js} +10 -4
  26. package/dist/relay-factory-cNgPfrWg.js.map +1 -0
  27. package/dist/{relay-worker-o0M2ndyJ.d.ts → relay-worker-BMSnlyls.d.ts} +39 -2
  28. package/dist/relay-worker-BMSnlyls.d.ts.map +1 -0
  29. package/dist/{relay-worker-CCAHLC4a.js → relay-worker-l34xNuci.js} +20 -3
  30. package/dist/{relay-worker-CCAHLC4a.js.map → relay-worker-l34xNuci.js.map} +1 -1
  31. package/dist/test-runner/bin.js +160 -20
  32. package/dist/test-runner/bin.js.map +1 -1
  33. package/dist/test-runner/config.d.ts +1 -1
  34. package/dist/test-runner/config.js +1 -1
  35. package/dist/test-runner/pool.d.ts +1 -1
  36. package/dist/test-runner/pool.js +1 -1
  37. package/dist/test-runner/pool.js.map +1 -1
  38. package/dist/test-runner/relay-factory.d.ts +14 -0
  39. package/dist/test-runner/relay-factory.d.ts.map +1 -1
  40. package/dist/test-runner/relay-factory.js +9 -3
  41. package/dist/test-runner/relay-factory.js.map +1 -1
  42. package/dist/test-runner/relay-worker.d.ts +2 -2
  43. package/dist/test-runner/relay-worker.js +2 -2
  44. package/dist/test-runner/report.d.ts +26 -4
  45. package/dist/test-runner/report.d.ts.map +1 -1
  46. package/dist/test-runner/report.js +41 -7
  47. package/dist/test-runner/report.js.map +1 -1
  48. package/dist/{tunnel-B2UCKAc7.cjs → tunnel-DP453L4e.cjs} +2 -2
  49. package/dist/{tunnel-B2UCKAc7.cjs.map → tunnel-DP453L4e.cjs.map} +1 -1
  50. package/dist/{tunnel-C-rQ34dM.js → tunnel-Djf-zmjJ.js} +2 -2
  51. package/dist/{tunnel-C-rQ34dM.js.map → tunnel-Djf-zmjJ.js.map} +1 -1
  52. package/dist/unplugin/index.cjs +1 -1
  53. package/dist/unplugin/index.js +1 -1
  54. package/dist/unplugin/tunnel.cjs +1 -1
  55. package/dist/unplugin/tunnel.js +1 -1
  56. package/package.json +1 -1
  57. package/dist/debug-server-BSbfnFR5.js.map +0 -1
  58. package/dist/debug-server-qRSRG7YZ.js.map +0 -1
  59. package/dist/qr-http-server-BxoRp2w7.cjs.map +0 -1
  60. package/dist/qr-http-server-CV6TA9ZN.cjs.map +0 -1
  61. package/dist/relay-factory-Cloog1jb.js.map +0 -1
  62. package/dist/relay-worker-o0M2ndyJ.d.ts.map +0 -1
@@ -22,6 +22,23 @@ import { fileURLToPath } from "node:url";
22
22
  * safe to import from the MCP daemon graph.
23
23
  */
24
24
  /**
25
+ * Filename suffix that opts a test file into manual-variant scheduling
26
+ * (devtools#741). A file named `<name>.manual.ait.test.ts` is EXCLUDED from
27
+ * `discoverTestFiles`'s default output (so existing unattended runs are
28
+ * unaffected — the zero-diff-when-off constraint) and is only surfaced when
29
+ * the caller explicitly asks for manual files via {@link partitionManualTests}
30
+ * (wired from the CLI's `--manual-blocking` flag).
31
+ *
32
+ * This is the entire tagging contract for v1 — no separate manifest/config,
33
+ * just a filename convention. Documented here + in the CLI `--help` text
34
+ * (cli.ts USAGE) and the test-runner README.
35
+ */
36
+ const MANUAL_TEST_SUFFIX = ".manual.ait.test.ts";
37
+ /** True when `file`'s basename ends with {@link MANUAL_TEST_SUFFIX}. */
38
+ function isManualTestFile(file) {
39
+ return basename(file).endsWith(MANUAL_TEST_SUFFIX);
40
+ }
41
+ /**
25
42
  * Expands `patterns` (globs or plain paths) into a sorted, de-duplicated list of
26
43
  * ABSOLUTE test file paths, resolved relative to `cwd`.
27
44
  *
@@ -30,14 +47,46 @@ import { fileURLToPath } from "node:url";
30
47
  * resolved against `cwd`. `bundleTestFile` requires an absolute path, so the
31
48
  * absolute output feeds it directly.
32
49
  *
50
+ * By default, files matching {@link MANUAL_TEST_SUFFIX} are EXCLUDED from the
51
+ * result (devtools#741) — blocking-UI tests opt in via that filename
52
+ * convention and must never appear in an unattended run unless the caller
53
+ * explicitly asks for them via `includeManual: true`. This keeps the
54
+ * default (flag-off) discovery path byte-for-byte identical to before this
55
+ * option existed.
56
+ *
33
57
  * @param patterns Glob patterns or file paths (e.g. `['src/**\/*.ait.test.ts']`).
34
58
  * @param cwd Base directory for relative patterns/results.
59
+ * @param opts `{ includeManual }` — when true, manual-tagged files are kept
60
+ * in the (still-sorted) output instead of being filtered out.
61
+ * Use {@link partitionManualTests} to separate + reorder them.
35
62
  * @returns Sorted, de-duplicated absolute file paths. Empty when nothing matches.
36
63
  */
37
- async function discoverTestFiles(patterns, cwd) {
64
+ async function discoverTestFiles(patterns, cwd, opts) {
38
65
  const out = /* @__PURE__ */ new Set();
39
66
  for await (const match of glob(patterns, { cwd })) out.add(isAbsolute(match) ? match : resolve(cwd, match));
40
- return [...out].sort();
67
+ const sorted = [...out].sort();
68
+ if (opts?.includeManual) return sorted;
69
+ return sorted.filter((f) => !isManualTestFile(f));
70
+ }
71
+ /**
72
+ * Splits an already-discovered file list into `{ regular, manual }`, each
73
+ * still sorted. Used by the CLI's `--manual-blocking` path to schedule manual
74
+ * files strictly AFTER every regular file (devtools#741) — regular files run
75
+ * first (and produce the unattended-shaped part of the report) and manual
76
+ * files run last, each preceded by a dashboard prompt.
77
+ *
78
+ * Pure partition — does not itself decide inclusion; call
79
+ * `discoverTestFiles(patterns, cwd, { includeManual: true })` first so manual
80
+ * files are present in `files` to partition.
81
+ */
82
+ function partitionManualTests(files) {
83
+ const regular = [];
84
+ const manual = [];
85
+ for (const f of files) (isManualTestFile(f) ? manual : regular).push(f);
86
+ return {
87
+ regular,
88
+ manual
89
+ };
41
90
  }
42
91
  //#endregion
43
92
  //#region src/test-runner/relay-factory.ts
@@ -65,12 +114,13 @@ function createRelayConnectionFactory(opts) {
65
114
  let family;
66
115
  let qrServer;
67
116
  let phase = "active";
117
+ let manualPrompt = null;
68
118
  return {
69
119
  async open() {
70
120
  const { prepareAttach, renderAndMaybeWait, mintAttachUrl } = await import("../attach-orchestrator-ca34r46y.js");
71
121
  const { injectDebugIndicator, injectGlobals } = await import("../cell-rr6gL9AT.js");
72
122
  const { loadRelaySecretReadOnly } = await import("../relay-secret-store-CFc9n0OA.js");
73
- const { bootRelayFamily, buildRelayVerifyAuth } = await import("../debug-server-CD4zYBq4.js");
123
+ const { bootRelayFamily, buildRelayVerifyAuth } = await import("../debug-server-BBUzxZSf.js");
74
124
  await loadRelaySecretReadOnly({ projectRoot });
75
125
  let resolveTunnelUp;
76
126
  const tunnelReady = new Promise((resolve) => {
@@ -100,13 +150,14 @@ function createRelayConnectionFactory(opts) {
100
150
  canOpenBrowser: () => !headless
101
151
  };
102
152
  try {
103
- const { startQrHttpServer } = await import("../qr-http-server-DCbjORFa.js");
153
+ const { startQrHttpServer } = await import("../qr-http-server-KB147M-S.js");
104
154
  const getDashboardState = () => ({
105
155
  tunnel: attachDeps.getTunnelStatus(),
106
156
  pages: null,
107
157
  attachUrl: lastAttachParts ? mintAttachUrl(attachDeps, lastAttachParts) : null,
108
158
  mode: "relay-dev",
109
- phase
159
+ phase,
160
+ manualPrompt
110
161
  });
111
162
  qrServer = await startQrHttpServer(getDashboardState);
112
163
  attachDeps.qrHttpServer = qrServer;
@@ -163,6 +214,10 @@ function createRelayConnectionFactory(opts) {
163
214
  phase = next;
164
215
  qrServer?.notifyStateChange();
165
216
  },
217
+ onManualPrompt(next) {
218
+ manualPrompt = next;
219
+ qrServer?.notifyStateChange();
220
+ },
166
221
  async close(connection) {
167
222
  if (phase !== "complete") {
168
223
  phase = "complete";
@@ -1316,6 +1371,13 @@ async function injectAndRunBundle(connection, bundleCode, timeoutMs = DEFAULT_TI
1316
1371
  //#endregion
1317
1372
  //#region src/test-runner/relay-worker.ts
1318
1373
  /**
1374
+ * Per-file evaluate timeout (ms) for manual-variant files (devtools#741) — a
1375
+ * human is expected to be tapping through a native sheet, so the timeout must
1376
+ * be far longer than the unattended default (60s, rpc.ts DEFAULT_TIMEOUT_MS).
1377
+ * Fixed constant (not configurable in v1) — documented here + CLI `--help`.
1378
+ */
1379
+ const MANUAL_FILE_TIMEOUT_MS = 5 * 6e4;
1380
+ /**
1319
1381
  * Sentinel string embedded in the error message by `injectAndRunBundle` when
1320
1382
  * the per-file evaluate race hits the timeout. Used by the retry guard so only
1321
1383
  * genuine timeouts get a second attempt — not bundle errors or parse failures.
@@ -1386,12 +1448,21 @@ async function runTestFilesOverRelay(connection, files, opts) {
1386
1448
  process.stderr.write(`relay-worker: reconnect failed (${e instanceof Error ? e.message : String(e)}) — continuing, remaining files may fail\n`);
1387
1449
  }
1388
1450
  };
1451
+ const manualFiles = opts?.manualFiles;
1452
+ const manualTotal = manualFiles?.size ?? 0;
1453
+ let manualIndex = 0;
1389
1454
  try {
1390
1455
  for (const file of files) {
1391
1456
  if (pendingReconnectCheck) {
1392
1457
  await attemptReconnect("next-file");
1393
1458
  pendingReconnectCheck = false;
1394
1459
  }
1460
+ const isManual = manualFiles?.has(file) === true;
1461
+ if (isManual) {
1462
+ manualIndex += 1;
1463
+ opts?.onManualFile?.(file, manualIndex, manualTotal);
1464
+ }
1465
+ const fileTimeoutMs = isManual ? MANUAL_FILE_TIMEOUT_MS : opts?.timeoutMs;
1395
1466
  let fileEntry;
1396
1467
  try {
1397
1468
  const { code } = await bundleTestFile(file, opts?.bundleOptions);
@@ -1417,7 +1488,7 @@ async function runTestFilesOverRelay(connection, files, opts) {
1417
1488
  const attempt = async () => {
1418
1489
  let rpcResult;
1419
1490
  try {
1420
- rpcResult = await injectAndRunBundle(connection, code, opts?.timeoutMs);
1491
+ rpcResult = await injectAndRunBundle(connection, code, fileTimeoutMs);
1421
1492
  } catch (e) {
1422
1493
  return {
1423
1494
  file,
@@ -1453,6 +1524,7 @@ async function runTestFilesOverRelay(connection, files, opts) {
1453
1524
  };
1454
1525
  }
1455
1526
  if ("error" in fileEntry.result && isRelayDisconnectMessage(fileEntry.result.error)) pendingReconnectCheck = true;
1527
+ if (isManual) fileEntry.mode = "manual";
1456
1528
  fileResults.push(fileEntry);
1457
1529
  }
1458
1530
  } finally {
@@ -1566,9 +1638,11 @@ function serializeRelayReport(report, meta) {
1566
1638
  totals: report.totals,
1567
1639
  files: report.files.map((f) => {
1568
1640
  const file = relativise(meta.projectRoot, f.file);
1641
+ const modeField = f.mode === "manual" ? { mode: "manual" } : {};
1569
1642
  if ("error" in f.result) return {
1570
1643
  file,
1571
- error: f.result.error
1644
+ error: f.result.error,
1645
+ ...modeField
1572
1646
  };
1573
1647
  return {
1574
1648
  file,
@@ -1576,33 +1650,65 @@ function serializeRelayReport(report, meta) {
1576
1650
  passed: f.result.passed,
1577
1651
  failed: f.result.failed,
1578
1652
  skipped: f.result.skipped,
1579
- tests: f.result.tests
1653
+ tests: f.result.tests,
1654
+ ...modeField
1580
1655
  };
1581
1656
  })
1582
1657
  };
1583
1658
  }
1584
1659
  /**
1585
1660
  * Writes the serialised report to `<dir>/<sdkLine>.<platform>.json`, creating
1586
- * `dir` if needed. Returns the absolute path written.
1661
+ * `dir` if needed. Returns the absolute path(s) written.
1587
1662
  *
1588
1663
  * The cell-suffixed filename keeps 2.x and 3.0 (and per-platform) runs as
1589
1664
  * distinct artifacts in the same directory; the same cell metadata is also baked
1590
1665
  * into the body so a renamed/moved file still carries its provenance.
1591
1666
  *
1667
+ * Manual-run provenance (devtools#741): when `report.files` contains ANY
1668
+ * `mode: 'manual'` entry (i.e. this run included `--manual-blocking` files),
1669
+ * the manual-tagged files are written to a SEPARATE
1670
+ * `<dir>/<sdkLine>.<platform>.manual.json` artifact instead of the standard
1671
+ * one — the standard `<sdkLine>.<platform>.json` filename is reserved for the
1672
+ * regular (unattended) files only, so a manual run's presence never mutates
1673
+ * what the unattended-baseline filename means. If ALL files in the run are
1674
+ * regular, only the standard artifact is written (today's behavior,
1675
+ * unchanged). If a run mixes both (regular files + `--manual-blocking`
1676
+ * scheduled last), BOTH artifacts are written — the manual one ALONGSIDE, not
1677
+ * replacing, the standard one.
1678
+ *
1592
1679
  * SECRET-HANDLING: the written body contains no relay/secret fields (the schema
1593
1680
  * has none). `dir`/`projectRoot` are local filesystem paths, never logged here.
1594
1681
  *
1595
1682
  * @param report - The core relay run report.
1596
1683
  * @param dir - Output directory (created recursively if missing).
1597
1684
  * @param meta - Cell axes + projectRoot.
1598
- * @returns The absolute path of the written file.
1685
+ * @returns The absolute path(s) written, in order: standard first (if any
1686
+ * regular files ran), then manual (if any manual files ran). At least one
1687
+ * path is always returned when `report.files` is non-empty.
1599
1688
  */
1600
1689
  async function writeReportArtifact(report, dir, meta) {
1601
1690
  const serialised = serializeRelayReport(report, meta);
1602
1691
  await mkdir(dir, { recursive: true });
1603
- const outFile = path.join(dir, `${meta.sdkLine}.${meta.platform}.json`);
1604
- await writeFile(outFile, `${JSON.stringify(serialised, null, 2)}\n`, "utf8");
1605
- return outFile;
1692
+ const regularFiles = serialised.files.filter((f) => f.mode !== "manual");
1693
+ const manualFiles = serialised.files.filter((f) => f.mode === "manual");
1694
+ const written = [];
1695
+ if (regularFiles.length > 0 || serialised.files.length === 0) {
1696
+ const outFile = path.join(dir, `${meta.sdkLine}.${meta.platform}.json`);
1697
+ await writeFile(outFile, `${JSON.stringify({
1698
+ ...serialised,
1699
+ files: regularFiles
1700
+ }, null, 2)}\n`, "utf8");
1701
+ written.push(outFile);
1702
+ }
1703
+ if (manualFiles.length > 0) {
1704
+ const manualOutFile = path.join(dir, `${meta.sdkLine}.${meta.platform}.manual.json`);
1705
+ await writeFile(manualOutFile, `${JSON.stringify({
1706
+ ...serialised,
1707
+ files: manualFiles
1708
+ }, null, 2)}\n`, "utf8");
1709
+ written.push(manualOutFile);
1710
+ }
1711
+ return written;
1606
1712
  }
1607
1713
  /**
1608
1714
  * Writes harvested `__AIT_CAPTURE__` lines to per-category files under `dir`,
@@ -1688,6 +1794,24 @@ OPTIONS
1688
1794
  --headless Disable browser auto-open (text QR only)
1689
1795
  --project-root <dir> Project root for .ait_relay secret lookup
1690
1796
  (default: current working directory)
1797
+ --manual-blocking Run manual-tagged test files (*.manual.ait.test.ts)
1798
+ LAST, after all regular files, with a human present.
1799
+ Before each manual file, the QR dashboard is pushed
1800
+ a step-by-step Korean prompt naming the file + its
1801
+ progress (k/n), and the same line is printed to
1802
+ stdout. Manual files get a 5-minute per-file evaluate
1803
+ timeout (vs. --timeout for everything else) since a
1804
+ human is expected to tap through a native sheet
1805
+ (photo picker, permission dialog, fullscreen ad).
1806
+ Without this flag (default off), *.manual.ait.test.ts
1807
+ files are EXCLUDED from the glob expansion entirely —
1808
+ existing unattended runs are byte-for-byte unaffected.
1809
+ With --report-dir, a run that included manual files
1810
+ ALSO writes <sdkLine>.<platform>.manual.json
1811
+ alongside (never replacing) the standard report, and
1812
+ each manual file's report entry is stamped
1813
+ mode: 'manual' — never diff a manual run against an
1814
+ unattended baseline as if they were equivalent.
1691
1815
  --help, -h Show this help message
1692
1816
 
1693
1817
  DESCRIPTION
@@ -1836,7 +1960,8 @@ async function main(argv = process.argv.slice(2)) {
1836
1960
  "report-dir": { type: "string" },
1837
1961
  "no-qr-stdout": { type: "boolean" },
1838
1962
  headless: { type: "boolean" },
1839
- "project-root": { type: "string" }
1963
+ "project-root": { type: "string" },
1964
+ "manual-blocking": { type: "boolean" }
1840
1965
  },
1841
1966
  allowPositionals: true
1842
1967
  });
@@ -1867,6 +1992,7 @@ async function main(argv = process.argv.slice(2)) {
1867
1992
  const projectRoot = typeof vals["project-root"] === "string" ? vals["project-root"] : process.cwd();
1868
1993
  const reportDir = typeof vals["report-dir"] === "string" ? vals["report-dir"] : void 0;
1869
1994
  const suppressQr = shouldSuppressQr(vals["no-qr-stdout"] === true);
1995
+ const manualBlocking = vals["manual-blocking"] === true;
1870
1996
  const cellSdkLine = typeof vals["cell-sdk-line"] === "string" ? vals["cell-sdk-line"] : void 0;
1871
1997
  const cellPlatform = typeof vals["cell-platform"] === "string" ? vals["cell-platform"] : process.env.AIT_CELL_PLATFORM;
1872
1998
  const hasCell = cellSdkLine !== void 0 || cellPlatform !== void 0;
@@ -1881,13 +2007,16 @@ async function main(argv = process.argv.slice(2)) {
1881
2007
  process.exitCode = 1;
1882
2008
  return;
1883
2009
  }
1884
- const files = await discoverTestFiles(globs, process.cwd());
1885
- if (files.length === 0) {
2010
+ const discovered = await discoverTestFiles(globs, process.cwd(), { includeManual: manualBlocking });
2011
+ if (discovered.length === 0) {
1886
2012
  process.stderr.write(`devtools-test: no test files matched ${globs.join(", ")}\n`);
1887
2013
  process.exitCode = 1;
1888
2014
  return;
1889
2015
  }
1890
- process.stderr.write(`devtools-test: found ${files.length} test file(s)\n`);
2016
+ const { regular, manual } = partitionManualTests(discovered);
2017
+ const files = manualBlocking ? [...regular, ...manual] : regular;
2018
+ const manualFileSet = new Set(manual);
2019
+ 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`);
1891
2020
  if (hasCell) process.stderr.write(`devtools-test: injecting __AIT_CELL__ = ${JSON.stringify(cell)}\n`);
1892
2021
  const factory = createRelayConnectionFactory({
1893
2022
  schemeUrl,
@@ -1917,15 +2046,26 @@ async function main(argv = process.argv.slice(2)) {
1917
2046
  const report = await runWithConnection(connection, files, {
1918
2047
  timeoutMs: evaluateTimeoutMs,
1919
2048
  printSummary: true,
1920
- collectCaptures: reportDir !== void 0
2049
+ collectCaptures: reportDir !== void 0,
2050
+ manualFiles: manualBlocking && manualFileSet.size > 0 ? manualFileSet : void 0,
2051
+ onManualFile: (file, index, total) => {
2052
+ const name = basename(file);
2053
+ process.stdout.write(`수동 단계: ${name} — 폰에서 네이티브 시트가 뜨면 안내에 따라 조작하세요 (${index}/${total})\n`);
2054
+ factory.onManualPrompt?.({
2055
+ file: name,
2056
+ index,
2057
+ total
2058
+ });
2059
+ }
1921
2060
  });
2061
+ if (manualBlocking && manualFileSet.size > 0) factory.onManualPrompt?.(null);
1922
2062
  if (reportDir !== void 0) try {
1923
- const reportPath = await writeReportArtifact(report, reportDir, {
2063
+ const reportPaths = await writeReportArtifact(report, reportDir, {
1924
2064
  sdkLine: cell.sdkLine,
1925
2065
  platform: cell.platform,
1926
2066
  projectRoot
1927
2067
  });
1928
- process.stderr.write(`devtools-test: wrote report ${reportPath}\n`);
2068
+ for (const reportPath of reportPaths) process.stderr.write(`devtools-test: wrote report ${reportPath}\n`);
1929
2069
  const capturePaths = await writeCaptureArtifacts(report.captures, `${reportDir}/.ait-capture`, cell);
1930
2070
  if (capturePaths.length > 0) process.stderr.write(`devtools-test: wrote ${capturePaths.length} capture file(s)\n`);
1931
2071
  } catch (e) {