@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.
- package/dist/{attach-orchestrator-DvrgUIx7.js → attach-orchestrator-BiaYiNOr.js} +31 -13
- package/dist/{attach-orchestrator-DvrgUIx7.js.map → attach-orchestrator-BiaYiNOr.js.map} +1 -1
- package/dist/{attach-orchestrator-RhOdO3DS.js → attach-orchestrator-Cjdhir2U.js} +31 -13
- package/dist/{attach-orchestrator-RhOdO3DS.js.map → attach-orchestrator-Cjdhir2U.js.map} +1 -1
- package/dist/{attach-orchestrator-Bdt5Zdo9.js → attach-orchestrator-ca34r46y.js} +31 -13
- package/dist/{attach-orchestrator-Bdt5Zdo9.js.map → attach-orchestrator-ca34r46y.js.map} +1 -1
- package/dist/{cell-Ba9lCI3B.js → cell-Bi8MAPA8.js} +11 -7
- package/dist/cell-Bi8MAPA8.js.map +1 -0
- package/dist/{cell-DA_lcpDI.js → cell-Cw4fVOl2.js} +11 -7
- package/dist/cell-Cw4fVOl2.js.map +1 -0
- package/dist/{cell-DLRv4iG6.js → cell-rr6gL9AT.js} +11 -7
- package/dist/cell-rr6gL9AT.js.map +1 -0
- package/dist/{debug-server-e1qPe0ZB.js → debug-server-BBUzxZSf.js} +4 -4
- package/dist/{debug-server-e1qPe0ZB.js.map → debug-server-BBUzxZSf.js.map} +1 -1
- package/dist/{debug-server--Y0QZVJS.js → debug-server-Ch5p0ryv.js} +33 -11
- package/dist/debug-server-Ch5p0ryv.js.map +1 -0
- package/dist/{debug-server-Bu3pCG9c.js → debug-server-DoWAtTNj.js} +27 -9
- package/dist/debug-server-DoWAtTNj.js.map +1 -0
- package/dist/in-app/auto.js +15 -0
- package/dist/in-app/auto.js.map +1 -1
- package/dist/in-app/index.d.ts +27 -0
- package/dist/in-app/index.d.ts.map +1 -1
- package/dist/in-app/index.js +15 -0
- package/dist/in-app/index.js.map +1 -1
- package/dist/mcp/cli.js +211 -38
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/panel/index.js +11 -1
- package/dist/panel/index.js.map +1 -1
- package/dist/{pool-C0pc7T2p.d.ts → pool-BtLh7U1y.d.ts} +24 -2
- package/dist/{pool-C0pc7T2p.d.ts.map → pool-BtLh7U1y.d.ts.map} +1 -1
- package/dist/{qr-http-server-DM2R25jP.cjs → qr-http-server-DX_9UA-p.cjs} +77 -6
- package/dist/qr-http-server-DX_9UA-p.cjs.map +1 -0
- package/dist/{qr-http-server-HANhOFwI.js → qr-http-server-DeRrKp_R.js} +77 -6
- package/dist/qr-http-server-DeRrKp_R.js.map +1 -0
- package/dist/{qr-http-server-CrGohOwC.js → qr-http-server-FVn61xAq.js} +77 -6
- package/dist/qr-http-server-FVn61xAq.js.map +1 -0
- package/dist/{qr-http-server-s1iW_5VQ.js → qr-http-server-KB147M-S.js} +77 -6
- package/dist/qr-http-server-KB147M-S.js.map +1 -0
- package/dist/{qr-http-server-BH0UFeUn.js → qr-http-server-V0xuU5TJ.js} +77 -6
- package/dist/qr-http-server-V0xuU5TJ.js.map +1 -0
- package/dist/{qr-http-server-Bt_RAxFn.cjs → qr-http-server-iH_Hh_si.cjs} +77 -6
- package/dist/qr-http-server-iH_Hh_si.cjs.map +1 -0
- package/dist/{relay-factory-Ck3vBonr.js → relay-factory-cNgPfrWg.js} +39 -13
- package/dist/relay-factory-cNgPfrWg.js.map +1 -0
- package/dist/{relay-worker-o0M2ndyJ.d.ts → relay-worker-BMSnlyls.d.ts} +39 -2
- package/dist/relay-worker-BMSnlyls.d.ts.map +1 -0
- package/dist/{relay-worker-Bt0QYKQH.js → relay-worker-l34xNuci.js} +28 -7
- package/dist/relay-worker-l34xNuci.js.map +1 -0
- package/dist/test-runner/bin.js +202 -36
- package/dist/test-runner/bin.js.map +1 -1
- package/dist/test-runner/config.d.ts +15 -9
- package/dist/test-runner/config.d.ts.map +1 -1
- package/dist/test-runner/config.js +1 -1
- package/dist/test-runner/pool.d.ts +1 -1
- package/dist/test-runner/pool.js +1 -1
- package/dist/test-runner/pool.js.map +1 -1
- package/dist/test-runner/relay-factory.d.ts +36 -8
- package/dist/test-runner/relay-factory.d.ts.map +1 -1
- package/dist/test-runner/relay-factory.js +38 -12
- package/dist/test-runner/relay-factory.js.map +1 -1
- package/dist/test-runner/relay-worker.d.ts +2 -2
- package/dist/test-runner/relay-worker.js +2 -2
- package/dist/test-runner/report.d.ts +26 -4
- package/dist/test-runner/report.d.ts.map +1 -1
- package/dist/test-runner/report.js +41 -7
- package/dist/test-runner/report.js.map +1 -1
- package/dist/{tunnel-DyEV6WwL.cjs → tunnel-DP453L4e.cjs} +4 -3
- package/dist/{tunnel-DyEV6WwL.cjs.map → tunnel-DP453L4e.cjs.map} +1 -1
- package/dist/{tunnel-BFwOSwT4.js → tunnel-Djf-zmjJ.js} +4 -3
- package/dist/{tunnel-BFwOSwT4.js.map → tunnel-Djf-zmjJ.js.map} +1 -1
- package/dist/unplugin/index.cjs +1 -1
- package/dist/unplugin/index.js +1 -1
- package/dist/unplugin/tunnel.cjs +3 -2
- package/dist/unplugin/tunnel.cjs.map +1 -1
- package/dist/unplugin/tunnel.d.cts.map +1 -1
- package/dist/unplugin/tunnel.d.ts.map +1 -1
- package/dist/unplugin/tunnel.js +3 -2
- package/dist/unplugin/tunnel.js.map +1 -1
- package/package.json +1 -1
- package/dist/cell-Ba9lCI3B.js.map +0 -1
- package/dist/cell-DA_lcpDI.js.map +0 -1
- package/dist/cell-DLRv4iG6.js.map +0 -1
- package/dist/debug-server--Y0QZVJS.js.map +0 -1
- package/dist/debug-server-Bu3pCG9c.js.map +0 -1
- package/dist/qr-http-server-BH0UFeUn.js.map +0 -1
- package/dist/qr-http-server-Bt_RAxFn.cjs.map +0 -1
- package/dist/qr-http-server-CrGohOwC.js.map +0 -1
- package/dist/qr-http-server-DM2R25jP.cjs.map +0 -1
- package/dist/qr-http-server-HANhOFwI.js.map +0 -1
- package/dist/qr-http-server-s1iW_5VQ.js.map +0 -1
- package/dist/relay-factory-Ck3vBonr.js.map +0 -1
- package/dist/relay-worker-Bt0QYKQH.js.map +0 -1
- package/dist/relay-worker-o0M2ndyJ.d.ts.map +0 -1
package/dist/test-runner/bin.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
@@ -45,11 +94,14 @@ async function discoverTestFiles(patterns, cwd) {
|
|
|
45
94
|
* Builds a {@link RelayConnectionFactory} that opens a standalone env3 relay
|
|
46
95
|
* connection.
|
|
47
96
|
*
|
|
48
|
-
* `open()` performs the full attach lifecycle and BLOCKS
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
97
|
+
* `open()` performs the full attach lifecycle and BLOCKS while a human scans
|
|
98
|
+
* the rendered QR with their phone — there is no way around the manual scan
|
|
99
|
+
* for env3. By default the wait is UNBOUNDED (`opts.timeoutMs` omitted): the
|
|
100
|
+
* runner stays up until the user stops it (Ctrl-C/SIGTERM), since QR-scan is
|
|
101
|
+
* a human-paced action with no sound default bound (devtools#735). Passing an
|
|
102
|
+
* explicit `timeoutMs` opts into the old bounded behavior (CI/headless
|
|
103
|
+
* callers). It resolves with the live `CdpConnection` once a matching page
|
|
104
|
+
* attaches; `close()` tears the relay family down.
|
|
53
105
|
*
|
|
54
106
|
* The factory holds the booted relay family in a closure so `close()` can stop
|
|
55
107
|
* it. A second `open()` on the same factory boots a fresh family (the previous
|
|
@@ -57,16 +109,18 @@ async function discoverTestFiles(patterns, cwd) {
|
|
|
57
109
|
*/
|
|
58
110
|
function createRelayConnectionFactory(opts) {
|
|
59
111
|
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
60
|
-
const timeoutMs = opts.timeoutMs ??
|
|
112
|
+
const timeoutMs = opts.timeoutMs ?? Number.POSITIVE_INFINITY;
|
|
61
113
|
const headless = opts.headless === true;
|
|
62
114
|
let family;
|
|
63
115
|
let qrServer;
|
|
116
|
+
let phase = "active";
|
|
117
|
+
let manualPrompt = null;
|
|
64
118
|
return {
|
|
65
119
|
async open() {
|
|
66
|
-
const { prepareAttach, renderAndMaybeWait, mintAttachUrl } = await import("../attach-orchestrator-
|
|
67
|
-
const { injectDebugIndicator, injectGlobals } = await import("../cell-
|
|
120
|
+
const { prepareAttach, renderAndMaybeWait, mintAttachUrl } = await import("../attach-orchestrator-ca34r46y.js");
|
|
121
|
+
const { injectDebugIndicator, injectGlobals } = await import("../cell-rr6gL9AT.js");
|
|
68
122
|
const { loadRelaySecretReadOnly } = await import("../relay-secret-store-CFc9n0OA.js");
|
|
69
|
-
const { bootRelayFamily, buildRelayVerifyAuth } = await import("../debug-server-
|
|
123
|
+
const { bootRelayFamily, buildRelayVerifyAuth } = await import("../debug-server-BBUzxZSf.js");
|
|
70
124
|
await loadRelaySecretReadOnly({ projectRoot });
|
|
71
125
|
let resolveTunnelUp;
|
|
72
126
|
const tunnelReady = new Promise((resolve) => {
|
|
@@ -77,6 +131,9 @@ function createRelayConnectionFactory(opts) {
|
|
|
77
131
|
onWssUrl: () => {
|
|
78
132
|
resolveTunnelUp();
|
|
79
133
|
qrServer?.notifyStateChange();
|
|
134
|
+
},
|
|
135
|
+
onTunnelDown: () => {
|
|
136
|
+
qrServer?.notifyStateChange();
|
|
80
137
|
}
|
|
81
138
|
});
|
|
82
139
|
family = booted;
|
|
@@ -93,12 +150,14 @@ function createRelayConnectionFactory(opts) {
|
|
|
93
150
|
canOpenBrowser: () => !headless
|
|
94
151
|
};
|
|
95
152
|
try {
|
|
96
|
-
const { startQrHttpServer } = await import("../qr-http-server-
|
|
153
|
+
const { startQrHttpServer } = await import("../qr-http-server-KB147M-S.js");
|
|
97
154
|
const getDashboardState = () => ({
|
|
98
155
|
tunnel: attachDeps.getTunnelStatus(),
|
|
99
156
|
pages: null,
|
|
100
157
|
attachUrl: lastAttachParts ? mintAttachUrl(attachDeps, lastAttachParts) : null,
|
|
101
|
-
mode: "relay-dev"
|
|
158
|
+
mode: "relay-dev",
|
|
159
|
+
phase,
|
|
160
|
+
manualPrompt
|
|
102
161
|
});
|
|
103
162
|
qrServer = await startQrHttpServer(getDashboardState);
|
|
104
163
|
attachDeps.qrHttpServer = qrServer;
|
|
@@ -151,7 +210,23 @@ function createRelayConnectionFactory(opts) {
|
|
|
151
210
|
if (opts.cell !== void 0) await injectGlobals(booted.connection, { __AIT_CELL__: opts.cell });
|
|
152
211
|
return booted.connection;
|
|
153
212
|
},
|
|
154
|
-
|
|
213
|
+
onSessionPhase(next) {
|
|
214
|
+
phase = next;
|
|
215
|
+
qrServer?.notifyStateChange();
|
|
216
|
+
},
|
|
217
|
+
onManualPrompt(next) {
|
|
218
|
+
manualPrompt = next;
|
|
219
|
+
qrServer?.notifyStateChange();
|
|
220
|
+
},
|
|
221
|
+
async close(connection) {
|
|
222
|
+
if (phase !== "complete") {
|
|
223
|
+
phase = "complete";
|
|
224
|
+
qrServer?.notifyStateChange();
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const { injectDebugIndicator } = await import("../cell-rr6gL9AT.js");
|
|
228
|
+
await injectDebugIndicator(connection, { state: "disconnected" });
|
|
229
|
+
} catch {}
|
|
155
230
|
family?.stop();
|
|
156
231
|
family = void 0;
|
|
157
232
|
await qrServer?.close();
|
|
@@ -484,7 +559,11 @@ var ChiiCdpConnection = class {
|
|
|
484
559
|
* EventEmitter is missed (defensive belt-and-suspenders).
|
|
485
560
|
*
|
|
486
561
|
* @param filterFn - Predicate that the returned targets must satisfy.
|
|
487
|
-
* @param timeoutMs - Reject after this many ms (default 90 000).
|
|
562
|
+
* @param timeoutMs - Reject after this many ms (default 90 000). Pass a
|
|
563
|
+
* non-finite value (`Infinity`) to disable the rejection timer entirely —
|
|
564
|
+
* used by the test-runner's unbounded QR-attach wait (devtools#735). Node
|
|
565
|
+
* clamps `setTimeout(fn, Infinity)` to ~1ms, so a non-finite value MUST
|
|
566
|
+
* skip arming the timer rather than pass it through.
|
|
488
567
|
* @param pollIntervalMs - Fallback poll interval (default 500ms).
|
|
489
568
|
*/
|
|
490
569
|
waitForFirstTarget(filterFn, timeoutMs = 9e4, pollIntervalMs = 500) {
|
|
@@ -496,7 +575,7 @@ var ChiiCdpConnection = class {
|
|
|
496
575
|
const settle = (targets) => {
|
|
497
576
|
if (settled) return;
|
|
498
577
|
settled = true;
|
|
499
|
-
clearTimeout(timeoutHandle);
|
|
578
|
+
if (timeoutHandle !== null) clearTimeout(timeoutHandle);
|
|
500
579
|
if (pollHandle !== null) {
|
|
501
580
|
clearInterval(pollHandle);
|
|
502
581
|
pollHandle = null;
|
|
@@ -507,7 +586,7 @@ var ChiiCdpConnection = class {
|
|
|
507
586
|
const onAttach = (targets) => {
|
|
508
587
|
if (filterFn(targets)) settle(targets);
|
|
509
588
|
};
|
|
510
|
-
const timeoutHandle = setTimeout(() => {
|
|
589
|
+
const timeoutHandle = Number.isFinite(timeoutMs) ? setTimeout(() => {
|
|
511
590
|
if (settled) return;
|
|
512
591
|
settled = true;
|
|
513
592
|
if (pollHandle !== null) {
|
|
@@ -516,7 +595,7 @@ var ChiiCdpConnection = class {
|
|
|
516
595
|
}
|
|
517
596
|
this.emitter.off("target:attached", onAttach);
|
|
518
597
|
reject(/* @__PURE__ */ new Error(`waitForFirstTarget: 타임아웃 (${timeoutMs}ms) — 폰이 relay에 attach되지 않았습니다.`));
|
|
519
|
-
}, timeoutMs);
|
|
598
|
+
}, timeoutMs) : null;
|
|
520
599
|
this.emitter.on("target:attached", onAttach);
|
|
521
600
|
pollHandle = setInterval(() => {
|
|
522
601
|
this.refreshTargets().then((targets) => {
|
|
@@ -1292,6 +1371,13 @@ async function injectAndRunBundle(connection, bundleCode, timeoutMs = DEFAULT_TI
|
|
|
1292
1371
|
//#endregion
|
|
1293
1372
|
//#region src/test-runner/relay-worker.ts
|
|
1294
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
|
+
/**
|
|
1295
1381
|
* Sentinel string embedded in the error message by `injectAndRunBundle` when
|
|
1296
1382
|
* the per-file evaluate race hits the timeout. Used by the retry guard so only
|
|
1297
1383
|
* genuine timeouts get a second attempt — not bundle errors or parse failures.
|
|
@@ -1362,12 +1448,21 @@ async function runTestFilesOverRelay(connection, files, opts) {
|
|
|
1362
1448
|
process.stderr.write(`relay-worker: reconnect failed (${e instanceof Error ? e.message : String(e)}) — continuing, remaining files may fail\n`);
|
|
1363
1449
|
}
|
|
1364
1450
|
};
|
|
1451
|
+
const manualFiles = opts?.manualFiles;
|
|
1452
|
+
const manualTotal = manualFiles?.size ?? 0;
|
|
1453
|
+
let manualIndex = 0;
|
|
1365
1454
|
try {
|
|
1366
1455
|
for (const file of files) {
|
|
1367
1456
|
if (pendingReconnectCheck) {
|
|
1368
1457
|
await attemptReconnect("next-file");
|
|
1369
1458
|
pendingReconnectCheck = false;
|
|
1370
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;
|
|
1371
1466
|
let fileEntry;
|
|
1372
1467
|
try {
|
|
1373
1468
|
const { code } = await bundleTestFile(file, opts?.bundleOptions);
|
|
@@ -1393,7 +1488,7 @@ async function runTestFilesOverRelay(connection, files, opts) {
|
|
|
1393
1488
|
const attempt = async () => {
|
|
1394
1489
|
let rpcResult;
|
|
1395
1490
|
try {
|
|
1396
|
-
rpcResult = await injectAndRunBundle(connection, code,
|
|
1491
|
+
rpcResult = await injectAndRunBundle(connection, code, fileTimeoutMs);
|
|
1397
1492
|
} catch (e) {
|
|
1398
1493
|
return {
|
|
1399
1494
|
file,
|
|
@@ -1429,6 +1524,7 @@ async function runTestFilesOverRelay(connection, files, opts) {
|
|
|
1429
1524
|
};
|
|
1430
1525
|
}
|
|
1431
1526
|
if ("error" in fileEntry.result && isRelayDisconnectMessage(fileEntry.result.error)) pendingReconnectCheck = true;
|
|
1527
|
+
if (isManual) fileEntry.mode = "manual";
|
|
1432
1528
|
fileResults.push(fileEntry);
|
|
1433
1529
|
}
|
|
1434
1530
|
} finally {
|
|
@@ -1542,9 +1638,11 @@ function serializeRelayReport(report, meta) {
|
|
|
1542
1638
|
totals: report.totals,
|
|
1543
1639
|
files: report.files.map((f) => {
|
|
1544
1640
|
const file = relativise(meta.projectRoot, f.file);
|
|
1641
|
+
const modeField = f.mode === "manual" ? { mode: "manual" } : {};
|
|
1545
1642
|
if ("error" in f.result) return {
|
|
1546
1643
|
file,
|
|
1547
|
-
error: f.result.error
|
|
1644
|
+
error: f.result.error,
|
|
1645
|
+
...modeField
|
|
1548
1646
|
};
|
|
1549
1647
|
return {
|
|
1550
1648
|
file,
|
|
@@ -1552,33 +1650,65 @@ function serializeRelayReport(report, meta) {
|
|
|
1552
1650
|
passed: f.result.passed,
|
|
1553
1651
|
failed: f.result.failed,
|
|
1554
1652
|
skipped: f.result.skipped,
|
|
1555
|
-
tests: f.result.tests
|
|
1653
|
+
tests: f.result.tests,
|
|
1654
|
+
...modeField
|
|
1556
1655
|
};
|
|
1557
1656
|
})
|
|
1558
1657
|
};
|
|
1559
1658
|
}
|
|
1560
1659
|
/**
|
|
1561
1660
|
* Writes the serialised report to `<dir>/<sdkLine>.<platform>.json`, creating
|
|
1562
|
-
* `dir` if needed. Returns the absolute path written.
|
|
1661
|
+
* `dir` if needed. Returns the absolute path(s) written.
|
|
1563
1662
|
*
|
|
1564
1663
|
* The cell-suffixed filename keeps 2.x and 3.0 (and per-platform) runs as
|
|
1565
1664
|
* distinct artifacts in the same directory; the same cell metadata is also baked
|
|
1566
1665
|
* into the body so a renamed/moved file still carries its provenance.
|
|
1567
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
|
+
*
|
|
1568
1679
|
* SECRET-HANDLING: the written body contains no relay/secret fields (the schema
|
|
1569
1680
|
* has none). `dir`/`projectRoot` are local filesystem paths, never logged here.
|
|
1570
1681
|
*
|
|
1571
1682
|
* @param report - The core relay run report.
|
|
1572
1683
|
* @param dir - Output directory (created recursively if missing).
|
|
1573
1684
|
* @param meta - Cell axes + projectRoot.
|
|
1574
|
-
* @returns The absolute path
|
|
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.
|
|
1575
1688
|
*/
|
|
1576
1689
|
async function writeReportArtifact(report, dir, meta) {
|
|
1577
1690
|
const serialised = serializeRelayReport(report, meta);
|
|
1578
1691
|
await mkdir(dir, { recursive: true });
|
|
1579
|
-
const
|
|
1580
|
-
|
|
1581
|
-
|
|
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;
|
|
1582
1712
|
}
|
|
1583
1713
|
/**
|
|
1584
1714
|
* Writes harvested `__AIT_CAPTURE__` lines to per-category files under `dir`,
|
|
@@ -1649,9 +1779,9 @@ OPTIONS
|
|
|
1649
1779
|
the CLI waits for a human to scan the QR code — use
|
|
1650
1780
|
--attach-timeout for that.
|
|
1651
1781
|
--attach-timeout <ms> How long to wait for a human to scan the QR code with
|
|
1652
|
-
their phone (default
|
|
1653
|
-
|
|
1654
|
-
|
|
1782
|
+
their phone. Omit (default) to wait indefinitely — the
|
|
1783
|
+
runner stays up until you stop it (Ctrl-C/SIGTERM).
|
|
1784
|
+
Pass a value to bound the wait for CI/headless runs.
|
|
1655
1785
|
--cell-sdk-line <line> SDK line to inject as __AIT_CELL__.sdkLine (2.x|3.x)
|
|
1656
1786
|
--cell-platform <plat> Platform to inject as __AIT_CELL__.platform
|
|
1657
1787
|
(mock|ios|android, default: AIT_CELL_PLATFORM env)
|
|
@@ -1664,6 +1794,24 @@ OPTIONS
|
|
|
1664
1794
|
--headless Disable browser auto-open (text QR only)
|
|
1665
1795
|
--project-root <dir> Project root for .ait_relay secret lookup
|
|
1666
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.
|
|
1667
1815
|
--help, -h Show this help message
|
|
1668
1816
|
|
|
1669
1817
|
DESCRIPTION
|
|
@@ -1812,7 +1960,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1812
1960
|
"report-dir": { type: "string" },
|
|
1813
1961
|
"no-qr-stdout": { type: "boolean" },
|
|
1814
1962
|
headless: { type: "boolean" },
|
|
1815
|
-
"project-root": { type: "string" }
|
|
1963
|
+
"project-root": { type: "string" },
|
|
1964
|
+
"manual-blocking": { type: "boolean" }
|
|
1816
1965
|
},
|
|
1817
1966
|
allowPositionals: true
|
|
1818
1967
|
});
|
|
@@ -1843,6 +1992,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1843
1992
|
const projectRoot = typeof vals["project-root"] === "string" ? vals["project-root"] : process.cwd();
|
|
1844
1993
|
const reportDir = typeof vals["report-dir"] === "string" ? vals["report-dir"] : void 0;
|
|
1845
1994
|
const suppressQr = shouldSuppressQr(vals["no-qr-stdout"] === true);
|
|
1995
|
+
const manualBlocking = vals["manual-blocking"] === true;
|
|
1846
1996
|
const cellSdkLine = typeof vals["cell-sdk-line"] === "string" ? vals["cell-sdk-line"] : void 0;
|
|
1847
1997
|
const cellPlatform = typeof vals["cell-platform"] === "string" ? vals["cell-platform"] : process.env.AIT_CELL_PLATFORM;
|
|
1848
1998
|
const hasCell = cellSdkLine !== void 0 || cellPlatform !== void 0;
|
|
@@ -1857,13 +2007,16 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1857
2007
|
process.exitCode = 1;
|
|
1858
2008
|
return;
|
|
1859
2009
|
}
|
|
1860
|
-
const
|
|
1861
|
-
if (
|
|
2010
|
+
const discovered = await discoverTestFiles(globs, process.cwd(), { includeManual: manualBlocking });
|
|
2011
|
+
if (discovered.length === 0) {
|
|
1862
2012
|
process.stderr.write(`devtools-test: no test files matched ${globs.join(", ")}\n`);
|
|
1863
2013
|
process.exitCode = 1;
|
|
1864
2014
|
return;
|
|
1865
2015
|
}
|
|
1866
|
-
|
|
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`);
|
|
1867
2020
|
if (hasCell) process.stderr.write(`devtools-test: injecting __AIT_CELL__ = ${JSON.stringify(cell)}\n`);
|
|
1868
2021
|
const factory = createRelayConnectionFactory({
|
|
1869
2022
|
schemeUrl,
|
|
@@ -1889,18 +2042,30 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1889
2042
|
}
|
|
1890
2043
|
let exitCode = 0;
|
|
1891
2044
|
try {
|
|
2045
|
+
factory.onSessionPhase?.("running");
|
|
1892
2046
|
const report = await runWithConnection(connection, files, {
|
|
1893
2047
|
timeoutMs: evaluateTimeoutMs,
|
|
1894
2048
|
printSummary: true,
|
|
1895
|
-
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
|
+
}
|
|
1896
2060
|
});
|
|
2061
|
+
if (manualBlocking && manualFileSet.size > 0) factory.onManualPrompt?.(null);
|
|
1897
2062
|
if (reportDir !== void 0) try {
|
|
1898
|
-
const
|
|
2063
|
+
const reportPaths = await writeReportArtifact(report, reportDir, {
|
|
1899
2064
|
sdkLine: cell.sdkLine,
|
|
1900
2065
|
platform: cell.platform,
|
|
1901
2066
|
projectRoot
|
|
1902
2067
|
});
|
|
1903
|
-
process.stderr.write(`devtools-test: wrote report ${reportPath}\n`);
|
|
2068
|
+
for (const reportPath of reportPaths) process.stderr.write(`devtools-test: wrote report ${reportPath}\n`);
|
|
1904
2069
|
const capturePaths = await writeCaptureArtifacts(report.captures, `${reportDir}/.ait-capture`, cell);
|
|
1905
2070
|
if (capturePaths.length > 0) process.stderr.write(`devtools-test: wrote ${capturePaths.length} capture file(s)\n`);
|
|
1906
2071
|
} catch (e) {
|
|
@@ -1908,6 +2073,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1908
2073
|
}
|
|
1909
2074
|
exitCode = report.totals.failed > 0 ? 1 : 0;
|
|
1910
2075
|
} finally {
|
|
2076
|
+
factory.onSessionPhase?.("complete");
|
|
1911
2077
|
await factory.close(connection);
|
|
1912
2078
|
process.exitCode = exitCode;
|
|
1913
2079
|
}
|