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
|
@@ -11,9 +11,12 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
import {
|
|
15
|
+
detectPluginType,
|
|
16
|
+
DEFAULT_BACKENDS_ROOT,
|
|
17
|
+
} from "../core/plugin-config.js";
|
|
18
|
+
import { PluginRegistry } from "../core/plugin-registry.js";
|
|
19
|
+
import type { PluginConfig } from "../core/plugin-config.js";
|
|
17
20
|
|
|
18
21
|
// ─── Helpers ─────────────────────────────────────────────────────
|
|
19
22
|
|
|
@@ -54,7 +57,7 @@ describe("detectPluginType", () => {
|
|
|
54
57
|
backendsRoot = createTestBackendsRoot({
|
|
55
58
|
"my-node-plugin": { "index.ts": "export class MyPlugin {}" },
|
|
56
59
|
});
|
|
57
|
-
const result = detectPluginType("my-node-plugin", backendsRoot);
|
|
60
|
+
const result = detectPluginType("my-node-plugin", [backendsRoot]);
|
|
58
61
|
expect(result.type).toBe("node");
|
|
59
62
|
expect(result.entryPoint).toBe(
|
|
60
63
|
join(backendsRoot, "my-node-plugin", "index.ts"),
|
|
@@ -65,7 +68,7 @@ describe("detectPluginType", () => {
|
|
|
65
68
|
backendsRoot = createTestBackendsRoot({
|
|
66
69
|
"my-py-plugin": { "bridge.py": "class MyBridge: pass" },
|
|
67
70
|
});
|
|
68
|
-
const result = detectPluginType("my-py-plugin", backendsRoot);
|
|
71
|
+
const result = detectPluginType("my-py-plugin", [backendsRoot]);
|
|
69
72
|
expect(result.type).toBe("python");
|
|
70
73
|
expect(result.entryPoint).toBe(
|
|
71
74
|
join(backendsRoot, "my-py-plugin", "bridge.py"),
|
|
@@ -79,7 +82,7 @@ describe("detectPluginType", () => {
|
|
|
79
82
|
"bridge.py": "class X: pass",
|
|
80
83
|
},
|
|
81
84
|
});
|
|
82
|
-
expect(() => detectPluginType("ambiguous", backendsRoot)).toThrow(
|
|
85
|
+
expect(() => detectPluginType("ambiguous", [backendsRoot])).toThrow(
|
|
83
86
|
/ambiguous/,
|
|
84
87
|
);
|
|
85
88
|
});
|
|
@@ -88,14 +91,14 @@ describe("detectPluginType", () => {
|
|
|
88
91
|
backendsRoot = createTestBackendsRoot({
|
|
89
92
|
"empty-plugin": { "readme.md": "# Nothing useful" },
|
|
90
93
|
});
|
|
91
|
-
expect(() => detectPluginType("empty-plugin", backendsRoot)).toThrow(
|
|
94
|
+
expect(() => detectPluginType("empty-plugin", [backendsRoot])).toThrow(
|
|
92
95
|
/no entry point/i,
|
|
93
96
|
);
|
|
94
97
|
});
|
|
95
98
|
|
|
96
99
|
it("throws for non-existent dir", () => {
|
|
97
100
|
backendsRoot = createTestBackendsRoot({});
|
|
98
|
-
expect(() => detectPluginType("nonexistent", backendsRoot)).toThrow(
|
|
101
|
+
expect(() => detectPluginType("nonexistent", [backendsRoot])).toThrow(
|
|
99
102
|
/no entry point/i,
|
|
100
103
|
);
|
|
101
104
|
});
|
|
@@ -106,28 +109,28 @@ describe("detectPluginType", () => {
|
|
|
106
109
|
describe("detectPluginType — production backends", () => {
|
|
107
110
|
it("detects the chromium backend as a Node plugin", () => {
|
|
108
111
|
// This tests the actual backends/chromium/ directory
|
|
109
|
-
const result = detectPluginType("chromium", DEFAULT_BACKENDS_ROOT);
|
|
112
|
+
const result = detectPluginType("chromium", [DEFAULT_BACKENDS_ROOT]);
|
|
110
113
|
expect(result.type).toBe("node");
|
|
111
114
|
expect(result.entryPoint).toContain("chromium/index.ts");
|
|
112
115
|
});
|
|
113
116
|
|
|
114
117
|
it("detects the firefox backend as a Node plugin", () => {
|
|
115
118
|
// This tests the actual backends/firefox/ directory
|
|
116
|
-
const result = detectPluginType("firefox", DEFAULT_BACKENDS_ROOT);
|
|
119
|
+
const result = detectPluginType("firefox", [DEFAULT_BACKENDS_ROOT]);
|
|
117
120
|
expect(result.type).toBe("node");
|
|
118
121
|
expect(result.entryPoint).toContain("firefox/index.ts");
|
|
119
122
|
});
|
|
120
123
|
|
|
121
124
|
it("detects the chromium-py backend as a Python plugin", () => {
|
|
122
125
|
// This tests the actual backends/chromium-py/ directory
|
|
123
|
-
const result = detectPluginType("chromium-py", DEFAULT_BACKENDS_ROOT);
|
|
126
|
+
const result = detectPluginType("chromium-py", [DEFAULT_BACKENDS_ROOT]);
|
|
124
127
|
expect(result.type).toBe("python");
|
|
125
128
|
expect(result.entryPoint).toContain("chromium-py/bridge.py");
|
|
126
129
|
});
|
|
127
130
|
|
|
128
131
|
it("detects the firefox-py backend as a Python plugin", () => {
|
|
129
132
|
// This tests the actual backends/firefox-py/ directory
|
|
130
|
-
const result = detectPluginType("firefox-py", DEFAULT_BACKENDS_ROOT);
|
|
133
|
+
const result = detectPluginType("firefox-py", [DEFAULT_BACKENDS_ROOT]);
|
|
131
134
|
expect(result.type).toBe("python");
|
|
132
135
|
expect(result.entryPoint).toContain("firefox-py/bridge.py");
|
|
133
136
|
});
|
|
@@ -170,7 +173,7 @@ describe("plugin startup loop integration", () => {
|
|
|
170
173
|
const errors: string[] = [];
|
|
171
174
|
for (const config of configs) {
|
|
172
175
|
try {
|
|
173
|
-
detectPluginType(config.dir, backendsRoot);
|
|
176
|
+
detectPluginType(config.dir, [backendsRoot]);
|
|
174
177
|
} catch (err) {
|
|
175
178
|
errors.push(
|
|
176
179
|
`Plugin '${config.name}' (dir: '${config.dir}'): ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -181,7 +184,6 @@ describe("plugin startup loop integration", () => {
|
|
|
181
184
|
expect(errors).toHaveLength(1);
|
|
182
185
|
expect(errors[0]).toContain("nonexistent");
|
|
183
186
|
expect(errors[0]).toContain("no entry point");
|
|
184
|
-
expect(registry.size).toBe(0);
|
|
185
187
|
});
|
|
186
188
|
|
|
187
189
|
it("skips plugins with ambiguous directories and logs error", () => {
|
|
@@ -198,7 +200,7 @@ describe("plugin startup loop integration", () => {
|
|
|
198
200
|
const errors: string[] = [];
|
|
199
201
|
for (const config of configs) {
|
|
200
202
|
try {
|
|
201
|
-
detectPluginType(config.dir, backendsRoot);
|
|
203
|
+
detectPluginType(config.dir, [backendsRoot]);
|
|
202
204
|
} catch (err) {
|
|
203
205
|
errors.push(
|
|
204
206
|
`Plugin '${config.name}' (dir: '${config.dir}'): ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -208,7 +210,6 @@ describe("plugin startup loop integration", () => {
|
|
|
208
210
|
|
|
209
211
|
expect(errors).toHaveLength(1);
|
|
210
212
|
expect(errors[0]).toContain("ambiguous");
|
|
211
|
-
expect(registry.size).toBe(0);
|
|
212
213
|
});
|
|
213
214
|
|
|
214
215
|
it("correctly classifies multiple plugins of different types", () => {
|
|
@@ -223,7 +224,7 @@ describe("plugin startup loop integration", () => {
|
|
|
223
224
|
|
|
224
225
|
const detections = configs.map((config) => ({
|
|
225
226
|
config,
|
|
226
|
-
detection: detectPluginType(config.dir, backendsRoot),
|
|
227
|
+
detection: detectPluginType(config.dir, [backendsRoot]),
|
|
227
228
|
}));
|
|
228
229
|
|
|
229
230
|
expect(detections[0]!.detection.type).toBe("node");
|
|
@@ -240,7 +241,7 @@ describe("plugin startup loop integration", () => {
|
|
|
240
241
|
|
|
241
242
|
const detections = configs.map((config) => ({
|
|
242
243
|
config,
|
|
243
|
-
detection: detectPluginType(config.dir, backendsRoot),
|
|
244
|
+
detection: detectPluginType(config.dir, [backendsRoot]),
|
|
244
245
|
}));
|
|
245
246
|
|
|
246
247
|
expect(detections[0]!.detection.type).toBe("python");
|
|
@@ -257,3 +258,104 @@ describe("DEFAULT_BACKENDS_ROOT", () => {
|
|
|
257
258
|
expect(DEFAULT_BACKENDS_ROOT).toMatch(/^\//);
|
|
258
259
|
});
|
|
259
260
|
});
|
|
261
|
+
|
|
262
|
+
// ─── Multi-root discovery (Phase 0b) ─────────────────────────────
|
|
263
|
+
|
|
264
|
+
describe("detectPluginType — multi-root", () => {
|
|
265
|
+
let packageRoot: string;
|
|
266
|
+
let userRoot: string;
|
|
267
|
+
const createdRoots: string[] = [];
|
|
268
|
+
|
|
269
|
+
beforeEach(() => {
|
|
270
|
+
packageRoot = createTestBackendsRoot({});
|
|
271
|
+
userRoot = createTestBackendsRoot({});
|
|
272
|
+
createdRoots.push(packageRoot, userRoot);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
afterEach(() => {
|
|
276
|
+
for (const r of createdRoots.splice(0)) {
|
|
277
|
+
try {
|
|
278
|
+
cleanupTestBackendsRoot(r);
|
|
279
|
+
} catch {
|
|
280
|
+
/* ignore */
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("resolves a backend found in the user root when absent from the package root", () => {
|
|
286
|
+
// Place a Python backend only in the user root
|
|
287
|
+
const dir = join(userRoot, "camoufox-py");
|
|
288
|
+
mkdirSync(dir, { recursive: true });
|
|
289
|
+
writeFileSync(join(dir, "bridge.py"), "class X: pass");
|
|
290
|
+
|
|
291
|
+
const result = detectPluginType("camoufox-py", [packageRoot, userRoot]);
|
|
292
|
+
expect(result.type).toBe("python");
|
|
293
|
+
expect(result.entryPoint).toBe(join(userRoot, "camoufox-py", "bridge.py"));
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("package root wins over user root when both contain the same dir name", () => {
|
|
297
|
+
// 'chromium' in package root (index.ts) AND user root (bridge.py)
|
|
298
|
+
const pkgDir = join(packageRoot, "shared-name");
|
|
299
|
+
mkdirSync(pkgDir, { recursive: true });
|
|
300
|
+
writeFileSync(join(pkgDir, "index.ts"), "export class X {}");
|
|
301
|
+
const usrDir = join(userRoot, "shared-name");
|
|
302
|
+
mkdirSync(usrDir, { recursive: true });
|
|
303
|
+
writeFileSync(join(usrDir, "bridge.py"), "class X: pass");
|
|
304
|
+
|
|
305
|
+
const result = detectPluginType("shared-name", [packageRoot, userRoot]);
|
|
306
|
+
expect(result.type).toBe("node");
|
|
307
|
+
expect(result.entryPoint).toBe(
|
|
308
|
+
join(packageRoot, "shared-name", "index.ts"),
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("absolute dir short-circuits the roots list", () => {
|
|
313
|
+
const absDir = mkdtempSync(join(tmpdir(), "pi-lean-portal-abs-"));
|
|
314
|
+
createdRoots.push(absDir);
|
|
315
|
+
writeFileSync(join(absDir, "bridge.py"), "class X: pass");
|
|
316
|
+
|
|
317
|
+
// Pass roots that do NOT contain this dir; the absolute path must still win.
|
|
318
|
+
const result = detectPluginType(absDir, [packageRoot, userRoot]);
|
|
319
|
+
expect(result.type).toBe("python");
|
|
320
|
+
expect(result.entryPoint).toBe(join(absDir, "bridge.py"));
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("throws an error naming all roots searched when no root has an entry point", () => {
|
|
324
|
+
expect(() => detectPluginType("missing", [packageRoot, userRoot])).toThrow(
|
|
325
|
+
/no entry point/i,
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
detectPluginType("missing", [packageRoot, userRoot]);
|
|
330
|
+
} catch (err) {
|
|
331
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
332
|
+
// Both roots are named in the error so users can see where it looked.
|
|
333
|
+
expect(msg).toContain(packageRoot);
|
|
334
|
+
expect(msg).toContain(userRoot);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("per-root ambiguity is still an error even with multiple roots", () => {
|
|
339
|
+
const dir = join(userRoot, "ambiguous");
|
|
340
|
+
mkdirSync(dir, { recursive: true });
|
|
341
|
+
writeFileSync(join(dir, "index.ts"), "export class X {}");
|
|
342
|
+
writeFileSync(join(dir, "bridge.py"), "class X: pass");
|
|
343
|
+
|
|
344
|
+
expect(() =>
|
|
345
|
+
detectPluginType("ambiguous", [packageRoot, userRoot]),
|
|
346
|
+
).toThrow(/ambiguous/);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("a later root with the opposite entry type is NOT treated as ambiguity", () => {
|
|
350
|
+
// package root: index.ts ; user root (same name): bridge.py — first wins, no error.
|
|
351
|
+
const pkgDir = join(packageRoot, "split");
|
|
352
|
+
mkdirSync(pkgDir, { recursive: true });
|
|
353
|
+
writeFileSync(join(pkgDir, "index.ts"), "export class X {}");
|
|
354
|
+
const usrDir = join(userRoot, "split");
|
|
355
|
+
mkdirSync(usrDir, { recursive: true });
|
|
356
|
+
writeFileSync(join(usrDir, "bridge.py"), "class X: pass");
|
|
357
|
+
|
|
358
|
+
const result = detectPluginType("split", [packageRoot, userRoot]);
|
|
359
|
+
expect(result.type).toBe("node");
|
|
360
|
+
});
|
|
361
|
+
});
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, it, expect, beforeEach } from "vitest";
|
|
6
|
-
import { PluginRegistry, validatePlugin } from "../core/plugin-registry";
|
|
7
|
-
import { MockPlugin, makeConfig } from "./helpers/mock-plugin";
|
|
6
|
+
import { PluginRegistry, validatePlugin } from "../core/plugin-registry.js";
|
|
7
|
+
import { MockPlugin, makeConfig } from "./helpers/mock-plugin.js";
|
|
8
8
|
|
|
9
9
|
// ─── validatePlugin ──────────────────────────────────────────────
|
|
10
10
|
|
|
@@ -70,7 +70,6 @@ describe("PluginRegistry.register", () => {
|
|
|
70
70
|
it("registers a valid plugin", () => {
|
|
71
71
|
const plugin = new MockPlugin("chromium");
|
|
72
72
|
registry.register(plugin, makeConfig({ name: "chromium" }));
|
|
73
|
-
expect(registry.size).toBe(1);
|
|
74
73
|
});
|
|
75
74
|
|
|
76
75
|
it("throws on duplicate name", () => {
|
|
@@ -98,7 +97,6 @@ describe("PluginRegistry.register", () => {
|
|
|
98
97
|
const p2 = new MockPlugin("firefox");
|
|
99
98
|
registry.register(p1, makeConfig({ name: "chromium" }));
|
|
100
99
|
registry.register(p2, makeConfig({ name: "firefox" }));
|
|
101
|
-
expect(registry.size).toBe(2);
|
|
102
100
|
});
|
|
103
101
|
});
|
|
104
102
|
|
|
@@ -181,11 +179,7 @@ describe("PluginRegistry.getOrdered", () => {
|
|
|
181
179
|
registry.register(p2, makeConfig({ name: "beta" }));
|
|
182
180
|
registry.register(p3, makeConfig({ name: "gamma" }));
|
|
183
181
|
const ordered = registry.getOrdered();
|
|
184
|
-
expect(ordered.map((
|
|
185
|
-
"alpha",
|
|
186
|
-
"beta",
|
|
187
|
-
"gamma",
|
|
188
|
-
]);
|
|
182
|
+
expect(ordered.map((p) => p.name)).toEqual(["alpha", "beta", "gamma"]);
|
|
189
183
|
});
|
|
190
184
|
|
|
191
185
|
it("excludes disabled plugins", () => {
|
|
@@ -196,17 +190,16 @@ describe("PluginRegistry.getOrdered", () => {
|
|
|
196
190
|
registry.register(p2, makeConfig({ name: "beta", enabled: false }));
|
|
197
191
|
registry.register(p3, makeConfig({ name: "gamma" }));
|
|
198
192
|
const ordered = registry.getOrdered();
|
|
199
|
-
expect(ordered.map((
|
|
193
|
+
expect(ordered.map((p) => p.name)).toEqual(["alpha", "gamma"]);
|
|
200
194
|
});
|
|
201
195
|
|
|
202
|
-
it("
|
|
196
|
+
it("preserves registration order across enabled plugins", () => {
|
|
203
197
|
const p1 = new MockPlugin("alpha");
|
|
204
198
|
const p2 = new MockPlugin("beta");
|
|
205
199
|
registry.register(p1, makeConfig({ name: "alpha" }));
|
|
206
200
|
registry.register(p2, makeConfig({ name: "beta" }));
|
|
207
201
|
const ordered = registry.getOrdered();
|
|
208
|
-
expect(ordered
|
|
209
|
-
expect(ordered[1]!.level).toBe(1);
|
|
202
|
+
expect(ordered.map((p) => p.name)).toEqual(["alpha", "beta"]);
|
|
210
203
|
});
|
|
211
204
|
|
|
212
205
|
it("returns empty array when no plugins are enabled", () => {
|
|
@@ -257,39 +250,6 @@ describe("PluginRegistry.availableAll", () => {
|
|
|
257
250
|
});
|
|
258
251
|
});
|
|
259
252
|
|
|
260
|
-
// ─── PluginRegistry.getHigherStealth ──────────────────────────────
|
|
261
|
-
|
|
262
|
-
describe("PluginRegistry.getHigherStealth", () => {
|
|
263
|
-
let registry: PluginRegistry;
|
|
264
|
-
|
|
265
|
-
beforeEach(() => {
|
|
266
|
-
registry = new PluginRegistry();
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it("returns plugins at higher priority levels", () => {
|
|
270
|
-
const p1 = new MockPlugin("chromium");
|
|
271
|
-
const p2 = new MockPlugin("stealth");
|
|
272
|
-
registry.register(p1, makeConfig({ name: "chromium" }));
|
|
273
|
-
registry.register(p2, makeConfig({ name: "stealth" }));
|
|
274
|
-
const higher = registry.getHigherStealth(0);
|
|
275
|
-
expect(higher.map((e) => e.plugin.name)).toEqual(["stealth"]);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it("returns empty array when no higher-priority plugins exist", () => {
|
|
279
|
-
const p1 = new MockPlugin("chromium");
|
|
280
|
-
registry.register(p1, makeConfig({ name: "chromium" }));
|
|
281
|
-
expect(registry.getHigherStealth(0)).toEqual([]);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it("excludes disabled plugins from higher priority", () => {
|
|
285
|
-
const p1 = new MockPlugin("chromium");
|
|
286
|
-
const p2 = new MockPlugin("stealth");
|
|
287
|
-
registry.register(p1, makeConfig({ name: "chromium" }));
|
|
288
|
-
registry.register(p2, makeConfig({ name: "stealth", enabled: false }));
|
|
289
|
-
expect(registry.getHigherStealth(0)).toEqual([]);
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
|
|
293
253
|
// ─── PluginRegistry.resolveStrategy ──────────────────────────────
|
|
294
254
|
|
|
295
255
|
describe("PluginRegistry.resolveStrategy", () => {
|
|
@@ -371,32 +331,11 @@ describe("PluginRegistry.clear", () => {
|
|
|
371
331
|
const p2 = new MockPlugin("firefox");
|
|
372
332
|
registry.register(p1, makeConfig({ name: "chromium" }));
|
|
373
333
|
registry.register(p2, makeConfig({ name: "firefox" }));
|
|
374
|
-
expect(registry.size).toBe(2);
|
|
375
334
|
registry.clear();
|
|
376
|
-
expect(registry.size).toBe(0);
|
|
377
335
|
expect(registry.available()).toEqual([]);
|
|
378
336
|
});
|
|
379
337
|
});
|
|
380
338
|
|
|
381
|
-
// ─── PluginRegistry.getLevel ─────────────────────────────────────
|
|
382
|
-
|
|
383
|
-
describe("PluginRegistry.getLevel", () => {
|
|
384
|
-
it("returns the priority level of a registered plugin", () => {
|
|
385
|
-
const registry = new PluginRegistry();
|
|
386
|
-
const p1 = new MockPlugin("chromium");
|
|
387
|
-
const p2 = new MockPlugin("stealth");
|
|
388
|
-
registry.register(p1, makeConfig({ name: "chromium" }));
|
|
389
|
-
registry.register(p2, makeConfig({ name: "stealth" }));
|
|
390
|
-
expect(registry.getLevel("chromium")).toBe(0);
|
|
391
|
-
expect(registry.getLevel("stealth")).toBe(1);
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
it("returns undefined for unknown plugin", () => {
|
|
395
|
-
const registry = new PluginRegistry();
|
|
396
|
-
expect(registry.getLevel("nonexistent")).toBeUndefined();
|
|
397
|
-
});
|
|
398
|
-
});
|
|
399
|
-
|
|
400
339
|
// ─── MockPlugin call tracking ────────────────────────────────────
|
|
401
340
|
|
|
402
341
|
describe("MockPlugin call tracking", () => {
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for `probeUserBackend` (Sprint 1, AC 1.2).
|
|
3
|
+
*
|
|
4
|
+
* Structural — no real browser, no MiniWoB content. Uses a temp
|
|
5
|
+
* directory as `PI_USER_BACKENDS_DIR` so nothing is written to the
|
|
6
|
+
* real `~/.pi/agent/pi-lean-portal/user-backends/`. The "venv runs"
|
|
7
|
+
* case uses a tiny `#!/bin/sh\nexit 0` stub executable so the test is
|
|
8
|
+
* self-contained and does not depend on python3 being installed at a
|
|
9
|
+
* known path; the "non-runnable" case uses an exit-1 stub.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
15
|
+
import {
|
|
16
|
+
chmodSync,
|
|
17
|
+
existsSync,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
mkdtempSync,
|
|
20
|
+
rmSync,
|
|
21
|
+
writeFileSync,
|
|
22
|
+
} from "node:fs";
|
|
23
|
+
import { homedir, tmpdir } from "node:os";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
probeUserBackend,
|
|
28
|
+
userBackendsDir,
|
|
29
|
+
} from "./helpers/probe-user-backend.js";
|
|
30
|
+
|
|
31
|
+
// ─── Helpers ──────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create a stub "python3" executable at `<dir>/.venv/bin/python3` that
|
|
35
|
+
* exits with `exitCode`. Mimics a runnable (exit 0) or broken (exit 1)
|
|
36
|
+
* venv interpreter for the probe's `spawnSync(…, ["--version"])` check.
|
|
37
|
+
*/
|
|
38
|
+
function makeStubPython(dir: string, exitCode: number): string {
|
|
39
|
+
const binDir = join(dir, ".venv", "bin");
|
|
40
|
+
mkdirSync(binDir, { recursive: true });
|
|
41
|
+
const pyPath = join(binDir, "python3");
|
|
42
|
+
writeFileSync(pyPath, `#!/bin/sh\nexit ${exitCode}\n`);
|
|
43
|
+
chmodSync(pyPath, 0o755);
|
|
44
|
+
return pyPath;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Write a stub `bridge.py` at `<dir>/bridge.py`. */
|
|
48
|
+
function makeStubBridge(dir: string): string {
|
|
49
|
+
mkdirSync(dir, { recursive: true });
|
|
50
|
+
const bridgePath = join(dir, "bridge.py");
|
|
51
|
+
writeFileSync(bridgePath, "# stub bridge\n");
|
|
52
|
+
return bridgePath;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Test env snapshot / restore ──────────────────────────────────
|
|
56
|
+
|
|
57
|
+
let tempRoot: string;
|
|
58
|
+
let savedDir: string | undefined;
|
|
59
|
+
/** Per-name `PI_USER_BACKEND_<UPPERNAME>_PYTHON` keys set during a test. */
|
|
60
|
+
let touchedPythonKeys: string[] = [];
|
|
61
|
+
/** Saved values of per-name python env overrides, for afterEach restore. */
|
|
62
|
+
const savedEnv = new Map<string, string | undefined>();
|
|
63
|
+
|
|
64
|
+
function setPythonOverride(name: string, value: string): void {
|
|
65
|
+
const key = `PI_USER_BACKEND_${name.toUpperCase()}_PYTHON`;
|
|
66
|
+
if (!savedEnv.has(key)) savedEnv.set(key, process.env[key]);
|
|
67
|
+
touchedPythonKeys.push(key);
|
|
68
|
+
process.env[key] = value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
tempRoot = mkdtempSync(join(tmpdir(), "probe-user-backend-"));
|
|
73
|
+
savedDir = process.env.PI_USER_BACKENDS_DIR;
|
|
74
|
+
savedEnv.clear();
|
|
75
|
+
touchedPythonKeys = [];
|
|
76
|
+
process.env.PI_USER_BACKENDS_DIR = tempRoot;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
afterEach(() => {
|
|
80
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
81
|
+
// Restore PI_USER_BACKENDS_DIR.
|
|
82
|
+
if (savedDir === undefined)
|
|
83
|
+
Reflect.deleteProperty(process.env, "PI_USER_BACKENDS_DIR");
|
|
84
|
+
else process.env.PI_USER_BACKENDS_DIR = savedDir;
|
|
85
|
+
// Restore (or delete) any per-name python overrides set during the test.
|
|
86
|
+
for (const key of touchedPythonKeys) {
|
|
87
|
+
const saved = savedEnv.get(key);
|
|
88
|
+
if (saved === undefined) Reflect.deleteProperty(process.env, key);
|
|
89
|
+
else process.env[key] = saved;
|
|
90
|
+
}
|
|
91
|
+
touchedPythonKeys = [];
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ─── userBackendsDir ──────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
describe("userBackendsDir", () => {
|
|
97
|
+
it("honors PI_USER_BACKENDS_DIR when set", () => {
|
|
98
|
+
expect(userBackendsDir()).toBe(tempRoot);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("falls back to ~/.pi/agent/pi-lean-portal/user-backends when unset", () => {
|
|
102
|
+
Reflect.deleteProperty(process.env, "PI_USER_BACKENDS_DIR");
|
|
103
|
+
const expected = join(
|
|
104
|
+
homedir(),
|
|
105
|
+
".pi",
|
|
106
|
+
"agent",
|
|
107
|
+
"pi-lean-portal",
|
|
108
|
+
"user-backends",
|
|
109
|
+
);
|
|
110
|
+
expect(userBackendsDir()).toBe(expected);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("treats an empty PI_USER_BACKENDS_DIR as unset", () => {
|
|
114
|
+
process.env.PI_USER_BACKENDS_DIR = " ";
|
|
115
|
+
const expected = join(
|
|
116
|
+
homedir(),
|
|
117
|
+
".pi",
|
|
118
|
+
"agent",
|
|
119
|
+
"pi-lean-portal",
|
|
120
|
+
"user-backends",
|
|
121
|
+
);
|
|
122
|
+
expect(userBackendsDir()).toBe(expected);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ─── probeUserBackend ─────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
describe("probeUserBackend", () => {
|
|
129
|
+
it("returns available=true with correct paths when bridge + venv present", () => {
|
|
130
|
+
const name = "camoufox-py";
|
|
131
|
+
const backendDir = join(tempRoot, name);
|
|
132
|
+
const bridge = makeStubBridge(backendDir);
|
|
133
|
+
const py = makeStubPython(backendDir, 0);
|
|
134
|
+
|
|
135
|
+
const result = probeUserBackend(name);
|
|
136
|
+
|
|
137
|
+
expect(result.available).toBe(true);
|
|
138
|
+
expect(result.reason).toBeUndefined();
|
|
139
|
+
expect(result.bridgePath).toBe(bridge);
|
|
140
|
+
expect(result.venvPython).toBe(py);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("returns available=false with a bridge-mentioning reason when bridge.py is missing", () => {
|
|
144
|
+
const name = "camoufox-py";
|
|
145
|
+
// venv present but no bridge.py
|
|
146
|
+
const py = makeStubPython(join(tempRoot, name), 0);
|
|
147
|
+
const expectedBridge = join(tempRoot, name, "bridge.py");
|
|
148
|
+
|
|
149
|
+
const result = probeUserBackend(name);
|
|
150
|
+
|
|
151
|
+
expect(result.available).toBe(false);
|
|
152
|
+
expect(result.reason).toMatch(/bridge/i);
|
|
153
|
+
expect(result.reason).toContain(expectedBridge);
|
|
154
|
+
expect(result.bridgePath).toBe(expectedBridge);
|
|
155
|
+
expect(result.venvPython).toBe(py);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("returns available=false with a venv-mentioning reason when venv python is missing", () => {
|
|
159
|
+
const name = "stealth-py";
|
|
160
|
+
const backendDir = join(tempRoot, name);
|
|
161
|
+
makeStubBridge(backendDir);
|
|
162
|
+
const expectedPy = join(backendDir, ".venv", "bin", "python3");
|
|
163
|
+
|
|
164
|
+
const result = probeUserBackend(name);
|
|
165
|
+
|
|
166
|
+
expect(result.available).toBe(false);
|
|
167
|
+
expect(result.reason).toMatch(/venv|python/i);
|
|
168
|
+
expect(result.reason).toContain(expectedPy);
|
|
169
|
+
expect(result.venvPython).toBe(expectedPy);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("returns available=false with a venv-mentioning reason when venv python exists but does not run", () => {
|
|
173
|
+
const name = "stealth-py";
|
|
174
|
+
const backendDir = join(tempRoot, name);
|
|
175
|
+
makeStubBridge(backendDir);
|
|
176
|
+
const py = makeStubPython(backendDir, 1); // exits non-zero
|
|
177
|
+
|
|
178
|
+
const result = probeUserBackend(name);
|
|
179
|
+
|
|
180
|
+
expect(result.available).toBe(false);
|
|
181
|
+
expect(result.reason).toMatch(/venv|python/i);
|
|
182
|
+
expect(result.reason).toContain(py);
|
|
183
|
+
expect(result.venvPython).toBe(py);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("respects PI_USER_BACKEND_<UPPERNAME>_PYTHON env override for venvPython", () => {
|
|
187
|
+
const name = "camoufox-py";
|
|
188
|
+
const backendDir = join(tempRoot, name);
|
|
189
|
+
makeStubBridge(backendDir);
|
|
190
|
+
// Do NOT create the default .venv; instead point the env override at a
|
|
191
|
+
// stub python that lives outside the default venv path (but still under
|
|
192
|
+
// tempRoot so afterEach cleans it up).
|
|
193
|
+
const overrideDir = join(tempRoot, "override-venv");
|
|
194
|
+
const overridePy = makeStubPython(overrideDir, 0);
|
|
195
|
+
setPythonOverride(name, overridePy);
|
|
196
|
+
|
|
197
|
+
const result = probeUserBackend(name);
|
|
198
|
+
|
|
199
|
+
expect(result.available).toBe(true);
|
|
200
|
+
expect(result.venvPython).toBe(overridePy);
|
|
201
|
+
expect(existsSync(join(backendDir, ".venv", "bin", "python3"))).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("respects opts.bridgePath absolute override for bridgePath", () => {
|
|
205
|
+
const name = "camoufox-py";
|
|
206
|
+
// Create a bridge.py at an arbitrary absolute location outside the
|
|
207
|
+
// default user-backends/<name>/ tree (but still under tempRoot so
|
|
208
|
+
// afterEach cleans it up).
|
|
209
|
+
const externalDir = join(tempRoot, "override-bridge");
|
|
210
|
+
const externalBridge = makeStubBridge(externalDir);
|
|
211
|
+
// Also provide a working venv under the default path so the only thing
|
|
212
|
+
// being overridden is the bridge location.
|
|
213
|
+
makeStubPython(join(tempRoot, name), 0);
|
|
214
|
+
|
|
215
|
+
const result = probeUserBackend(name, { bridgePath: externalBridge });
|
|
216
|
+
|
|
217
|
+
expect(result.available).toBe(true);
|
|
218
|
+
expect(result.bridgePath).toBe(externalBridge);
|
|
219
|
+
// The default bridge path is NOT used.
|
|
220
|
+
expect(result.bridgePath).not.toBe(join(tempRoot, name, "bridge.py"));
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("ignores a non-absolute opts.bridgePath and falls back to default resolution", () => {
|
|
224
|
+
const name = "camoufox-py";
|
|
225
|
+
const backendDir = join(tempRoot, name);
|
|
226
|
+
const bridge = makeStubBridge(backendDir);
|
|
227
|
+
makeStubPython(backendDir, 0);
|
|
228
|
+
|
|
229
|
+
// A relative bridgePath must not short-circuit (mirrors detectPluginType,
|
|
230
|
+
// which only short-circuits on isAbsolute(dir)).
|
|
231
|
+
const result = probeUserBackend(name, { bridgePath: "relative/bridge.py" });
|
|
232
|
+
|
|
233
|
+
expect(result.available).toBe(true);
|
|
234
|
+
expect(result.bridgePath).toBe(bridge);
|
|
235
|
+
});
|
|
236
|
+
});
|