pi-lean-dimension 0.1.0 → 0.2.1
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/README.md +96 -36
- package/node_modules/pi-lean-portal/AGENTS.md +146 -4
- package/node_modules/pi-lean-portal/README.md +112 -50
- package/node_modules/pi-lean-portal/__tests__/browser-data.test.ts +124 -0
- package/node_modules/pi-lean-portal/__tests__/browser-inspect.test.ts +192 -16
- package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -3
- package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +49 -35
- package/node_modules/pi-lean-portal/__tests__/chromium-py-persistence.test.ts +21 -195
- package/node_modules/pi-lean-portal/__tests__/chromium-py.test.ts +17 -81
- package/node_modules/pi-lean-portal/__tests__/chromium.test.ts +25 -0
- package/node_modules/pi-lean-portal/__tests__/contributed/invisible-py/invisible-py.test.ts +299 -0
- package/node_modules/pi-lean-portal/__tests__/cookie-persistence.test.ts +22 -182
- package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/firefox-py-persistence.test.ts +21 -184
- package/node_modules/pi-lean-portal/__tests__/firefox-py.test.ts +17 -101
- package/node_modules/pi-lean-portal/__tests__/firefox.test.ts +2 -18
- package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/create-py-backend-harness.ts +105 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/load-plugin-config-from-file.ts +53 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-plugin.ts +11 -7
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +4 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/persistence-suite.ts +218 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/plugin-contract.ts +198 -318
- package/node_modules/pi-lean-portal/__tests__/helpers/probe-user-backend.ts +198 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/test-server.ts +14 -0
- package/node_modules/pi-lean-portal/__tests__/plugin-config-browser.test.ts +150 -15
- package/node_modules/pi-lean-portal/__tests__/plugin-loading.test.ts +120 -18
- package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +6 -67
- package/node_modules/pi-lean-portal/__tests__/probe-user-backend.test.ts +236 -0
- package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +401 -11
- package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +4 -1
- package/node_modules/pi-lean-portal/__tests__/run-contributed-suites.test.ts +318 -0
- package/node_modules/pi-lean-portal/__tests__/session-manager.test.ts +50 -0
- package/node_modules/pi-lean-portal/__tests__/snapshot-cache.test.ts +2 -2
- package/node_modules/pi-lean-portal/__tests__/url-safety.test.ts +1 -1
- package/node_modules/pi-lean-portal/backends/chromium/index.ts +5 -13
- package/node_modules/pi-lean-portal/backends/chromium-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/chromium-py/bridge.py +0 -2
- package/node_modules/pi-lean-portal/backends/firefox/index.ts +6 -5
- package/node_modules/pi-lean-portal/backends/firefox-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/firefox-py/bridge.py +7 -6
- package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +241 -398
- package/node_modules/pi-lean-portal/backends/python-adapter.ts +182 -83
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__init__.py +1 -42
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/accessibility.py +12 -147
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bot_detection.py +12 -37
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bridge.py +249 -322
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/browser_data.py +92 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/patch_playwright.py +321 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +511 -299
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_accessibility.cpython-313-pytest-9.1.0.pyc → test_accessibility.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_chromium_py_bridge.cpython-313-pytest-9.1.0.pyc → test_chromium_py_bridge.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_firefox_py_bridge.cpython-313-pytest-9.1.0.pyc → test_firefox_py_bridge.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_py_bridges.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_transport.cpython-313-pytest-9.1.0.pyc → test_transport.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/conftest.py +95 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_accessibility.py +8 -132
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_bot_detection.py +8 -147
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_browser_data.py +131 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +768 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_py_bridges.py +198 -0
- package/node_modules/pi-lean-portal/browser-toggle.ts +33 -69
- package/node_modules/pi-lean-portal/contributed/CHOOSING.md +126 -0
- package/node_modules/pi-lean-portal/contributed/README.md +304 -0
- package/node_modules/pi-lean-portal/contributed/camoufox-py/bridge.py +216 -0
- package/node_modules/pi-lean-portal/contributed/invisible-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/contributed/invisible-py/bridge.py +434 -0
- package/node_modules/pi-lean-portal/core/fetch-backend.ts +0 -5
- package/node_modules/pi-lean-portal/core/plugin-api.ts +6 -33
- package/node_modules/pi-lean-portal/core/plugin-config.ts +75 -58
- package/node_modules/pi-lean-portal/core/plugin-registry.ts +11 -49
- package/node_modules/pi-lean-portal/core/router.ts +59 -98
- package/node_modules/pi-lean-portal/core/shared/accessibility-tree.ts +10 -143
- package/node_modules/pi-lean-portal/core/shared/bot-detection.ts +31 -77
- package/node_modules/pi-lean-portal/core/shared/browser-data.json +183 -0
- package/node_modules/pi-lean-portal/core/shared/browser-data.ts +50 -0
- package/node_modules/pi-lean-portal/core/shared/browser-events.ts +6 -6
- package/node_modules/pi-lean-portal/core/shared/dom-extractor.ts +97 -36
- package/node_modules/pi-lean-portal/core/shared/nav-settle.ts +12 -15
- package/node_modules/pi-lean-portal/core/shared/paths.ts +3 -0
- package/node_modules/pi-lean-portal/core/shared/session-manager.ts +7 -21
- package/node_modules/pi-lean-portal/{verify-ship-manifest.ts → core/shared/ship-manifest.ts} +34 -9
- package/node_modules/pi-lean-portal/core/shared/snapshot-cache.ts +7 -6
- package/node_modules/pi-lean-portal/core/shared/storage-state.ts +40 -9
- package/node_modules/pi-lean-portal/index.ts +42 -7
- package/node_modules/pi-lean-portal/package.json +8 -3
- package/node_modules/pi-lean-portal/ship-manifest.test.ts +8 -3
- package/node_modules/pi-lean-portal/tools/browser-inspect.ts +2 -6
- package/node_modules/pi-lean-portal/tools/browser-navigate.ts +7 -5
- package/node_modules/pi-lean-portal/tools/browser-snapshot.ts +2 -5
- package/node_modules/pi-lean-portal/tools/utils.ts +22 -4
- package/node_modules/pi-lean-portal/tools/web-fetch.ts +3 -4
- package/node_modules/pi-lean-search/README.md +6 -2
- package/node_modules/pi-lean-search/__tests__/web-search.test.ts +11 -0
- package/node_modules/pi-lean-search/index.ts +4 -21
- package/node_modules/pi-lean-search/package.json +2 -2
- package/node_modules/pi-lean-search/verify-ship-manifest.ts +6 -92
- package/node_modules/pi-lean-search/web-search-tool.ts +19 -13
- package/package.json +4 -4
- package/node_modules/pi-lean-portal/__tests__/helpers/reddit-fixture.ts +0 -264
- package/node_modules/pi-lean-portal/__tests__/helpers/toggle-test-utils.ts +0 -31
- package/node_modules/pi-lean-portal/__tests__/reddit-dialog.test.ts +0 -302
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/occlusion.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.0.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_chromium_py_bridge.py +0 -281
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_firefox_py_bridge.py +0 -212
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `probeUserBackend` — availability probe for a user-managed stealth
|
|
3
|
+
* Python backend.
|
|
4
|
+
*
|
|
5
|
+
* Lets a user-managed stealth backend be exercised through the 130-task
|
|
6
|
+
* MiniWoB++ harness (and the shipped parity-test template) without
|
|
7
|
+
* forking the repo. The probe is engine-agnostic — it checks only that
|
|
8
|
+
* the backend's `bridge.py` exists on disk and the backend's venv
|
|
9
|
+
* python interpreter runs. No engine-specific binary probing; a
|
|
10
|
+
* template that needs a binary probe (e.g. Camoufox's
|
|
11
|
+
* `python -m camoufox fetch` check) can layer that on top.
|
|
12
|
+
*
|
|
13
|
+
* Resolution mirrors the user-backend discovery in `detectPluginType` in
|
|
14
|
+
* `pi-lean-portal/core/plugin-config.ts` so this probe and the runtime
|
|
15
|
+
* loader agree on where a backend lives:
|
|
16
|
+
* - `bridgePath` defaults to `${userBackendsDir()}/${name}/bridge.py`;
|
|
17
|
+
* an absolute `opts.bridgePath` short-circuits (the power-user escape
|
|
18
|
+
* hatch, mirroring the absolute-`dir` short-circuit).
|
|
19
|
+
* - `venvPython` defaults to
|
|
20
|
+
* `${userBackendsDir()}/${name}/.venv/bin/python3`; overridable via
|
|
21
|
+
* `PI_USER_BACKEND_<UPPERNAME>_PYTHON` so a user can point at a
|
|
22
|
+
* non-`user-backends` venv without editing the test/template.
|
|
23
|
+
*
|
|
24
|
+
* Lives in `__tests__/helpers/` because it is only ever called from
|
|
25
|
+
* tests (and the user-backends parity template). The env overrides
|
|
26
|
+
* (`PI_USER_BACKENDS_DIR` + per-name `PI_USER_BACKEND_<NAME>_PYTHON`)
|
|
27
|
+
* are preserved from the original vendored helper.
|
|
28
|
+
*
|
|
29
|
+
* @module
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { spawnSync } from "node:child_process";
|
|
33
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
34
|
+
import { homedir } from "node:os";
|
|
35
|
+
import { isAbsolute, join } from "node:path";
|
|
36
|
+
|
|
37
|
+
// ─── User-backends root ───────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the user-backends root directory.
|
|
41
|
+
*
|
|
42
|
+
* Honors `PI_USER_BACKENDS_DIR` (an absolute path override) when set
|
|
43
|
+
* and non-empty; otherwise defaults to
|
|
44
|
+
* `~/.pi/agent/pi-lean-portal/user-backends/`.
|
|
45
|
+
*
|
|
46
|
+
* Vendored locally — see the module doc for the rationale. Sprint 2's
|
|
47
|
+
* generic runner reuses this same helper for consistency.
|
|
48
|
+
*/
|
|
49
|
+
export function userBackendsDir(): string {
|
|
50
|
+
const env = process.env.PI_USER_BACKENDS_DIR;
|
|
51
|
+
if (env !== undefined && env.trim() !== "") return env;
|
|
52
|
+
return join(homedir(), ".pi", "agent", "pi-lean-portal", "user-backends");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Backend discovery ──────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Discover user-managed Python backends under a user-backends root.
|
|
59
|
+
*
|
|
60
|
+
* Returns the list of `<name>` such that `${root}/<name>/bridge.py`
|
|
61
|
+
* exists and `${root}/<name>` is a directory whose basename ends with
|
|
62
|
+
* `-py` (the conventional suffix for Python bridge backends, mirroring
|
|
63
|
+
* the shipped `chromium-py` / `firefox-py` naming). Sorted for
|
|
64
|
+
* deterministic registration order. Empty when the root is missing or
|
|
65
|
+
* contains no matching directory (the normal bare-CI state — the
|
|
66
|
+
* caller treats this as a no-op).
|
|
67
|
+
*
|
|
68
|
+
* Used by the generic `bench/miniwob/suites/` runners that exercise
|
|
69
|
+
* whatever stealth backend the user has installed, without naming any.
|
|
70
|
+
*/
|
|
71
|
+
export function discoverUserBackends(root: string): string[] {
|
|
72
|
+
if (!existsSync(root) || !statSync(root).isDirectory()) return [];
|
|
73
|
+
let entries: string[];
|
|
74
|
+
try {
|
|
75
|
+
entries = readdirSync(root);
|
|
76
|
+
} catch {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
const found: string[] = [];
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
if (!entry.endsWith("-py")) continue;
|
|
82
|
+
const dir = join(root, entry);
|
|
83
|
+
try {
|
|
84
|
+
if (!statSync(dir).isDirectory()) continue;
|
|
85
|
+
} catch {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (existsSync(join(dir, "bridge.py"))) found.push(entry);
|
|
89
|
+
}
|
|
90
|
+
return found.sort();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Types ────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
/** Options for {@link probeUserBackend}. */
|
|
96
|
+
export interface ProbeUserBackendOptions {
|
|
97
|
+
/**
|
|
98
|
+
* Absolute path to the backend's `bridge.py`, overriding the default
|
|
99
|
+
* `${userBackendsDir()}/${name}/bridge.py` resolution. Mirrors the
|
|
100
|
+
* absolute-`dir` short-circuit in `detectPluginType`: when this is an
|
|
101
|
+
* absolute path it wins; any other value is ignored in favor of the
|
|
102
|
+
* default resolution.
|
|
103
|
+
*/
|
|
104
|
+
bridgePath?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Result of {@link probeUserBackend}. */
|
|
108
|
+
export interface ProbeUserBackendResult {
|
|
109
|
+
/**
|
|
110
|
+
* `true` iff the bridge file exists AND the venv python interpreter
|
|
111
|
+
* runs (`python3 --version` exits 0).
|
|
112
|
+
*/
|
|
113
|
+
available: boolean;
|
|
114
|
+
/** Resolved path to the backend's `bridge.py`. */
|
|
115
|
+
bridgePath: string;
|
|
116
|
+
/** Resolved path to the backend's venv python interpreter. */
|
|
117
|
+
venvPython: string;
|
|
118
|
+
/**
|
|
119
|
+
* When `available` is `false`, explains the first missing piece so a
|
|
120
|
+
* skip message is useful. `undefined` when `available` is `true`.
|
|
121
|
+
*/
|
|
122
|
+
reason?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── Public API ───────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Probe a user-managed Python backend for availability.
|
|
129
|
+
*
|
|
130
|
+
* Checks, in order:
|
|
131
|
+
* 1. The `bridge.py` exists at the resolved `bridgePath`.
|
|
132
|
+
* 2. The venv python interpreter runs (`--version` exits 0).
|
|
133
|
+
*
|
|
134
|
+
* Returns a plain object (synchronous — one `spawnSync` for the venv
|
|
135
|
+
* check, mirroring the existing `probePythonBackend` in
|
|
136
|
+
* `suites/miniwob-suite-helper.ts`). Runs once at suite-load, so
|
|
137
|
+
* blocking briefly is acceptable.
|
|
138
|
+
*
|
|
139
|
+
* @param name Backend directory name under `user-backends/`
|
|
140
|
+
* (e.g. `"camoufox-py"`).
|
|
141
|
+
* @param opts Optional overrides; see {@link ProbeUserBackendOptions}.
|
|
142
|
+
*
|
|
143
|
+
* @note The `PI_USER_BACKEND_<UPPERNAME>_PYTHON` override uppercases
|
|
144
|
+
* `name` verbatim. For names containing `-` (e.g. `camoufox-py`)
|
|
145
|
+
* the env var contains a hyphen (`PI_USER_BACKEND_CAMOUFOX-PY_PYTHON`);
|
|
146
|
+
* shells that forbid hyphens in `export`ed names must set it via
|
|
147
|
+
* `settings.json` `env` or another Node-respected channel.
|
|
148
|
+
*/
|
|
149
|
+
export function probeUserBackend(
|
|
150
|
+
name: string,
|
|
151
|
+
opts?: ProbeUserBackendOptions,
|
|
152
|
+
): ProbeUserBackendResult {
|
|
153
|
+
const root = userBackendsDir();
|
|
154
|
+
|
|
155
|
+
const bridgePath =
|
|
156
|
+
opts?.bridgePath !== undefined && isAbsolute(opts.bridgePath)
|
|
157
|
+
? opts.bridgePath
|
|
158
|
+
: join(root, name, "bridge.py");
|
|
159
|
+
|
|
160
|
+
const envPython = process.env[`PI_USER_BACKEND_${name.toUpperCase()}_PYTHON`];
|
|
161
|
+
const venvPython =
|
|
162
|
+
envPython !== undefined && envPython.trim() !== ""
|
|
163
|
+
? envPython
|
|
164
|
+
: join(root, name, ".venv", "bin", "python3");
|
|
165
|
+
|
|
166
|
+
if (!existsSync(bridgePath)) {
|
|
167
|
+
return {
|
|
168
|
+
available: false,
|
|
169
|
+
bridgePath,
|
|
170
|
+
venvPython,
|
|
171
|
+
reason: `bridge script not found: ${bridgePath}`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const pythonRuns = (() => {
|
|
176
|
+
if (!existsSync(venvPython)) return false;
|
|
177
|
+
try {
|
|
178
|
+
const result = spawnSync(venvPython, ["--version"], {
|
|
179
|
+
stdio: "ignore",
|
|
180
|
+
timeout: 5_000,
|
|
181
|
+
});
|
|
182
|
+
return result.status === 0;
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
})();
|
|
187
|
+
|
|
188
|
+
if (!pythonRuns) {
|
|
189
|
+
return {
|
|
190
|
+
available: false,
|
|
191
|
+
bridgePath,
|
|
192
|
+
venvPython,
|
|
193
|
+
reason: `venv python not runnable: ${venvPython}`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { available: true, bridgePath, venvPython };
|
|
198
|
+
}
|
|
@@ -34,6 +34,20 @@ export const COOKIE_PERSISTENCE_HTML = `<!DOCTYPE html>
|
|
|
34
34
|
</script>
|
|
35
35
|
</body></html>`;
|
|
36
36
|
|
|
37
|
+
/** Convenience: start a test server serving COOKIE_PERSISTENCE_HTML at "/". */
|
|
38
|
+
export async function startCookiePersistenceServer(): Promise<TestServer> {
|
|
39
|
+
return startTestServer((req, res) => {
|
|
40
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
41
|
+
if (url.pathname === "/") {
|
|
42
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
43
|
+
res.end(COOKIE_PERSISTENCE_HTML);
|
|
44
|
+
} else {
|
|
45
|
+
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
46
|
+
res.end("404");
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
37
51
|
export async function startTestServer(
|
|
38
52
|
handler: (req: IncomingMessage, res: ServerResponse) => void,
|
|
39
53
|
): Promise<TestServer> {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Tests for the browser configuration parsing in plugin-config.ts.
|
|
3
3
|
*
|
|
4
4
|
* Covers:
|
|
5
|
-
* -
|
|
5
|
+
* - loadFullConfig().browser defaults when no config exists
|
|
6
6
|
* - defaultProfile validation ("none", "session", or a named profile)
|
|
7
7
|
* - defaultProfile validation
|
|
8
8
|
* - maxStorageStateSize validation
|
|
@@ -11,11 +11,14 @@
|
|
|
11
11
|
* - Merged global + project settings
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
14
16
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
15
17
|
import {
|
|
16
|
-
|
|
18
|
+
loadFullConfig,
|
|
17
19
|
invalidateConfigCache,
|
|
18
|
-
} from "../core/plugin-config";
|
|
20
|
+
} from "../core/plugin-config.js";
|
|
21
|
+
import { loadPluginConfigFromFile } from "./helpers/load-plugin-config-from-file.js";
|
|
19
22
|
|
|
20
23
|
// ─── Mock fs to intercept settings.json reads ────────────────────
|
|
21
24
|
|
|
@@ -72,22 +75,22 @@ beforeEach(() => {
|
|
|
72
75
|
|
|
73
76
|
// ─── Tests ───────────────────────────────────────────────────────
|
|
74
77
|
|
|
75
|
-
describe("
|
|
78
|
+
describe("loadFullConfig().browser", () => {
|
|
76
79
|
it("returns defaults when no settings.json exists", () => {
|
|
77
80
|
mockNoSettings();
|
|
78
|
-
const config =
|
|
81
|
+
const config = loadFullConfig().browser;
|
|
79
82
|
expect(config.defaultProfile).toBe("session");
|
|
80
83
|
});
|
|
81
84
|
|
|
82
85
|
it("returns defaults when browser section is missing", () => {
|
|
83
86
|
mockGlobalSettings({});
|
|
84
|
-
const config =
|
|
87
|
+
const config = loadFullConfig().browser;
|
|
85
88
|
expect(config.defaultProfile).toBe("session");
|
|
86
89
|
});
|
|
87
90
|
|
|
88
91
|
it("returns defaults when browser section has no config keys", () => {
|
|
89
92
|
mockGlobalSettings({ unrelated: true });
|
|
90
|
-
const config =
|
|
93
|
+
const config = loadFullConfig().browser;
|
|
91
94
|
expect(config.defaultProfile).toBe("session");
|
|
92
95
|
});
|
|
93
96
|
|
|
@@ -96,36 +99,36 @@ describe("loadBrowserConfig()", () => {
|
|
|
96
99
|
describe("defaultProfile", () => {
|
|
97
100
|
it("accepts a valid profile name", () => {
|
|
98
101
|
mockGlobalSettings({ defaultProfile: "work" });
|
|
99
|
-
expect(
|
|
102
|
+
expect(loadFullConfig().browser.defaultProfile).toBe("work");
|
|
100
103
|
});
|
|
101
104
|
|
|
102
105
|
it("accepts hyphens and underscores", () => {
|
|
103
106
|
mockGlobalSettings({ defaultProfile: "my-work-profile" });
|
|
104
|
-
expect(
|
|
107
|
+
expect(loadFullConfig().browser.defaultProfile).toBe("my-work-profile");
|
|
105
108
|
});
|
|
106
109
|
|
|
107
110
|
it("rejects reserved names and falls back to default", () => {
|
|
108
111
|
mockGlobalSettings({ defaultProfile: "none" });
|
|
109
112
|
// "none" is reserved as a profile name but valid as a defaultProfile mode
|
|
110
|
-
const config =
|
|
113
|
+
const config = loadFullConfig().browser;
|
|
111
114
|
expect(config.defaultProfile).toBe("none");
|
|
112
115
|
});
|
|
113
116
|
|
|
114
117
|
it("rejects 'session' as a profile name for named profiles", () => {
|
|
115
118
|
mockGlobalSettings({ defaultProfile: "session" });
|
|
116
119
|
// "session" is reserved as a profile name but valid as a defaultProfile mode
|
|
117
|
-
const config =
|
|
120
|
+
const config = loadFullConfig().browser;
|
|
118
121
|
expect(config.defaultProfile).toBe("session");
|
|
119
122
|
});
|
|
120
123
|
|
|
121
124
|
it("rejects empty string", () => {
|
|
122
125
|
mockGlobalSettings({ defaultProfile: "" });
|
|
123
|
-
expect(
|
|
126
|
+
expect(loadFullConfig().browser.defaultProfile).toBe("session");
|
|
124
127
|
});
|
|
125
128
|
|
|
126
129
|
it("rejects names with path traversal", () => {
|
|
127
130
|
mockGlobalSettings({ defaultProfile: "../../evil" });
|
|
128
|
-
expect(
|
|
131
|
+
expect(loadFullConfig().browser.defaultProfile).toBe("session");
|
|
129
132
|
});
|
|
130
133
|
});
|
|
131
134
|
|
|
@@ -137,7 +140,7 @@ describe("loadBrowserConfig()", () => {
|
|
|
137
140
|
{ browser: { defaultProfile: "none" } },
|
|
138
141
|
{ browser: { defaultProfile: "work" } },
|
|
139
142
|
);
|
|
140
|
-
expect(
|
|
143
|
+
expect(loadFullConfig().browser.defaultProfile).toBe("work");
|
|
141
144
|
});
|
|
142
145
|
|
|
143
146
|
it("project settings entirely replace global browser config (shallow merge)", () => {
|
|
@@ -153,8 +156,140 @@ describe("loadBrowserConfig()", () => {
|
|
|
153
156
|
},
|
|
154
157
|
},
|
|
155
158
|
);
|
|
156
|
-
const config =
|
|
159
|
+
const config = loadFullConfig().browser;
|
|
157
160
|
expect(config.defaultProfile).toBe("session");
|
|
158
161
|
});
|
|
159
162
|
});
|
|
160
163
|
});
|
|
164
|
+
|
|
165
|
+
// ─── Default plugin fallback (Phase 0b) ─────────────────────────
|
|
166
|
+
|
|
167
|
+
describe("loadFullConfig().plugins default fallback", () => {
|
|
168
|
+
it("returns the four shipped backends when browser.plugins is absent", () => {
|
|
169
|
+
// No settings files → default fallback branch in parsePluginConfig fires.
|
|
170
|
+
mockNoSettings();
|
|
171
|
+
const { plugins, errors } = loadFullConfig().plugins;
|
|
172
|
+
expect(errors).toEqual([]);
|
|
173
|
+
const names = plugins.map((p) => p.name);
|
|
174
|
+
expect(names).toEqual(["chromium", "firefox", "chromium-py", "firefox-py"]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("does NOT include stealth backends (camoufox-py / stealth-py) in the default fallback", () => {
|
|
178
|
+
mockNoSettings();
|
|
179
|
+
const { plugins } = loadFullConfig().plugins;
|
|
180
|
+
const names = plugins.map((p) => p.name);
|
|
181
|
+
expect(names).not.toContain("camoufox-py");
|
|
182
|
+
expect(names).not.toContain("stealth-py");
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ─── loadPluginConfigFromFile ─────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
describe("loadPluginConfigFromFile()", () => {
|
|
189
|
+
/** Set up mock to behave like one valid bridge.py exists at a root dir. */
|
|
190
|
+
function mockValidBackendRoot(dirPath: string): void {
|
|
191
|
+
mockFs.existsSync.mockImplementation((p: string) => {
|
|
192
|
+
// The settings file itself exists
|
|
193
|
+
if (p === "/tmp/settings.json") return true;
|
|
194
|
+
if (p === "/tmp/empty.json") return true;
|
|
195
|
+
if (p === "/tmp/cached.json") return true;
|
|
196
|
+
// The backend directory exists
|
|
197
|
+
if (p === join(dirPath, "example-py")) return true;
|
|
198
|
+
if (p === join(dirPath, "my-backend")) return true;
|
|
199
|
+
// bridge.py exists (Python plugin)
|
|
200
|
+
if (p === join(dirPath, "example-py", "bridge.py")) return true;
|
|
201
|
+
if (p === join(dirPath, "my-backend", "bridge.py")) return true;
|
|
202
|
+
// index.ts does NOT exist → Python plugin, unambiguous
|
|
203
|
+
// Default stub dirs that detectPluginType checks as directories
|
|
204
|
+
if (p.startsWith("/tmp/")) return false;
|
|
205
|
+
// Everything else (e.g. global settings paths from fallback) is absent
|
|
206
|
+
return false;
|
|
207
|
+
});
|
|
208
|
+
mockFs.readFileSync.mockImplementation((p: string) => {
|
|
209
|
+
if (p === "/tmp/settings.json") {
|
|
210
|
+
return JSON.stringify({
|
|
211
|
+
browser: {
|
|
212
|
+
plugins: [
|
|
213
|
+
{
|
|
214
|
+
name: "example-py",
|
|
215
|
+
dir: "example-py",
|
|
216
|
+
enabled: true,
|
|
217
|
+
config: {
|
|
218
|
+
pythonPath: "/custom/python3",
|
|
219
|
+
capabilities: { engine: "firefox" },
|
|
220
|
+
transportTimeoutMs: 30_000,
|
|
221
|
+
launch: { headless: true },
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (p === "/tmp/empty.json") return "{}";
|
|
229
|
+
return "{}";
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
it("parses a test-local settings file with browser.plugins", () => {
|
|
234
|
+
// Point at a root with a valid bridge.py so detectPluginType passes.
|
|
235
|
+
mockValidBackendRoot("/tmp/backends");
|
|
236
|
+
|
|
237
|
+
const { plugins, errors } = loadPluginConfigFromFile("/tmp/settings.json", [
|
|
238
|
+
"/tmp/backends",
|
|
239
|
+
]);
|
|
240
|
+
expect(errors).toEqual([]);
|
|
241
|
+
expect(plugins).toHaveLength(1);
|
|
242
|
+
const p = plugins[0]!;
|
|
243
|
+
expect(p.name).toBe("example-py");
|
|
244
|
+
expect(p.enabled).toBe(true);
|
|
245
|
+
expect(p.config.pythonPath).toBe("/custom/python3");
|
|
246
|
+
expect(p.config.launch).toEqual({ headless: true });
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("falls back to default plugins when settings file has no browser key", () => {
|
|
250
|
+
// No browser key → falls through to parsePluginConfig(undefined) → defaults
|
|
251
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
252
|
+
mockFs.readFileSync.mockImplementation((p: string) => {
|
|
253
|
+
if (p === "/tmp/no-browser.json")
|
|
254
|
+
return JSON.stringify({ unrelated: true });
|
|
255
|
+
return "{}";
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const { plugins, errors } = loadPluginConfigFromFile(
|
|
259
|
+
"/tmp/no-browser.json",
|
|
260
|
+
);
|
|
261
|
+
expect(errors).toEqual([]);
|
|
262
|
+
const names = plugins.map((p) => p.name);
|
|
263
|
+
expect(names).toEqual(["chromium", "firefox", "chromium-py", "firefox-py"]);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("falls back to default plugins when settings file is empty", () => {
|
|
267
|
+
// Empty JSON object → no "browser" key → same fallback.
|
|
268
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
269
|
+
mockFs.readFileSync.mockImplementation((p: string) => {
|
|
270
|
+
if (p === "/tmp/empty.json") return "{}";
|
|
271
|
+
return "{}";
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const { plugins, errors } = loadPluginConfigFromFile("/tmp/empty.json");
|
|
275
|
+
expect(errors).toEqual([]);
|
|
276
|
+
const names = plugins.map((p) => p.name);
|
|
277
|
+
expect(names).toEqual(["chromium", "firefox", "chromium-py", "firefox-py"]);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("is one-shot with no caching (consecutive calls read the file each time)", () => {
|
|
281
|
+
const spy = vi.fn((_: string) =>
|
|
282
|
+
JSON.stringify({ browser: { plugins: [] } }),
|
|
283
|
+
);
|
|
284
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
285
|
+
mockFs.readFileSync.mockImplementation(spy);
|
|
286
|
+
|
|
287
|
+
const testPath = "/tmp/cached.json";
|
|
288
|
+
// Call twice
|
|
289
|
+
loadPluginConfigFromFile(testPath);
|
|
290
|
+
loadPluginConfigFromFile(testPath);
|
|
291
|
+
|
|
292
|
+
// Must have been called twice (no caching)
|
|
293
|
+
expect(spy).toHaveBeenCalledTimes(2);
|
|
294
|
+
});
|
|
295
|
+
});
|