pi-lean-dimension 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +96 -36
  2. package/node_modules/pi-lean-portal/AGENTS.md +146 -4
  3. package/node_modules/pi-lean-portal/README.md +112 -50
  4. package/node_modules/pi-lean-portal/__tests__/browser-data.test.ts +124 -0
  5. package/node_modules/pi-lean-portal/__tests__/browser-inspect.test.ts +192 -16
  6. package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -3
  7. package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +49 -35
  8. package/node_modules/pi-lean-portal/__tests__/chromium-py-persistence.test.ts +21 -195
  9. package/node_modules/pi-lean-portal/__tests__/chromium-py.test.ts +17 -81
  10. package/node_modules/pi-lean-portal/__tests__/chromium.test.ts +25 -0
  11. package/node_modules/pi-lean-portal/__tests__/contributed/invisible-py/invisible-py.test.ts +299 -0
  12. package/node_modules/pi-lean-portal/__tests__/cookie-persistence.test.ts +22 -182
  13. package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +1 -1
  14. package/node_modules/pi-lean-portal/__tests__/firefox-py-persistence.test.ts +21 -184
  15. package/node_modules/pi-lean-portal/__tests__/firefox-py.test.ts +17 -101
  16. package/node_modules/pi-lean-portal/__tests__/firefox.test.ts +2 -18
  17. package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
  18. package/node_modules/pi-lean-portal/__tests__/helpers/create-py-backend-harness.ts +105 -0
  19. package/node_modules/pi-lean-portal/__tests__/helpers/load-plugin-config-from-file.ts +53 -0
  20. package/node_modules/pi-lean-portal/__tests__/helpers/mock-plugin.ts +11 -7
  21. package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +4 -0
  22. package/node_modules/pi-lean-portal/__tests__/helpers/persistence-suite.ts +218 -0
  23. package/node_modules/pi-lean-portal/__tests__/helpers/plugin-contract.ts +198 -318
  24. package/node_modules/pi-lean-portal/__tests__/helpers/probe-user-backend.ts +198 -0
  25. package/node_modules/pi-lean-portal/__tests__/helpers/test-server.ts +14 -0
  26. package/node_modules/pi-lean-portal/__tests__/plugin-config-browser.test.ts +150 -15
  27. package/node_modules/pi-lean-portal/__tests__/plugin-loading.test.ts +120 -18
  28. package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +6 -67
  29. package/node_modules/pi-lean-portal/__tests__/probe-user-backend.test.ts +236 -0
  30. package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +401 -11
  31. package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +4 -1
  32. package/node_modules/pi-lean-portal/__tests__/run-contributed-suites.test.ts +318 -0
  33. package/node_modules/pi-lean-portal/__tests__/session-manager.test.ts +50 -0
  34. package/node_modules/pi-lean-portal/__tests__/snapshot-cache.test.ts +2 -2
  35. package/node_modules/pi-lean-portal/__tests__/url-safety.test.ts +1 -1
  36. package/node_modules/pi-lean-portal/backends/chromium/index.ts +5 -13
  37. package/node_modules/pi-lean-portal/backends/chromium-py/__pycache__/bridge.cpython-313.pyc +0 -0
  38. package/node_modules/pi-lean-portal/backends/chromium-py/bridge.py +0 -2
  39. package/node_modules/pi-lean-portal/backends/firefox/index.ts +6 -5
  40. package/node_modules/pi-lean-portal/backends/firefox-py/__pycache__/bridge.cpython-313.pyc +0 -0
  41. package/node_modules/pi-lean-portal/backends/firefox-py/bridge.py +7 -6
  42. package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +241 -398
  43. package/node_modules/pi-lean-portal/backends/python-adapter.ts +182 -83
  44. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__init__.py +1 -42
  45. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-312.pyc +0 -0
  46. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  47. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-312.pyc +0 -0
  48. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-313.pyc +0 -0
  49. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-312.pyc +0 -0
  50. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-313.pyc +0 -0
  51. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-312.pyc +0 -0
  52. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-313.pyc +0 -0
  53. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-312.pyc +0 -0
  54. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-313.pyc +0 -0
  55. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-312.pyc +0 -0
  56. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-313.pyc +0 -0
  57. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
  58. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
  59. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-312.pyc +0 -0
  60. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
  61. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/accessibility.py +12 -147
  62. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bot_detection.py +12 -37
  63. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bridge.py +249 -322
  64. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/browser_data.py +92 -0
  65. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/patch_playwright.py +321 -0
  66. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +511 -299
  67. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
  68. 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
  69. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313.pyc +0 -0
  70. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.1.pyc +0 -0
  71. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313.pyc +0 -0
  72. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313-pytest-9.1.1.pyc +0 -0
  73. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313.pyc +0 -0
  74. 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
  75. 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
  76. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
  77. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
  78. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_py_bridges.cpython-313-pytest-9.1.1.pyc +0 -0
  79. 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
  80. package/node_modules/pi-lean-portal/backends/python-base/tests/conftest.py +95 -0
  81. package/node_modules/pi-lean-portal/backends/python-base/tests/test_accessibility.py +8 -132
  82. package/node_modules/pi-lean-portal/backends/python-base/tests/test_bot_detection.py +8 -147
  83. package/node_modules/pi-lean-portal/backends/python-base/tests/test_browser_data.py +131 -0
  84. package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +768 -0
  85. package/node_modules/pi-lean-portal/backends/python-base/tests/test_py_bridges.py +198 -0
  86. package/node_modules/pi-lean-portal/browser-toggle.ts +33 -69
  87. package/node_modules/pi-lean-portal/contributed/CHOOSING.md +126 -0
  88. package/node_modules/pi-lean-portal/contributed/README.md +304 -0
  89. package/node_modules/pi-lean-portal/contributed/camoufox-py/bridge.py +216 -0
  90. package/node_modules/pi-lean-portal/contributed/invisible-py/__pycache__/bridge.cpython-313.pyc +0 -0
  91. package/node_modules/pi-lean-portal/contributed/invisible-py/bridge.py +434 -0
  92. package/node_modules/pi-lean-portal/core/fetch-backend.ts +0 -5
  93. package/node_modules/pi-lean-portal/core/plugin-api.ts +6 -33
  94. package/node_modules/pi-lean-portal/core/plugin-config.ts +75 -58
  95. package/node_modules/pi-lean-portal/core/plugin-registry.ts +11 -49
  96. package/node_modules/pi-lean-portal/core/router.ts +59 -98
  97. package/node_modules/pi-lean-portal/core/shared/accessibility-tree.ts +10 -143
  98. package/node_modules/pi-lean-portal/core/shared/bot-detection.ts +31 -77
  99. package/node_modules/pi-lean-portal/core/shared/browser-data.json +183 -0
  100. package/node_modules/pi-lean-portal/core/shared/browser-data.ts +50 -0
  101. package/node_modules/pi-lean-portal/core/shared/browser-events.ts +6 -6
  102. package/node_modules/pi-lean-portal/core/shared/dom-extractor.ts +97 -36
  103. package/node_modules/pi-lean-portal/core/shared/nav-settle.ts +12 -15
  104. package/node_modules/pi-lean-portal/core/shared/paths.ts +3 -0
  105. package/node_modules/pi-lean-portal/core/shared/session-manager.ts +7 -21
  106. package/node_modules/pi-lean-portal/{verify-ship-manifest.ts → core/shared/ship-manifest.ts} +34 -9
  107. package/node_modules/pi-lean-portal/core/shared/snapshot-cache.ts +7 -6
  108. package/node_modules/pi-lean-portal/core/shared/storage-state.ts +40 -9
  109. package/node_modules/pi-lean-portal/index.ts +42 -7
  110. package/node_modules/pi-lean-portal/package.json +8 -3
  111. package/node_modules/pi-lean-portal/ship-manifest.test.ts +8 -3
  112. package/node_modules/pi-lean-portal/tools/browser-inspect.ts +2 -6
  113. package/node_modules/pi-lean-portal/tools/browser-navigate.ts +7 -5
  114. package/node_modules/pi-lean-portal/tools/browser-snapshot.ts +2 -5
  115. package/node_modules/pi-lean-portal/tools/utils.ts +22 -4
  116. package/node_modules/pi-lean-portal/tools/web-fetch.ts +3 -4
  117. package/node_modules/pi-lean-search/README.md +6 -2
  118. package/node_modules/pi-lean-search/__tests__/web-search.test.ts +11 -0
  119. package/node_modules/pi-lean-search/index.ts +4 -21
  120. package/node_modules/pi-lean-search/package.json +2 -2
  121. package/node_modules/pi-lean-search/verify-ship-manifest.ts +6 -92
  122. package/node_modules/pi-lean-search/web-search-tool.ts +19 -13
  123. package/package.json +4 -4
  124. package/node_modules/pi-lean-portal/__tests__/helpers/reddit-fixture.ts +0 -264
  125. package/node_modules/pi-lean-portal/__tests__/helpers/toggle-test-utils.ts +0 -31
  126. package/node_modules/pi-lean-portal/__tests__/reddit-dialog.test.ts +0 -302
  127. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/occlusion.cpython-313.pyc +0 -0
  128. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.0.pyc +0 -0
  129. package/node_modules/pi-lean-portal/backends/python-base/tests/test_chromium_py_bridge.py +0 -281
  130. 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
- const { default: _sm } = await import("../core/shared/session-manager.js");
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
- const { default: _sm } = await import("../core/shared/session-manager.js");
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();
@@ -1,5 +1,5 @@
1
1
  import { expect } from "vitest";
2
- import { validateUrl } from "../core/shared/url-safety";
2
+ import { validateUrl } from "../core/shared/url-safety.js";
3
3
 
4
4
  // ─── Allowed URLs ──────────────────────────────────────────────
5
5
 
@@ -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 = CHROMIUM_CAPABILITIES;
18
+ readonly capabilities = DEFAULT_CAPABILITIES;
27
19
 
28
20
  /** Hardcoded Chrome user-agent — no dynamic capture needed. */
29
21
  protected get userAgent(): string {
@@ -23,8 +23,6 @@ Requires
23
23
  * Playwright Chromium browsers installed (``playwright install chromium``)
24
24
  """
25
25
 
26
- import sys
27
-
28
26
  from pi_browser_bridge.playwright_base import PlaywrightBridge, check_playwright_or_exit
29
27
 
30
28
 
@@ -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. Launch args are empty — Firefox does
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 {
@@ -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
- return self._pw.firefox.launch(
53
- headless=True,
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
  # ═══════════════════════════════════════════════════════════════════════