@ait-co/devtools 0.1.135 → 0.1.137
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundle-BKqyhEK9.d.ts.map +1 -1
- package/dist/{chii-relay-DkTOopRj.js → chii-relay-BZ3HqWL5.js} +2 -1
- package/dist/{chii-relay-DkTOopRj.js.map → chii-relay-BZ3HqWL5.js.map} +1 -1
- package/dist/{chii-relay-P6SKmB1g.cjs → chii-relay-D7eK2acz.cjs} +2 -1
- package/dist/{chii-relay-P6SKmB1g.cjs.map → chii-relay-D7eK2acz.cjs.map} +1 -1
- package/dist/{debug-server-DR84Z243.js → debug-server-BQplCzf8.js} +23 -4
- package/dist/debug-server-BQplCzf8.js.map +1 -0
- package/dist/{debug-server-B2aCwwtV.js → debug-server-CBK7dn8O.js} +21 -2
- package/dist/debug-server-CBK7dn8O.js.map +1 -0
- package/dist/{debug-server-CBqMfcUi.js → debug-server-D2wpPK3_.js} +3 -2
- package/dist/debug-server-D2wpPK3_.js.map +1 -0
- package/dist/mcp/cli.js +72 -19
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/panel/index.js +1 -1
- package/dist/{pool-C6TgrcyW.d.ts → pool-CmfvXnwh.d.ts} +2 -2
- package/dist/{pool-C6TgrcyW.d.ts.map → pool-CmfvXnwh.d.ts.map} +1 -1
- package/dist/{qr-http-server-yQ1EWTwf.js → qr-http-server-C7q8Gn04.js} +2 -1
- package/dist/{qr-http-server-iBqsAKor.js.map → qr-http-server-C7q8Gn04.js.map} +1 -1
- package/dist/{qr-http-server-iBqsAKor.js → qr-http-server-CTLdclg_.js} +2 -1
- package/dist/{qr-http-server-yQ1EWTwf.js.map → qr-http-server-CTLdclg_.js.map} +1 -1
- package/dist/{qr-http-server-B1en4tTL.cjs → qr-http-server-Cz1mA07b.cjs} +2 -1
- package/dist/{qr-http-server-hnuUw9TK.cjs.map → qr-http-server-Cz1mA07b.cjs.map} +1 -1
- package/dist/{qr-http-server-hnuUw9TK.cjs → qr-http-server-D16Lq2ag.cjs} +2 -1
- package/dist/{qr-http-server-B1en4tTL.cjs.map → qr-http-server-D16Lq2ag.cjs.map} +1 -1
- package/dist/{qr-http-server-DlNWJEaH.js → qr-http-server-ZkT6F7is.js} +2 -1
- package/dist/{qr-http-server-CLt72Dwe.js.map → qr-http-server-ZkT6F7is.js.map} +1 -1
- package/dist/{qr-http-server-CLt72Dwe.js → qr-http-server-wy3L7Yut.js} +2 -1
- package/dist/{qr-http-server-DlNWJEaH.js.map → qr-http-server-wy3L7Yut.js.map} +1 -1
- package/dist/{relay-factory-CjqAJ3Y8.js → relay-factory-DVzpBRxz.js} +4 -3
- package/dist/relay-factory-DVzpBRxz.js.map +1 -0
- package/dist/{relay-worker-UK0nBfDX.js → relay-worker-6fy1eaIo.js} +6 -3
- package/dist/{relay-worker-UK0nBfDX.js.map → relay-worker-6fy1eaIo.js.map} +1 -1
- package/dist/{relay-worker-Dw3aIzIK.d.ts → relay-worker-DJnZkXza.d.ts} +41 -7
- package/dist/relay-worker-DJnZkXza.d.ts.map +1 -0
- package/dist/test-runner/bin.js +198 -30
- package/dist/test-runner/bin.js.map +1 -1
- package/dist/test-runner/bridge-stub.d.ts +125 -0
- package/dist/test-runner/bridge-stub.d.ts.map +1 -0
- package/dist/test-runner/bridge-stub.js +92 -0
- package/dist/test-runner/bridge-stub.js.map +1 -0
- package/dist/test-runner/bundle.js +44 -14
- package/dist/test-runner/bundle.js.map +1 -1
- package/dist/test-runner/config.d.ts +11 -1
- 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/relay-factory.d.ts +10 -0
- package/dist/test-runner/relay-factory.d.ts.map +1 -1
- package/dist/test-runner/relay-factory.js +3 -2
- package/dist/test-runner/relay-factory.js.map +1 -1
- package/dist/test-runner/relay-worker.d.ts +1 -1
- package/dist/test-runner/relay-worker.js +1 -1
- package/dist/test-runner/report.d.ts +41 -15
- package/dist/test-runner/report.d.ts.map +1 -1
- package/dist/test-runner/report.js +33 -9
- package/dist/test-runner/report.js.map +1 -1
- package/dist/{tunnel-DUCogcQi.js → tunnel-5eBncITa.js} +2 -2
- package/dist/{tunnel-DUCogcQi.js.map → tunnel-5eBncITa.js.map} +1 -1
- package/dist/{tunnel-Cr_oTkaj.cjs → tunnel-DjtSENJU.cjs} +2 -2
- package/dist/{tunnel-Cr_oTkaj.cjs.map → tunnel-DjtSENJU.cjs.map} +1 -1
- package/dist/unplugin/index.cjs +2 -2
- package/dist/unplugin/index.js +2 -2
- package/dist/unplugin/tunnel.cjs +1 -1
- package/dist/unplugin/tunnel.js +1 -1
- package/package.json +1 -1
- package/dist/debug-server-B2aCwwtV.js.map +0 -1
- package/dist/debug-server-CBqMfcUi.js.map +0 -1
- package/dist/debug-server-DR84Z243.js.map +0 -1
- package/dist/relay-factory-CjqAJ3Y8.js.map +0 -1
- package/dist/relay-worker-Dw3aIzIK.d.ts.map +0 -1
package/dist/test-runner/bin.js
CHANGED
|
@@ -122,7 +122,7 @@ function createRelayConnectionFactory(opts) {
|
|
|
122
122
|
const { prepareAttach, renderAndMaybeWait, mintAttachUrl } = await import("../attach-orchestrator-CE0S09YU.js").then((n) => n.i);
|
|
123
123
|
const { injectDebugIndicator, injectGlobals } = await import("../cell-BIdSQb9T.js").then((n) => n.t);
|
|
124
124
|
const { loadRelaySecretReadOnly } = await import("../relay-secret-store-CLEGyHou.js");
|
|
125
|
-
const { bootRelayFamily, buildRelayVerifyAuth } = await import("../debug-server-
|
|
125
|
+
const { bootRelayFamily, buildRelayVerifyAuth } = await import("../debug-server-D2wpPK3_.js");
|
|
126
126
|
await loadRelaySecretReadOnly({ projectRoot });
|
|
127
127
|
let resolveTunnelUp;
|
|
128
128
|
const tunnelReady = new Promise((resolve) => {
|
|
@@ -152,7 +152,7 @@ function createRelayConnectionFactory(opts) {
|
|
|
152
152
|
canOpenBrowser: () => !headless
|
|
153
153
|
};
|
|
154
154
|
try {
|
|
155
|
-
const { startQrHttpServer } = await import("../qr-http-server-
|
|
155
|
+
const { startQrHttpServer } = await import("../qr-http-server-C7q8Gn04.js");
|
|
156
156
|
const getDashboardState = () => ({
|
|
157
157
|
tunnel: attachDeps.getTunnelStatus(),
|
|
158
158
|
pages: null,
|
|
@@ -210,6 +210,7 @@ function createRelayConnectionFactory(opts) {
|
|
|
210
210
|
}
|
|
211
211
|
await injectDebugIndicator(booted.connection);
|
|
212
212
|
if (opts.cell !== void 0) await injectGlobals(booted.connection, { __AIT_CELL__: opts.cell });
|
|
213
|
+
if (opts.stubBlocking === true) await injectGlobals(booted.connection, { __AIT_STUB_BLOCKING__: true });
|
|
213
214
|
return booted.connection;
|
|
214
215
|
},
|
|
215
216
|
onSessionPhase(next) {
|
|
@@ -906,6 +907,16 @@ const SDK_IMPORT_FILTER = new RegExp(`^${SDK_PACKAGE.replace(/[.*+?^${}()|[\]\\]
|
|
|
906
907
|
*
|
|
907
908
|
* If `window.__sdk` is absent (non-dog-food build), every access throws a
|
|
908
909
|
* descriptive error rather than returning `undefined` silently.
|
|
910
|
+
*
|
|
911
|
+
* Bridge-stub interception (devtools#740, DT-2): the virtual module runs the
|
|
912
|
+
* resolved `window.__sdk` object through `wrapSdkWithStub` before exporting
|
|
913
|
+
* it. `wrapSdkWithStub` is a pass-through (returns the same object,
|
|
914
|
+
* unwrapped) unless `isStubBlockingEnabled()` reads `true` off
|
|
915
|
+
* `globalThis.__AIT_STUB_BLOCKING__` — an opt-in flag injected the same way
|
|
916
|
+
* `__AIT_CELL__` is (`cell.ts#injectGlobals`, wired from the CLI's
|
|
917
|
+
* `--stub-blocking` flag). Default off => zero behavior change: every existing
|
|
918
|
+
* (non-stub) test run resolves the exact same `window.__sdk` reference it did
|
|
919
|
+
* before this plugin learned about stubbing.
|
|
909
920
|
*/
|
|
910
921
|
function sdkRedirectPlugin() {
|
|
911
922
|
return {
|
|
@@ -920,16 +931,19 @@ function sdkRedirectPlugin() {
|
|
|
920
931
|
namespace: "sdk-redirect"
|
|
921
932
|
}, () => ({
|
|
922
933
|
contents: `
|
|
923
|
-
|
|
934
|
+
import { wrapSdkWithStub, isStubBlockingEnabled } from ${JSON.stringify(getBridgeStubPath())};
|
|
935
|
+
var __rawSdk = (typeof window !== 'undefined' && window.__sdk)
|
|
924
936
|
? window.__sdk
|
|
925
937
|
: new Proxy({}, {
|
|
926
938
|
get: function(_t, p) {
|
|
927
939
|
throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));
|
|
928
940
|
}
|
|
929
941
|
});
|
|
942
|
+
var __proxy = wrapSdkWithStub(__rawSdk, isStubBlockingEnabled());
|
|
930
943
|
module.exports = __proxy;
|
|
931
944
|
`,
|
|
932
|
-
loader: "js"
|
|
945
|
+
loader: "js",
|
|
946
|
+
resolveDir: process.cwd()
|
|
933
947
|
}));
|
|
934
948
|
}
|
|
935
949
|
};
|
|
@@ -1063,8 +1077,9 @@ function userFactoryPlugin(absPath) {
|
|
|
1063
1077
|
};
|
|
1064
1078
|
}
|
|
1065
1079
|
/**
|
|
1066
|
-
* Returns the absolute filesystem path to
|
|
1067
|
-
* (
|
|
1080
|
+
* Returns the absolute filesystem path to a page-side leaf module shipped
|
|
1081
|
+
* alongside `bundle.ts` in dist (e.g. `runtime.js`, `bridge-stub.js`) — a
|
|
1082
|
+
* fully self-contained module with no further page-side deps of its own.
|
|
1068
1083
|
*
|
|
1069
1084
|
* Rolldown code-splitting duplicates this bundling logic into shared chunks
|
|
1070
1085
|
* emitted at ARBITRARY dist depths: the `devtools-test` CLI pulls it from
|
|
@@ -1074,26 +1089,30 @@ function userFactoryPlugin(absPath) {
|
|
|
1074
1089
|
* is therefore wrong from at least one chunk — the live #697 regression.
|
|
1075
1090
|
*
|
|
1076
1091
|
* This resolves WITHOUT assuming chunk depth: from `import.meta.url`'s dir it
|
|
1077
|
-
* probes the co-located
|
|
1092
|
+
* probes the co-located `<name>.js` and the nested `test-runner/<name>.js`,
|
|
1078
1093
|
* then ascends one directory at a time (bounded) repeating both probes. The
|
|
1079
|
-
* nested probe catches dist/test-runner
|
|
1094
|
+
* nested probe catches dist/test-runner/<name>.js from the dist/ root level no
|
|
1080
1095
|
* matter which depth the chunk was hoisted to (root, dist/mcp/, or a future
|
|
1081
|
-
* relocation). The build always emits dist/test-runner
|
|
1082
|
-
* `'test-runner
|
|
1096
|
+
* relocation). The build always emits dist/test-runner/<name>.js (tsdown entry
|
|
1097
|
+
* `'test-runner/<name>'`; guarded by scripts/check-test-runner-dist.sh for
|
|
1098
|
+
* `runtime`).
|
|
1083
1099
|
*
|
|
1084
1100
|
* An ABSOLUTE path is returned deliberately: esbuild loads it as a literal file
|
|
1085
1101
|
* read, bypassing Node module resolution entirely, so this works identically in
|
|
1086
1102
|
* the npx-daemon context (its own dist tree) and the consumer-CLI context
|
|
1087
1103
|
* (the mini-app's installed @ait-co/devtools dist) — neither needs the package
|
|
1088
1104
|
* to be node-resolvable from the caller.
|
|
1105
|
+
*
|
|
1106
|
+
* @param moduleName - The leaf module's basename without extension, e.g.
|
|
1107
|
+
* `'runtime'` or `'bridge-stub'`.
|
|
1089
1108
|
*/
|
|
1090
|
-
function
|
|
1109
|
+
function getPageSideModulePath(moduleName) {
|
|
1091
1110
|
const startDir = path$1.dirname(fileURLToPath(import.meta.url));
|
|
1092
1111
|
const RELATIVE_PROBES = [
|
|
1093
|
-
[
|
|
1094
|
-
["test-runner",
|
|
1095
|
-
[
|
|
1096
|
-
["test-runner",
|
|
1112
|
+
[`${moduleName}.js`],
|
|
1113
|
+
["test-runner", `${moduleName}.js`],
|
|
1114
|
+
[`${moduleName}.ts`],
|
|
1115
|
+
["test-runner", `${moduleName}.ts`]
|
|
1097
1116
|
];
|
|
1098
1117
|
let dir = startDir;
|
|
1099
1118
|
for (let i = 0; i < 12; i++) {
|
|
@@ -1108,7 +1127,19 @@ function getRuntimePath() {
|
|
|
1108
1127
|
if (parent === dir) break;
|
|
1109
1128
|
dir = parent;
|
|
1110
1129
|
}
|
|
1111
|
-
return path$1.join(startDir,
|
|
1130
|
+
return path$1.join(startDir, `${moduleName}.js`);
|
|
1131
|
+
}
|
|
1132
|
+
/** Absolute path to the test-runner page-side runtime (describe/it/test/expect). */
|
|
1133
|
+
function getRuntimePath() {
|
|
1134
|
+
return getPageSideModulePath("runtime");
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Absolute path to the bridge-stub interceptor (devtools#740, DT-2) —
|
|
1138
|
+
* `wrapSdkWithStub`/`isStubBlockingEnabled`, statically imported by the
|
|
1139
|
+
* `sdk-redirect` virtual module.
|
|
1140
|
+
*/
|
|
1141
|
+
function getBridgeStubPath() {
|
|
1142
|
+
return getPageSideModulePath("bridge-stub");
|
|
1112
1143
|
}
|
|
1113
1144
|
/**
|
|
1114
1145
|
* Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.
|
|
@@ -1382,6 +1413,7 @@ async function runTestFilesOverRelay(connection, files, opts) {
|
|
|
1382
1413
|
const manualFiles = opts?.manualFiles;
|
|
1383
1414
|
const manualTotal = manualFiles?.size ?? 0;
|
|
1384
1415
|
let manualIndex = 0;
|
|
1416
|
+
const stubBlockingFiles = opts?.stubBlockingFiles;
|
|
1385
1417
|
let preflightPermissions;
|
|
1386
1418
|
if (files.length > 0) preflightPermissions = await runPermissionPreflight(connection);
|
|
1387
1419
|
try {
|
|
@@ -1390,7 +1422,8 @@ async function runTestFilesOverRelay(connection, files, opts) {
|
|
|
1390
1422
|
await attemptReconnect("next-file");
|
|
1391
1423
|
pendingReconnectCheck = false;
|
|
1392
1424
|
}
|
|
1393
|
-
const
|
|
1425
|
+
const isStubbed = stubBlockingFiles?.has(file) === true;
|
|
1426
|
+
const isManual = !isStubbed && manualFiles?.has(file) === true;
|
|
1394
1427
|
if (isManual) {
|
|
1395
1428
|
manualIndex += 1;
|
|
1396
1429
|
opts?.onManualFile?.(file, manualIndex, manualTotal);
|
|
@@ -1457,7 +1490,8 @@ async function runTestFilesOverRelay(connection, files, opts) {
|
|
|
1457
1490
|
};
|
|
1458
1491
|
}
|
|
1459
1492
|
if ("error" in fileEntry.result && isRelayDisconnectMessage(fileEntry.result.error)) pendingReconnectCheck = true;
|
|
1460
|
-
if (
|
|
1493
|
+
if (isStubbed) fileEntry.mode = "stubbed";
|
|
1494
|
+
else if (isManual) fileEntry.mode = "manual";
|
|
1461
1495
|
fileResults.push(fileEntry);
|
|
1462
1496
|
}
|
|
1463
1497
|
} finally {
|
|
@@ -1573,7 +1607,7 @@ function serializeRelayReport(report, meta) {
|
|
|
1573
1607
|
...report.preflight ? { preflight: report.preflight } : {},
|
|
1574
1608
|
files: report.files.map((f) => {
|
|
1575
1609
|
const file = relativise(meta.projectRoot, f.file);
|
|
1576
|
-
const modeField = f.mode === "manual" ? { mode:
|
|
1610
|
+
const modeField = f.mode === "manual" || f.mode === "stubbed" ? { mode: f.mode } : {};
|
|
1577
1611
|
if ("error" in f.result) return {
|
|
1578
1612
|
file,
|
|
1579
1613
|
error: f.result.error,
|
|
@@ -1605,11 +1639,21 @@ function serializeRelayReport(report, meta) {
|
|
|
1605
1639
|
* `<dir>/<sdkLine>.<platform>.manual.json` artifact instead of the standard
|
|
1606
1640
|
* one — the standard `<sdkLine>.<platform>.json` filename is reserved for the
|
|
1607
1641
|
* regular (unattended) files only, so a manual run's presence never mutates
|
|
1608
|
-
* what the unattended-baseline filename means.
|
|
1609
|
-
*
|
|
1610
|
-
*
|
|
1611
|
-
*
|
|
1612
|
-
*
|
|
1642
|
+
* what the unattended-baseline filename means.
|
|
1643
|
+
*
|
|
1644
|
+
* Stub-blocking provenance (devtools#740, DT-2): `mode: 'stubbed'` files are
|
|
1645
|
+
* written to a THIRD, separate `<dir>/<sdkLine>.<platform>.stubbed.json`
|
|
1646
|
+
* artifact — never merged into the standard file (it is unattended, like the
|
|
1647
|
+
* standard file, so a naive filename split could conflate the two) and never
|
|
1648
|
+
* merged into `.manual.json` (it did NOT have a human present). Its body is
|
|
1649
|
+
* ALSO stamped `cell.bridgeStub: true` — the mandatory HYBRID-cell provenance
|
|
1650
|
+
* from the issue: a stubbed result must never be silently mixed into the
|
|
1651
|
+
* real-device baseline report.
|
|
1652
|
+
*
|
|
1653
|
+
* If ALL files in the run are regular, only the standard artifact is written
|
|
1654
|
+
* (today's behavior, unchanged). Any combination of the three modes present
|
|
1655
|
+
* in a single run writes ONLY the corresponding artifacts — each is additive,
|
|
1656
|
+
* never replacing another.
|
|
1613
1657
|
*
|
|
1614
1658
|
* SECRET-HANDLING: the written body contains no relay/secret fields (the schema
|
|
1615
1659
|
* has none). `dir`/`projectRoot` are local filesystem paths, never logged here.
|
|
@@ -1618,14 +1662,16 @@ function serializeRelayReport(report, meta) {
|
|
|
1618
1662
|
* @param dir - Output directory (created recursively if missing).
|
|
1619
1663
|
* @param meta - Cell axes + projectRoot.
|
|
1620
1664
|
* @returns The absolute path(s) written, in order: standard first (if any
|
|
1621
|
-
* regular files ran), then manual (if any manual files ran)
|
|
1622
|
-
* path is always returned when
|
|
1665
|
+
* regular files ran), then manual (if any manual files ran), then stubbed
|
|
1666
|
+
* (if any stubbed files ran). At least one path is always returned when
|
|
1667
|
+
* `report.files` is non-empty.
|
|
1623
1668
|
*/
|
|
1624
1669
|
async function writeReportArtifact(report, dir, meta) {
|
|
1625
1670
|
const serialised = serializeRelayReport(report, meta);
|
|
1626
1671
|
await mkdir(dir, { recursive: true });
|
|
1627
|
-
const regularFiles = serialised.files.filter((f) => f.mode
|
|
1672
|
+
const regularFiles = serialised.files.filter((f) => f.mode === void 0);
|
|
1628
1673
|
const manualFiles = serialised.files.filter((f) => f.mode === "manual");
|
|
1674
|
+
const stubbedFiles = serialised.files.filter((f) => f.mode === "stubbed");
|
|
1629
1675
|
const written = [];
|
|
1630
1676
|
if (regularFiles.length > 0 || serialised.files.length === 0) {
|
|
1631
1677
|
const outFile = path.join(dir, `${meta.sdkLine}.${meta.platform}.json`);
|
|
@@ -1643,6 +1689,18 @@ async function writeReportArtifact(report, dir, meta) {
|
|
|
1643
1689
|
}, null, 2)}\n`, "utf8");
|
|
1644
1690
|
written.push(manualOutFile);
|
|
1645
1691
|
}
|
|
1692
|
+
if (stubbedFiles.length > 0) {
|
|
1693
|
+
const stubbedOutFile = path.join(dir, `${meta.sdkLine}.${meta.platform}.stubbed.json`);
|
|
1694
|
+
await writeFile(stubbedOutFile, `${JSON.stringify({
|
|
1695
|
+
...serialised,
|
|
1696
|
+
cell: {
|
|
1697
|
+
...serialised.cell,
|
|
1698
|
+
bridgeStub: true
|
|
1699
|
+
},
|
|
1700
|
+
files: stubbedFiles
|
|
1701
|
+
}, null, 2)}\n`, "utf8");
|
|
1702
|
+
written.push(stubbedOutFile);
|
|
1703
|
+
}
|
|
1646
1704
|
return written;
|
|
1647
1705
|
}
|
|
1648
1706
|
/**
|
|
@@ -1686,6 +1744,76 @@ async function writeCaptureArtifacts(captures, dir, cell) {
|
|
|
1686
1744
|
return written;
|
|
1687
1745
|
}
|
|
1688
1746
|
//#endregion
|
|
1747
|
+
//#region src/test-runner/teardown.ts
|
|
1748
|
+
/**
|
|
1749
|
+
* Runs `steps` in strict order, each bounded by `perStepTimeoutMs` so a
|
|
1750
|
+
* single hung `close()` cannot block the rest — a later step (e.g. "close
|
|
1751
|
+
* the QR HTTP server") still runs even if an earlier one (e.g. "flip the
|
|
1752
|
+
* on-phone badge over CDP") times out.
|
|
1753
|
+
*
|
|
1754
|
+
* Order is load-bearing: `relay-factory.ts`'s existing `close()` already
|
|
1755
|
+
* encodes "badge over still-open channel, THEN close that channel" — this
|
|
1756
|
+
* function does not reorder or parallelize, it just bounds+reports whatever
|
|
1757
|
+
* sequence the caller provides.
|
|
1758
|
+
*/
|
|
1759
|
+
async function runTeardownSteps(steps, options = {}) {
|
|
1760
|
+
const { perStepTimeoutMs = 5e3, setTimeoutFn = setTimeout, clearTimeoutFn = clearTimeout } = options;
|
|
1761
|
+
const results = [];
|
|
1762
|
+
for (const step of steps) results.push(await runOneStep(step, perStepTimeoutMs, setTimeoutFn, clearTimeoutFn));
|
|
1763
|
+
return results;
|
|
1764
|
+
}
|
|
1765
|
+
async function runOneStep(step, perStepTimeoutMs, setTimeoutFn, clearTimeoutFn) {
|
|
1766
|
+
let timeoutHandle;
|
|
1767
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
1768
|
+
timeoutHandle = setTimeoutFn(() => resolve("timeout"), perStepTimeoutMs);
|
|
1769
|
+
});
|
|
1770
|
+
try {
|
|
1771
|
+
const outcome = await Promise.race([Promise.resolve().then(() => step.close()).then(() => "ok"), timeoutPromise]);
|
|
1772
|
+
if (timeoutHandle !== void 0) clearTimeoutFn(timeoutHandle);
|
|
1773
|
+
return {
|
|
1774
|
+
name: step.name,
|
|
1775
|
+
status: outcome
|
|
1776
|
+
};
|
|
1777
|
+
} catch (e) {
|
|
1778
|
+
if (timeoutHandle !== void 0) clearTimeoutFn(timeoutHandle);
|
|
1779
|
+
return {
|
|
1780
|
+
name: step.name,
|
|
1781
|
+
status: "error",
|
|
1782
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Arms a grace-period timer that force-exits the process if it has not
|
|
1788
|
+
* already exited on its own by the time the timer fires. The timer is
|
|
1789
|
+
* deliberately NOT `.unref()`'d — its entire purpose is to hold the event
|
|
1790
|
+
* loop open long enough to fire if nothing else does, so unref'ing it would
|
|
1791
|
+
* defeat the backstop. `disarm()` clears it — the CLI calls `disarm()`
|
|
1792
|
+
* immediately once `main()`'s own teardown (Step 6, cli.ts) has finished,
|
|
1793
|
+
* so on the happy path the timer never fires (Node drains the loop and
|
|
1794
|
+
* exits naturally at whatever `process.exitCode` was already set to).
|
|
1795
|
+
*
|
|
1796
|
+
* SECRET-HANDLING: this function never touches stdout/stderr content — the
|
|
1797
|
+
* caller (cli.ts) is responsible for flushing any final output before the
|
|
1798
|
+
* backstop's `exitFn` runs.
|
|
1799
|
+
*/
|
|
1800
|
+
function armExitBackstop(options) {
|
|
1801
|
+
const { graceMs = 3e3, exitCode, setTimeoutFn = setTimeout, exitFn = (code) => process.exit(code) } = options;
|
|
1802
|
+
let fired = false;
|
|
1803
|
+
const handle = setTimeoutFn(() => {
|
|
1804
|
+
fired = true;
|
|
1805
|
+
exitFn(exitCode);
|
|
1806
|
+
}, graceMs);
|
|
1807
|
+
return {
|
|
1808
|
+
disarm() {
|
|
1809
|
+
clearTimeout(handle);
|
|
1810
|
+
},
|
|
1811
|
+
get fired() {
|
|
1812
|
+
return fired;
|
|
1813
|
+
}
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
//#endregion
|
|
1689
1817
|
//#region src/test-runner/cli.ts
|
|
1690
1818
|
/**
|
|
1691
1819
|
* `devtools-test` CLI.
|
|
@@ -1752,6 +1880,24 @@ OPTIONS
|
|
|
1752
1880
|
each manual file's report entry is stamped
|
|
1753
1881
|
mode: 'manual' — never diff a manual run against an
|
|
1754
1882
|
unattended baseline as if they were equivalent.
|
|
1883
|
+
--stub-blocking Run manual-tagged test files (*.manual.ait.test.ts)
|
|
1884
|
+
UNATTENDED (devtools#740, DT-2) by intercepting a
|
|
1885
|
+
fixed allowlist of blocking-UI SDK calls (ads
|
|
1886
|
+
show*, openPermissionDialog/requestPermission,
|
|
1887
|
+
saveBase64Data) in the page and answering them from
|
|
1888
|
+
fixtures captured by a real --manual-blocking run,
|
|
1889
|
+
instead of forwarding them to native UI. Implies
|
|
1890
|
+
--manual-blocking (manual files are included in the
|
|
1891
|
+
run); no human presence or QR-dashboard prompt is
|
|
1892
|
+
needed for them. HYBRID cell, not pure env3 — every
|
|
1893
|
+
other SDK call in the same run still hits the real
|
|
1894
|
+
native bridge. With --report-dir, files that ran
|
|
1895
|
+
under the stub are written to a SEPARATE
|
|
1896
|
+
<sdkLine>.<platform>.stubbed.json artifact (never
|
|
1897
|
+
merged into the standard or .manual.json report) and
|
|
1898
|
+
the report body is stamped cell.bridgeStub: true —
|
|
1899
|
+
never diff a stubbed run against a real device
|
|
1900
|
+
baseline (manual or unattended) as if equivalent.
|
|
1755
1901
|
--help, -h Show this help message
|
|
1756
1902
|
|
|
1757
1903
|
DESCRIPTION
|
|
@@ -1778,6 +1924,15 @@ EXAMPLE
|
|
|
1778
1924
|
|
|
1779
1925
|
`.trimStart();
|
|
1780
1926
|
/**
|
|
1927
|
+
* Grace period (ms) for the exit backstop armed around Step 6 teardown
|
|
1928
|
+
* (devtools#755). See `teardown.ts`'s module doc for the root-cause writeup
|
|
1929
|
+
* — the two upstream `http.Server#close()` hangs are fixed at the source, so
|
|
1930
|
+
* this backstop is a last-mile safety net that should never fire in
|
|
1931
|
+
* practice. Exported so tests can assert the backstop is armed with the
|
|
1932
|
+
* expected value without hardcoding the literal twice.
|
|
1933
|
+
*/
|
|
1934
|
+
const EXIT_BACKSTOP_GRACE_MS = 3e3;
|
|
1935
|
+
/**
|
|
1781
1936
|
* Parses --timeout and --attach-timeout raw string values into the two
|
|
1782
1937
|
* distinct clocks.
|
|
1783
1938
|
*
|
|
@@ -1921,7 +2076,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1921
2076
|
"no-qr-stdout": { type: "boolean" },
|
|
1922
2077
|
headless: { type: "boolean" },
|
|
1923
2078
|
"project-root": { type: "string" },
|
|
1924
|
-
"manual-blocking": { type: "boolean" }
|
|
2079
|
+
"manual-blocking": { type: "boolean" },
|
|
2080
|
+
"stub-blocking": { type: "boolean" }
|
|
1925
2081
|
},
|
|
1926
2082
|
allowPositionals: true
|
|
1927
2083
|
});
|
|
@@ -1958,7 +2114,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1958
2114
|
return;
|
|
1959
2115
|
}
|
|
1960
2116
|
const suppressQr = shouldSuppressQr(vals["no-qr-stdout"] === true);
|
|
1961
|
-
const
|
|
2117
|
+
const stubBlocking = vals["stub-blocking"] === true;
|
|
2118
|
+
const manualBlocking = vals["manual-blocking"] === true || stubBlocking;
|
|
1962
2119
|
const cellSdkLine = typeof vals["cell-sdk-line"] === "string" ? vals["cell-sdk-line"] : void 0;
|
|
1963
2120
|
const cellPlatform = typeof vals["cell-platform"] === "string" ? vals["cell-platform"] : process.env.AIT_CELL_PLATFORM;
|
|
1964
2121
|
const hasCell = cellSdkLine !== void 0 || cellPlatform !== void 0;
|
|
@@ -1984,6 +2141,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1984
2141
|
const manualFileSet = new Set(manual);
|
|
1985
2142
|
process.stderr.write(manualBlocking && manual.length > 0 ? `devtools-test: found ${regular.length} regular + ${manual.length} manual (${MANUAL_TEST_SUFFIX}) test file(s)\n` : `devtools-test: found ${files.length} test file(s)\n`);
|
|
1986
2143
|
if (hasCell) process.stderr.write(`devtools-test: injecting __AIT_CELL__ = ${JSON.stringify(cell)}\n`);
|
|
2144
|
+
if (stubBlocking) process.stderr.write("devtools-test: --stub-blocking enabled — blocking-UI calls in manual-tagged files will be answered from fixtures (devtools#740), not forwarded to native UI\n");
|
|
1987
2145
|
const factory = createRelayConnectionFactory({
|
|
1988
2146
|
schemeUrl,
|
|
1989
2147
|
projectRoot,
|
|
@@ -1991,6 +2149,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1991
2149
|
...dashboardPort !== void 0 ? { dashboardPort } : {},
|
|
1992
2150
|
headless,
|
|
1993
2151
|
cell: hasCell ? cell : void 0,
|
|
2152
|
+
stubBlocking,
|
|
1994
2153
|
onQrContent: (chunks) => {
|
|
1995
2154
|
if (suppressQr) {
|
|
1996
2155
|
process.stdout.write("QR suppressed (non-interactive)\n");
|
|
@@ -2015,6 +2174,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2015
2174
|
printSummary: true,
|
|
2016
2175
|
collectCaptures: reportDir !== void 0,
|
|
2017
2176
|
manualFiles: manualBlocking && manualFileSet.size > 0 ? manualFileSet : void 0,
|
|
2177
|
+
stubBlockingFiles: stubBlocking && manualFileSet.size > 0 ? manualFileSet : void 0,
|
|
2018
2178
|
onManualFile: (file, index, total) => {
|
|
2019
2179
|
const name = basename(file);
|
|
2020
2180
|
process.stdout.write(`수동 단계: ${name} — 폰에서 네이티브 시트가 뜨면 안내에 따라 조작하세요 (${index}/${total})\n`);
|
|
@@ -2041,7 +2201,15 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2041
2201
|
exitCode = report.totals.failed > 0 ? 1 : 0;
|
|
2042
2202
|
} finally {
|
|
2043
2203
|
factory.onSessionPhase?.("complete");
|
|
2044
|
-
|
|
2204
|
+
const backstop = armExitBackstop({
|
|
2205
|
+
graceMs: EXIT_BACKSTOP_GRACE_MS,
|
|
2206
|
+
exitCode
|
|
2207
|
+
});
|
|
2208
|
+
await runTeardownSteps([{
|
|
2209
|
+
name: "factory.close",
|
|
2210
|
+
close: () => factory.close(connection)
|
|
2211
|
+
}]);
|
|
2212
|
+
backstop.disarm();
|
|
2045
2213
|
process.exitCode = exitCode;
|
|
2046
2214
|
}
|
|
2047
2215
|
}
|