@ait-co/devtools 0.1.131 → 0.1.133

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 (95) hide show
  1. package/dist/{attach-orchestrator-ca34r46y.js → attach-orchestrator-CE0S09YU.js} +110 -5
  2. package/dist/attach-orchestrator-CE0S09YU.js.map +1 -0
  3. package/dist/{attach-orchestrator-BiaYiNOr.js → attach-orchestrator-CMoDuG2A.js} +110 -5
  4. package/dist/attach-orchestrator-CMoDuG2A.js.map +1 -0
  5. package/dist/cell-BIdSQb9T.js +201 -0
  6. package/dist/cell-BIdSQb9T.js.map +1 -0
  7. package/dist/{cell-Cw4fVOl2.js → cell-D1y4shoV.js} +1 -21
  8. package/dist/cell-D1y4shoV.js.map +1 -0
  9. package/dist/cell-DHA578lX.js +200 -0
  10. package/dist/cell-DHA578lX.js.map +1 -0
  11. package/dist/{debug-server-qRSRG7YZ.js → debug-server-CS1BS2BX.js} +21 -3
  12. package/dist/debug-server-CS1BS2BX.js.map +1 -0
  13. package/dist/{debug-server-BSbfnFR5.js → debug-server-DmzuK2c6.js} +26 -9
  14. package/dist/debug-server-DmzuK2c6.js.map +1 -0
  15. package/dist/{debug-server-CD4zYBq4.js → debug-server-DnNCoNiR.js} +7 -7
  16. package/dist/{debug-server-CD4zYBq4.js.map → debug-server-DnNCoNiR.js.map} +1 -1
  17. package/dist/mcp/cli.js +180 -8
  18. package/dist/mcp/cli.js.map +1 -1
  19. package/dist/mcp/server.js +1 -1
  20. package/dist/panel/index.js +1 -1
  21. package/dist/{pool-BaSCZALv.d.ts → pool-x28Wv1ty.d.ts} +16 -2
  22. package/dist/{pool-BaSCZALv.d.ts.map → pool-x28Wv1ty.d.ts.map} +1 -1
  23. package/dist/{qr-http-server-DCbjORFa.js → qr-http-server-C_DTp9WU.js} +3 -2
  24. package/dist/{qr-http-server-DCbjORFa.js.map → qr-http-server-C_DTp9WU.js.map} +1 -1
  25. package/dist/{qr-http-server-BxoRp2w7.cjs → qr-http-server-DX_9UA-p.cjs} +3 -2
  26. package/dist/qr-http-server-DX_9UA-p.cjs.map +1 -0
  27. package/dist/{qr-http-server-BdNGPRoL.js → qr-http-server-DeRrKp_R.js} +3 -2
  28. package/dist/{qr-http-server-BdNGPRoL.js.map → qr-http-server-DeRrKp_R.js.map} +1 -1
  29. package/dist/{qr-http-server-cKbciY5k.js → qr-http-server-FVn61xAq.js} +3 -2
  30. package/dist/{qr-http-server-Bq9zR7-z.js.map → qr-http-server-FVn61xAq.js.map} +1 -1
  31. package/dist/{qr-http-server-Bq9zR7-z.js → qr-http-server-V0xuU5TJ.js} +3 -2
  32. package/dist/{qr-http-server-cKbciY5k.js.map → qr-http-server-V0xuU5TJ.js.map} +1 -1
  33. package/dist/{qr-http-server-CV6TA9ZN.cjs → qr-http-server-iH_Hh_si.cjs} +3 -2
  34. package/dist/qr-http-server-iH_Hh_si.cjs.map +1 -0
  35. package/dist/{relay-factory-Cloog1jb.js → relay-factory-BQfmK7RP.js} +14 -8
  36. package/dist/relay-factory-BQfmK7RP.js.map +1 -0
  37. package/dist/{relay-secret-store-CFc9n0OA.js → relay-secret-store-CLEGyHou.js} +2 -2
  38. package/dist/{relay-secret-store-CFc9n0OA.js.map → relay-secret-store-CLEGyHou.js.map} +1 -1
  39. package/dist/{relay-secret-store-DzxDsK6S.js → relay-secret-store-DGduVJhs.js} +2 -2
  40. package/dist/{relay-secret-store-DzxDsK6S.js.map → relay-secret-store-DGduVJhs.js.map} +1 -1
  41. package/dist/{relay-url-store-D3rY-GJJ.js → relay-url-store-B0X8TsGr.js} +2 -2
  42. package/dist/{relay-url-store-D3rY-GJJ.js.map → relay-url-store-B0X8TsGr.js.map} +1 -1
  43. package/dist/{relay-url-store-DGQ-HPQC.js → relay-url-store-CKW8RQzf.js} +2 -2
  44. package/dist/{relay-url-store-DGQ-HPQC.js.map → relay-url-store-CKW8RQzf.js.map} +1 -1
  45. package/dist/{relay-worker-o0M2ndyJ.d.ts → relay-worker-DtX_eszH.d.ts} +51 -2
  46. package/dist/relay-worker-DtX_eszH.d.ts.map +1 -0
  47. package/dist/{relay-worker-CCAHLC4a.js → relay-worker-MZq4fLv8.js} +27 -6
  48. package/dist/{relay-worker-CCAHLC4a.js.map → relay-worker-MZq4fLv8.js.map} +1 -1
  49. package/dist/rolldown-runtime-DGkTqVfb.js +15 -0
  50. package/dist/rolldown-runtime-DUslC3ob.js +14 -0
  51. package/dist/test-runner/bin.js +173 -119
  52. package/dist/test-runner/bin.js.map +1 -1
  53. package/dist/test-runner/config.d.ts +1 -1
  54. package/dist/test-runner/config.js +1 -1
  55. package/dist/test-runner/pool.d.ts +1 -1
  56. package/dist/test-runner/pool.js +1 -1
  57. package/dist/test-runner/pool.js.map +1 -1
  58. package/dist/test-runner/relay-factory.d.ts +14 -0
  59. package/dist/test-runner/relay-factory.d.ts.map +1 -1
  60. package/dist/test-runner/relay-factory.js +11 -5
  61. package/dist/test-runner/relay-factory.js.map +1 -1
  62. package/dist/test-runner/relay-worker.d.ts +2 -2
  63. package/dist/test-runner/relay-worker.js +2 -2
  64. package/dist/test-runner/report.d.ts +35 -4
  65. package/dist/test-runner/report.d.ts.map +1 -1
  66. package/dist/test-runner/report.js +42 -7
  67. package/dist/test-runner/report.js.map +1 -1
  68. package/dist/{totp-DvOYkYim.js → totp-CZLLKfOC.js} +2 -14
  69. package/dist/{totp-DvOYkYim.js.map → totp-CZLLKfOC.js.map} +1 -1
  70. package/dist/totp-DAxys-r0.js +199 -0
  71. package/dist/totp-DAxys-r0.js.map +1 -0
  72. package/dist/{tunnel-B2UCKAc7.cjs → tunnel-DP453L4e.cjs} +2 -2
  73. package/dist/{tunnel-B2UCKAc7.cjs.map → tunnel-DP453L4e.cjs.map} +1 -1
  74. package/dist/{tunnel-C-rQ34dM.js → tunnel-Djf-zmjJ.js} +2 -2
  75. package/dist/{tunnel-C-rQ34dM.js.map → tunnel-Djf-zmjJ.js.map} +1 -1
  76. package/dist/unplugin/index.cjs +1 -1
  77. package/dist/unplugin/index.js +1 -1
  78. package/dist/unplugin/tunnel.cjs +1 -1
  79. package/dist/unplugin/tunnel.js +1 -1
  80. package/package.json +1 -1
  81. package/dist/attach-orchestrator-BiaYiNOr.js.map +0 -1
  82. package/dist/attach-orchestrator-ca34r46y.js.map +0 -1
  83. package/dist/cell-Bi8MAPA8.js +0 -88
  84. package/dist/cell-Bi8MAPA8.js.map +0 -1
  85. package/dist/cell-Cw4fVOl2.js.map +0 -1
  86. package/dist/cell-rr6gL9AT.js +0 -89
  87. package/dist/cell-rr6gL9AT.js.map +0 -1
  88. package/dist/debug-server-BSbfnFR5.js.map +0 -1
  89. package/dist/debug-server-qRSRG7YZ.js.map +0 -1
  90. package/dist/log-CUik_tHp.js +0 -95
  91. package/dist/log-CUik_tHp.js.map +0 -1
  92. package/dist/qr-http-server-BxoRp2w7.cjs.map +0 -1
  93. package/dist/qr-http-server-CV6TA9ZN.cjs.map +0 -1
  94. package/dist/relay-factory-Cloog1jb.js.map +0 -1
  95. package/dist/relay-worker-o0M2ndyJ.d.ts.map +0 -1
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { r as generateTotp } from "../totp-DvOYkYim.js";
2
+ import { m as logInfo } from "../attach-orchestrator-CE0S09YU.js";
3
+ import { r as generateTotp } from "../totp-CZLLKfOC.js";
4
+ import { n as runPermissionPreflight } from "../cell-BIdSQb9T.js";
3
5
  import * as path$1 from "node:path";
4
6
  import path, { basename, isAbsolute, resolve } from "node:path";
5
7
  import { parseArgs } from "node:util";
@@ -22,6 +24,23 @@ import { fileURLToPath } from "node:url";
22
24
  * safe to import from the MCP daemon graph.
23
25
  */
24
26
  /**
27
+ * Filename suffix that opts a test file into manual-variant scheduling
28
+ * (devtools#741). A file named `<name>.manual.ait.test.ts` is EXCLUDED from
29
+ * `discoverTestFiles`'s default output (so existing unattended runs are
30
+ * unaffected — the zero-diff-when-off constraint) and is only surfaced when
31
+ * the caller explicitly asks for manual files via {@link partitionManualTests}
32
+ * (wired from the CLI's `--manual-blocking` flag).
33
+ *
34
+ * This is the entire tagging contract for v1 — no separate manifest/config,
35
+ * just a filename convention. Documented here + in the CLI `--help` text
36
+ * (cli.ts USAGE) and the test-runner README.
37
+ */
38
+ const MANUAL_TEST_SUFFIX = ".manual.ait.test.ts";
39
+ /** True when `file`'s basename ends with {@link MANUAL_TEST_SUFFIX}. */
40
+ function isManualTestFile(file) {
41
+ return basename(file).endsWith(MANUAL_TEST_SUFFIX);
42
+ }
43
+ /**
25
44
  * Expands `patterns` (globs or plain paths) into a sorted, de-duplicated list of
26
45
  * ABSOLUTE test file paths, resolved relative to `cwd`.
27
46
  *
@@ -30,14 +49,46 @@ import { fileURLToPath } from "node:url";
30
49
  * resolved against `cwd`. `bundleTestFile` requires an absolute path, so the
31
50
  * absolute output feeds it directly.
32
51
  *
52
+ * By default, files matching {@link MANUAL_TEST_SUFFIX} are EXCLUDED from the
53
+ * result (devtools#741) — blocking-UI tests opt in via that filename
54
+ * convention and must never appear in an unattended run unless the caller
55
+ * explicitly asks for them via `includeManual: true`. This keeps the
56
+ * default (flag-off) discovery path byte-for-byte identical to before this
57
+ * option existed.
58
+ *
33
59
  * @param patterns Glob patterns or file paths (e.g. `['src/**\/*.ait.test.ts']`).
34
60
  * @param cwd Base directory for relative patterns/results.
61
+ * @param opts `{ includeManual }` — when true, manual-tagged files are kept
62
+ * in the (still-sorted) output instead of being filtered out.
63
+ * Use {@link partitionManualTests} to separate + reorder them.
35
64
  * @returns Sorted, de-duplicated absolute file paths. Empty when nothing matches.
36
65
  */
37
- async function discoverTestFiles(patterns, cwd) {
66
+ async function discoverTestFiles(patterns, cwd, opts) {
38
67
  const out = /* @__PURE__ */ new Set();
39
68
  for await (const match of glob(patterns, { cwd })) out.add(isAbsolute(match) ? match : resolve(cwd, match));
40
- return [...out].sort();
69
+ const sorted = [...out].sort();
70
+ if (opts?.includeManual) return sorted;
71
+ return sorted.filter((f) => !isManualTestFile(f));
72
+ }
73
+ /**
74
+ * Splits an already-discovered file list into `{ regular, manual }`, each
75
+ * still sorted. Used by the CLI's `--manual-blocking` path to schedule manual
76
+ * files strictly AFTER every regular file (devtools#741) — regular files run
77
+ * first (and produce the unattended-shaped part of the report) and manual
78
+ * files run last, each preceded by a dashboard prompt.
79
+ *
80
+ * Pure partition — does not itself decide inclusion; call
81
+ * `discoverTestFiles(patterns, cwd, { includeManual: true })` first so manual
82
+ * files are present in `files` to partition.
83
+ */
84
+ function partitionManualTests(files) {
85
+ const regular = [];
86
+ const manual = [];
87
+ for (const f of files) (isManualTestFile(f) ? manual : regular).push(f);
88
+ return {
89
+ regular,
90
+ manual
91
+ };
41
92
  }
42
93
  //#endregion
43
94
  //#region src/test-runner/relay-factory.ts
@@ -65,12 +116,13 @@ function createRelayConnectionFactory(opts) {
65
116
  let family;
66
117
  let qrServer;
67
118
  let phase = "active";
119
+ let manualPrompt = null;
68
120
  return {
69
121
  async open() {
70
- const { prepareAttach, renderAndMaybeWait, mintAttachUrl } = await import("../attach-orchestrator-ca34r46y.js");
71
- const { injectDebugIndicator, injectGlobals } = await import("../cell-rr6gL9AT.js");
72
- const { loadRelaySecretReadOnly } = await import("../relay-secret-store-CFc9n0OA.js");
73
- const { bootRelayFamily, buildRelayVerifyAuth } = await import("../debug-server-CD4zYBq4.js");
122
+ const { prepareAttach, renderAndMaybeWait, mintAttachUrl } = await import("../attach-orchestrator-CE0S09YU.js").then((n) => n.i);
123
+ const { injectDebugIndicator, injectGlobals } = await import("../cell-BIdSQb9T.js").then((n) => n.t);
124
+ const { loadRelaySecretReadOnly } = await import("../relay-secret-store-CLEGyHou.js");
125
+ const { bootRelayFamily, buildRelayVerifyAuth } = await import("../debug-server-DnNCoNiR.js");
74
126
  await loadRelaySecretReadOnly({ projectRoot });
75
127
  let resolveTunnelUp;
76
128
  const tunnelReady = new Promise((resolve) => {
@@ -100,13 +152,14 @@ function createRelayConnectionFactory(opts) {
100
152
  canOpenBrowser: () => !headless
101
153
  };
102
154
  try {
103
- const { startQrHttpServer } = await import("../qr-http-server-DCbjORFa.js");
155
+ const { startQrHttpServer } = await import("../qr-http-server-C_DTp9WU.js");
104
156
  const getDashboardState = () => ({
105
157
  tunnel: attachDeps.getTunnelStatus(),
106
158
  pages: null,
107
159
  attachUrl: lastAttachParts ? mintAttachUrl(attachDeps, lastAttachParts) : null,
108
160
  mode: "relay-dev",
109
- phase
161
+ phase,
162
+ manualPrompt
110
163
  });
111
164
  qrServer = await startQrHttpServer(getDashboardState);
112
165
  attachDeps.qrHttpServer = qrServer;
@@ -163,13 +216,17 @@ function createRelayConnectionFactory(opts) {
163
216
  phase = next;
164
217
  qrServer?.notifyStateChange();
165
218
  },
219
+ onManualPrompt(next) {
220
+ manualPrompt = next;
221
+ qrServer?.notifyStateChange();
222
+ },
166
223
  async close(connection) {
167
224
  if (phase !== "complete") {
168
225
  phase = "complete";
169
226
  qrServer?.notifyStateChange();
170
227
  }
171
228
  try {
172
- const { injectDebugIndicator } = await import("../cell-rr6gL9AT.js");
229
+ const { injectDebugIndicator } = await import("../cell-BIdSQb9T.js").then((n) => n.t);
173
230
  await injectDebugIndicator(connection, { state: "disconnected" });
174
231
  } catch {}
175
232
  family?.stop();
@@ -221,98 +278,6 @@ const RELAY_AUTH_REJECT_CLOSE_CODE = 4401;
221
278
  */
222
279
  const RELAY_AUTH_REJECT_REASON = "totp-rejected";
223
280
  //#endregion
224
- //#region src/mcp/log.ts
225
- /**
226
- * Allowed field keys that may pass through to a log line.
227
- * Unknown keys are dropped. Values are still redact-scanned.
228
- */
229
- const ALLOWED_KEYS = new Set([
230
- "ts",
231
- "level",
232
- "event",
233
- "msg",
234
- "port",
235
- "totpEnabled",
236
- "env",
237
- "tool",
238
- "deploymentId",
239
- "errorKind",
240
- "reason",
241
- "prevTargetId",
242
- "mode",
243
- "fileCount",
244
- "passed",
245
- "failed",
246
- "skipped"
247
- ]);
248
- /**
249
- * Patterns that match secret values.
250
- * Match order matters — more-specific patterns first.
251
- *
252
- * #268 redact script covers: relay=wss://…, at=<TOTP>, _deploymentId=<uuid>.
253
- * Here we extend to in-process value-level patterns used in server logs.
254
- */
255
- const SECRET_PATTERNS = [
256
- /^\d{6}$/,
257
- /^(aitcc_|AITCC_)/i,
258
- /^[A-Za-z0-9_-]+=.{4,}/,
259
- /^wss:\/\//,
260
- /(?:^|[?&])at=[A-Z0-9]{6}/i
261
- ];
262
- /**
263
- * Returns `true` when the string value matches any known-secret pattern.
264
- * Only string values are tested — numbers/booleans are always safe.
265
- */
266
- function isSecretValue(value) {
267
- return SECRET_PATTERNS.some((re) => re.test(value));
268
- }
269
- /**
270
- * Redacts a single scalar value.
271
- * - strings: return "***" if the value matches a secret pattern.
272
- * - other: return as-is.
273
- */
274
- function redactValue(value) {
275
- if (typeof value === "string" && isSecretValue(value)) return "***";
276
- return value;
277
- }
278
- /**
279
- * Builds a safe log payload from raw fields.
280
- *
281
- * - Only keys in `ALLOWED_KEYS` are included.
282
- * - String values are scanned for secret patterns and replaced with "***".
283
- * - `ts` and `level` and `event` are always included (they are injected by the
284
- * logger functions below, not by callers).
285
- */
286
- function buildPayload(level, event, fields) {
287
- const out = {
288
- ts: (/* @__PURE__ */ new Date()).toISOString(),
289
- level,
290
- event
291
- };
292
- for (const [key, value] of Object.entries(fields)) {
293
- if (!ALLOWED_KEYS.has(key)) continue;
294
- if (key === "ts" || key === "level" || key === "event") continue;
295
- out[key] = redactValue(value);
296
- }
297
- return out;
298
- }
299
- /**
300
- * Writes a single JSON log line to stderr.
301
- * MCP stdio transport uses stdout; all diagnostics go to stderr.
302
- */
303
- function writeLog(level, event, fields = {}) {
304
- const payload = buildPayload(level, event, fields);
305
- process.stderr.write(`${JSON.stringify(payload)}\n`);
306
- }
307
- /** Log an informational structured event. */
308
- function logInfo(event, fields = {}) {
309
- writeLog("info", event, fields);
310
- }
311
- /** Log an error structured event. */
312
- function logError(event, fields = {}) {
313
- writeLog("error", event, fields);
314
- }
315
- //#endregion
316
281
  //#region src/mcp/chii-connection.ts
317
282
  /**
318
283
  * Production `CdpConnection` backed by the local Chii relay.
@@ -1316,6 +1281,13 @@ async function injectAndRunBundle(connection, bundleCode, timeoutMs = DEFAULT_TI
1316
1281
  //#endregion
1317
1282
  //#region src/test-runner/relay-worker.ts
1318
1283
  /**
1284
+ * Per-file evaluate timeout (ms) for manual-variant files (devtools#741) — a
1285
+ * human is expected to be tapping through a native sheet, so the timeout must
1286
+ * be far longer than the unattended default (60s, rpc.ts DEFAULT_TIMEOUT_MS).
1287
+ * Fixed constant (not configurable in v1) — documented here + CLI `--help`.
1288
+ */
1289
+ const MANUAL_FILE_TIMEOUT_MS = 5 * 6e4;
1290
+ /**
1319
1291
  * Sentinel string embedded in the error message by `injectAndRunBundle` when
1320
1292
  * the per-file evaluate race hits the timeout. Used by the retry guard so only
1321
1293
  * genuine timeouts get a second attempt — not bundle errors or parse failures.
@@ -1386,12 +1358,23 @@ async function runTestFilesOverRelay(connection, files, opts) {
1386
1358
  process.stderr.write(`relay-worker: reconnect failed (${e instanceof Error ? e.message : String(e)}) — continuing, remaining files may fail\n`);
1387
1359
  }
1388
1360
  };
1361
+ const manualFiles = opts?.manualFiles;
1362
+ const manualTotal = manualFiles?.size ?? 0;
1363
+ let manualIndex = 0;
1364
+ let preflightPermissions;
1365
+ if (files.length > 0) preflightPermissions = await runPermissionPreflight(connection);
1389
1366
  try {
1390
1367
  for (const file of files) {
1391
1368
  if (pendingReconnectCheck) {
1392
1369
  await attemptReconnect("next-file");
1393
1370
  pendingReconnectCheck = false;
1394
1371
  }
1372
+ const isManual = manualFiles?.has(file) === true;
1373
+ if (isManual) {
1374
+ manualIndex += 1;
1375
+ opts?.onManualFile?.(file, manualIndex, manualTotal);
1376
+ }
1377
+ const fileTimeoutMs = isManual ? MANUAL_FILE_TIMEOUT_MS : opts?.timeoutMs;
1395
1378
  let fileEntry;
1396
1379
  try {
1397
1380
  const { code } = await bundleTestFile(file, opts?.bundleOptions);
@@ -1417,7 +1400,7 @@ async function runTestFilesOverRelay(connection, files, opts) {
1417
1400
  const attempt = async () => {
1418
1401
  let rpcResult;
1419
1402
  try {
1420
- rpcResult = await injectAndRunBundle(connection, code, opts?.timeoutMs);
1403
+ rpcResult = await injectAndRunBundle(connection, code, fileTimeoutMs);
1421
1404
  } catch (e) {
1422
1405
  return {
1423
1406
  file,
@@ -1453,6 +1436,7 @@ async function runTestFilesOverRelay(connection, files, opts) {
1453
1436
  };
1454
1437
  }
1455
1438
  if ("error" in fileEntry.result && isRelayDisconnectMessage(fileEntry.result.error)) pendingReconnectCheck = true;
1439
+ if (isManual) fileEntry.mode = "manual";
1456
1440
  fileResults.push(fileEntry);
1457
1441
  }
1458
1442
  } finally {
@@ -1481,7 +1465,8 @@ async function runTestFilesOverRelay(connection, files, opts) {
1481
1465
  duration: Date.now() - wallStart,
1482
1466
  files: fileResults,
1483
1467
  totals,
1484
- captures
1468
+ captures,
1469
+ ...preflightPermissions ? { preflight: { permissions: preflightPermissions } } : {}
1485
1470
  };
1486
1471
  }
1487
1472
  /**
@@ -1564,11 +1549,14 @@ function serializeRelayReport(report, meta) {
1564
1549
  startedAt: report.startedAt,
1565
1550
  duration: report.duration,
1566
1551
  totals: report.totals,
1552
+ ...report.preflight ? { preflight: report.preflight } : {},
1567
1553
  files: report.files.map((f) => {
1568
1554
  const file = relativise(meta.projectRoot, f.file);
1555
+ const modeField = f.mode === "manual" ? { mode: "manual" } : {};
1569
1556
  if ("error" in f.result) return {
1570
1557
  file,
1571
- error: f.result.error
1558
+ error: f.result.error,
1559
+ ...modeField
1572
1560
  };
1573
1561
  return {
1574
1562
  file,
@@ -1576,33 +1564,65 @@ function serializeRelayReport(report, meta) {
1576
1564
  passed: f.result.passed,
1577
1565
  failed: f.result.failed,
1578
1566
  skipped: f.result.skipped,
1579
- tests: f.result.tests
1567
+ tests: f.result.tests,
1568
+ ...modeField
1580
1569
  };
1581
1570
  })
1582
1571
  };
1583
1572
  }
1584
1573
  /**
1585
1574
  * Writes the serialised report to `<dir>/<sdkLine>.<platform>.json`, creating
1586
- * `dir` if needed. Returns the absolute path written.
1575
+ * `dir` if needed. Returns the absolute path(s) written.
1587
1576
  *
1588
1577
  * The cell-suffixed filename keeps 2.x and 3.0 (and per-platform) runs as
1589
1578
  * distinct artifacts in the same directory; the same cell metadata is also baked
1590
1579
  * into the body so a renamed/moved file still carries its provenance.
1591
1580
  *
1581
+ * Manual-run provenance (devtools#741): when `report.files` contains ANY
1582
+ * `mode: 'manual'` entry (i.e. this run included `--manual-blocking` files),
1583
+ * the manual-tagged files are written to a SEPARATE
1584
+ * `<dir>/<sdkLine>.<platform>.manual.json` artifact instead of the standard
1585
+ * one — the standard `<sdkLine>.<platform>.json` filename is reserved for the
1586
+ * regular (unattended) files only, so a manual run's presence never mutates
1587
+ * what the unattended-baseline filename means. If ALL files in the run are
1588
+ * regular, only the standard artifact is written (today's behavior,
1589
+ * unchanged). If a run mixes both (regular files + `--manual-blocking`
1590
+ * scheduled last), BOTH artifacts are written — the manual one ALONGSIDE, not
1591
+ * replacing, the standard one.
1592
+ *
1592
1593
  * SECRET-HANDLING: the written body contains no relay/secret fields (the schema
1593
1594
  * has none). `dir`/`projectRoot` are local filesystem paths, never logged here.
1594
1595
  *
1595
1596
  * @param report - The core relay run report.
1596
1597
  * @param dir - Output directory (created recursively if missing).
1597
1598
  * @param meta - Cell axes + projectRoot.
1598
- * @returns The absolute path of the written file.
1599
+ * @returns The absolute path(s) written, in order: standard first (if any
1600
+ * regular files ran), then manual (if any manual files ran). At least one
1601
+ * path is always returned when `report.files` is non-empty.
1599
1602
  */
1600
1603
  async function writeReportArtifact(report, dir, meta) {
1601
1604
  const serialised = serializeRelayReport(report, meta);
1602
1605
  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;
1606
+ const regularFiles = serialised.files.filter((f) => f.mode !== "manual");
1607
+ const manualFiles = serialised.files.filter((f) => f.mode === "manual");
1608
+ const written = [];
1609
+ if (regularFiles.length > 0 || serialised.files.length === 0) {
1610
+ const outFile = path.join(dir, `${meta.sdkLine}.${meta.platform}.json`);
1611
+ await writeFile(outFile, `${JSON.stringify({
1612
+ ...serialised,
1613
+ files: regularFiles
1614
+ }, null, 2)}\n`, "utf8");
1615
+ written.push(outFile);
1616
+ }
1617
+ if (manualFiles.length > 0) {
1618
+ const manualOutFile = path.join(dir, `${meta.sdkLine}.${meta.platform}.manual.json`);
1619
+ await writeFile(manualOutFile, `${JSON.stringify({
1620
+ ...serialised,
1621
+ files: manualFiles
1622
+ }, null, 2)}\n`, "utf8");
1623
+ written.push(manualOutFile);
1624
+ }
1625
+ return written;
1606
1626
  }
1607
1627
  /**
1608
1628
  * Writes harvested `__AIT_CAPTURE__` lines to per-category files under `dir`,
@@ -1688,6 +1708,24 @@ OPTIONS
1688
1708
  --headless Disable browser auto-open (text QR only)
1689
1709
  --project-root <dir> Project root for .ait_relay secret lookup
1690
1710
  (default: current working directory)
1711
+ --manual-blocking Run manual-tagged test files (*.manual.ait.test.ts)
1712
+ LAST, after all regular files, with a human present.
1713
+ Before each manual file, the QR dashboard is pushed
1714
+ a step-by-step Korean prompt naming the file + its
1715
+ progress (k/n), and the same line is printed to
1716
+ stdout. Manual files get a 5-minute per-file evaluate
1717
+ timeout (vs. --timeout for everything else) since a
1718
+ human is expected to tap through a native sheet
1719
+ (photo picker, permission dialog, fullscreen ad).
1720
+ Without this flag (default off), *.manual.ait.test.ts
1721
+ files are EXCLUDED from the glob expansion entirely —
1722
+ existing unattended runs are byte-for-byte unaffected.
1723
+ With --report-dir, a run that included manual files
1724
+ ALSO writes <sdkLine>.<platform>.manual.json
1725
+ alongside (never replacing) the standard report, and
1726
+ each manual file's report entry is stamped
1727
+ mode: 'manual' — never diff a manual run against an
1728
+ unattended baseline as if they were equivalent.
1691
1729
  --help, -h Show this help message
1692
1730
 
1693
1731
  DESCRIPTION
@@ -1836,7 +1874,8 @@ async function main(argv = process.argv.slice(2)) {
1836
1874
  "report-dir": { type: "string" },
1837
1875
  "no-qr-stdout": { type: "boolean" },
1838
1876
  headless: { type: "boolean" },
1839
- "project-root": { type: "string" }
1877
+ "project-root": { type: "string" },
1878
+ "manual-blocking": { type: "boolean" }
1840
1879
  },
1841
1880
  allowPositionals: true
1842
1881
  });
@@ -1867,6 +1906,7 @@ async function main(argv = process.argv.slice(2)) {
1867
1906
  const projectRoot = typeof vals["project-root"] === "string" ? vals["project-root"] : process.cwd();
1868
1907
  const reportDir = typeof vals["report-dir"] === "string" ? vals["report-dir"] : void 0;
1869
1908
  const suppressQr = shouldSuppressQr(vals["no-qr-stdout"] === true);
1909
+ const manualBlocking = vals["manual-blocking"] === true;
1870
1910
  const cellSdkLine = typeof vals["cell-sdk-line"] === "string" ? vals["cell-sdk-line"] : void 0;
1871
1911
  const cellPlatform = typeof vals["cell-platform"] === "string" ? vals["cell-platform"] : process.env.AIT_CELL_PLATFORM;
1872
1912
  const hasCell = cellSdkLine !== void 0 || cellPlatform !== void 0;
@@ -1881,13 +1921,16 @@ async function main(argv = process.argv.slice(2)) {
1881
1921
  process.exitCode = 1;
1882
1922
  return;
1883
1923
  }
1884
- const files = await discoverTestFiles(globs, process.cwd());
1885
- if (files.length === 0) {
1924
+ const discovered = await discoverTestFiles(globs, process.cwd(), { includeManual: manualBlocking });
1925
+ if (discovered.length === 0) {
1886
1926
  process.stderr.write(`devtools-test: no test files matched ${globs.join(", ")}\n`);
1887
1927
  process.exitCode = 1;
1888
1928
  return;
1889
1929
  }
1890
- process.stderr.write(`devtools-test: found ${files.length} test file(s)\n`);
1930
+ const { regular, manual } = partitionManualTests(discovered);
1931
+ const files = manualBlocking ? [...regular, ...manual] : regular;
1932
+ const manualFileSet = new Set(manual);
1933
+ 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
1934
  if (hasCell) process.stderr.write(`devtools-test: injecting __AIT_CELL__ = ${JSON.stringify(cell)}\n`);
1892
1935
  const factory = createRelayConnectionFactory({
1893
1936
  schemeUrl,
@@ -1917,15 +1960,26 @@ async function main(argv = process.argv.slice(2)) {
1917
1960
  const report = await runWithConnection(connection, files, {
1918
1961
  timeoutMs: evaluateTimeoutMs,
1919
1962
  printSummary: true,
1920
- collectCaptures: reportDir !== void 0
1963
+ collectCaptures: reportDir !== void 0,
1964
+ manualFiles: manualBlocking && manualFileSet.size > 0 ? manualFileSet : void 0,
1965
+ onManualFile: (file, index, total) => {
1966
+ const name = basename(file);
1967
+ process.stdout.write(`수동 단계: ${name} — 폰에서 네이티브 시트가 뜨면 안내에 따라 조작하세요 (${index}/${total})\n`);
1968
+ factory.onManualPrompt?.({
1969
+ file: name,
1970
+ index,
1971
+ total
1972
+ });
1973
+ }
1921
1974
  });
1975
+ if (manualBlocking && manualFileSet.size > 0) factory.onManualPrompt?.(null);
1922
1976
  if (reportDir !== void 0) try {
1923
- const reportPath = await writeReportArtifact(report, reportDir, {
1977
+ const reportPaths = await writeReportArtifact(report, reportDir, {
1924
1978
  sdkLine: cell.sdkLine,
1925
1979
  platform: cell.platform,
1926
1980
  projectRoot
1927
1981
  });
1928
- process.stderr.write(`devtools-test: wrote report ${reportPath}\n`);
1982
+ for (const reportPath of reportPaths) process.stderr.write(`devtools-test: wrote report ${reportPath}\n`);
1929
1983
  const capturePaths = await writeCaptureArtifacts(report.captures, `${reportDir}/.ait-capture`, cell);
1930
1984
  if (capturePaths.length > 0) process.stderr.write(`devtools-test: wrote ${capturePaths.length} capture file(s)\n`);
1931
1985
  } catch (e) {
@@ -1966,6 +2020,6 @@ main().catch((e) => {
1966
2020
  process.exitCode = 1;
1967
2021
  });
1968
2022
  //#endregion
1969
- export { RELAY_AUTH_REJECT_REASON as a, RELAY_AUTH_REJECT_CLOSE_CODE as i, logError as n, logInfo as r, ChiiCdpConnection as t };
2023
+ export { RELAY_AUTH_REJECT_CLOSE_CODE as n, RELAY_AUTH_REJECT_REASON as r, ChiiCdpConnection as t };
1970
2024
 
1971
2025
  //# sourceMappingURL=bin.js.map