pi-lean-dimension 0.5.0 → 0.6.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.
- package/README.md +5 -3
- package/node_modules/pi-lean-host/AGENTS.md +13 -6
- package/node_modules/pi-lean-host/__tests__/api-learn-fetch-recipe.test.ts +282 -10
- package/node_modules/pi-lean-host/__tests__/api-learn-multi-file.test.ts +8 -16
- package/node_modules/pi-lean-host/__tests__/api-probe.test.ts +276 -279
- package/node_modules/pi-lean-host/__tests__/api-scaffold.test.ts +12 -33
- package/node_modules/pi-lean-host/__tests__/api-toggle.test.ts +14 -12
- package/node_modules/pi-lean-host/__tests__/bootstrap-command.test.ts +1 -7
- package/node_modules/pi-lean-host/__tests__/delete-command.test.ts +1 -12
- package/node_modules/pi-lean-host/__tests__/guide-catalog.test.ts +484 -0
- package/node_modules/pi-lean-host/__tests__/helpers.test.ts +0 -132
- package/node_modules/pi-lean-host/__tests__/oauth-command.test.ts +95 -0
- package/node_modules/pi-lean-host/__tests__/oauth-flow.test.ts +163 -0
- package/node_modules/pi-lean-host/__tests__/oauth-mint.test.ts +79 -6
- package/node_modules/pi-lean-host/__tests__/parse-api-guide.test.ts +4 -409
- package/node_modules/pi-lean-host/__tests__/response-spill.test.ts +0 -1
- package/node_modules/pi-lean-host/__tests__/secrets-command.test.ts +19 -35
- package/node_modules/pi-lean-host/__tests__/smoke.test.ts +1 -82
- package/node_modules/pi-lean-host/__tests__/ssrf-guard.test.ts +98 -0
- package/node_modules/pi-lean-host/__tests__/test-utils.ts +73 -0
- package/node_modules/pi-lean-host/__tests__/tools.test.ts +2 -518
- package/node_modules/pi-lean-host/__tests__/transport.test.ts +178 -31
- package/node_modules/pi-lean-host/__tests__/verify-command.test.ts +1 -8
- package/node_modules/pi-lean-host/__tests__/verify-stamp.test.ts +1 -4
- package/node_modules/pi-lean-host/api-guides/boe/local-helper.test.ts +9 -20
- package/node_modules/pi-lean-host/api-guides/dnb/error-envelope.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/dnb/resumption-token.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/frost-sensorthings/dotted-key.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/github/static-key.test.ts +7 -25
- package/node_modules/pi-lean-host/api-guides/inaturalist/derived-id.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/internet-archive/multi-recipe.test.ts +6 -26
- package/node_modules/pi-lean-host/api-guides/stripe/has-more.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/telegram-bot/path-auth.test.ts +7 -25
- package/node_modules/pi-lean-host/api-guides/twitch/oauth2.test.ts +12 -47
- package/node_modules/pi-lean-host/api-guides/twitch-user/oauth-user.test.ts +11 -52
- package/node_modules/pi-lean-host/api-guides/usgs/transform.test.ts +10 -27
- package/node_modules/pi-lean-host/api-guides/wikidata-search/numeric-cursor.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/wikimedia-action/token-bag.test.ts +9 -20
- package/node_modules/pi-lean-host/core/auth.ts +2 -1
- package/node_modules/pi-lean-host/core/helpers.ts +29 -35
- package/node_modules/pi-lean-host/core/oauth-command.ts +10 -8
- package/node_modules/pi-lean-host/core/oauth-flow.ts +20 -4
- package/node_modules/pi-lean-host/core/parse-api-guide.ts +3 -5
- package/node_modules/pi-lean-host/core/transport.ts +1 -1
- package/node_modules/pi-lean-host/package.json +1 -1
- package/node_modules/pi-lean-host/tools/api-guide.ts +18 -57
- package/node_modules/pi-lean-host/tools/api-learn.ts +20 -49
- package/node_modules/pi-lean-host/tools/api-probe.ts +17 -26
- package/node_modules/pi-lean-host/tools/api-scaffold.ts +20 -42
- package/node_modules/pi-lean-host/tools/utils.ts +69 -0
- package/node_modules/pi-lean-portal/AGENTS.md +4 -3
- package/node_modules/pi-lean-portal/README.md +9 -9
- package/node_modules/pi-lean-portal/__tests__/accessibility-tree.test.ts +0 -15
- package/node_modules/pi-lean-portal/__tests__/browser-install.test.ts +737 -0
- package/node_modules/pi-lean-portal/__tests__/browser-navigate.test.ts +33 -2
- package/node_modules/pi-lean-portal/__tests__/browser-status.test.ts +48 -25
- package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -9
- package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +3 -29
- package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +51 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313-pytest-9.1.1.pyc +0 -0
- 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/mock-pi.ts +34 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +14 -24
- package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +0 -90
- package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +32 -125
- package/node_modules/pi-lean-portal/__tests__/router-dispatch.test.ts +12 -170
- package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +39 -151
- package/node_modules/pi-lean-portal/__tests__/web-guides.test.ts +1 -44
- package/node_modules/pi-lean-portal/backends/chromium/index.ts +1 -1
- package/node_modules/pi-lean-portal/backends/firefox/index.ts +1 -1
- package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +1 -1
- 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/playwright_base.py +43 -57
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/transport.py +6 -21
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.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_transport.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +157 -73
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_transport.py +15 -41
- package/node_modules/pi-lean-portal/browser-install.ts +454 -0
- package/node_modules/pi-lean-portal/browser-status.ts +21 -0
- package/node_modules/pi-lean-portal/browser-toggle.ts +13 -11
- package/node_modules/pi-lean-portal/core/fetch-backend.ts +2 -2
- package/node_modules/pi-lean-portal/core/router.ts +7 -32
- package/node_modules/pi-lean-portal/core/shared/session-manager.ts +1 -3
- package/node_modules/pi-lean-portal/index.ts +1 -18
- package/node_modules/pi-lean-portal/package.json +2 -1
- package/node_modules/pi-lean-portal/tools/browser-navigate.ts +6 -2
- package/node_modules/pi-lean-portal/tools/utils.ts +5 -6
- package/node_modules/pi-lean-search/AGENTS.md +5 -3
- package/node_modules/pi-lean-search/__tests__/web-search.test.ts +180 -127
- package/node_modules/pi-lean-search/index.ts +0 -3
- package/node_modules/pi-lean-search/package.json +1 -1
- package/node_modules/pi-lean-search/web-search-tool.ts +22 -13
- package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
- package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/yaml/dist/nodes/Alias.js +25 -23
- package/node_modules/yaml/package.json +1 -1
- package/package.json +4 -4
|
@@ -120,6 +120,29 @@ describe("Router session / profile dispatch", () => {
|
|
|
120
120
|
const result = await router.navigate("https://example.com");
|
|
121
121
|
expect(result.profileMode).toBe("none");
|
|
122
122
|
});
|
|
123
|
+
|
|
124
|
+
it('profile="none" destroys existing session with profile', async () => {
|
|
125
|
+
// First create a session with a named profile
|
|
126
|
+
await router.navigate("https://example.com", {
|
|
127
|
+
profile: "work",
|
|
128
|
+
});
|
|
129
|
+
expect(sessionManager.getSession("default")?.profileName).toBe("work");
|
|
130
|
+
expect(sessionManager.getSession("default")?.persistState).toBe(true);
|
|
131
|
+
mock.calls.delete("navigate");
|
|
132
|
+
|
|
133
|
+
// Switch to none — should destroy old context
|
|
134
|
+
const result = await router.navigate("https://example.com/other", {
|
|
135
|
+
profile: "none",
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(result.success).toBe(true);
|
|
139
|
+
expect(result.profileMode).toBe("none");
|
|
140
|
+
expect(result.profileName).toBeUndefined();
|
|
141
|
+
|
|
142
|
+
const session = sessionManager.getSession("default");
|
|
143
|
+
expect(session?.persistState).toBeFalsy();
|
|
144
|
+
expect(session?.profileName).toBeUndefined();
|
|
145
|
+
});
|
|
123
146
|
});
|
|
124
147
|
|
|
125
148
|
// ─── profile="session" ───────────────────────────────────────
|
|
@@ -175,6 +198,22 @@ describe("Router session / profile dispatch", () => {
|
|
|
175
198
|
const session = sessionManager.getSession("default");
|
|
176
199
|
expect(session?.persistState).toBeFalsy();
|
|
177
200
|
});
|
|
201
|
+
|
|
202
|
+
it('clears persistState and profileName for profile="none"', async () => {
|
|
203
|
+
await router.navigate("https://example.com", {
|
|
204
|
+
profile: "session",
|
|
205
|
+
piSessionId: TEST_PI_SESSION_ID,
|
|
206
|
+
});
|
|
207
|
+
expect(sessionManager.getSession("default")?.persistState).toBe(true);
|
|
208
|
+
|
|
209
|
+
await router.navigate("https://example.com/other", {
|
|
210
|
+
profile: "none",
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const session = sessionManager.getSession("default");
|
|
214
|
+
expect(session?.persistState).toBe(false);
|
|
215
|
+
expect(session?.profileName).toBeUndefined();
|
|
216
|
+
});
|
|
178
217
|
});
|
|
179
218
|
|
|
180
219
|
// ─── Named profile ──────────────────────────────────────────
|
|
@@ -268,157 +307,6 @@ describe("Router session / profile dispatch", () => {
|
|
|
268
307
|
expect(result.error).toContain("Invalid profile name");
|
|
269
308
|
});
|
|
270
309
|
});
|
|
271
|
-
|
|
272
|
-
// ─── profile parameter ─────────────────────────────────────
|
|
273
|
-
|
|
274
|
-
describe("profile parameter", () => {
|
|
275
|
-
it('profile="none" creates ephemeral session', async () => {
|
|
276
|
-
const result = await router.navigate("https://example.com", {
|
|
277
|
-
profile: "none",
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
expect(result.success).toBe(true);
|
|
281
|
-
expect(result.profileMode).toBe("none");
|
|
282
|
-
expect(result.profileName).toBeUndefined();
|
|
283
|
-
|
|
284
|
-
const session = sessionManager.getSession("default");
|
|
285
|
-
expect(session?.persistState).toBeFalsy();
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it('profile="session" with piSessionId resolves to _session-<id>', async () => {
|
|
289
|
-
const result = await router.navigate("https://example.com", {
|
|
290
|
-
profile: "session",
|
|
291
|
-
piSessionId: TEST_PI_SESSION_ID,
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
expect(result.success).toBe(true);
|
|
295
|
-
expect(result.profileMode).toBe("session");
|
|
296
|
-
expect(result.profileName).toContain("_session-");
|
|
297
|
-
expect(result.profileName).toContain(TEST_PI_SESSION_ID);
|
|
298
|
-
|
|
299
|
-
const session = sessionManager.getSession("default");
|
|
300
|
-
expect(session?.persistState).toBe(true);
|
|
301
|
-
expect(session?.profileName).toContain("_session-");
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it('profile="session" without piSessionId falls back to "none"', async () => {
|
|
305
|
-
const result = await router.navigate("https://example.com", {
|
|
306
|
-
profile: "session",
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
expect(result.success).toBe(true);
|
|
310
|
-
expect(result.profileMode).toBe("none");
|
|
311
|
-
expect(result.profileName).toBeUndefined();
|
|
312
|
-
|
|
313
|
-
const session = sessionManager.getSession("default");
|
|
314
|
-
expect(session?.persistState).toBeFalsy();
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
it('profile="work" creates named profile session', async () => {
|
|
318
|
-
const result = await router.navigate("https://example.com", {
|
|
319
|
-
profile: "work",
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
expect(result.success).toBe(true);
|
|
323
|
-
expect(result.profileMode).toBe("named");
|
|
324
|
-
expect(result.profileName).toBe("work");
|
|
325
|
-
|
|
326
|
-
const session = sessionManager.getSession("default");
|
|
327
|
-
expect(session?.persistState).toBe(true);
|
|
328
|
-
expect(session?.profileName).toBe("work");
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
it('profile="none" destroys existing session with profile', async () => {
|
|
332
|
-
// First create a session with a named profile
|
|
333
|
-
await router.navigate("https://example.com", {
|
|
334
|
-
profile: "work",
|
|
335
|
-
});
|
|
336
|
-
expect(sessionManager.getSession("default")?.profileName).toBe("work");
|
|
337
|
-
expect(sessionManager.getSession("default")?.persistState).toBe(true);
|
|
338
|
-
mock.calls.delete("navigate");
|
|
339
|
-
|
|
340
|
-
// Switch to none — should destroy old context
|
|
341
|
-
const result = await router.navigate("https://example.com/other", {
|
|
342
|
-
profile: "none",
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
expect(result.success).toBe(true);
|
|
346
|
-
expect(result.profileMode).toBe("none");
|
|
347
|
-
expect(result.profileName).toBeUndefined();
|
|
348
|
-
|
|
349
|
-
const session = sessionManager.getSession("default");
|
|
350
|
-
expect(session?.persistState).toBeFalsy();
|
|
351
|
-
expect(session?.profileName).toBeUndefined();
|
|
352
|
-
});
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
// ─── profileMode / profileName in result ────────────────────
|
|
356
|
-
|
|
357
|
-
describe("result fields", () => {
|
|
358
|
-
it("shows profileMode none and no profileName for default", async () => {
|
|
359
|
-
const result = await router.navigate("https://example.com");
|
|
360
|
-
expect(result.profileMode).toBe("none");
|
|
361
|
-
expect(result.profileName).toBeUndefined();
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
it("includes profileMode and profileName for named profile", async () => {
|
|
365
|
-
const result = await router.navigate("https://example.com", {
|
|
366
|
-
profile: "test-profile",
|
|
367
|
-
});
|
|
368
|
-
expect(result.profileMode).toBe("named");
|
|
369
|
-
expect(result.profileName).toBe("test-profile");
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
it('includes profileMode and profileName for profile="session" with piSessionId', async () => {
|
|
373
|
-
const result = await router.navigate("https://example.com", {
|
|
374
|
-
profile: "session",
|
|
375
|
-
piSessionId: TEST_PI_SESSION_ID,
|
|
376
|
-
});
|
|
377
|
-
expect(result.profileMode).toBe("session");
|
|
378
|
-
expect(result.profileName).toContain("_session-");
|
|
379
|
-
});
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
// ─── BrowserSession fields ──────────────────────────────────
|
|
383
|
-
|
|
384
|
-
describe("BrowserSession fields", () => {
|
|
385
|
-
it("sets persistState=true and profileName for named profile", async () => {
|
|
386
|
-
await router.navigate("https://example.com", {
|
|
387
|
-
profile: "my-projects",
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
const session = sessionManager.getSession("default");
|
|
391
|
-
expect(session?.persistState).toBe(true);
|
|
392
|
-
expect(session?.profileName).toBe("my-projects");
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
it('sets persistState=true and profileName for profile="session" with piSessionId', async () => {
|
|
396
|
-
await router.navigate("https://example.com", {
|
|
397
|
-
profile: "session",
|
|
398
|
-
piSessionId: TEST_PI_SESSION_ID,
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
const session = sessionManager.getSession("default");
|
|
402
|
-
expect(session?.persistState).toBe(true);
|
|
403
|
-
expect(session?.profileName).toContain("_session-");
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
it('clears persistState and profileName for profile="none"', async () => {
|
|
407
|
-
await router.navigate("https://example.com", {
|
|
408
|
-
profile: "session",
|
|
409
|
-
piSessionId: TEST_PI_SESSION_ID,
|
|
410
|
-
});
|
|
411
|
-
expect(sessionManager.getSession("default")?.persistState).toBe(true);
|
|
412
|
-
|
|
413
|
-
await router.navigate("https://example.com/other", {
|
|
414
|
-
profile: "none",
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
const session = sessionManager.getSession("default");
|
|
418
|
-
expect(session?.persistState).toBe(false);
|
|
419
|
-
expect(session?.profileName).toBeUndefined();
|
|
420
|
-
});
|
|
421
|
-
});
|
|
422
310
|
});
|
|
423
311
|
|
|
424
312
|
// ─── Router defaultProfile config routing ────────────────────────
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for Web Navigation Guides (core/guides.ts)
|
|
3
3
|
*
|
|
4
|
-
* Covers
|
|
4
|
+
* Covers resolveApplicableGuides, formatGuideFooter, formatGuideList,
|
|
5
5
|
* parseGuideFile, buildDomainMap, and guide structure.
|
|
6
6
|
* All tests run without Chromium.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
10
10
|
import {
|
|
11
|
-
type Guide,
|
|
12
11
|
type ApplicableGuide,
|
|
13
12
|
resolveApplicableGuides,
|
|
14
13
|
parseGuideContent,
|
|
@@ -37,48 +36,6 @@ vi.mock("node:fs", async (importActual) => {
|
|
|
37
36
|
};
|
|
38
37
|
});
|
|
39
38
|
|
|
40
|
-
// ─── Types ──────────────────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
describe("types", () => {
|
|
43
|
-
it("Guide interface is structural — icon and shortName now required", () => {
|
|
44
|
-
const g: Guide = {
|
|
45
|
-
content: "test",
|
|
46
|
-
updated: "2026-01-01",
|
|
47
|
-
category: "site",
|
|
48
|
-
source: "builtin",
|
|
49
|
-
icon: "📖",
|
|
50
|
-
shortName: "test",
|
|
51
|
-
};
|
|
52
|
-
expect(g.icon).toBe("📖");
|
|
53
|
-
expect(g.shortName).toBe("test");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("triggerSignal field on Guide works correctly", () => {
|
|
57
|
-
const g: Guide = {
|
|
58
|
-
content: "test",
|
|
59
|
-
updated: "2026-01-01",
|
|
60
|
-
category: "pattern",
|
|
61
|
-
source: "builtin",
|
|
62
|
-
icon: "⚠",
|
|
63
|
-
shortName: "test",
|
|
64
|
-
triggerSignal: "botDetected",
|
|
65
|
-
};
|
|
66
|
-
expect(g.triggerSignal).toBe("botDetected");
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("ApplicableGuide interface — name, icon, shortName, reason, category required", () => {
|
|
70
|
-
const r: ApplicableGuide = {
|
|
71
|
-
name: "test",
|
|
72
|
-
icon: "⚠",
|
|
73
|
-
shortName: "test",
|
|
74
|
-
reason: "test reason",
|
|
75
|
-
category: "pattern",
|
|
76
|
-
};
|
|
77
|
-
expect(r.name).toBe("test");
|
|
78
|
-
expect(r.category).toBe("pattern");
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
39
|
// ─── resolveApplicableGuides ────────────────────────────────────
|
|
83
40
|
|
|
84
41
|
function injectTestFixture(): void {
|
|
@@ -35,7 +35,7 @@ export class ChromiumPlugin extends PlaywrightPluginBase {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
protected get installHint(): string {
|
|
38
|
-
return "Browser not installed. Run
|
|
38
|
+
return "Browser not installed. Run /web install to install it.";
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -54,7 +54,7 @@ export class FirefoxPlugin extends PlaywrightPluginBase {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
protected get installHint(): string {
|
|
57
|
-
return "Browser not installed. Run
|
|
57
|
+
return "Browser not installed. Run /web install to install it.";
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -83,7 +83,7 @@ export abstract class PlaywrightPluginBase implements BrowserPlugin {
|
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
85
|
* Engine-specific install hint, shown when the browser executable
|
|
86
|
-
* is not installed.
|
|
86
|
+
* is not installed, e.g. "Browser not installed. Run /web install to install it".
|
|
87
87
|
*/
|
|
88
88
|
protected abstract get installHint(): string;
|
|
89
89
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py
CHANGED
|
@@ -38,6 +38,7 @@ contexts across tasks.
|
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
40
|
import base64
|
|
41
|
+
import contextlib
|
|
41
42
|
import json
|
|
42
43
|
import os
|
|
43
44
|
import re
|
|
@@ -299,13 +300,14 @@ class PlaywrightBridge:
|
|
|
299
300
|
_csp_safe_readonly_via_init_script: bool = False
|
|
300
301
|
|
|
301
302
|
#: When non-zero, ``_wait_for_navigation_settle`` uses this as the
|
|
302
|
-
#: settle poll budget (ms) instead of the default
|
|
303
|
+
#: settle poll budget (ms) instead of the default. Stealth backends
|
|
303
304
|
#: whose patched browser fires navigation events with higher latency
|
|
304
305
|
#: (e.g. Camoufox's Juggler) set a larger value to avoid settling before
|
|
305
|
-
#: the navigation commit is observable. Default
|
|
306
|
-
#:
|
|
307
|
-
#:
|
|
308
|
-
|
|
306
|
+
#: the navigation commit is observable. Default reads
|
|
307
|
+
#: ``navSettle.settleTimeoutMs`` from the shared TS↔Python parity JSON
|
|
308
|
+
#: (``browser-data.json``) so both runtimes stay in lockstep; shipped
|
|
309
|
+
#: chromium-py / firefox-py are unchanged.
|
|
310
|
+
_settle_budget_ms: int = NAV_SETTLE["settleTimeoutMs"]
|
|
309
311
|
|
|
310
312
|
#: When True, ``_wait_for_navigation_settle`` switches the no-nav
|
|
311
313
|
#: polling branch from a fixed budget to "URL stable for 150 ms **or**
|
|
@@ -448,10 +450,8 @@ class PlaywrightBridge:
|
|
|
448
450
|
self._log("captureUA", success=False, ua="(fallback)")
|
|
449
451
|
finally:
|
|
450
452
|
if page is not None:
|
|
451
|
-
|
|
453
|
+
with contextlib.suppress(Exception):
|
|
452
454
|
page.close()
|
|
453
|
-
except Exception:
|
|
454
|
-
pass
|
|
455
455
|
|
|
456
456
|
# ── Debug logging ───────────────────────────────────────────
|
|
457
457
|
|
|
@@ -518,15 +518,11 @@ class PlaywrightBridge:
|
|
|
518
518
|
def _maybe_stop_playwright(self) -> None:
|
|
519
519
|
"""Stop the shared Playwright if no sessions remain."""
|
|
520
520
|
if not self.sessions and self._pw is not None:
|
|
521
|
-
|
|
522
|
-
|
|
521
|
+
if self._browser:
|
|
522
|
+
with contextlib.suppress(Exception):
|
|
523
523
|
self._browser.close()
|
|
524
|
-
|
|
525
|
-
pass
|
|
526
|
-
try:
|
|
524
|
+
with contextlib.suppress(Exception):
|
|
527
525
|
self._pw.stop()
|
|
528
|
-
except Exception:
|
|
529
|
-
pass
|
|
530
526
|
self._pw = None
|
|
531
527
|
self._browser = None
|
|
532
528
|
|
|
@@ -577,7 +573,7 @@ class PlaywrightBridge:
|
|
|
577
573
|
# Start Playwright trace capture if BROWSER_TRACE_DIR is set.
|
|
578
574
|
_trace_dir = os.environ.get("BROWSER_TRACE_DIR")
|
|
579
575
|
if _trace_dir:
|
|
580
|
-
|
|
576
|
+
with contextlib.suppress(Exception):
|
|
581
577
|
context.tracing.start(
|
|
582
578
|
screenshots=True,
|
|
583
579
|
snapshots=True,
|
|
@@ -588,9 +584,7 @@ class PlaywrightBridge:
|
|
|
588
584
|
taskId=config.get("_task_id", "shared"),
|
|
589
585
|
action="start",
|
|
590
586
|
dir=_trace_dir,
|
|
591
|
-
)
|
|
592
|
-
except Exception:
|
|
593
|
-
pass # Best-effort
|
|
587
|
+
) # Best-effort
|
|
594
588
|
|
|
595
589
|
# CSP-safe read-only eval path (see _csp_safe_readonly_via_init_script).
|
|
596
590
|
# Registers an init script that stashes the EXTRACTOR_SCRIPT result in
|
|
@@ -749,12 +743,10 @@ class PlaywrightBridge:
|
|
|
749
743
|
if session is not None:
|
|
750
744
|
context: Any = session.get("context")
|
|
751
745
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
746
|
+
page: Any = session.get("page")
|
|
747
|
+
if page and not page.is_closed():
|
|
748
|
+
with contextlib.suppress(Exception):
|
|
755
749
|
page.close()
|
|
756
|
-
except Exception:
|
|
757
|
-
pass
|
|
758
750
|
|
|
759
751
|
# Stop and save Playwright trace if BROWSER_TRACE_DIR is set.
|
|
760
752
|
_trace_dir = os.environ.get("BROWSER_TRACE_DIR")
|
|
@@ -767,14 +759,15 @@ class PlaywrightBridge:
|
|
|
767
759
|
)
|
|
768
760
|
context.tracing.stop(path=_trace_path)
|
|
769
761
|
self._log("tracing", taskId=task_id, action="stop", dir=_trace_dir)
|
|
770
|
-
except Exception:
|
|
771
|
-
|
|
762
|
+
except Exception as exc:
|
|
763
|
+
# Best-effort: a failed trace save must not fail cleanup.
|
|
764
|
+
self._log(
|
|
765
|
+
"tracing", taskId=task_id, action="stop-failed", error=str(exc)
|
|
766
|
+
)
|
|
772
767
|
|
|
773
|
-
|
|
774
|
-
|
|
768
|
+
if context:
|
|
769
|
+
with contextlib.suppress(Exception):
|
|
775
770
|
context.close()
|
|
776
|
-
except Exception:
|
|
777
|
-
pass
|
|
778
771
|
|
|
779
772
|
# Remove session + element cache
|
|
780
773
|
self.sessions.pop(task_id, None)
|
|
@@ -926,15 +919,11 @@ class PlaywrightBridge:
|
|
|
926
919
|
stealth patched-Firefox binaries don't fire it reliably and
|
|
927
920
|
loitering for it can deadlock the Juggler driver.
|
|
928
921
|
"""
|
|
929
|
-
|
|
922
|
+
with contextlib.suppress(Exception):
|
|
930
923
|
page.wait_for_load_state("load", timeout=timeout_ms)
|
|
931
|
-
except Exception:
|
|
932
|
-
pass
|
|
933
924
|
if not skip_networkidle:
|
|
934
|
-
|
|
925
|
+
with contextlib.suppress(Exception):
|
|
935
926
|
page.wait_for_load_state("networkidle", timeout=timeout_ms)
|
|
936
|
-
except Exception:
|
|
937
|
-
pass
|
|
938
927
|
|
|
939
928
|
def _wait_for_navigation_settle(
|
|
940
929
|
self,
|
|
@@ -974,8 +963,10 @@ class PlaywrightBridge:
|
|
|
974
963
|
page.on("framenavigated", _on_nav)
|
|
975
964
|
|
|
976
965
|
try:
|
|
977
|
-
# Wait for a potential navigation to start (
|
|
978
|
-
|
|
966
|
+
# Wait for a potential navigation to start (shared race window,
|
|
967
|
+
# ``navSettle.settleRaceMs`` in browser-data.json — same value
|
|
968
|
+
# the TS nav-settle races against ``framenavigated``).
|
|
969
|
+
page.wait_for_timeout(NAV_SETTLE["settleRaceMs"])
|
|
979
970
|
|
|
980
971
|
waited_for_load = False
|
|
981
972
|
if navigated:
|
|
@@ -1099,13 +1090,12 @@ class PlaywrightBridge:
|
|
|
1099
1090
|
break
|
|
1100
1091
|
|
|
1101
1092
|
# ── DOM stabilization wait ──────────────────────────────
|
|
1102
|
-
|
|
1093
|
+
# Stabilization timeout is best-effort — proceed.
|
|
1094
|
+
with contextlib.suppress(Exception):
|
|
1103
1095
|
page.wait_for_function(
|
|
1104
1096
|
_DOM_STABILIZE_JS,
|
|
1105
1097
|
timeout=NAV_SETTLE["navTimeoutMs"],
|
|
1106
1098
|
)
|
|
1107
|
-
except Exception:
|
|
1108
|
-
pass # Stabilization timed out — proceed
|
|
1109
1099
|
|
|
1110
1100
|
# ── Bot detection ───────────────────────────────────────
|
|
1111
1101
|
bot_detected = check_bot_detection(page)
|
|
@@ -1331,11 +1321,9 @@ class PlaywrightBridge:
|
|
|
1331
1321
|
|
|
1332
1322
|
new_url: str | None = None
|
|
1333
1323
|
new_title: str | None = None
|
|
1334
|
-
|
|
1324
|
+
with contextlib.suppress(Exception):
|
|
1335
1325
|
new_url = page.url
|
|
1336
1326
|
new_title = page.title()
|
|
1337
|
-
except Exception:
|
|
1338
|
-
pass
|
|
1339
1327
|
|
|
1340
1328
|
extra: dict[str, Any] = {}
|
|
1341
1329
|
if new_url is not None:
|
|
@@ -1554,20 +1542,18 @@ class PlaywrightBridge:
|
|
|
1554
1542
|
# ponytail: single retry, no backoff — challenge pages settle in
|
|
1555
1543
|
# one load cycle; add exponential backoff if a real challenge
|
|
1556
1544
|
# needs >1 retry.
|
|
1557
|
-
|
|
1558
|
-
# `self._wrap_mw_eval_in_eval and "Execution context was destroyed" in err_msg`)
|
|
1559
|
-
# keep the no-boolean-in-except lint calm; behavior is identical.
|
|
1545
|
+
retry_destroyed = False
|
|
1560
1546
|
if self._wrap_mw_eval_in_eval:
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1547
|
+
retry_destroyed = "Execution context was destroyed" in err_msg
|
|
1548
|
+
if retry_destroyed:
|
|
1549
|
+
# Best-effort: proceed to retry even if wait fails
|
|
1550
|
+
with contextlib.suppress(Exception):
|
|
1551
|
+
page.wait_for_load_state("load")
|
|
1552
|
+
try:
|
|
1553
|
+
result = page.evaluate(effective_expression)
|
|
1554
|
+
return {"success": True, "result": result}
|
|
1555
|
+
except Exception as retry_exc:
|
|
1556
|
+
return {"success": False, "error": str(retry_exc)}
|
|
1571
1557
|
return {
|
|
1572
1558
|
"success": False,
|
|
1573
1559
|
"error": err_msg,
|
|
@@ -21,9 +21,7 @@ Error response::
|
|
|
21
21
|
|
|
22
22
|
import json
|
|
23
23
|
import sys
|
|
24
|
-
import
|
|
25
|
-
from typing import Any, Optional
|
|
26
|
-
|
|
24
|
+
from typing import Any
|
|
27
25
|
|
|
28
26
|
# ─── Standard JSON-RPC error codes ────────────────────────────────────
|
|
29
27
|
|
|
@@ -31,7 +29,6 @@ PARSE_ERROR = -32700
|
|
|
31
29
|
INVALID_REQUEST = -32600
|
|
32
30
|
METHOD_NOT_FOUND = -32601
|
|
33
31
|
INVALID_PARAMS = -32602
|
|
34
|
-
INTERNAL_ERROR = -32603
|
|
35
32
|
|
|
36
33
|
# Custom error codes (application-level)
|
|
37
34
|
APPLICATION_ERROR = -32000
|
|
@@ -50,7 +47,8 @@ class InvalidRequestError(ValueError):
|
|
|
50
47
|
|
|
51
48
|
# ─── Reading requests ─────────────────────────────────────────────────
|
|
52
49
|
|
|
53
|
-
|
|
50
|
+
|
|
51
|
+
def read_request() -> dict[str, Any] | None:
|
|
54
52
|
"""Read one JSON-RPC request from stdin (line-delimited).
|
|
55
53
|
|
|
56
54
|
Returns the parsed request dict, or None on EOF.
|
|
@@ -86,6 +84,7 @@ def read_request() -> Optional[dict[str, Any]]:
|
|
|
86
84
|
|
|
87
85
|
# ─── Writing responses ────────────────────────────────────────────────
|
|
88
86
|
|
|
87
|
+
|
|
89
88
|
def write_response(response: dict[str, Any]) -> None:
|
|
90
89
|
"""Write a JSON-RPC response to stdout as a single JSON line, then flush.
|
|
91
90
|
|
|
@@ -110,7 +109,7 @@ def make_error_response(
|
|
|
110
109
|
request_id: Any,
|
|
111
110
|
code: int,
|
|
112
111
|
message: str,
|
|
113
|
-
data:
|
|
112
|
+
data: dict[str, Any] | None = None,
|
|
114
113
|
) -> dict[str, Any]:
|
|
115
114
|
"""Build a JSON-RPC error response."""
|
|
116
115
|
error: dict[str, Any] = {
|
|
@@ -136,24 +135,10 @@ def make_invalid_request(request_id: Any = None) -> dict[str, Any]:
|
|
|
136
135
|
return make_error_response(request_id, INVALID_REQUEST, "Invalid Request")
|
|
137
136
|
|
|
138
137
|
|
|
139
|
-
def make_internal_error(
|
|
140
|
-
request_id: Any,
|
|
141
|
-
exc: Exception,
|
|
142
|
-
) -> dict[str, Any]:
|
|
143
|
-
"""Build an internal error response including Python traceback."""
|
|
144
|
-
tb = traceback.format_exception(type(exc), exc, exc.__traceback__)
|
|
145
|
-
return make_error_response(
|
|
146
|
-
request_id,
|
|
147
|
-
INTERNAL_ERROR,
|
|
148
|
-
f"Internal error: {exc}",
|
|
149
|
-
data={"traceback": "".join(tb)},
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
|
|
153
138
|
def make_application_error(
|
|
154
139
|
request_id: Any,
|
|
155
140
|
message: str,
|
|
156
|
-
traceback_str:
|
|
141
|
+
traceback_str: str | None = None,
|
|
157
142
|
) -> dict[str, Any]:
|
|
158
143
|
"""Build an application-level error response."""
|
|
159
144
|
data: dict[str, Any] = {}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|