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.
Files changed (104) hide show
  1. package/README.md +5 -3
  2. package/node_modules/pi-lean-host/AGENTS.md +13 -6
  3. package/node_modules/pi-lean-host/__tests__/api-learn-fetch-recipe.test.ts +282 -10
  4. package/node_modules/pi-lean-host/__tests__/api-learn-multi-file.test.ts +8 -16
  5. package/node_modules/pi-lean-host/__tests__/api-probe.test.ts +276 -279
  6. package/node_modules/pi-lean-host/__tests__/api-scaffold.test.ts +12 -33
  7. package/node_modules/pi-lean-host/__tests__/api-toggle.test.ts +14 -12
  8. package/node_modules/pi-lean-host/__tests__/bootstrap-command.test.ts +1 -7
  9. package/node_modules/pi-lean-host/__tests__/delete-command.test.ts +1 -12
  10. package/node_modules/pi-lean-host/__tests__/guide-catalog.test.ts +484 -0
  11. package/node_modules/pi-lean-host/__tests__/helpers.test.ts +0 -132
  12. package/node_modules/pi-lean-host/__tests__/oauth-command.test.ts +95 -0
  13. package/node_modules/pi-lean-host/__tests__/oauth-flow.test.ts +163 -0
  14. package/node_modules/pi-lean-host/__tests__/oauth-mint.test.ts +79 -6
  15. package/node_modules/pi-lean-host/__tests__/parse-api-guide.test.ts +4 -409
  16. package/node_modules/pi-lean-host/__tests__/response-spill.test.ts +0 -1
  17. package/node_modules/pi-lean-host/__tests__/secrets-command.test.ts +19 -35
  18. package/node_modules/pi-lean-host/__tests__/smoke.test.ts +1 -82
  19. package/node_modules/pi-lean-host/__tests__/ssrf-guard.test.ts +98 -0
  20. package/node_modules/pi-lean-host/__tests__/test-utils.ts +73 -0
  21. package/node_modules/pi-lean-host/__tests__/tools.test.ts +2 -518
  22. package/node_modules/pi-lean-host/__tests__/transport.test.ts +178 -31
  23. package/node_modules/pi-lean-host/__tests__/verify-command.test.ts +1 -8
  24. package/node_modules/pi-lean-host/__tests__/verify-stamp.test.ts +1 -4
  25. package/node_modules/pi-lean-host/api-guides/boe/local-helper.test.ts +9 -20
  26. package/node_modules/pi-lean-host/api-guides/dnb/error-envelope.test.ts +6 -18
  27. package/node_modules/pi-lean-host/api-guides/dnb/resumption-token.test.ts +6 -18
  28. package/node_modules/pi-lean-host/api-guides/frost-sensorthings/dotted-key.test.ts +6 -18
  29. package/node_modules/pi-lean-host/api-guides/github/static-key.test.ts +7 -25
  30. package/node_modules/pi-lean-host/api-guides/inaturalist/derived-id.test.ts +6 -18
  31. package/node_modules/pi-lean-host/api-guides/internet-archive/multi-recipe.test.ts +6 -26
  32. package/node_modules/pi-lean-host/api-guides/stripe/has-more.test.ts +6 -18
  33. package/node_modules/pi-lean-host/api-guides/telegram-bot/path-auth.test.ts +7 -25
  34. package/node_modules/pi-lean-host/api-guides/twitch/oauth2.test.ts +12 -47
  35. package/node_modules/pi-lean-host/api-guides/twitch-user/oauth-user.test.ts +11 -52
  36. package/node_modules/pi-lean-host/api-guides/usgs/transform.test.ts +10 -27
  37. package/node_modules/pi-lean-host/api-guides/wikidata-search/numeric-cursor.test.ts +6 -18
  38. package/node_modules/pi-lean-host/api-guides/wikimedia-action/token-bag.test.ts +9 -20
  39. package/node_modules/pi-lean-host/core/auth.ts +2 -1
  40. package/node_modules/pi-lean-host/core/helpers.ts +29 -35
  41. package/node_modules/pi-lean-host/core/oauth-command.ts +10 -8
  42. package/node_modules/pi-lean-host/core/oauth-flow.ts +20 -4
  43. package/node_modules/pi-lean-host/core/parse-api-guide.ts +3 -5
  44. package/node_modules/pi-lean-host/core/transport.ts +1 -1
  45. package/node_modules/pi-lean-host/package.json +1 -1
  46. package/node_modules/pi-lean-host/tools/api-guide.ts +18 -57
  47. package/node_modules/pi-lean-host/tools/api-learn.ts +20 -49
  48. package/node_modules/pi-lean-host/tools/api-probe.ts +17 -26
  49. package/node_modules/pi-lean-host/tools/api-scaffold.ts +20 -42
  50. package/node_modules/pi-lean-host/tools/utils.ts +69 -0
  51. package/node_modules/pi-lean-portal/AGENTS.md +4 -3
  52. package/node_modules/pi-lean-portal/README.md +9 -9
  53. package/node_modules/pi-lean-portal/__tests__/accessibility-tree.test.ts +0 -15
  54. package/node_modules/pi-lean-portal/__tests__/browser-install.test.ts +737 -0
  55. package/node_modules/pi-lean-portal/__tests__/browser-navigate.test.ts +33 -2
  56. package/node_modules/pi-lean-portal/__tests__/browser-status.test.ts +48 -25
  57. package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -9
  58. package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +3 -29
  59. package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +51 -0
  60. package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313-pytest-9.1.1.pyc +0 -0
  61. package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
  62. package/node_modules/pi-lean-portal/__tests__/helpers/mock-pi.ts +34 -0
  63. package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +14 -24
  64. package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +0 -90
  65. package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +32 -125
  66. package/node_modules/pi-lean-portal/__tests__/router-dispatch.test.ts +12 -170
  67. package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +39 -151
  68. package/node_modules/pi-lean-portal/__tests__/web-guides.test.ts +1 -44
  69. package/node_modules/pi-lean-portal/backends/chromium/index.ts +1 -1
  70. package/node_modules/pi-lean-portal/backends/firefox/index.ts +1 -1
  71. package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +1 -1
  72. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
  73. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
  74. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-312.pyc +0 -0
  75. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
  76. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +43 -57
  77. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/transport.py +6 -21
  78. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313-pytest-9.1.1.pyc +0 -0
  79. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
  80. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
  81. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_transport.cpython-313-pytest-9.1.1.pyc +0 -0
  82. package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +157 -73
  83. package/node_modules/pi-lean-portal/backends/python-base/tests/test_transport.py +15 -41
  84. package/node_modules/pi-lean-portal/browser-install.ts +454 -0
  85. package/node_modules/pi-lean-portal/browser-status.ts +21 -0
  86. package/node_modules/pi-lean-portal/browser-toggle.ts +13 -11
  87. package/node_modules/pi-lean-portal/core/fetch-backend.ts +2 -2
  88. package/node_modules/pi-lean-portal/core/router.ts +7 -32
  89. package/node_modules/pi-lean-portal/core/shared/session-manager.ts +1 -3
  90. package/node_modules/pi-lean-portal/index.ts +1 -18
  91. package/node_modules/pi-lean-portal/package.json +2 -1
  92. package/node_modules/pi-lean-portal/tools/browser-navigate.ts +6 -2
  93. package/node_modules/pi-lean-portal/tools/utils.ts +5 -6
  94. package/node_modules/pi-lean-search/AGENTS.md +5 -3
  95. package/node_modules/pi-lean-search/__tests__/web-search.test.ts +180 -127
  96. package/node_modules/pi-lean-search/index.ts +0 -3
  97. package/node_modules/pi-lean-search/package.json +1 -1
  98. package/node_modules/pi-lean-search/web-search-tool.ts +22 -13
  99. package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
  100. package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
  101. package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
  102. package/node_modules/yaml/dist/nodes/Alias.js +25 -23
  103. package/node_modules/yaml/package.json +1 -1
  104. 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 types, resolveApplicableGuides, formatGuideFooter, formatGuideList,
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: npx playwright install chromium firefox";
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: npx playwright install chromium firefox";
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. Example: "Run: npx playwright install firefox".
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
 
@@ -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 400. Stealth backends
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 400 matches the
306
- #: historical hard-coded value so shipped chromium-py / firefox-py are
307
- #: unchanged.
308
- _settle_budget_ms: int = 400
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
- try:
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
- try:
522
- if self._browser:
521
+ if self._browser:
522
+ with contextlib.suppress(Exception):
523
523
  self._browser.close()
524
- except Exception:
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
- try:
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
- try:
753
- page: Any = session.get("page")
754
- if page and not page.is_closed():
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
- pass
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
- try:
774
- if context:
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
- try:
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
- try:
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 (150 ms window)
978
- page.wait_for_timeout(150)
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
- try:
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
- try:
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
- # Nested ifs (rather than
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
- if "Execution context was destroyed" in err_msg:
1562
- try:
1563
- page.wait_for_load_state("load")
1564
- except Exception:
1565
- pass # Best-effort: proceed to retry even if wait fails
1566
- try:
1567
- result = page.evaluate(effective_expression)
1568
- return {"success": True, "result": result}
1569
- except Exception as retry_exc:
1570
- return {"success": False, "error": str(retry_exc)}
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 traceback
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
- def read_request() -> Optional[dict[str, Any]]:
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: Optional[dict[str, Any]] = None,
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: Optional[str] = None,
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] = {}