doc-detective 4.22.0 → 4.23.0

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 (53) hide show
  1. package/dist/common/src/schemas/schemas.json +274 -74
  2. package/dist/common/src/types/generated/config_v3.d.ts +1 -1
  3. package/dist/common/src/types/generated/context_v3.d.ts +1 -1
  4. package/dist/common/src/types/generated/report_v3.d.ts +1 -1
  5. package/dist/common/src/types/generated/resolvedTests_v3.d.ts +2 -2
  6. package/dist/common/src/types/generated/spec_v3.d.ts +1 -1
  7. package/dist/common/src/types/generated/test_v3.d.ts +1 -1
  8. package/dist/core/appium.d.ts +2 -1
  9. package/dist/core/appium.d.ts.map +1 -1
  10. package/dist/core/appium.js +47 -13
  11. package/dist/core/appium.js.map +1 -1
  12. package/dist/core/ptyProbeWorker.d.ts +2 -0
  13. package/dist/core/ptyProbeWorker.d.ts.map +1 -0
  14. package/dist/core/ptyProbeWorker.js +61 -0
  15. package/dist/core/ptyProbeWorker.js.map +1 -0
  16. package/dist/core/ptyWatchdog.d.ts +77 -0
  17. package/dist/core/ptyWatchdog.d.ts.map +1 -0
  18. package/dist/core/ptyWatchdog.js +179 -0
  19. package/dist/core/ptyWatchdog.js.map +1 -0
  20. package/dist/core/resolveTests.d.ts.map +1 -1
  21. package/dist/core/resolveTests.js +16 -4
  22. package/dist/core/resolveTests.js.map +1 -1
  23. package/dist/core/tests/appSurface.d.ts +21 -2
  24. package/dist/core/tests/appSurface.d.ts.map +1 -1
  25. package/dist/core/tests/appSurface.js +117 -3
  26. package/dist/core/tests/appSurface.js.map +1 -1
  27. package/dist/core/tests/browserWait.d.ts +1 -1
  28. package/dist/core/tests/browserWait.d.ts.map +1 -1
  29. package/dist/core/tests/browserWait.js +108 -84
  30. package/dist/core/tests/browserWait.js.map +1 -1
  31. package/dist/core/tests/findElement.d.ts.map +1 -1
  32. package/dist/core/tests/findElement.js +16 -5
  33. package/dist/core/tests/findElement.js.map +1 -1
  34. package/dist/core/tests/mobileBrowser.d.ts +46 -0
  35. package/dist/core/tests/mobileBrowser.d.ts.map +1 -0
  36. package/dist/core/tests/mobileBrowser.js +140 -0
  37. package/dist/core/tests/mobileBrowser.js.map +1 -0
  38. package/dist/core/tests/mobilePlatform.d.ts +1 -7
  39. package/dist/core/tests/mobilePlatform.d.ts.map +1 -1
  40. package/dist/core/tests/mobilePlatform.js +5 -27
  41. package/dist/core/tests/mobilePlatform.js.map +1 -1
  42. package/dist/core/tests.d.ts +7 -1
  43. package/dist/core/tests.d.ts.map +1 -1
  44. package/dist/core/tests.js +234 -58
  45. package/dist/core/tests.js.map +1 -1
  46. package/dist/core/utils.d.ts.map +1 -1
  47. package/dist/core/utils.js +26 -1
  48. package/dist/core/utils.js.map +1 -1
  49. package/dist/index.cjs +1039 -409
  50. package/dist/runtime/loader.d.ts.map +1 -1
  51. package/dist/runtime/loader.js +76 -0
  52. package/dist/runtime/loader.js.map +1 -1
  53. package/package.json +1 -1
@@ -180,7 +180,7 @@ export interface Context {
180
180
  */
181
181
  platforms?: ("linux" | "mac" | "windows" | "android" | "ios") | ("linux" | "mac" | "windows" | "android" | "ios")[];
182
182
  /**
183
- * Browsers to run tests on.
183
+ * Browsers to run tests on. On a mobile (`android`/`ios`) platform entry, the browser runs on the managed device: `chrome` on Android, `safari` on iOS (other combinations skip the context), and it fills in automatically when omitted. Device browsers don't take desktop display config: authored `window`/`viewport` dimensions and `headless: false` are rejected on mobile entries (the device owns its display — control it via the device descriptor's `headless`/`deviceType`); `headless: true` matches this schema's default and is ignored.
184
184
  */
185
185
  browsers?: ("chrome" | "firefox" | "safari" | "webkit") | Browser | (("chrome" | "firefox" | "safari" | "webkit") | Browser1)[];
186
186
  /**
@@ -20,7 +20,7 @@ export interface Context {
20
20
  */
21
21
  platforms?: ("linux" | "mac" | "windows" | "android" | "ios") | ("linux" | "mac" | "windows" | "android" | "ios")[];
22
22
  /**
23
- * Browsers to run tests on.
23
+ * Browsers to run tests on. On a mobile (`android`/`ios`) platform entry, the browser runs on the managed device: `chrome` on Android, `safari` on iOS (other combinations skip the context), and it fills in automatically when omitted. Device browsers don't take desktop display config: authored `window`/`viewport` dimensions and `headless: false` are rejected on mobile entries (the device owns its display — control it via the device descriptor's `headless`/`deviceType`); `headless: true` matches this schema's default and is ignored.
24
24
  */
25
25
  browsers?: ("chrome" | "firefox" | "safari" | "webkit") | Browser | (("chrome" | "firefox" | "safari" | "webkit") | Browser1)[];
26
26
  /**
@@ -104,7 +104,7 @@ export interface Context {
104
104
  */
105
105
  platforms?: ("linux" | "mac" | "windows" | "android" | "ios") | ("linux" | "mac" | "windows" | "android" | "ios")[];
106
106
  /**
107
- * Browsers to run tests on.
107
+ * Browsers to run tests on. On a mobile (`android`/`ios`) platform entry, the browser runs on the managed device: `chrome` on Android, `safari` on iOS (other combinations skip the context), and it fills in automatically when omitted. Device browsers don't take desktop display config: authored `window`/`viewport` dimensions and `headless: false` are rejected on mobile entries (the device owns its display — control it via the device descriptor's `headless`/`deviceType`); `headless: true` matches this schema's default and is ignored.
108
108
  */
109
109
  browsers?: ("chrome" | "firefox" | "safari" | "webkit") | Browser | (("chrome" | "firefox" | "safari" | "webkit") | Browser1)[];
110
110
  /**
@@ -218,7 +218,7 @@ export interface Context {
218
218
  */
219
219
  platforms?: ("linux" | "mac" | "windows" | "android" | "ios") | ("linux" | "mac" | "windows" | "android" | "ios")[];
220
220
  /**
221
- * Browsers to run tests on.
221
+ * Browsers to run tests on. On a mobile (`android`/`ios`) platform entry, the browser runs on the managed device: `chrome` on Android, `safari` on iOS (other combinations skip the context), and it fills in automatically when omitted. Device browsers don't take desktop display config: authored `window`/`viewport` dimensions and `headless: false` are rejected on mobile entries (the device owns its display — control it via the device descriptor's `headless`/`deviceType`); `headless: true` matches this schema's default and is ignored.
222
222
  */
223
223
  browsers?: ("chrome" | "firefox" | "safari" | "webkit") | Browser | (("chrome" | "firefox" | "safari" | "webkit") | Browser1)[];
224
224
  /**
@@ -657,7 +657,7 @@ export interface Context1 {
657
657
  */
658
658
  platforms?: ("linux" | "mac" | "windows" | "android" | "ios") | ("linux" | "mac" | "windows" | "android" | "ios")[];
659
659
  /**
660
- * Browsers to run tests on.
660
+ * Browsers to run tests on. On a mobile (`android`/`ios`) platform entry, the browser runs on the managed device: `chrome` on Android, `safari` on iOS (other combinations skip the context), and it fills in automatically when omitted. Device browsers don't take desktop display config: authored `window`/`viewport` dimensions and `headless: false` are rejected on mobile entries (the device owns its display — control it via the device descriptor's `headless`/`deviceType`); `headless: true` matches this schema's default and is ignored.
661
661
  */
662
662
  browsers?: ("chrome" | "firefox" | "safari" | "webkit") | Browser2 | (("chrome" | "firefox" | "safari" | "webkit") | Browser3)[];
663
663
  /**
@@ -81,7 +81,7 @@ export interface Context {
81
81
  */
82
82
  platforms?: ("linux" | "mac" | "windows" | "android" | "ios") | ("linux" | "mac" | "windows" | "android" | "ios")[];
83
83
  /**
84
- * Browsers to run tests on.
84
+ * Browsers to run tests on. On a mobile (`android`/`ios`) platform entry, the browser runs on the managed device: `chrome` on Android, `safari` on iOS (other combinations skip the context), and it fills in automatically when omitted. Device browsers don't take desktop display config: authored `window`/`viewport` dimensions and `headless: false` are rejected on mobile entries (the device owns its display — control it via the device descriptor's `headless`/`deviceType`); `headless: true` matches this schema's default and is ignored.
85
85
  */
86
86
  browsers?: ("chrome" | "firefox" | "safari" | "webkit") | Browser | (("chrome" | "firefox" | "safari" | "webkit") | Browser1)[];
87
87
  /**
@@ -4022,7 +4022,7 @@ export interface Context {
4022
4022
  */
4023
4023
  platforms?: ("linux" | "mac" | "windows" | "android" | "ios") | ("linux" | "mac" | "windows" | "android" | "ios")[];
4024
4024
  /**
4025
- * Browsers to run tests on.
4025
+ * Browsers to run tests on. On a mobile (`android`/`ios`) platform entry, the browser runs on the managed device: `chrome` on Android, `safari` on iOS (other combinations skip the context), and it fills in automatically when omitted. Device browsers don't take desktop display config: authored `window`/`viewport` dimensions and `headless: false` are rejected on mobile entries (the device owns its display — control it via the device descriptor's `headless`/`deviceType`); `headless: true` matches this schema's default and is ignored.
4026
4026
  */
4027
4027
  browsers?: ("chrome" | "firefox" | "safari" | "webkit") | Browser | (("chrome" | "firefox" | "safari" | "webkit") | Browser1)[];
4028
4028
  /**
@@ -1,5 +1,6 @@
1
- export { setAppiumHome, appiumHomeForDriverPath };
1
+ export { setAppiumHome, appiumHomeForDriverPath, runtimeHomeHasBrowserDriver };
2
2
  declare function appiumHomeForDriverPath(driverEntry: string): string | null;
3
+ declare function runtimeHomeHasBrowserDriver(runtimeDir: string): boolean;
3
4
  declare function setAppiumHome(ctx?: {
4
5
  cacheDir?: string;
5
6
  }): void;
@@ -1 +1 @@
1
- {"version":3,"file":"appium.d.ts","sourceRoot":"","sources":["../../src/core/appium.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,aAAa,EAAE,uBAAuB,EAAE,CAAC;AAOlD,iBAAS,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAInE;AAED,iBAAS,aAAa,CAAC,GAAG,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,QA8DrD"}
1
+ {"version":3,"file":"appium.d.ts","sourceRoot":"","sources":["../../src/core/appium.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,aAAa,EAAE,uBAAuB,EAAE,2BAA2B,EAAE,CAAC;AAO/E,iBAAS,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAInE;AAsBD,iBAAS,2BAA2B,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAMhE;AAED,iBAAS,aAAa,CAAC,GAAG,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,QAiErD"}
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  import { getRuntimeDir } from "../runtime/cacheDir.js";
5
5
  import { resolveHeavyDepPath } from "../runtime/loader.js";
6
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
- export { setAppiumHome, appiumHomeForDriverPath };
7
+ export { setAppiumHome, appiumHomeForDriverPath, runtimeHomeHasBrowserDriver };
8
8
  // Given a resolved driver module path like
9
9
  // `<X>/node_modules/appium-geckodriver/build/lib/index.js`, return `<X>` — the
10
10
  // directory whose `node_modules` holds the driver. `appium driver list` looks
@@ -15,12 +15,44 @@ function appiumHomeForDriverPath(driverEntry) {
15
15
  const idx = driverEntry.lastIndexOf(marker);
16
16
  return idx === -1 ? null : driverEntry.slice(0, idx);
17
17
  }
18
+ // The browser drivers `appium driver list` must report as `installed (npm)`
19
+ // for Chrome/Firefox/Safari detection (getAvailableApps) to mark the browser
20
+ // available. A home missing all of these can't drive a browser, so it must
21
+ // not be chosen as APPIUM_HOME for the browser paths. Safari is included
22
+ // because its availability check (config.ts) also gates on
23
+ // `npmInstalled("appium-safari-driver")` via `appium driver list`.
24
+ const BROWSER_DRIVER_PACKAGES = [
25
+ "appium-chromium-driver",
26
+ "appium-geckodriver",
27
+ "appium-safari-driver",
28
+ ];
29
+ // True when <runtimeDir>/node_modules contains appium AND at least one browser
30
+ // driver — i.e. the runtime cache is a COMPLETE browser home. `appium driver
31
+ // list` scans <APPIUM_HOME>/node_modules, so a runtime that holds appium but no
32
+ // browser driver (e.g. appium pulled in only as a peer of a lazily-installed
33
+ // native driver like appium-xcuitest-driver) reports every browser driver
34
+ // "not installed" and browser detection comes up empty. Requiring a browser
35
+ // driver's presence lets setAppiumHome fall through to the shim home (which
36
+ // carries the full driver set) in that case.
37
+ function runtimeHomeHasBrowserDriver(runtimeDir) {
38
+ const nm = path.join(runtimeDir, "node_modules");
39
+ if (!existsSync(path.join(nm, "appium")))
40
+ return false;
41
+ return BROWSER_DRIVER_PACKAGES.some((driver) => existsSync(path.join(nm, driver)));
42
+ }
18
43
  function setAppiumHome(ctx = {}) {
19
- // 1. Prefer the lazy-installed copy in <cacheDir>/runtime/node_modules/appium.
20
- // The cache install is the canonical home post-shim.
21
- const runtimeAppium = path.join(getRuntimeDir({ cacheDir: ctx.cacheDir }), "node_modules", "appium");
22
- if (existsSync(runtimeAppium)) {
23
- process.env.APPIUM_HOME = path.join(getRuntimeDir({ cacheDir: ctx.cacheDir }));
44
+ // 1. Prefer the lazy-installed copy in <cacheDir>/runtime — but only when it
45
+ // is a COMPLETE browser home (appium + a browser driver). A runtime that has
46
+ // appium without any browser driver (e.g. appium arrived only as a peer of a
47
+ // lazily-installed native driver such as appium-xcuitest-driver) would make
48
+ // `appium driver list` report chromium/gecko "not installed" and browser
49
+ // detection come up empty — the pre-fix bug that failed the browser unit
50
+ // tests on Linux/macOS after `install all` seeded such a partial runtime.
51
+ // When the runtime is incomplete, fall through to step 2, which homes at the
52
+ // shim's node_modules that carries every driver.
53
+ const runtimeDir = getRuntimeDir({ cacheDir: ctx.cacheDir });
54
+ if (runtimeHomeHasBrowserDriver(runtimeDir)) {
55
+ process.env.APPIUM_HOME = runtimeDir;
24
56
  return;
25
57
  }
26
58
  // 2. Otherwise anchor APPIUM_HOME to the directory whose node_modules holds
@@ -34,8 +66,10 @@ function setAppiumHome(ctx = {}) {
34
66
  // detection come up empty.
35
67
  // Try each driver in turn: resolveHeavyDepPath can return a shim path with no
36
68
  // node_modules segment (appiumHomeForDriverPath then yields null), so a bad
37
- // first candidate must not stop us from deriving the home from the second.
38
- for (const driverName of ["appium-chromium-driver", "appium-geckodriver"]) {
69
+ // first candidate must not stop us from deriving the home from the next. The
70
+ // candidate list is BROWSER_DRIVER_PACKAGES so it stays in sync with the
71
+ // completeness gate above (Safari included).
72
+ for (const driverName of BROWSER_DRIVER_PACKAGES) {
39
73
  const driverEntry = resolveHeavyDepPath(driverName, {
40
74
  cacheDir: ctx.cacheDir,
41
75
  });
@@ -45,11 +79,11 @@ function setAppiumHome(ctx = {}) {
45
79
  return;
46
80
  }
47
81
  }
48
- /* c8 ignore start - legacy fallback (step 3), reached only when NEITHER
49
- * appium-chromium-driver NOR appium-geckodriver resolves via
50
- * resolveHeavyDepPath in step 2 above. Both are installed dependencies of
51
- * this repo that shim-resolve first on every measured CI leg -- the
52
- * cross-platform coverage union shows step 2's return (lines 49-57)
82
+ /* c8 ignore start - legacy fallback (step 3), reached only when NONE of the
83
+ * BROWSER_DRIVER_PACKAGES (appium-chromium-driver first) resolves via
84
+ * resolveHeavyDepPath in step 2 above. chromium/gecko are installed
85
+ * dependencies of this repo that shim-resolve first on every measured CI leg
86
+ * -- the cross-platform coverage union shows step 2's return (lines 49-57)
53
87
  * covered and never reaches here -- so step 2 always returns before this
54
88
  * fallback runs. It exists for driver-less installs and can't be exercised
55
89
  * hermetically without uninstalling a real dependency the runner and the
@@ -1 +1 @@
1
- {"version":3,"file":"appium.js","sourceRoot":"","sources":["../../src/core/appium.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"appium.js","sourceRoot":"","sources":["../../src/core/appium.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ptyProbeWorker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ptyProbeWorker.d.ts","sourceRoot":"","sources":["../../src/core/ptyProbeWorker.ts"],"names":[],"mappings":""}
@@ -0,0 +1,61 @@
1
+ // Worker-thread half of the ConPTY watchdog (issue #501, ADR 01024). Runs in
2
+ // the SAME process as the runner (worker threads share the process's state and
3
+ // console attachment), so a throwaway ConPTY allocation here exercises the
4
+ // conditions the real spawn would hit — where a child process, with its own
5
+ // fresh state, could report a false "healthy".
6
+ //
7
+ // Protocol: post exactly one message to the parent.
8
+ // { ok: true } — a throwaway ConPTY allocated and exited: healthy.
9
+ // { ok: false, error } — the backend couldn't load or the spawn threw.
10
+ // If the allocation WEDGES (the #501 failure), `pty.spawn`/`onExit` never
11
+ // returns and this worker posts nothing — the parent's timeout declares it
12
+ // "wedged" and terminates the worker.
13
+ import { parentPort, workerData } from "node:worker_threads";
14
+ import { pathToFileURL } from "node:url";
15
+ async function main() {
16
+ const { ptyModulePath } = (workerData ?? {});
17
+ if (!ptyModulePath) {
18
+ parentPort?.postMessage({ ok: false, error: "no ptyModulePath provided" });
19
+ return;
20
+ }
21
+ // node-pty is native/CJS; dynamic-import it by absolute path. `.spawn` may be
22
+ // on the namespace or under `default` depending on interop.
23
+ const mod = await import(pathToFileURL(ptyModulePath).href);
24
+ const pty = typeof mod?.spawn === "function" ? mod : mod?.default;
25
+ if (!pty || typeof pty.spawn !== "function") {
26
+ parentPort?.postMessage({
27
+ ok: false,
28
+ error: "node-pty module has no spawn()",
29
+ });
30
+ return;
31
+ }
32
+ const isWin = process.platform === "win32";
33
+ const shell = isWin ? process.env.ComSpec || "cmd.exe" : "/bin/sh";
34
+ // A command that exits immediately — we only care that the PTY allocates.
35
+ const args = isWin ? ["/d", "/s", "/c", "exit"] : ["-c", "exit 0"];
36
+ const ptyProcess = pty.spawn(shell, args, {
37
+ name: "xterm-color",
38
+ cols: 80,
39
+ rows: 24,
40
+ cwd: process.cwd(),
41
+ env: process.env,
42
+ });
43
+ let posted = false;
44
+ const post = (msg) => {
45
+ if (posted)
46
+ return;
47
+ posted = true;
48
+ try {
49
+ ptyProcess.kill();
50
+ }
51
+ catch {
52
+ // best-effort — it may already have exited
53
+ }
54
+ parentPort?.postMessage(msg);
55
+ };
56
+ ptyProcess.onExit(() => post({ ok: true }));
57
+ }
58
+ main().catch((error) => {
59
+ parentPort?.postMessage({ ok: false, error: String(error?.message ?? error) });
60
+ });
61
+ //# sourceMappingURL=ptyProbeWorker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ptyProbeWorker.js","sourceRoot":"","sources":["../../src/core/ptyProbeWorker.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,+EAA+E;AAC/E,2EAA2E;AAC3E,4EAA4E;AAC5E,+CAA+C;AAC/C,EAAE;AACF,oDAAoD;AACpD,gFAAgF;AAChF,4EAA4E;AAC5E,0EAA0E;AAC1E,2EAA2E;AAC3E,sCAAsC;AAEtC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,KAAK,UAAU,IAAI;IACjB,MAAM,EAAE,aAAa,EAAE,GAAG,CAAC,UAAU,IAAI,EAAE,CAA+B,CAAC;IAC3E,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,UAAU,EAAE,WAAW,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAC,CAAC;QAC3E,OAAO;IACT,CAAC;IAED,8EAA8E;IAC9E,4DAA4D;IAC5D,MAAM,GAAG,GAAQ,MAAM,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC;IACjE,MAAM,GAAG,GAAQ,OAAO,GAAG,EAAE,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,OAAO,CAAC;IACvE,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;QAC5C,UAAU,EAAE,WAAW,CAAC;YACtB,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,gCAAgC;SACxC,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC;IAC3C,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;IACnE,0EAA0E;IAC1E,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAEnE,MAAM,UAAU,GAAG,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE;QACxC,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE,EAAE;QACR,IAAI,EAAE,EAAE;QACR,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE;QAClB,GAAG,EAAE,OAAO,CAAC,GAAG;KACjB,CAAC,CAAC;IAEH,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,MAAM,IAAI,GAAG,CAAC,GAAQ,EAAE,EAAE;QACxB,IAAI,MAAM;YAAE,OAAO;QACnB,MAAM,GAAG,IAAI,CAAC;QACd,IAAI,CAAC;YACH,UAAU,CAAC,IAAI,EAAE,CAAC;QACpB,CAAC;QAAC,MAAM,CAAC;YACP,2CAA2C;QAC7C,CAAC;QACD,UAAU,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC,CAAC;IAEF,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;AAC9C,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAU,EAAE,EAAE;IAC1B,UAAU,EAAE,WAAW,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,OAAO,IAAI,KAAK,CAAC,EAAE,CAAC,CAAC;AACjF,CAAC,CAAC,CAAC"}
@@ -0,0 +1,77 @@
1
+ /** Default wall-clock budget for the ConPTY probe. A healthy allocation
2
+ * returns in well under a second; 15s is generous headroom that still bounds
3
+ * the freeze. Overridable via env for tests (and as an escape hatch). */
4
+ export declare const PTY_PROBE_TIMEOUT_MS: number;
5
+ export type PtyProbeOutcome = "healthy" | "inconclusive" | "wedged";
6
+ export interface PtyProbeResult {
7
+ outcome: PtyProbeOutcome;
8
+ /** Human-readable context for logs / the SKIP message. */
9
+ detail?: string;
10
+ }
11
+ /** Minimal surface of a worker the probe drives. `node:worker_threads`'s
12
+ * `Worker` satisfies it; tests inject a fake. */
13
+ export interface ProbeWorkerLike {
14
+ on(event: "message", cb: (msg: any) => void): void;
15
+ on(event: "error", cb: (err: any) => void): void;
16
+ on(event: "exit", cb: (code: number) => void): void;
17
+ terminate(): Promise<number> | void;
18
+ }
19
+ /**
20
+ * Probe whether a new ConPTY can be allocated in this process without wedging.
21
+ * Never rejects — every failure mode maps to an outcome the caller can act on.
22
+ *
23
+ * @param opts.ptyModulePath Absolute path to the node-pty backend the worker
24
+ * should load (resolved on the main thread; the worker can't run the async
25
+ * heavy-dep resolver itself).
26
+ * @param opts.timeoutMs Budget before a non-responding probe is declared
27
+ * "wedged". Defaults to {@link PTY_PROBE_TIMEOUT_MS}.
28
+ * @param opts.createWorker Injectable worker factory (tests).
29
+ */
30
+ export declare function probePtyAllocation(opts: {
31
+ ptyModulePath: string;
32
+ timeoutMs?: number;
33
+ createWorker?: (data: {
34
+ ptyModulePath: string;
35
+ }) => ProbeWorkerLike;
36
+ }): Promise<PtyProbeResult>;
37
+ /**
38
+ * Verify the PTY backend is PHYSICALLY on disk before any spawn, healing a
39
+ * pruned install by force-reinstalling — issue #501's actual mechanism.
40
+ *
41
+ * A mid-run JIT `npm install` of another heavy dep used to prune node-pty's
42
+ * files from the runtime cache while the module stayed loaded (the OS-locked
43
+ * .node binary survives; the JS support files node-pty needs at spawn time do
44
+ * not). The stale resolution + ESM caches then serve the in-memory module
45
+ * without touching disk, and the next `pty.spawn` freezes the process inside
46
+ * a native wait. So "the module loaded" is NOT sufficient — the resolved path
47
+ * must exist on disk, and when it doesn't we reinstall (same paths → the
48
+ * already-loaded module's support files are back) rather than proceed onto a
49
+ * spawn we know can wedge. If the reinstall can't produce the files, throw
50
+ * the `NODE_PTY_UNAVAILABLE`-tagged error so runShell degrades to SKIPPED.
51
+ *
52
+ * All collaborators are injectable for tests; `exists` defaults to
53
+ * `fs.existsSync`.
54
+ */
55
+ export declare function ensurePtyBackendOnDisk(opts: {
56
+ resolvePath: () => string | null;
57
+ reinstall: () => Promise<void>;
58
+ exists?: (p: string) => boolean;
59
+ }): Promise<string>;
60
+ /**
61
+ * Windows-only gate for the `tty` spawn path. Probes ConPTY allocation and, if
62
+ * it is wedged (the #501 freeze signature), throws a `NODE_PTY_UNAVAILABLE`-
63
+ * tagged error so the caller (runShell) degrades the step to SKIPPED instead of
64
+ * hanging. Returns (no-op) on non-Windows, when no backend path resolved, or
65
+ * when the probe is healthy/inconclusive — the caller then proceeds to the
66
+ * direct spawn.
67
+ *
68
+ * `platform` and `probe` are injectable so the gate is unit-testable on any host.
69
+ */
70
+ export declare function assertConptyAllocatable(opts: {
71
+ ptyModulePath: string | null | undefined;
72
+ platform?: string;
73
+ probe?: (o: {
74
+ ptyModulePath: string;
75
+ }) => Promise<PtyProbeResult>;
76
+ }): Promise<void>;
77
+ //# sourceMappingURL=ptyWatchdog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ptyWatchdog.d.ts","sourceRoot":"","sources":["../../src/core/ptyWatchdog.ts"],"names":[],"mappings":"AAqCA;;yEAEyE;AACzE,eAAO,MAAM,oBAAoB,QACgC,CAAC;AAElE,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,cAAc,GAAG,QAAQ,CAAC;AAEpE,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,eAAe,CAAC;IACzB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;iDACiD;AACjD,MAAM,WAAW,eAAe;IAC9B,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI,CAAC;IACnD,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI,CAAC;IACjD,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IACpD,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;CACrC;AAQD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE,KAAK,eAAe,CAAC;CACrE,GAAG,OAAO,CAAC,cAAc,CAAC,CAgE1B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IACjD,WAAW,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IACjC,SAAS,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;CACjC,GAAG,OAAO,CAAC,MAAM,CAAC,CAoBlB;AAED;;;;;;;;;GASG;AACH,wBAAsB,uBAAuB,CAAC,IAAI,EAAE;IAClD,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;CACnE,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhB"}
@@ -0,0 +1,179 @@
1
+ // Guards for the `tty` spawn path (issue #501, ADR 01024).
2
+ //
3
+ // On Windows, `pty.spawn` can block the libuv event loop FOREVER inside a
4
+ // synchronous native call — no timer, log line, or step result ever fires
5
+ // again, and the run dies at an external timeout. The proven #501 mechanism
6
+ // was a mid-run JIT install pruning node-pty's files from the runtime cache
7
+ // while the module stayed loaded (see `ensurePtyBackendOnDisk`, the targeted
8
+ // fix, and ADR 01025 for the root fix); the same wedged-spawn symptom also
9
+ // exists upstream for other reasons (microsoft/node-pty #640/#532 — the
10
+ // native connect runs inline on the calling thread with no timeout).
11
+ //
12
+ // Two layers, ordered:
13
+ //
14
+ // 1. `ensurePtyBackendOnDisk` — the backend's files must physically exist
15
+ // before any spawn; heals a pruned cache by force-reinstalling.
16
+ // 2. `probePtyAllocation` / `assertConptyAllocatable` — defense-in-depth for
17
+ // wedges the disk check can't see. Because a wedged spawn blocks the event
18
+ // loop, a same-thread `Promise.race([spawn, setTimeout])` can never fire;
19
+ // the probe must run OFF the main thread, in a WORKER THREAD. A worker
20
+ // shares this process's state (module tree, console attachment) where a
21
+ // child process would not — a fresh process could report a false
22
+ // "healthy". The probe spawns a throwaway ConPTY that exits immediately:
23
+ //
24
+ // - it exits fast → allocation works → "healthy"
25
+ // - the worker errors / can't
26
+ // host the native addon → probe inconclusive → "inconclusive"
27
+ // - no response within budget → a wedged allocation → "wedged"
28
+ //
29
+ // Only "wedged" (a genuine timeout) is treated as a reason to SKIP the
30
+ // step. "inconclusive" falls through to the direct spawn, so a platform
31
+ // where workers can't host node-pty behaves exactly as before — the happy
32
+ // path can never regress; the watchdog only ever converts an
33
+ // otherwise-infinite freeze into a bounded, observable outcome.
34
+ import fs from "node:fs";
35
+ import { Worker } from "node:worker_threads";
36
+ /** Default wall-clock budget for the ConPTY probe. A healthy allocation
37
+ * returns in well under a second; 15s is generous headroom that still bounds
38
+ * the freeze. Overridable via env for tests (and as an escape hatch). */
39
+ export const PTY_PROBE_TIMEOUT_MS = Number(process.env.DOC_DETECTIVE_PTY_PROBE_TIMEOUT_MS) || 15000;
40
+ function defaultCreateWorker(data) {
41
+ // ptyProbeWorker.js sits next to this module in dist/core/.
42
+ const url = new URL("./ptyProbeWorker.js", import.meta.url);
43
+ return new Worker(url, { workerData: data });
44
+ }
45
+ /**
46
+ * Probe whether a new ConPTY can be allocated in this process without wedging.
47
+ * Never rejects — every failure mode maps to an outcome the caller can act on.
48
+ *
49
+ * @param opts.ptyModulePath Absolute path to the node-pty backend the worker
50
+ * should load (resolved on the main thread; the worker can't run the async
51
+ * heavy-dep resolver itself).
52
+ * @param opts.timeoutMs Budget before a non-responding probe is declared
53
+ * "wedged". Defaults to {@link PTY_PROBE_TIMEOUT_MS}.
54
+ * @param opts.createWorker Injectable worker factory (tests).
55
+ */
56
+ export function probePtyAllocation(opts) {
57
+ const timeoutMs = opts.timeoutMs ?? PTY_PROBE_TIMEOUT_MS;
58
+ const createWorker = opts.createWorker ?? defaultCreateWorker;
59
+ let worker;
60
+ try {
61
+ worker = createWorker({ ptyModulePath: opts.ptyModulePath });
62
+ }
63
+ catch (error) {
64
+ // Couldn't even start a worker (workers disabled, out of threads). That's
65
+ // not the freeze — never report "wedged" here; fall through to the direct
66
+ // spawn.
67
+ return Promise.resolve({
68
+ outcome: "inconclusive",
69
+ detail: `worker-unavailable: ${error?.message ?? error}`,
70
+ });
71
+ }
72
+ return new Promise((resolve) => {
73
+ let settled = false;
74
+ const terminate = () => {
75
+ // Fire-and-forget: a worker wedged in a synchronous native call may not
76
+ // stop promptly, and the main thread must never block on it. A leaked
77
+ // worker thread is reaped at process exit — strictly better than a frozen
78
+ // run.
79
+ try {
80
+ Promise.resolve(worker.terminate()).catch(() => { });
81
+ }
82
+ catch {
83
+ // terminate() may throw if the worker is already gone — ignore.
84
+ }
85
+ };
86
+ const finish = (outcome, detail) => {
87
+ if (settled)
88
+ return;
89
+ settled = true;
90
+ clearTimeout(timer);
91
+ resolve({ outcome, detail });
92
+ };
93
+ const timer = setTimeout(() => {
94
+ // No verdict within budget == the #501 freeze signature.
95
+ terminate();
96
+ finish("wedged", `no response in ${timeoutMs}ms`);
97
+ }, timeoutMs);
98
+ // Don't let the watchdog timer itself keep the process alive.
99
+ if (typeof timer.unref === "function")
100
+ timer.unref();
101
+ worker.on("message", (msg) => {
102
+ terminate();
103
+ if (msg && msg.ok === true)
104
+ finish("healthy");
105
+ else
106
+ finish("inconclusive", msg?.error ? String(msg.error) : "probe reported not-ok");
107
+ });
108
+ worker.on("error", (err) => {
109
+ // The worker threw (e.g. the native addon can't load in a worker). Not a
110
+ // freeze — inconclusive, so the caller falls through to the direct spawn.
111
+ finish("inconclusive", `worker-error: ${err?.message ?? err}`);
112
+ });
113
+ worker.on("exit", () => {
114
+ // Exited without ever posting a verdict (load/spawn error path).
115
+ finish("inconclusive", "worker exited without a result");
116
+ });
117
+ });
118
+ }
119
+ /**
120
+ * Verify the PTY backend is PHYSICALLY on disk before any spawn, healing a
121
+ * pruned install by force-reinstalling — issue #501's actual mechanism.
122
+ *
123
+ * A mid-run JIT `npm install` of another heavy dep used to prune node-pty's
124
+ * files from the runtime cache while the module stayed loaded (the OS-locked
125
+ * .node binary survives; the JS support files node-pty needs at spawn time do
126
+ * not). The stale resolution + ESM caches then serve the in-memory module
127
+ * without touching disk, and the next `pty.spawn` freezes the process inside
128
+ * a native wait. So "the module loaded" is NOT sufficient — the resolved path
129
+ * must exist on disk, and when it doesn't we reinstall (same paths → the
130
+ * already-loaded module's support files are back) rather than proceed onto a
131
+ * spawn we know can wedge. If the reinstall can't produce the files, throw
132
+ * the `NODE_PTY_UNAVAILABLE`-tagged error so runShell degrades to SKIPPED.
133
+ *
134
+ * All collaborators are injectable for tests; `exists` defaults to
135
+ * `fs.existsSync`.
136
+ */
137
+ export async function ensurePtyBackendOnDisk(opts) {
138
+ const exists = opts.exists ?? fs.existsSync;
139
+ const first = opts.resolvePath();
140
+ if (first && exists(first))
141
+ return first;
142
+ try {
143
+ await opts.reinstall();
144
+ }
145
+ catch (error) {
146
+ const err = new Error(`The PTY backend's files are missing from the runtime cache (a JIT install of another dependency previously pruned them — doc-detective issue #501) and reinstalling failed: ${error?.message ?? error}. Skipping this \`tty\` background process.`);
147
+ err.code = "NODE_PTY_UNAVAILABLE";
148
+ throw err;
149
+ }
150
+ const healed = opts.resolvePath();
151
+ if (healed && exists(healed))
152
+ return healed;
153
+ const err = new Error(`The PTY backend's files are missing from the runtime cache (doc-detective issue #501) and did not materialize after a reinstall. Skipping this \`tty\` background process.`);
154
+ err.code = "NODE_PTY_UNAVAILABLE";
155
+ throw err;
156
+ }
157
+ /**
158
+ * Windows-only gate for the `tty` spawn path. Probes ConPTY allocation and, if
159
+ * it is wedged (the #501 freeze signature), throws a `NODE_PTY_UNAVAILABLE`-
160
+ * tagged error so the caller (runShell) degrades the step to SKIPPED instead of
161
+ * hanging. Returns (no-op) on non-Windows, when no backend path resolved, or
162
+ * when the probe is healthy/inconclusive — the caller then proceeds to the
163
+ * direct spawn.
164
+ *
165
+ * `platform` and `probe` are injectable so the gate is unit-testable on any host.
166
+ */
167
+ export async function assertConptyAllocatable(opts) {
168
+ const platform = opts.platform ?? process.platform;
169
+ if (platform !== "win32" || !opts.ptyModulePath)
170
+ return;
171
+ const probe = opts.probe ?? probePtyAllocation;
172
+ const result = await probe({ ptyModulePath: opts.ptyModulePath });
173
+ if (result.outcome === "wedged") {
174
+ const err = new Error(`ConPTY allocation is wedged on this Windows environment (${result.detail}). This matches doc-detective issue #501: a native app-surface (NovaWindows) context in the same run can leave the console subsystem unable to allocate a new pseudo-terminal, which otherwise freezes the whole process. Skipping this \`tty\` background process instead of hanging the run.`);
175
+ err.code = "NODE_PTY_UNAVAILABLE";
176
+ throw err;
177
+ }
178
+ }
179
+ //# sourceMappingURL=ptyWatchdog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ptyWatchdog.js","sourceRoot":"","sources":["../../src/core/ptyWatchdog.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"resolveTests.d.ts","sourceRoot":"","sources":["../../src/core/resolveTests.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAUH,iBAAS,gBAAgB,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,GAAG,CAAA;CAAE,WAShD;AAED,iBAAS,eAAe,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE;IAAE,QAAQ,EAAE,GAAG,EAAE,CAAC;IAAC,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,SAsG/F;AAsJD;;;;;;;GAOG;AACH,iBAAe,YAAY,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,EAAE;IAAE,MAAM,EAAE,GAAG,CAAC;IAAC,aAAa,EAAE,GAAG,EAAE,CAAA;CAAE;;;WAM3E,GAAG,EAAE;GAWrB;AAED,OAAO,EACL,YAAY,EACZ,eAAe,EACf,gBAAgB,GACjB,CAAC"}
1
+ {"version":3,"file":"resolveTests.d.ts","sourceRoot":"","sources":["../../src/core/resolveTests.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAWH,iBAAS,gBAAgB,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,GAAG,CAAA;CAAE,WAShD;AAED,iBAAS,eAAe,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE;IAAE,QAAQ,EAAE,GAAG,EAAE,CAAC;IAAC,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,SAmH/F;AAsJD;;;;;;;GAOG;AACH,iBAAe,YAAY,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,EAAE;IAAE,MAAM,EAAE,GAAG,CAAC;IAAC,aAAa,EAAE,GAAG,EAAE,CAAA;CAAE;;;WAM3E,GAAG,EAAE;GAWrB;AAED,OAAO,EACL,YAAY,EACZ,eAAe,EACf,gBAAgB,GACjB,CAAC"}
@@ -10,6 +10,7 @@ import { contentHash } from "../common/src/detectTests.js";
10
10
  import { loadDescription } from "./openapi.js";
11
11
  // Single source of truth for browser/driver-requiring step keys.
12
12
  import { BROWSER_STEP_KEYS as driverActions } from "../runtime/browserStepKeys.js";
13
+ import { isMobileTargetPlatform } from "./tests/mobilePlatform.js";
13
14
  function isDriverRequired({ test }) {
14
15
  let driverRequired = false;
15
16
  test.steps.forEach((step) => {
@@ -46,8 +47,6 @@ function resolveContexts({ contexts, test, config }) {
46
47
  if (typeof browser === "string") {
47
48
  browser = { name: browser };
48
49
  }
49
- if (browser.name === "safari")
50
- browser.name = "webkit";
51
50
  // Mark the engine as explicitly requested by the author. The runner's
52
51
  // cross-engine fallback uses this to decide PASS vs WARNING when it has
53
52
  // to substitute another browser: an auto-selected default falls back
@@ -91,8 +90,21 @@ function resolveContexts({ contexts, test, config }) {
91
90
  const staticContext = { ...carry };
92
91
  if (platform !== undefined)
93
92
  staticContext.platform = platform;
94
- if (browser !== undefined)
95
- staticContext.browser = browser;
93
+ if (browser !== undefined) {
94
+ // Desktop `safari` is an alias for the `webkit` engine, so the
95
+ // rewrite happens per platform pair (not during normalization):
96
+ // on a mobile target platform (android/ios) the authored name is
97
+ // preserved — `safari` on ios is the real device browser (phase
98
+ // A5), and the mobile support matrix, not engine aliasing,
99
+ // decides unsupported combinations. Every pair gets its own
100
+ // clone so contexts sharing one authored browser object can't
101
+ // leak a rewrite (or any later per-context mutation) across
102
+ // platforms.
103
+ staticContext.browser =
104
+ browser.name === "safari" && !isMobileTargetPlatform(platform)
105
+ ? { ...browser, name: "webkit" }
106
+ : { ...browser };
107
+ }
96
108
  staticContexts.push(staticContext);
97
109
  });
98
110
  }