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,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic contributed-backend runner (tracked) — discovers every
|
|
3
|
+
* user-managed Python backend installed under `user-backends/` and runs
|
|
4
|
+
* the **shared contract suite + shared persistence suite + shared
|
|
5
|
+
* MiniWoB parity suite + quirks introspection suite** against each.
|
|
6
|
+
*
|
|
7
|
+
* The quirks suite reads the bridge's declared quirks flags via the
|
|
8
|
+
* ``browser.describeQuirks`` RPC and runs parametrized ``it`` blocks
|
|
9
|
+
* that only activate when the corresponding quirk is declared.
|
|
10
|
+
* Backends with behavioural tests beyond the quirk flags (e.g.
|
|
11
|
+
* invisible-py's lifecycle/deadlock tests) keep a hand-authored
|
|
12
|
+
* file under ``__tests__/contributed/<name>-py/``.
|
|
13
|
+
*
|
|
14
|
+
* Gated by `CONTRIB_RUN === "1"` (the same env var Sprint 2 established
|
|
15
|
+
* for the contributed MiniWoB parity helper). No-op in bare CI (no
|
|
16
|
+
* user-backends installed → discovered list empty → registers nothing)
|
|
17
|
+
* and also no-op when `CONTRIB_RUN` is unset, so `npm run test:ci`
|
|
18
|
+
* stays green with the file present even on a machine that has
|
|
19
|
+
* user-backends installed.
|
|
20
|
+
*
|
|
21
|
+
* No stealth engine is **named** in this file. Discovery is purely
|
|
22
|
+
* runtime — whatever the user has installed under `user-backends/` is
|
|
23
|
+
* what runs. The shared suites own parametrized `it` titles (Sprint 1
|
|
24
|
+
* mitigation) so a helper regression is attributable per-backend.
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { existsSync } from "node:fs";
|
|
30
|
+
import { join } from "node:path";
|
|
31
|
+
|
|
32
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
DEFAULT_CAPABILITIES,
|
|
36
|
+
type PluginCapabilities,
|
|
37
|
+
} from "../core/plugin-api.js";
|
|
38
|
+
import {
|
|
39
|
+
PythonPluginAdapter,
|
|
40
|
+
type QuirksDescriptor,
|
|
41
|
+
} from "../backends/python-adapter.js";
|
|
42
|
+
import { loadPluginConfigFromFile } from "./helpers/load-plugin-config-from-file.js";
|
|
43
|
+
import type { PluginConfigLoadResult } from "../core/plugin-config.js";
|
|
44
|
+
import { runContractTests } from "./helpers/plugin-contract.js";
|
|
45
|
+
import { runPersistenceSuite } from "./helpers/persistence-suite.js";
|
|
46
|
+
import {
|
|
47
|
+
probeUserBackend,
|
|
48
|
+
discoverUserBackends,
|
|
49
|
+
userBackendsDir,
|
|
50
|
+
} from "./helpers/probe-user-backend.js";
|
|
51
|
+
import {
|
|
52
|
+
startTestServer,
|
|
53
|
+
COOKIE_PERSISTENCE_HTML,
|
|
54
|
+
} from "./helpers/test-server.js";
|
|
55
|
+
import { registerContributedParitySuite } from "../../../bench/miniwob/solvers/contributed-parity.js";
|
|
56
|
+
import { startMiniwobServer } from "../../../bench/miniwob/scripts/miniwob-server.js";
|
|
57
|
+
|
|
58
|
+
// ─── Opt-in gate ─────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
const CONTRIB_RUN = process.env.CONTRIB_RUN === "1";
|
|
61
|
+
|
|
62
|
+
/** Path to the test-local settings file. Override via CONTRIB_SETTINGS env. */
|
|
63
|
+
const SETTINGS_PATH =
|
|
64
|
+
process.env.CONTRIB_SETTINGS ??
|
|
65
|
+
join(__dirname, "contributed", "settings.json");
|
|
66
|
+
|
|
67
|
+
// ─── Backend discovery ──────────────────────────────────────────────
|
|
68
|
+
// `discoverUserBackends` (imported from the shared probe helper) scans
|
|
69
|
+
// the user-backends root for `<name>-py/` dirs containing a `bridge.py`.
|
|
70
|
+
|
|
71
|
+
// ─── Capabilities merge helper ──────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Merge a user-authored capabilities override onto DEFAULT_CAPABILITIES.
|
|
75
|
+
*
|
|
76
|
+
* User entries in the test-local settings file may omit fields;
|
|
77
|
+
* PluginCapabilities is non-optional. This helper fills in the gaps.
|
|
78
|
+
* When the override is absent or malformed (not a plain object), returns
|
|
79
|
+
* the full synthetic set — the same fallback as the pre-enhancement
|
|
80
|
+
* runner.
|
|
81
|
+
*/
|
|
82
|
+
function mergeCapabilities(override: unknown): PluginCapabilities {
|
|
83
|
+
if (!override || typeof override !== "object" || Array.isArray(override)) {
|
|
84
|
+
return DEFAULT_CAPABILITIES;
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
...DEFAULT_CAPABILITIES,
|
|
88
|
+
...(override as Partial<PluginCapabilities>),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── Entry ───────────────────────────────────────────────────────────
|
|
93
|
+
//
|
|
94
|
+
// Gated by CONTRIB_RUN. No-op when the env var is unset OR when
|
|
95
|
+
// user-backends/ is absent/empty.
|
|
96
|
+
|
|
97
|
+
if (CONTRIB_RUN) {
|
|
98
|
+
const root = userBackendsDir();
|
|
99
|
+
const discovered = discoverUserBackends(root);
|
|
100
|
+
|
|
101
|
+
if (discovered.length > 0) {
|
|
102
|
+
// ── Test-local config ──────────────────────────────────────
|
|
103
|
+
//
|
|
104
|
+
// Load the settings file once (index by name for lookup inside the
|
|
105
|
+
// per-backend loop). When absent, all discovered backends get the
|
|
106
|
+
// synthetic fallback (pre-enhancement behaviour).
|
|
107
|
+
let pluginCfg: PluginConfigLoadResult | null = null;
|
|
108
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
109
|
+
pluginCfg = loadPluginConfigFromFile(SETTINGS_PATH);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Shared servers ────────────────────────────────────────
|
|
113
|
+
//
|
|
114
|
+
// One persistence HTTP server (consent-dialog page) and one MiniWoB
|
|
115
|
+
// static server per file, shared across ALL discovered backends.
|
|
116
|
+
// Torn down at file level via afterAll.
|
|
117
|
+
let serverUrl: string;
|
|
118
|
+
let stopServer: () => Promise<void>;
|
|
119
|
+
let miniwobBaseUrl: string;
|
|
120
|
+
let stopMiniwob: () => Promise<void>;
|
|
121
|
+
|
|
122
|
+
beforeAll(async () => {
|
|
123
|
+
// Persistence suite HTTP server
|
|
124
|
+
const server = await startTestServer((req, res) => {
|
|
125
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
126
|
+
if (url.pathname === "/") {
|
|
127
|
+
res.writeHead(200, {
|
|
128
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
129
|
+
});
|
|
130
|
+
res.end(COOKIE_PERSISTENCE_HTML);
|
|
131
|
+
} else if (url.pathname === "/simple") {
|
|
132
|
+
res.writeHead(200, {
|
|
133
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
134
|
+
});
|
|
135
|
+
res.end(
|
|
136
|
+
"<!DOCTYPE html><html><head><title>Simple Page</title></head><body><h1>Simple</h1></body></html>",
|
|
137
|
+
);
|
|
138
|
+
} else if (url.pathname === "/scroll") {
|
|
139
|
+
res.writeHead(200, {
|
|
140
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
141
|
+
});
|
|
142
|
+
res.end(`<!DOCTYPE html>
|
|
143
|
+
<html><head><title>Scroll Test</title></head>
|
|
144
|
+
<body>
|
|
145
|
+
<h1>Scroll Test</h1>
|
|
146
|
+
${Array.from({ length: 80 }, (_, i) => `<p>Paragraph ${i + 1}: content to make the page scrollable.</p>`).join("\n")}
|
|
147
|
+
<p id="bottom-marker">You reached the bottom!</p>
|
|
148
|
+
</body></html>`);
|
|
149
|
+
} else {
|
|
150
|
+
res.writeHead(404, {
|
|
151
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
152
|
+
});
|
|
153
|
+
res.end("404");
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
serverUrl = server.url;
|
|
157
|
+
stopServer = server.stop;
|
|
158
|
+
|
|
159
|
+
// MiniWoB static server (shared across all parity registrations).
|
|
160
|
+
// Check content availability before starting — mirrors the check
|
|
161
|
+
// in the parity helper so we never fail beforeAll (which would
|
|
162
|
+
// block contract + persistence tests). When content is absent,
|
|
163
|
+
// the parity helper independently detects this via its own
|
|
164
|
+
// CONTENT_AVAILABLE check and describe.skips internally.
|
|
165
|
+
const miniwobHtmlRoot =
|
|
166
|
+
process.env.MINIWOB_HTML_ROOT ?? "/tmp/miniwob-plusplus/miniwob/html";
|
|
167
|
+
const miniwobContentAvailable =
|
|
168
|
+
Boolean(process.env.MINIWOB_URL) || existsSync(miniwobHtmlRoot);
|
|
169
|
+
if (miniwobContentAvailable) {
|
|
170
|
+
const miniwobServer = await startMiniwobServer();
|
|
171
|
+
miniwobBaseUrl = miniwobServer.url;
|
|
172
|
+
stopMiniwob = miniwobServer.stop;
|
|
173
|
+
} else {
|
|
174
|
+
miniwobBaseUrl = "";
|
|
175
|
+
stopMiniwob = async () => {};
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
afterAll(async () => {
|
|
180
|
+
await stopServer();
|
|
181
|
+
await stopMiniwob();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
for (const name of discovered) {
|
|
185
|
+
const probe = probeUserBackend(name);
|
|
186
|
+
|
|
187
|
+
// Look up the backend in the test-local config. When the file is
|
|
188
|
+
// absent or has no matching entry, cfg stays {} (synthetic fallback).
|
|
189
|
+
const entry = pluginCfg?.plugins.find((p) => p.name === name);
|
|
190
|
+
const cfg = entry?.config ?? {};
|
|
191
|
+
if (!entry) {
|
|
192
|
+
console.warn(
|
|
193
|
+
`[contributed-runner] ${name}: no settings entry — synthetic config`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Adapter factory — picks up configured pythonPath, capabilities,
|
|
198
|
+
// and transportTimeoutMs from the test-local settings when available.
|
|
199
|
+
// With exactOptionalPropertyTypes, undefined values must be omitted
|
|
200
|
+
// from the config object rather than set explicitly.
|
|
201
|
+
const buildAdapter = (idSuffix: string): PythonPluginAdapter =>
|
|
202
|
+
new PythonPluginAdapter(`${name}-${idSuffix}`, {
|
|
203
|
+
bridgeScript: probe.bridgePath,
|
|
204
|
+
pythonPath:
|
|
205
|
+
typeof cfg.pythonPath === "string"
|
|
206
|
+
? cfg.pythonPath
|
|
207
|
+
: probe.venvPython,
|
|
208
|
+
capabilities: mergeCapabilities(cfg.capabilities),
|
|
209
|
+
...(typeof cfg.transportTimeoutMs === "number"
|
|
210
|
+
? { transportTimeoutMs: cfg.transportTimeoutMs }
|
|
211
|
+
: {}),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const describeFn = probe.available ? describe : describe.skip;
|
|
215
|
+
|
|
216
|
+
describeFn(`contributed: ${name}`, () => {
|
|
217
|
+
// 1. Shared contract suite — built with the configured adapter
|
|
218
|
+
// so the pipeline exercises the backend's real capabilities,
|
|
219
|
+
// not a synthetic stand-in.
|
|
220
|
+
runContractTests(name, () => buildAdapter("contract"), {
|
|
221
|
+
realBrowser: true,
|
|
222
|
+
navigateTimeout: 30_000,
|
|
223
|
+
navigationSettle: true,
|
|
224
|
+
testTimeout: 60_000,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// 2. Shared persistence suite — forwards cfg (the launch
|
|
228
|
+
// object) via init(cfg) so the bridge receives the configured
|
|
229
|
+
// launch options, not the default {}.
|
|
230
|
+
runPersistenceSuite(describeFn, {
|
|
231
|
+
name,
|
|
232
|
+
getServerUrl: () => serverUrl,
|
|
233
|
+
createPlugin: async () => {
|
|
234
|
+
const p = buildAdapter("persist");
|
|
235
|
+
await p.init(cfg);
|
|
236
|
+
return p;
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// 3. MiniWoB parity suite (folded into the runner) — shares
|
|
241
|
+
// the file-level MiniWoB server via getBaseUrl and forwards
|
|
242
|
+
// cfg (the launch object) through config so the 130-task
|
|
243
|
+
// behavioral suite exercises the configured launch path.
|
|
244
|
+
//
|
|
245
|
+
// With exactOptionalPropertyTypes, omit the capabilities key
|
|
246
|
+
// entirely when undefined rather than passing undefined.
|
|
247
|
+
registerContributedParitySuite({
|
|
248
|
+
name,
|
|
249
|
+
probe,
|
|
250
|
+
...(cfg.capabilities !== undefined
|
|
251
|
+
? {
|
|
252
|
+
capabilities: cfg.capabilities as Partial<PluginCapabilities>,
|
|
253
|
+
}
|
|
254
|
+
: {}),
|
|
255
|
+
config: cfg,
|
|
256
|
+
getBaseUrl: async () => miniwobBaseUrl,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// 4. Quirks introspection suite — reads the bridge's declared quirks
|
|
260
|
+
// flags via the browser.describeQuirks RPC and runs behavioural
|
|
261
|
+
// integration tests for quirks that affect the bridge's runtime
|
|
262
|
+
// behaviour (eval_prefix, scroll_via_wheel). Flag-only assertions
|
|
263
|
+
// (fingerprint_managed_context, skip_default_viewport, etc.) are
|
|
264
|
+
// owned by the Python bridge unit tests
|
|
265
|
+
// (test_playwright_base_quirks.py). The beforeAll here proves the
|
|
266
|
+
// RPC works; the behavioural it() blocks exercise the actual quirk.
|
|
267
|
+
let quirks_: QuirksDescriptor;
|
|
268
|
+
|
|
269
|
+
describe("quirks", () => {
|
|
270
|
+
beforeAll(async () => {
|
|
271
|
+
const p = buildAdapter("quirks");
|
|
272
|
+
await p.init(cfg);
|
|
273
|
+
quirks_ = await p.describeQuirks();
|
|
274
|
+
await p.cleanupAll().catch(() => {});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("eval_prefix: evaluate('1 + 1') === 2", async () => {
|
|
278
|
+
if (!quirks_.eval_prefix) return;
|
|
279
|
+
const p = buildAdapter("quirks-eval");
|
|
280
|
+
await p.init(cfg);
|
|
281
|
+
const nav = await p.navigate(
|
|
282
|
+
`${serverUrl}/simple`,
|
|
283
|
+
"quirks-eval",
|
|
284
|
+
30_000,
|
|
285
|
+
);
|
|
286
|
+
expect(nav.success).toBe(true);
|
|
287
|
+
const r = await p.evaluate("quirks-eval", "1 + 1");
|
|
288
|
+
expect(r.success).toBe(true);
|
|
289
|
+
expect(r.result).toBe(2);
|
|
290
|
+
await p.cleanupAll().catch(() => {});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("scroll_via_wheel: scroll moves the page", async () => {
|
|
294
|
+
if (!quirks_.scroll_via_wheel) return;
|
|
295
|
+
const p = buildAdapter("quirks-scroll");
|
|
296
|
+
await p.init(cfg);
|
|
297
|
+
const nav = await p.navigate(
|
|
298
|
+
`${serverUrl}/scroll`,
|
|
299
|
+
"quirks-scroll",
|
|
300
|
+
30_000,
|
|
301
|
+
);
|
|
302
|
+
expect(nav.success).toBe(true);
|
|
303
|
+
const before = await p.evaluate("quirks-scroll", "window.scrollY");
|
|
304
|
+
expect(before.success).toBe(true);
|
|
305
|
+
expect(before.result).toBe(0);
|
|
306
|
+
const scrollResult = await p.scroll("quirks-scroll", "down");
|
|
307
|
+
expect(scrollResult.success).toBe(true);
|
|
308
|
+
const after = await p.evaluate("quirks-scroll", "window.scrollY");
|
|
309
|
+
expect(after.success).toBe(true);
|
|
310
|
+
expect(typeof after.result).toBe("number");
|
|
311
|
+
expect((after.result as number) > 0).toBe(true);
|
|
312
|
+
await p.cleanupAll().catch(() => {});
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionManager unit tests — focused on the updateSession skip-undefined
|
|
3
|
+
* contract (passing an explicit `undefined` must preserve the existing field,
|
|
4
|
+
* not narrow it back to absent).
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
7
|
+
import { sessionManager } from "../core/shared/session-manager.js";
|
|
8
|
+
|
|
9
|
+
type Updates = Parameters<typeof sessionManager.updateSession>[1];
|
|
10
|
+
|
|
11
|
+
describe("SessionManager.updateSession — skip-undefined contract", () => {
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
await sessionManager.removeAll();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("preserves currentUrl when { currentUrl: undefined } is passed", () => {
|
|
17
|
+
sessionManager.createSession("t1", "chromium");
|
|
18
|
+
sessionManager.updateSession("t1", { currentUrl: "https://example.com" });
|
|
19
|
+
expect(sessionManager.getSession("t1")?.currentUrl).toBe(
|
|
20
|
+
"https://example.com",
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// Under exactOptionalPropertyTypes the typed API rejects an explicit
|
|
24
|
+
// undefined, but a loosely-typed caller (spread/JSON/widened) can still
|
|
25
|
+
// deliver one — the skip-undefined guard must preserve the field.
|
|
26
|
+
sessionManager.updateSession("t1", {
|
|
27
|
+
currentUrl: undefined,
|
|
28
|
+
} as unknown as Updates);
|
|
29
|
+
expect(sessionManager.getSession("t1")?.currentUrl).toBe(
|
|
30
|
+
"https://example.com",
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("applies real values", () => {
|
|
35
|
+
sessionManager.createSession("t1", "chromium");
|
|
36
|
+
sessionManager.updateSession("t1", {
|
|
37
|
+
currentTitle: "Hello",
|
|
38
|
+
crashed: true,
|
|
39
|
+
});
|
|
40
|
+
const s = sessionManager.getSession("t1");
|
|
41
|
+
expect(s?.currentTitle).toBe("Hello");
|
|
42
|
+
expect(s?.crashed).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("is a no-op for an unknown taskId", () => {
|
|
46
|
+
expect(() =>
|
|
47
|
+
sessionManager.updateSession("nope", { currentUrl: "x" }),
|
|
48
|
+
).not.toThrow();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -336,7 +336,7 @@ describe("formatCacheNotice()", () => {
|
|
|
336
336
|
describe("Integration: router + cache (via MockPlugin)", () => {
|
|
337
337
|
beforeEach(async () => {
|
|
338
338
|
const { pluginRegistry } = await import("../core/plugin-registry.js");
|
|
339
|
-
|
|
339
|
+
await import("../core/shared/session-manager.js");
|
|
340
340
|
const { MockPlugin, makeConfig } = await import("./helpers/mock-plugin.js");
|
|
341
341
|
pluginRegistry.clear();
|
|
342
342
|
cleanCacheDir();
|
|
@@ -424,7 +424,7 @@ describe("Integration: router + cache (via MockPlugin)", () => {
|
|
|
424
424
|
describe("Interaction tool caching (via MockPlugin)", () => {
|
|
425
425
|
beforeEach(async () => {
|
|
426
426
|
const { pluginRegistry } = await import("../core/plugin-registry.js");
|
|
427
|
-
|
|
427
|
+
await import("../core/shared/session-manager.js");
|
|
428
428
|
const { MockPlugin, makeConfig } = await import("./helpers/mock-plugin.js");
|
|
429
429
|
pluginRegistry.clear();
|
|
430
430
|
cleanCacheDir();
|
|
@@ -3,27 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Thin subclass of PlaywrightPluginBase. All shared logic lives in
|
|
5
5
|
* backends/playwright-base/playwright-plugin.ts.
|
|
6
|
+
*
|
|
7
|
+
* Launches Chromium directly and drives its own page; the plugin is the
|
|
8
|
+
* sole browser owner. No external-attach endpoint is exposed.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
import { chromium } from "playwright";
|
|
9
12
|
import type { Browser } from "playwright";
|
|
10
13
|
import { PlaywrightPluginBase } from "../playwright-base/playwright-plugin.js";
|
|
11
|
-
import {
|
|
12
|
-
DEFAULT_CAPABILITIES,
|
|
13
|
-
type PluginCapabilities,
|
|
14
|
-
} from "../../core/plugin-api.js";
|
|
15
|
-
|
|
16
|
-
// ─── Capabilities ──────────────────────────────────────────────────
|
|
17
|
-
|
|
18
|
-
const CHROMIUM_CAPABILITIES: PluginCapabilities = {
|
|
19
|
-
...DEFAULT_CAPABILITIES,
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
// ─── ChromiumPlugin ───────────────────────────────────────────────
|
|
14
|
+
import { DEFAULT_CAPABILITIES } from "../../core/plugin-api.js";
|
|
23
15
|
|
|
24
16
|
export class ChromiumPlugin extends PlaywrightPluginBase {
|
|
25
17
|
readonly name = "chromium";
|
|
26
|
-
readonly capabilities =
|
|
18
|
+
readonly capabilities = DEFAULT_CAPABILITIES;
|
|
27
19
|
|
|
28
20
|
/** Hardcoded Chrome user-agent — no dynamic capture needed. */
|
|
29
21
|
protected get userAgent(): string {
|
|
Binary file
|
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
* Thin subclass of PlaywrightPluginBase. All shared logic lives in
|
|
5
5
|
* backends/playwright-base/playwright-plugin.ts.
|
|
6
6
|
*
|
|
7
|
+
* Launches Firefox directly with `firefox.launch()` and drives its own
|
|
8
|
+
* page; the plugin is the sole browser owner. No external-attach
|
|
9
|
+
* endpoint is exposed.
|
|
10
|
+
*
|
|
7
11
|
* Uses probe-then-cache UA capture at first launch, with a hardcoded
|
|
8
|
-
* fallback Firefox UA string.
|
|
9
|
-
* not accept Chromium sandbox flags.
|
|
12
|
+
* fallback Firefox UA string.
|
|
10
13
|
*/
|
|
11
14
|
|
|
12
15
|
import { firefox } from "playwright";
|
|
@@ -47,9 +50,7 @@ export class FirefoxPlugin extends PlaywrightPluginBase {
|
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
protected async launchBrowser(): Promise<Browser> {
|
|
50
|
-
return firefox.launch({
|
|
51
|
-
headless: true,
|
|
52
|
-
});
|
|
53
|
+
return firefox.launch({ headless: true });
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
protected get installHint(): string {
|
|
Binary file
|
|
@@ -26,8 +26,6 @@ Requires
|
|
|
26
26
|
* Playwright Firefox browsers installed (``playwright install firefox``)
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
|
-
import sys
|
|
30
|
-
|
|
31
29
|
from pi_browser_bridge.playwright_base import PlaywrightBridge, check_playwright_or_exit
|
|
32
30
|
|
|
33
31
|
|
|
@@ -48,10 +46,13 @@ class FirefoxPyBridge(PlaywrightBridge):
|
|
|
48
46
|
)
|
|
49
47
|
|
|
50
48
|
def _launch_browser(self):
|
|
51
|
-
"""Launch a Firefox browser instance.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
"""Launch a Firefox browser instance.
|
|
50
|
+
|
|
51
|
+
Launches Firefox directly with ``firefox.launch()`` and drives its
|
|
52
|
+
own page; the plugin is the sole browser owner. No external-attach
|
|
53
|
+
endpoint is exposed.
|
|
54
|
+
"""
|
|
55
|
+
return self._pw.firefox.launch(headless=True)
|
|
55
56
|
|
|
56
57
|
|
|
57
58
|
# ═══════════════════════════════════════════════════════════════════════
|