@ait-co/devtools 0.1.129 → 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 (94) hide show
  1. package/dist/{attach-orchestrator-DvrgUIx7.js → attach-orchestrator-BiaYiNOr.js} +31 -13
  2. package/dist/{attach-orchestrator-DvrgUIx7.js.map → attach-orchestrator-BiaYiNOr.js.map} +1 -1
  3. package/dist/{attach-orchestrator-RhOdO3DS.js → attach-orchestrator-Cjdhir2U.js} +31 -13
  4. package/dist/{attach-orchestrator-RhOdO3DS.js.map → attach-orchestrator-Cjdhir2U.js.map} +1 -1
  5. package/dist/{attach-orchestrator-Bdt5Zdo9.js → attach-orchestrator-ca34r46y.js} +31 -13
  6. package/dist/{attach-orchestrator-Bdt5Zdo9.js.map → attach-orchestrator-ca34r46y.js.map} +1 -1
  7. package/dist/{cell-Ba9lCI3B.js → cell-Bi8MAPA8.js} +11 -7
  8. package/dist/cell-Bi8MAPA8.js.map +1 -0
  9. package/dist/{cell-DA_lcpDI.js → cell-Cw4fVOl2.js} +11 -7
  10. package/dist/cell-Cw4fVOl2.js.map +1 -0
  11. package/dist/{cell-DLRv4iG6.js → cell-rr6gL9AT.js} +11 -7
  12. package/dist/cell-rr6gL9AT.js.map +1 -0
  13. package/dist/{debug-server-e1qPe0ZB.js → debug-server-BBUzxZSf.js} +4 -4
  14. package/dist/{debug-server-e1qPe0ZB.js.map → debug-server-BBUzxZSf.js.map} +1 -1
  15. package/dist/{debug-server--Y0QZVJS.js → debug-server-Ch5p0ryv.js} +33 -11
  16. package/dist/debug-server-Ch5p0ryv.js.map +1 -0
  17. package/dist/{debug-server-Bu3pCG9c.js → debug-server-DoWAtTNj.js} +27 -9
  18. package/dist/debug-server-DoWAtTNj.js.map +1 -0
  19. package/dist/in-app/auto.js +15 -0
  20. package/dist/in-app/auto.js.map +1 -1
  21. package/dist/in-app/index.d.ts +27 -0
  22. package/dist/in-app/index.d.ts.map +1 -1
  23. package/dist/in-app/index.js +15 -0
  24. package/dist/in-app/index.js.map +1 -1
  25. package/dist/mcp/cli.js +211 -38
  26. package/dist/mcp/cli.js.map +1 -1
  27. package/dist/mcp/server.js +1 -1
  28. package/dist/panel/index.js +11 -1
  29. package/dist/panel/index.js.map +1 -1
  30. package/dist/{pool-C0pc7T2p.d.ts → pool-BtLh7U1y.d.ts} +24 -2
  31. package/dist/{pool-C0pc7T2p.d.ts.map → pool-BtLh7U1y.d.ts.map} +1 -1
  32. package/dist/{qr-http-server-DM2R25jP.cjs → qr-http-server-DX_9UA-p.cjs} +77 -6
  33. package/dist/qr-http-server-DX_9UA-p.cjs.map +1 -0
  34. package/dist/{qr-http-server-HANhOFwI.js → qr-http-server-DeRrKp_R.js} +77 -6
  35. package/dist/qr-http-server-DeRrKp_R.js.map +1 -0
  36. package/dist/{qr-http-server-CrGohOwC.js → qr-http-server-FVn61xAq.js} +77 -6
  37. package/dist/qr-http-server-FVn61xAq.js.map +1 -0
  38. package/dist/{qr-http-server-s1iW_5VQ.js → qr-http-server-KB147M-S.js} +77 -6
  39. package/dist/qr-http-server-KB147M-S.js.map +1 -0
  40. package/dist/{qr-http-server-BH0UFeUn.js → qr-http-server-V0xuU5TJ.js} +77 -6
  41. package/dist/qr-http-server-V0xuU5TJ.js.map +1 -0
  42. package/dist/{qr-http-server-Bt_RAxFn.cjs → qr-http-server-iH_Hh_si.cjs} +77 -6
  43. package/dist/qr-http-server-iH_Hh_si.cjs.map +1 -0
  44. package/dist/{relay-factory-Ck3vBonr.js → relay-factory-cNgPfrWg.js} +39 -13
  45. package/dist/relay-factory-cNgPfrWg.js.map +1 -0
  46. package/dist/{relay-worker-o0M2ndyJ.d.ts → relay-worker-BMSnlyls.d.ts} +39 -2
  47. package/dist/relay-worker-BMSnlyls.d.ts.map +1 -0
  48. package/dist/{relay-worker-Bt0QYKQH.js → relay-worker-l34xNuci.js} +28 -7
  49. package/dist/relay-worker-l34xNuci.js.map +1 -0
  50. package/dist/test-runner/bin.js +202 -36
  51. package/dist/test-runner/bin.js.map +1 -1
  52. package/dist/test-runner/config.d.ts +15 -9
  53. package/dist/test-runner/config.d.ts.map +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 +36 -8
  59. package/dist/test-runner/relay-factory.d.ts.map +1 -1
  60. package/dist/test-runner/relay-factory.js +38 -12
  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 +26 -4
  65. package/dist/test-runner/report.d.ts.map +1 -1
  66. package/dist/test-runner/report.js +41 -7
  67. package/dist/test-runner/report.js.map +1 -1
  68. package/dist/{tunnel-DyEV6WwL.cjs → tunnel-DP453L4e.cjs} +4 -3
  69. package/dist/{tunnel-DyEV6WwL.cjs.map → tunnel-DP453L4e.cjs.map} +1 -1
  70. package/dist/{tunnel-BFwOSwT4.js → tunnel-Djf-zmjJ.js} +4 -3
  71. package/dist/{tunnel-BFwOSwT4.js.map → tunnel-Djf-zmjJ.js.map} +1 -1
  72. package/dist/unplugin/index.cjs +1 -1
  73. package/dist/unplugin/index.js +1 -1
  74. package/dist/unplugin/tunnel.cjs +3 -2
  75. package/dist/unplugin/tunnel.cjs.map +1 -1
  76. package/dist/unplugin/tunnel.d.cts.map +1 -1
  77. package/dist/unplugin/tunnel.d.ts.map +1 -1
  78. package/dist/unplugin/tunnel.js +3 -2
  79. package/dist/unplugin/tunnel.js.map +1 -1
  80. package/package.json +1 -1
  81. package/dist/cell-Ba9lCI3B.js.map +0 -1
  82. package/dist/cell-DA_lcpDI.js.map +0 -1
  83. package/dist/cell-DLRv4iG6.js.map +0 -1
  84. package/dist/debug-server--Y0QZVJS.js.map +0 -1
  85. package/dist/debug-server-Bu3pCG9c.js.map +0 -1
  86. package/dist/qr-http-server-BH0UFeUn.js.map +0 -1
  87. package/dist/qr-http-server-Bt_RAxFn.cjs.map +0 -1
  88. package/dist/qr-http-server-CrGohOwC.js.map +0 -1
  89. package/dist/qr-http-server-DM2R25jP.cjs.map +0 -1
  90. package/dist/qr-http-server-HANhOFwI.js.map +0 -1
  91. package/dist/qr-http-server-s1iW_5VQ.js.map +0 -1
  92. package/dist/relay-factory-Ck3vBonr.js.map +0 -1
  93. package/dist/relay-worker-Bt0QYKQH.js.map +0 -1
  94. package/dist/relay-worker-o0M2ndyJ.d.ts.map +0 -1
package/dist/mcp/cli.js CHANGED
@@ -2024,7 +2024,7 @@ async function readMcpSdkVersion() {
2024
2024
  * some test environments that skip the build step).
2025
2025
  */
2026
2026
  function readDevtoolsVersion() {
2027
- return "0.1.129";
2027
+ return "0.1.132";
2028
2028
  }
2029
2029
  /**
2030
2030
  * Derives the next recommended action from a completed diagnostics snapshot.
@@ -2864,32 +2864,50 @@ async function renderAndMaybeWait(deps, prep, waitForAttach, callTimeoutMs, conn
2864
2864
  }, null, 2)}\n\n${qr}`);
2865
2865
  }
2866
2866
  /**
2867
- * Builds a self-contained IIFE DOM expression that renders a dismissible
2868
- * "Debugger Connected" badge on the bottom-left of the phone screen.
2867
+ * Builds a self-contained IIFE DOM expression that renders a LIVE
2868
+ * "Debugger Connected" / "Debugger Disconnected" badge on the bottom-left of
2869
+ * the phone screen (#730).
2869
2870
  *
2870
2871
  * **Pure function** — returns a JS expression string; does NOT inject it.
2871
2872
  * Injection is performed by {@link injectDebugIndicator} in `cell.ts`.
2872
2873
  *
2873
2874
  * The expression, when evaluated on the page, does the following:
2874
- * 1. Early-returns if `#__ait_debug_indicator` already exists (idempotent
2875
- * prevents duplicate badges on re-injection after page reload).
2876
- * 2. Appends a fixed-position `<div id="__ait_debug_indicator">` at the
2877
- * bottom-left, accounting for safe-area insets.
2878
- * 3. Attaches a `{ once: true }` `pointerdown` listener that removes the
2879
- * badge on first touch one-tap dismiss.
2875
+ * 1. Idempotent controller a single `window.__ait_indicator` controller
2876
+ * object is created once (keyed on its presence, not on the DOM node).
2877
+ * Re-injection (e.g. explicit disconnect notice from `close()`) UPDATES
2878
+ * the existing badge's state/label in place — it never duplicates the
2879
+ * `<div>` or stacks a second observer.
2880
+ * 2. Renders a fixed-position `<div id="__ait_debug_indicator">` at the
2881
+ * bottom-left, accounting for safe-area insets. Background/text flip
2882
+ * between the attached (red) and disconnected (grey) visuals.
2883
+ * 3. A `pointerdown` listener dismisses (hides) the badge on tap — dismiss
2884
+ * is NON-terminal: a later state transition un-dismisses it, so a
2885
+ * genuine disconnect after a dismissed tap is still visible.
2886
+ * 4. Observes relay-socket lifecycle WITHOUT opening any new connection:
2887
+ * - Preferred path: if the in-app module (`src/in-app/attach.ts`) has
2888
+ * already installed its relay-WS observer (`window.__ait_relay_ws_observed`),
2889
+ * subscribe to the `ait:relay-ws-state` CustomEvent it broadcasts.
2890
+ * - Fallback path: wrap `window.WebSocket` in a `Proxy` and match dials
2891
+ * by PATHNAME SHAPE only (`/target/`) — never by host/wss value — so a
2892
+ * bare CDP-injected badge (no in-app bundle) still reacts to the
2893
+ * chii target socket's own open/close lifecycle.
2880
2894
  *
2881
2895
  * The expression intentionally contains NO relay URLs, wss addresses, TOTP
2882
- * codes, or any other secrets. It is pure DOM UI text only.
2896
+ * codes, or any other secrets. It is pure DOM UI text + enum state only.
2883
2897
  *
2884
2898
  * SECRET-HANDLING: this expression contains no secrets, relay URLs, wss
2885
- * addresses, or TOTP codes whatsoever — DOM label text only.
2899
+ * addresses, or TOTP codes whatsoever — DOM label text + a structural
2900
+ * pathname match (`/target/`) only.
2886
2901
  *
2887
- * @param opts.label - Badge text (default: `'Debugger Connected'`).
2902
+ * @param opts.label - Attached-state badge text (default: `'Debugger Connected'`).
2903
+ * @param opts.disconnectedLabel - Disconnected-state badge text (default: `'Debugger Disconnected'`).
2904
+ * @param opts.state - Initial/forced state for THIS injection call (default: `'attached'`).
2888
2905
  * @returns A JS expression string suitable for `Runtime.evaluate`.
2889
2906
  */
2890
2907
  function buildIndicatorExpression(opts) {
2891
2908
  const label = opts?.label ?? "Debugger Connected";
2892
- 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);})()`;
2909
+ const disconnectedLabel = opts?.disconnectedLabel ?? "Debugger Disconnected";
2910
+ return `(() => {var W = window;function render(c) {if (c.dismissed) { c.el.style.display = 'none'; return; }c.el.style.display = 'block';var up = c.state !== 'disconnected';c.el.style.background = up ? '#e5484d' : '#8a8f98';c.el.textContent = up ? ${JSON.stringify(label)} : ${JSON.stringify(disconnectedLabel)};}function setState(c, next) { c.state = next; c.dismissed = false; render(c); }var c = W.__ait_indicator;if (!c) {var 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','color:#fff','font:bold 11px/1 system-ui,sans-serif','padding:5px 9px','border-radius:6px','pointer-events:auto','user-select:none',].join(';');document.body.appendChild(el);c = { el: el, state: 'attached', dismissed: false };el.addEventListener('pointerdown', function () { c.dismissed = true; render(c); }, { passive: true });W.__ait_indicator = c;if (W.__ait_relay_ws_observed) {W.addEventListener('ait:relay-ws-state', function (e) {setState(c, e.detail && e.detail.state === 'open' ? 'attached' : 'disconnected');});} else {try {var Native = W.WebSocket;W.WebSocket = new Proxy(Native, {construct: function (t, a) {var ws = Reflect.construct(t, a);try {var u = new URL(String(a[0]), location.href);if (/\\/target\\//.test(u.pathname)) {ws.addEventListener('open', function () { setState(c, 'attached'); });ws.addEventListener('close', function () { setState(c, 'disconnected'); });ws.addEventListener('error', function () { setState(c, 'disconnected'); });}} catch (_) {}return ws;},});} catch (_) {}}}setState(c, ${JSON.stringify(opts?.state ?? "attached")});})()`;
2893
2911
  }
2894
2912
  //#endregion
2895
2913
  //#region src/test-runner/cell.ts
@@ -2935,12 +2953,16 @@ async function injectGlobals(conn, globals) {
2935
2953
  }
2936
2954
  }
2937
2955
  /**
2938
- * Injects the "Debugger Connected" on-phone indicator via `Runtime.evaluate`.
2956
+ * Injects (or updates) the "Debugger Connected"/"Debugger Disconnected"
2957
+ * on-phone indicator via `Runtime.evaluate` (#730).
2939
2958
  *
2940
2959
  * Uses the same CDP mechanism as {@link injectGlobals} — a single
2941
2960
  * `Runtime.evaluate` round-trip with the expression built by
2942
- * {@link buildIndicatorExpression}. The indicator is a dismissible red badge
2943
- * rendered at the bottom-left of the page (position:fixed, safe-area-aware).
2961
+ * {@link buildIndicatorExpression}. The indicator is a LIVE badge rendered at
2962
+ * the bottom-left of the page (position:fixed, safe-area-aware): the badge's
2963
+ * controller is idempotent, so calling this again (e.g. right before
2964
+ * `close()` with `{ state: 'disconnected' }`) UPDATES the existing badge in
2965
+ * place instead of injecting a duplicate.
2944
2966
  *
2945
2967
  * **Isolation**: unlike {@link injectGlobals}, this function NEVER throws to
2946
2968
  * its caller. Injection failure (e.g. page detached during inject, CSS not
@@ -2950,12 +2972,12 @@ async function injectGlobals(conn, globals) {
2950
2972
  * `src/in-app/attach.ts`.
2951
2973
  *
2952
2974
  * Call this ONLY on the manual debug paths (start_attach MCP, devtools-test
2953
- * CLI). Do NOT call on the `run_tests` auto-attach path — the red badge
2975
+ * CLI). Do NOT call on the `run_tests` auto-attach path — the badge
2954
2976
  * would contaminate screenshots, measure_safe_area probes, and DOM snapshots
2955
2977
  * taken during automated measurement runs.
2956
2978
  *
2957
2979
  * SECRET-HANDLING: the injected expression contains no secrets, relay URLs,
2958
- * wss addresses, or TOTP codes — it is pure DOM UI text built by
2980
+ * wss addresses, or TOTP codes — it is pure DOM UI text + enum state built by
2959
2981
  * {@link buildIndicatorExpression}.
2960
2982
  *
2961
2983
  * @param conn - The live CDP connection (relay-attached page).
@@ -2985,6 +3007,23 @@ async function injectDebugIndicator(conn, opts) {
2985
3007
  * safe to import from the MCP daemon graph.
2986
3008
  */
2987
3009
  /**
3010
+ * Filename suffix that opts a test file into manual-variant scheduling
3011
+ * (devtools#741). A file named `<name>.manual.ait.test.ts` is EXCLUDED from
3012
+ * `discoverTestFiles`'s default output (so existing unattended runs are
3013
+ * unaffected — the zero-diff-when-off constraint) and is only surfaced when
3014
+ * the caller explicitly asks for manual files via {@link partitionManualTests}
3015
+ * (wired from the CLI's `--manual-blocking` flag).
3016
+ *
3017
+ * This is the entire tagging contract for v1 — no separate manifest/config,
3018
+ * just a filename convention. Documented here + in the CLI `--help` text
3019
+ * (cli.ts USAGE) and the test-runner README.
3020
+ */
3021
+ const MANUAL_TEST_SUFFIX = ".manual.ait.test.ts";
3022
+ /** True when `file`'s basename ends with {@link MANUAL_TEST_SUFFIX}. */
3023
+ function isManualTestFile(file) {
3024
+ return basename(file).endsWith(MANUAL_TEST_SUFFIX);
3025
+ }
3026
+ /**
2988
3027
  * Expands `patterns` (globs or plain paths) into a sorted, de-duplicated list of
2989
3028
  * ABSOLUTE test file paths, resolved relative to `cwd`.
2990
3029
  *
@@ -2993,14 +3032,26 @@ async function injectDebugIndicator(conn, opts) {
2993
3032
  * resolved against `cwd`. `bundleTestFile` requires an absolute path, so the
2994
3033
  * absolute output feeds it directly.
2995
3034
  *
3035
+ * By default, files matching {@link MANUAL_TEST_SUFFIX} are EXCLUDED from the
3036
+ * result (devtools#741) — blocking-UI tests opt in via that filename
3037
+ * convention and must never appear in an unattended run unless the caller
3038
+ * explicitly asks for them via `includeManual: true`. This keeps the
3039
+ * default (flag-off) discovery path byte-for-byte identical to before this
3040
+ * option existed.
3041
+ *
2996
3042
  * @param patterns Glob patterns or file paths (e.g. `['src/**\/*.ait.test.ts']`).
2997
3043
  * @param cwd Base directory for relative patterns/results.
3044
+ * @param opts `{ includeManual }` — when true, manual-tagged files are kept
3045
+ * in the (still-sorted) output instead of being filtered out.
3046
+ * Use {@link partitionManualTests} to separate + reorder them.
2998
3047
  * @returns Sorted, de-duplicated absolute file paths. Empty when nothing matches.
2999
3048
  */
3000
- async function discoverTestFiles(patterns, cwd) {
3049
+ async function discoverTestFiles(patterns, cwd, opts) {
3001
3050
  const out = /* @__PURE__ */ new Set();
3002
3051
  for await (const match of glob(patterns, { cwd })) out.add(isAbsolute(match) ? match : resolve(cwd, match));
3003
- return [...out].sort();
3052
+ const sorted = [...out].sort();
3053
+ if (opts?.includeManual) return sorted;
3054
+ return sorted.filter((f) => !isManualTestFile(f));
3004
3055
  }
3005
3056
  //#endregion
3006
3057
  //#region src/shared/relay-auth-close.ts
@@ -3235,7 +3286,11 @@ var ChiiCdpConnection = class {
3235
3286
  * EventEmitter is missed (defensive belt-and-suspenders).
3236
3287
  *
3237
3288
  * @param filterFn - Predicate that the returned targets must satisfy.
3238
- * @param timeoutMs - Reject after this many ms (default 90 000).
3289
+ * @param timeoutMs - Reject after this many ms (default 90 000). Pass a
3290
+ * non-finite value (`Infinity`) to disable the rejection timer entirely —
3291
+ * used by the test-runner's unbounded QR-attach wait (devtools#735). Node
3292
+ * clamps `setTimeout(fn, Infinity)` to ~1ms, so a non-finite value MUST
3293
+ * skip arming the timer rather than pass it through.
3239
3294
  * @param pollIntervalMs - Fallback poll interval (default 500ms).
3240
3295
  */
3241
3296
  waitForFirstTarget(filterFn, timeoutMs = 9e4, pollIntervalMs = 500) {
@@ -3247,7 +3302,7 @@ var ChiiCdpConnection = class {
3247
3302
  const settle = (targets) => {
3248
3303
  if (settled) return;
3249
3304
  settled = true;
3250
- clearTimeout(timeoutHandle);
3305
+ if (timeoutHandle !== null) clearTimeout(timeoutHandle);
3251
3306
  if (pollHandle !== null) {
3252
3307
  clearInterval(pollHandle);
3253
3308
  pollHandle = null;
@@ -3258,7 +3313,7 @@ var ChiiCdpConnection = class {
3258
3313
  const onAttach = (targets) => {
3259
3314
  if (filterFn(targets)) settle(targets);
3260
3315
  };
3261
- const timeoutHandle = setTimeout(() => {
3316
+ const timeoutHandle = Number.isFinite(timeoutMs) ? setTimeout(() => {
3262
3317
  if (settled) return;
3263
3318
  settled = true;
3264
3319
  if (pollHandle !== null) {
@@ -3267,7 +3322,7 @@ var ChiiCdpConnection = class {
3267
3322
  }
3268
3323
  this.emitter.off("target:attached", onAttach);
3269
3324
  reject(/* @__PURE__ */ new Error(`waitForFirstTarget: 타임아웃 (${timeoutMs}ms) — 폰이 relay에 attach되지 않았습니다.`));
3270
- }, timeoutMs);
3325
+ }, timeoutMs) : null;
3271
3326
  this.emitter.on("target:attached", onAttach);
3272
3327
  pollHandle = setInterval(() => {
3273
3328
  this.refreshTargets().then((targets) => {
@@ -4043,6 +4098,13 @@ async function injectAndRunBundle(connection, bundleCode, timeoutMs = DEFAULT_TI
4043
4098
  //#endregion
4044
4099
  //#region src/test-runner/relay-worker.ts
4045
4100
  /**
4101
+ * Per-file evaluate timeout (ms) for manual-variant files (devtools#741) — a
4102
+ * human is expected to be tapping through a native sheet, so the timeout must
4103
+ * be far longer than the unattended default (60s, rpc.ts DEFAULT_TIMEOUT_MS).
4104
+ * Fixed constant (not configurable in v1) — documented here + CLI `--help`.
4105
+ */
4106
+ const MANUAL_FILE_TIMEOUT_MS = 5 * 6e4;
4107
+ /**
4046
4108
  * Sentinel string embedded in the error message by `injectAndRunBundle` when
4047
4109
  * the per-file evaluate race hits the timeout. Used by the retry guard so only
4048
4110
  * genuine timeouts get a second attempt — not bundle errors or parse failures.
@@ -4113,12 +4175,21 @@ async function runTestFilesOverRelay(connection, files, opts) {
4113
4175
  process.stderr.write(`relay-worker: reconnect failed (${e instanceof Error ? e.message : String(e)}) — continuing, remaining files may fail\n`);
4114
4176
  }
4115
4177
  };
4178
+ const manualFiles = opts?.manualFiles;
4179
+ const manualTotal = manualFiles?.size ?? 0;
4180
+ let manualIndex = 0;
4116
4181
  try {
4117
4182
  for (const file of files) {
4118
4183
  if (pendingReconnectCheck) {
4119
4184
  await attemptReconnect("next-file");
4120
4185
  pendingReconnectCheck = false;
4121
4186
  }
4187
+ const isManual = manualFiles?.has(file) === true;
4188
+ if (isManual) {
4189
+ manualIndex += 1;
4190
+ opts?.onManualFile?.(file, manualIndex, manualTotal);
4191
+ }
4192
+ const fileTimeoutMs = isManual ? MANUAL_FILE_TIMEOUT_MS : opts?.timeoutMs;
4122
4193
  let fileEntry;
4123
4194
  try {
4124
4195
  const { code } = await bundleTestFile(file, opts?.bundleOptions);
@@ -4144,7 +4215,7 @@ async function runTestFilesOverRelay(connection, files, opts) {
4144
4215
  const attempt = async () => {
4145
4216
  let rpcResult;
4146
4217
  try {
4147
- rpcResult = await injectAndRunBundle(connection, code, opts?.timeoutMs);
4218
+ rpcResult = await injectAndRunBundle(connection, code, fileTimeoutMs);
4148
4219
  } catch (e) {
4149
4220
  return {
4150
4221
  file,
@@ -4180,6 +4251,7 @@ async function runTestFilesOverRelay(connection, files, opts) {
4180
4251
  };
4181
4252
  }
4182
4253
  if ("error" in fileEntry.result && isRelayDisconnectMessage(fileEntry.result.error)) pendingReconnectCheck = true;
4254
+ if (isManual) fileEntry.mode = "manual";
4183
4255
  fileResults.push(fileEntry);
4184
4256
  }
4185
4257
  } finally {
@@ -4252,9 +4324,9 @@ OPTIONS
4252
4324
  the CLI waits for a human to scan the QR code — use
4253
4325
  --attach-timeout for that.
4254
4326
  --attach-timeout <ms> How long to wait for a human to scan the QR code with
4255
- their phone (default: 600000 10 minutes). Omit to
4256
- use the relay factory's generous default. Decrease for
4257
- CI environments where a scan should arrive quickly.
4327
+ their phone. Omit (default) to wait indefinitely the
4328
+ runner stays up until you stop it (Ctrl-C/SIGTERM).
4329
+ Pass a value to bound the wait for CI/headless runs.
4258
4330
  --cell-sdk-line <line> SDK line to inject as __AIT_CELL__.sdkLine (2.x|3.x)
4259
4331
  --cell-platform <plat> Platform to inject as __AIT_CELL__.platform
4260
4332
  (mock|ios|android, default: AIT_CELL_PLATFORM env)
@@ -4267,6 +4339,24 @@ OPTIONS
4267
4339
  --headless Disable browser auto-open (text QR only)
4268
4340
  --project-root <dir> Project root for .ait_relay secret lookup
4269
4341
  (default: current working directory)
4342
+ --manual-blocking Run manual-tagged test files (*.manual.ait.test.ts)
4343
+ LAST, after all regular files, with a human present.
4344
+ Before each manual file, the QR dashboard is pushed
4345
+ a step-by-step Korean prompt naming the file + its
4346
+ progress (k/n), and the same line is printed to
4347
+ stdout. Manual files get a 5-minute per-file evaluate
4348
+ timeout (vs. --timeout for everything else) since a
4349
+ human is expected to tap through a native sheet
4350
+ (photo picker, permission dialog, fullscreen ad).
4351
+ Without this flag (default off), *.manual.ait.test.ts
4352
+ files are EXCLUDED from the glob expansion entirely —
4353
+ existing unattended runs are byte-for-byte unaffected.
4354
+ With --report-dir, a run that included manual files
4355
+ ALSO writes <sdkLine>.<platform>.manual.json
4356
+ alongside (never replacing) the standard report, and
4357
+ each manual file's report entry is stamped
4358
+ mode: 'manual' — never diff a manual run against an
4359
+ unattended baseline as if they were equivalent.
4270
4360
  --help, -h Show this help message
4271
4361
 
4272
4362
  DESCRIPTION
@@ -5397,6 +5487,11 @@ const en = {
5397
5487
  "dashboard.watchdog.title": "Server has shut down",
5398
5488
  "dashboard.watchdog.body": "The MCP server has stopped. This session is no longer active. You may close this tab.",
5399
5489
  "dashboard.watchdog.close": "Close tab",
5490
+ "dashboard.conn.lost": "Connection lost — reconnecting…",
5491
+ "dashboard.session.completeTitle": "Test run complete",
5492
+ "dashboard.session.completeBody": "The runner exited cleanly. See your terminal for results.",
5493
+ "dashboard.session.shutdownTitle": "Debug server stopped",
5494
+ "dashboard.session.shutdownBody": "The MCP server has shut down; this session is no longer live. You can close this tab.",
5400
5495
  "attach.title": "AIT Debug Session — QR Scan",
5401
5496
  "attach.deployment": "deployment: {label}",
5402
5497
  "attach.steps.section": "How to scan",
@@ -5635,6 +5730,11 @@ const tables = {
5635
5730
  "dashboard.watchdog.title": "서버가 종료되었습니다",
5636
5731
  "dashboard.watchdog.body": "MCP server가 종료되어 이 세션이 더 이상 유효하지 않습니다. 이 탭을 닫아도 됩니다.",
5637
5732
  "dashboard.watchdog.close": "탭 닫기",
5733
+ "dashboard.conn.lost": "연결 끊김 — 재연결 시도 중…",
5734
+ "dashboard.session.completeTitle": "테스트 실행 완료",
5735
+ "dashboard.session.completeBody": "러너가 정상 종료되었습니다. 결과는 터미널에서 확인하세요.",
5736
+ "dashboard.session.shutdownTitle": "디버그 서버가 종료되었습니다",
5737
+ "dashboard.session.shutdownBody": "MCP server가 종료되어 이 세션이 더 이상 유효하지 않습니다. 이 탭을 닫아도 됩니다.",
5638
5738
  "attach.title": "AIT 디버그 세션 — QR 스캔",
5639
5739
  "attach.deployment": "deployment: {label}",
5640
5740
  "attach.steps.section": "스캔 절차",
@@ -6216,7 +6316,12 @@ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new U
6216
6316
  watchdogTimeoutMs,
6217
6317
  watchdogTitle: JSON.stringify(s("dashboard.watchdog.title")),
6218
6318
  watchdogBody: JSON.stringify(s("dashboard.watchdog.body")),
6219
- watchdogCloseLabel: JSON.stringify(s("dashboard.watchdog.close"))
6319
+ watchdogCloseLabel: JSON.stringify(s("dashboard.watchdog.close")),
6320
+ connectionLost: JSON.stringify(s("dashboard.conn.lost")),
6321
+ completeTitle: JSON.stringify(s("dashboard.session.completeTitle")),
6322
+ completeBody: JSON.stringify(s("dashboard.session.completeBody")),
6323
+ shutdownTitle: JSON.stringify(s("dashboard.session.shutdownTitle")),
6324
+ shutdownBody: JSON.stringify(s("dashboard.session.shutdownBody"))
6220
6325
  };
6221
6326
  const langSwitcher = buildLangSwitcher(path, params, locale, s);
6222
6327
  const filled = dashboardChromeByLocale[locale].replaceAll("__LANG_SWITCHER__", langSwitcher).replaceAll("__NOW__", escapeHtml(now)).replaceAll("__TUNNEL_CLASS__", tunnelClass).replaceAll("__TUNNEL_STATUS__", escapeHtml(tunnelStatus)).replaceAll("__ATTACH_SECTION__", attachSection).replaceAll("__INSPECTOR_SECTION__", inspectorSection).replaceAll("__PAGES_SECTION__", pagesSection);
@@ -6273,6 +6378,14 @@ function buildSseScript(strings) {
6273
6378
  var WATCHDOG_BODY = ${strings.watchdogBody};
6274
6379
  var WATCHDOG_CLOSE_LABEL = ${strings.watchdogCloseLabel};
6275
6380
 
6381
+ // ── 실시간 상태 표면 상수 (#730) ───────────────────────────────────────
6382
+ var CONN_LOST = ${strings.connectionLost};
6383
+ var COMPLETE_TITLE = ${strings.completeTitle};
6384
+ var COMPLETE_BODY = ${strings.completeBody};
6385
+ var SHUTDOWN_TITLE = ${strings.shutdownTitle};
6386
+ var SHUTDOWN_BODY = ${strings.shutdownBody};
6387
+ var disconnectedShown = false;
6388
+
6276
6389
  // ── 클립보드 복사 헬퍼 ────────────────────────────────────────────────
6277
6390
  function copyText(text) {
6278
6391
  if (navigator.clipboard && navigator.clipboard.writeText) {
@@ -6350,6 +6463,36 @@ function buildSseScript(strings) {
6350
6463
  }
6351
6464
  }
6352
6465
 
6466
+ // ── Terminal 상태 배너 (#730) ─────────────────────────────────────────
6467
+ // "완료"/"종료"는 watchdog(최대 210s 지연)을 기다리지 않고 close() 직전에
6468
+ // 서버가 직접 push하는 마지막 SSE 프레임으로 즉시 렌더한다. watchdogFired를
6469
+ // 함께 세워 뒤이은 onerror의 showConnLost가 이 배너를 덮어쓰지 않게 한다.
6470
+ function showTerminalBanner(title, body) {
6471
+ if (watchdogTimer) { clearInterval(watchdogTimer); watchdogTimer = null; }
6472
+ watchdogFired = true; // 이후 watchdog/onerror 발화를 억제
6473
+ if (document.body) {
6474
+ document.body.innerHTML =
6475
+ '<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;' +
6476
+ 'height:100vh;font-family:sans-serif;text-align:center;padding:2rem;box-sizing:border-box;">' +
6477
+ '<p style="font-size:1.4rem;font-weight:bold;margin-bottom:1rem;">' + title + '</p>' +
6478
+ '<p style="color:#555;">' + body + '</p></div>';
6479
+ }
6480
+ }
6481
+
6482
+ // ── SSE onerror 즉시 신호 (#730) ──────────────────────────────────────
6483
+ // EventSource는 "재시도 중"과 "서버 종료"를 구분하지 못하므로, onerror에서
6484
+ // 즉시 구분되는 '연결 끊김' 상태를 tunnel-status 배지에 표시한다. 이미
6485
+ // terminal 배너가 뜬 뒤(watchdogFired)라면 덮어쓰지 않는다.
6486
+ function showConnLost() {
6487
+ if (disconnectedShown || watchdogFired) return;
6488
+ disconnectedShown = true;
6489
+ var el = document.getElementById('tunnel-status');
6490
+ if (el) { el.textContent = CONN_LOST; el.className = 'status status-down'; }
6491
+ }
6492
+ function clearConnLost() {
6493
+ disconnectedShown = false;
6494
+ }
6495
+
6353
6496
  function fireWatchdog() {
6354
6497
  if (watchdogFired) return;
6355
6498
  watchdogFired = true;
@@ -6380,7 +6523,7 @@ function buildSseScript(strings) {
6380
6523
  src.onopen = function () {
6381
6524
  // 연결 복구 시 lastSeen 갱신 — 일시적 끊김이 watchdog을 발화시키지 않게.
6382
6525
  // watchdog이 이미 발화했으면 갱신하지 않는다(폴백 UI를 그대로 유지).
6383
- if (!watchdogFired) { lastSeen = Date.now(); }
6526
+ if (!watchdogFired) { lastSeen = Date.now(); clearConnLost(); }
6384
6527
  };
6385
6528
  src.onmessage = function (e) {
6386
6529
  // SSE 메시지 수신 — lastSeen 갱신으로 watchdog 리셋.
@@ -6388,6 +6531,13 @@ function buildSseScript(strings) {
6388
6531
  if (!watchdogFired) { lastSeen = Date.now(); }
6389
6532
  try {
6390
6533
  var s = JSON.parse(e.data);
6534
+ // 어떤 프레임이든 성공적으로 수신됐다는 건 서버가 살아있다는 뜻 —
6535
+ // onerror가 세운 '연결 끊김' 표시를 해제한다 (#730).
6536
+ clearConnLost();
6537
+ // phase gate (#730) — 'complete'/'shutdown'은 terminal 상태이므로
6538
+ // 나머지 필드 렌더보다 먼저 처리하고 즉시 반환한다.
6539
+ if (s.phase === 'complete') { showTerminalBanner(COMPLETE_TITLE, COMPLETE_BODY); return; }
6540
+ if (s.phase === 'shutdown') { showTerminalBanner(SHUTDOWN_TITLE, SHUTDOWN_BODY); return; }
6391
6541
  // 터널 상태 갱신
6392
6542
  var el = document.getElementById('tunnel-status');
6393
6543
  if (el) {
@@ -6466,7 +6616,11 @@ function buildSseScript(strings) {
6466
6616
  } catch (_) { /* 파싱 오류 무시 */ }
6467
6617
  };
6468
6618
  src.onerror = function () {
6469
- // 재연결은 EventSource 자동 처리 (spec 기본 동작).
6619
+ // EventSource "재시도 중"과 "서버 종료"를 구분하지 못하므로 즉시
6620
+ // 구분되는 "연결 끊김"을 표시한다(#730). 네이티브 auto-reconnect는 그대로
6621
+ // 진행되며 onopen/onmessage가 성공 즉시 이 상태를 해제한다. watchdog은
6622
+ // 여전히 2차 fallback으로 남는다(최종 폴백 UI 전환).
6623
+ showConnLost();
6470
6624
  };
6471
6625
  })();
6472
6626
  <\/script>`;
@@ -6515,7 +6669,12 @@ function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/a
6515
6669
  watchdogTimeoutMs,
6516
6670
  watchdogTitle: JSON.stringify(s("dashboard.watchdog.title")),
6517
6671
  watchdogBody: JSON.stringify(s("dashboard.watchdog.body")),
6518
- watchdogCloseLabel: JSON.stringify(s("dashboard.watchdog.close"))
6672
+ watchdogCloseLabel: JSON.stringify(s("dashboard.watchdog.close")),
6673
+ connectionLost: JSON.stringify(s("dashboard.conn.lost")),
6674
+ completeTitle: JSON.stringify(s("dashboard.session.completeTitle")),
6675
+ completeBody: JSON.stringify(s("dashboard.session.completeBody")),
6676
+ shutdownTitle: JSON.stringify(s("dashboard.session.shutdownTitle")),
6677
+ shutdownBody: JSON.stringify(s("dashboard.session.shutdownBody"))
6519
6678
  });
6520
6679
  return filled.replace("</body>", `${sseScript}\n</body>`);
6521
6680
  }
@@ -6542,7 +6701,9 @@ async function startQrHttpServer(getDashboardState, options) {
6542
6701
  },
6543
6702
  pages: state.pages,
6544
6703
  attachUrl: state.attachUrl,
6545
- inspectorUrl: state.inspectorUrl ?? null
6704
+ inspectorUrl: state.inspectorUrl ?? null,
6705
+ phase: state.phase ?? "active",
6706
+ manualPrompt: state.manualPrompt ?? null
6546
6707
  });
6547
6708
  res.write(`data: ${payload}\n\n`);
6548
6709
  }
@@ -6851,7 +7012,7 @@ function createDebugServer(deps) {
6851
7012
  };
6852
7013
  const server = new Server({
6853
7014
  name: "ait-debug",
6854
- version: "0.1.129"
7015
+ version: "0.1.132"
6855
7016
  }, { capabilities: { tools: { listChanged: true } } });
6856
7017
  server.setRequestHandler(ListToolsRequestSchema, () => {
6857
7018
  const conn = router.active;
@@ -7813,6 +7974,7 @@ async function runDebugServer(options = {}) {
7813
7974
  return router.active;
7814
7975
  });
7815
7976
  let lastAttachParts = null;
7977
+ let currentPhase = "active";
7816
7978
  const getDashboardState = () => {
7817
7979
  const targets = router.active.listTargets();
7818
7980
  const inspectorUrl = qrServer?.inspectorStableUrl ?? null;
@@ -7827,7 +7989,8 @@ async function runDebugServer(options = {}) {
7827
7989
  })),
7828
7990
  attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null,
7829
7991
  inspectorUrl,
7830
- mode: deriveEnvironment(router.active.kind, router.activeRelayOrigin)
7992
+ mode: deriveEnvironment(router.active.kind, router.activeRelayOrigin),
7993
+ phase: currentPhase
7831
7994
  };
7832
7995
  };
7833
7996
  const getDirectInspectorUrl = () => {
@@ -7891,6 +8054,8 @@ async function runDebugServer(options = {}) {
7891
8054
  const shutdown = () => {
7892
8055
  if (closed) return;
7893
8056
  closed = true;
8057
+ currentPhase = "shutdown";
8058
+ qrServer?.notifyStateChange();
7894
8059
  parentWatcher?.stop();
7895
8060
  maxAgeWatchdog?.stop();
7896
8061
  if (totpRefreshHandle) clearInterval(totpRefreshHandle);
@@ -8028,6 +8193,7 @@ async function runLocalDebugServer(options = {}) {
8028
8193
  return router.active;
8029
8194
  });
8030
8195
  let lastAttachParts = null;
8196
+ let currentPhase = "active";
8031
8197
  const getDashboardState = () => {
8032
8198
  const targets = router.active.listTargets();
8033
8199
  const inspectorUrl = qrServer?.inspectorStableUrl ?? null;
@@ -8041,7 +8207,8 @@ async function runLocalDebugServer(options = {}) {
8041
8207
  url: t.url
8042
8208
  })),
8043
8209
  attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null,
8044
- inspectorUrl
8210
+ inspectorUrl,
8211
+ phase: currentPhase
8045
8212
  };
8046
8213
  };
8047
8214
  const getDirectInspectorUrl = () => {
@@ -8105,6 +8272,8 @@ async function runLocalDebugServer(options = {}) {
8105
8272
  const shutdown = () => {
8106
8273
  if (closed) return;
8107
8274
  closed = true;
8275
+ currentPhase = "shutdown";
8276
+ qrServer?.notifyStateChange();
8108
8277
  parentWatcher?.stop();
8109
8278
  maxAgeWatchdog?.stop();
8110
8279
  if (totpRefreshHandle) clearInterval(totpRefreshHandle);
@@ -8225,6 +8394,7 @@ async function runMobileDebugServer(options = {}) {
8225
8394
  return router.active;
8226
8395
  });
8227
8396
  let lastAttachParts = null;
8397
+ let currentPhase = "active";
8228
8398
  const getDashboardState = () => {
8229
8399
  const targets = router.active.listTargets();
8230
8400
  const inspectorUrl = qrServer?.inspectorStableUrl ?? null;
@@ -8238,7 +8408,8 @@ async function runMobileDebugServer(options = {}) {
8238
8408
  url: t.url
8239
8409
  })),
8240
8410
  attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null,
8241
- inspectorUrl
8411
+ inspectorUrl,
8412
+ phase: currentPhase
8242
8413
  };
8243
8414
  };
8244
8415
  const getDirectInspectorUrl = () => {
@@ -8302,6 +8473,8 @@ async function runMobileDebugServer(options = {}) {
8302
8473
  const shutdown = () => {
8303
8474
  if (closed) return;
8304
8475
  closed = true;
8476
+ currentPhase = "shutdown";
8477
+ qrServer?.notifyStateChange();
8305
8478
  parentWatcher?.stop();
8306
8479
  maxAgeWatchdog?.stop();
8307
8480
  if (totpRefreshHandle) clearInterval(totpRefreshHandle);
@@ -8823,7 +8996,7 @@ function createDevServer(deps = {}) {
8823
8996
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
8824
8997
  const server = new Server({
8825
8998
  name: "ait-devtools",
8826
- version: "0.1.129"
8999
+ version: "0.1.132"
8827
9000
  }, { capabilities: { tools: {} } });
8828
9001
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
8829
9002
  server.setRequestHandler(CallToolRequestSchema, async (request) => {