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
@@ -17,6 +17,7 @@ These are pure-logic tests — no Playwright browser required. A small
17
17
  fake page/context mocks the few Playwright methods the code paths touch.
18
18
  """
19
19
 
20
+ import inspect
20
21
  import sys
21
22
  from typing import Any
22
23
 
@@ -24,7 +25,6 @@ import pytest
24
25
 
25
26
  from pi_browser_bridge.playwright_base import PlaywrightBridge
26
27
 
27
-
28
28
  # ═══════════════════════════════════════════════════════════════════════
29
29
  # Fakes
30
30
  # ═══════════════════════════════════════════════════════════════════════
@@ -167,9 +167,7 @@ class TestPluginConfig:
167
167
  def test_init_handler_on_playwright_bridge(self):
168
168
  """The handler is available on a plain PlaywrightBridge without Playwright."""
169
169
  bridge = PlaywrightBridge()
170
- result = bridge.handle_command(
171
- "browser.init", {"config": {"x": 1}}, cmd_id=7
172
- )
170
+ result = bridge.handle_command("browser.init", {"config": {"x": 1}}, cmd_id=7)
173
171
  assert result["result"] == {"ok": True}
174
172
  assert bridge.plugin_config == {"x": 1}
175
173
 
@@ -180,7 +178,9 @@ class TestPluginConfig:
180
178
 
181
179
 
182
180
  class TestFingerprintManagedContext:
183
- @pytest.mark.parametrize("flag,has_viewport,has_ua", [(False, True, True), (True, False, False)])
181
+ @pytest.mark.parametrize(
182
+ "flag,has_viewport,has_ua", [(False, True, True), (True, False, False)]
183
+ )
184
184
  def test_context_viewport_and_user_agent_presence(self, flag, has_viewport, has_ua):
185
185
  bridge = _FakePlaywrightBridge()
186
186
  bridge._fingerprint_managed_context = flag
@@ -294,11 +294,14 @@ class TestScrollViaWheel:
294
294
 
295
295
 
296
296
  class TestEvalPrefix:
297
- @pytest.mark.parametrize("prefix,expression,eval_key,expected_result", [
298
- ("", "() => 1 + 1", "1 + 1", 2),
299
- ("mw:", "() => 1 + 1", "mw:", 2),
300
- ("mw:", "() => navigator.userAgent", "mw:", "UA-string"),
301
- ])
297
+ @pytest.mark.parametrize(
298
+ "prefix,expression,eval_key,expected_result",
299
+ [
300
+ ("", "() => 1 + 1", "1 + 1", 2),
301
+ ("mw:", "() => 1 + 1", "mw:", 2),
302
+ ("mw:", "() => navigator.userAgent", "mw:", "UA-string"),
303
+ ],
304
+ )
302
305
  def test_eval_prefix(self, prefix, expression, eval_key, expected_result):
303
306
  # Prefix is prepended unconditionally — writes and reads both get it.
304
307
  bridge = _FakePlaywrightBridge()
@@ -320,12 +323,12 @@ class TestEvalPrefix:
320
323
  class TestQuirksDefaultOff:
321
324
  def test_all_quirks_default_off(self):
322
325
  bridge = _FakePlaywrightBridge()
323
- assert bridge._fingerprint_managed_context == False
326
+ assert not bridge._fingerprint_managed_context
324
327
  assert bridge._eval_prefix == ""
325
- assert bridge._scroll_via_wheel == False
326
- assert bridge._skip_default_viewport == False
327
- assert bridge._skip_networkidle == False
328
- assert bridge._wrap_mw_eval_in_eval == False
328
+ assert not bridge._scroll_via_wheel
329
+ assert not bridge._skip_default_viewport
330
+ assert not bridge._skip_networkidle
331
+ assert not bridge._wrap_mw_eval_in_eval
329
332
 
330
333
 
331
334
  # ═══════════════════════════════════════════════════════════════════════
@@ -340,7 +343,9 @@ class TestWrapMwEvalInEval:
340
343
  ``let _s = (${script})`` main-world wrapper.
341
344
  """
342
345
 
343
- @pytest.mark.parametrize("prefix,expected", [("", "let x = 5; x + 1"), ("mw:", "mw:let x = 5; x + 1")])
346
+ @pytest.mark.parametrize(
347
+ "prefix,expected", [("", "let x = 5; x + 1"), ("mw:", "mw:let x = 5; x + 1")]
348
+ )
344
349
  def test_default_off_expression_unwrapped(self, prefix, expected):
345
350
  """Default off: expression is passed through unwrapped (bit-identical),
346
351
  with only the prefix prepended when set."""
@@ -352,12 +357,17 @@ class TestWrapMwEvalInEval:
352
357
  assert page.eval_calls[0][0] == expected
353
358
  assert len(page.eval_calls) == 1
354
359
 
355
- @pytest.mark.parametrize("prefix,script,expected_expr,eval_key,expected_result", [
356
- ("", "let x = 5; x + 1", 'eval("let x = 5; x + 1")', None, None),
357
- ("mw:", "let x = 5; x + 1", 'mw:eval("let x = 5; x + 1")', None, None),
358
- ("mw:", "3 + 4", 'mw:eval("3 + 4")', "eval(", 7),
359
- ])
360
- def test_flag_on_wraps_in_eval(self, prefix, script, expected_expr, eval_key, expected_result):
360
+ @pytest.mark.parametrize(
361
+ "prefix,script,expected_expr,eval_key,expected_result",
362
+ [
363
+ ("", "let x = 5; x + 1", 'eval("let x = 5; x + 1")', None, None),
364
+ ("mw:", "let x = 5; x + 1", 'mw:eval("let x = 5; x + 1")', None, None),
365
+ ("mw:", "3 + 4", 'mw:eval("3 + 4")', "eval(", 7),
366
+ ],
367
+ )
368
+ def test_flag_on_wraps_in_eval(
369
+ self, prefix, script, expected_expr, eval_key, expected_result
370
+ ):
361
371
  """Flag on: expression becomes ``eval(<json>)`` (with ``_eval_prefix``
362
372
  prepended when set) — the production shape; a plain expression still
363
373
  round-trips its value."""
@@ -372,7 +382,7 @@ class TestWrapMwEvalInEval:
372
382
  assert page.eval_calls[0][0] == expected_expr
373
383
  assert len(page.eval_calls) == 1
374
384
  if expected_result is not None:
375
- assert result["success"] == True
385
+ assert result["success"] is True
376
386
  assert result["result"] == expected_result
377
387
 
378
388
  def test_flag_on_escapes_special_characters(self):
@@ -391,7 +401,8 @@ class TestWrapMwEvalInEval:
391
401
  assert sent.endswith(")")
392
402
  # The inner literal must be a valid JSON string (round-trips to script)
393
403
  import json as _json
394
- literal = sent[len("mw:eval("):-1]
404
+
405
+ literal = sent[len("mw:eval(") : -1]
395
406
  assert _json.loads(literal) == script
396
407
 
397
408
  def test_no_retry_on_syntax_error(self):
@@ -407,7 +418,7 @@ class TestWrapMwEvalInEval:
407
418
  page._eval_raise_error = "SyntaxError: Unexpected identifier"
408
419
  _bind_session(bridge, page)
409
420
  result = bridge.do_evaluate("t", "let x = 5; x + 1")
410
- assert result["success"] == False
421
+ assert result["success"] is False
411
422
  assert "syntaxerror" in result["error"].lower()
412
423
  assert len(page.eval_calls) == 1 # no retry
413
424
  assert page.wait_for_load_state_calls == [] # no recovery machinery
@@ -420,11 +431,13 @@ class TestWrapMwEvalInEval:
420
431
  bridge._wrap_mw_eval_in_eval = True
421
432
  bridge._eval_prefix = "mw:"
422
433
  page = _FakePage()
423
- page._eval_raise_once_error = "Execution context was destroyed, most likely because of a navigation."
434
+ page._eval_raise_once_error = (
435
+ "Execution context was destroyed, most likely because of a navigation."
436
+ )
424
437
  page.eval_results = {"eval(": "retry_success"}
425
438
  _bind_session(bridge, page)
426
439
  result = bridge.do_evaluate("t", "let x = 5; x + 1")
427
- assert result["success"] == True
440
+ assert result["success"]
428
441
  assert result["result"] == "retry_success"
429
442
  assert len(page.eval_calls) == 2 # first raise, retry succeeds
430
443
  assert len(page.wait_for_load_state_calls) == 1
@@ -444,20 +457,55 @@ class TestDescribeQuirks:
444
457
  flags correctly, including default values.
445
458
  """
446
459
 
447
- @pytest.mark.parametrize("overrides,expected", [
448
- (
449
- {},
450
- {"fingerprint_managed_context": False, "eval_prefix": "", "scroll_via_wheel": False, "skip_default_viewport": False, "skip_networkidle": False, "wrap_mw_eval_in_eval": False, "csp_safe_readonly_via_init_script": False},
451
- ),
452
- (
453
- {"_fingerprint_managed_context": True, "_eval_prefix": "mw:", "_scroll_via_wheel": True, "_skip_default_viewport": True, "_skip_networkidle": True, "_wrap_mw_eval_in_eval": True, "_csp_safe_readonly_via_init_script": True},
454
- {"fingerprint_managed_context": True, "eval_prefix": "mw:", "scroll_via_wheel": True, "skip_default_viewport": True, "skip_networkidle": True, "wrap_mw_eval_in_eval": True, "csp_safe_readonly_via_init_script": True},
455
- ),
456
- (
457
- {"_scroll_via_wheel": True, "_eval_prefix": "mw:"},
458
- {"scroll_via_wheel": True, "eval_prefix": "mw:", "fingerprint_managed_context": False, "skip_default_viewport": False, "skip_networkidle": False, "wrap_mw_eval_in_eval": False, "csp_safe_readonly_via_init_script": False},
459
- ),
460
- ])
460
+ @pytest.mark.parametrize(
461
+ "overrides,expected",
462
+ [
463
+ (
464
+ {},
465
+ {
466
+ "fingerprint_managed_context": False,
467
+ "eval_prefix": "",
468
+ "scroll_via_wheel": False,
469
+ "skip_default_viewport": False,
470
+ "skip_networkidle": False,
471
+ "wrap_mw_eval_in_eval": False,
472
+ "csp_safe_readonly_via_init_script": False,
473
+ },
474
+ ),
475
+ (
476
+ {
477
+ "_fingerprint_managed_context": True,
478
+ "_eval_prefix": "mw:",
479
+ "_scroll_via_wheel": True,
480
+ "_skip_default_viewport": True,
481
+ "_skip_networkidle": True,
482
+ "_wrap_mw_eval_in_eval": True,
483
+ "_csp_safe_readonly_via_init_script": True,
484
+ },
485
+ {
486
+ "fingerprint_managed_context": True,
487
+ "eval_prefix": "mw:",
488
+ "scroll_via_wheel": True,
489
+ "skip_default_viewport": True,
490
+ "skip_networkidle": True,
491
+ "wrap_mw_eval_in_eval": True,
492
+ "csp_safe_readonly_via_init_script": True,
493
+ },
494
+ ),
495
+ (
496
+ {"_scroll_via_wheel": True, "_eval_prefix": "mw:"},
497
+ {
498
+ "scroll_via_wheel": True,
499
+ "eval_prefix": "mw:",
500
+ "fingerprint_managed_context": False,
501
+ "skip_default_viewport": False,
502
+ "skip_networkidle": False,
503
+ "wrap_mw_eval_in_eval": False,
504
+ "csp_safe_readonly_via_init_script": False,
505
+ },
506
+ ),
507
+ ],
508
+ )
461
509
  def test_describe_quirks_surfaces_overrides(self, overrides, expected):
462
510
  """The handler surfaces overridden quirks alongside the defaults."""
463
511
  bridge = _FakePlaywrightBridge()
@@ -473,13 +521,13 @@ class TestDescribeQuirks:
473
521
  result = bridge.handle_command("browser.describeQuirks", {}, 4)
474
522
  assert "result" in result
475
523
  q = result["result"]
476
- assert q["fingerprint_managed_context"] == False
524
+ assert q["fingerprint_managed_context"] is False
477
525
  assert q["eval_prefix"] == ""
478
- assert q["scroll_via_wheel"] == False
479
- assert q["skip_default_viewport"] == False
480
- assert q["skip_networkidle"] == False
481
- assert q["wrap_mw_eval_in_eval"] == False
482
- assert q["csp_safe_readonly_via_init_script"] == False
526
+ assert q["scroll_via_wheel"] is False
527
+ assert q["skip_default_viewport"] is False
528
+ assert q["skip_networkidle"] is False
529
+ assert q["wrap_mw_eval_in_eval"] is False
530
+ assert q["csp_safe_readonly_via_init_script"] is False
483
531
 
484
532
 
485
533
  # ═══════════════════════════════════════════════════════════════════════
@@ -493,11 +541,16 @@ class TestReadOnlyEval:
493
541
  context instead of the main-world (``mw:``) context.
494
542
  """
495
543
 
496
- @pytest.mark.parametrize("prefix,wrap,eval_key,expected_result", [
497
- ("mw:", True, None, None), # bypasses prefix and wrap
498
- ("", False, "1 + 1", 2), # bit-identical to read_only=False
499
- ])
500
- def test_read_only_bypasses_prefix_and_wrap(self, prefix, wrap, eval_key, expected_result):
544
+ @pytest.mark.parametrize(
545
+ "prefix,wrap,eval_key,expected_result",
546
+ [
547
+ ("mw:", True, None, None), # bypasses prefix and wrap
548
+ ("", False, "1 + 1", 2), # bit-identical to read_only=False
549
+ ],
550
+ )
551
+ def test_read_only_bypasses_prefix_and_wrap(
552
+ self, prefix, wrap, eval_key, expected_result
553
+ ):
501
554
  """read_only=True: no mw: prefix, no eval() wrap — raw expression."""
502
555
  bridge = _FakePlaywrightBridge()
503
556
  bridge._eval_prefix = prefix
@@ -512,10 +565,13 @@ class TestReadOnlyEval:
512
565
  assert result["success"]
513
566
  assert result["result"] == expected_result
514
567
 
515
- @pytest.mark.parametrize("prefix,read_only,expected_expr", [
516
- ("", None, 'eval("let x = 5; x + 1")'), # default (no read_only arg)
517
- ("mw:", False, 'mw:eval("let x = 5; x + 1")'), # explicit False
518
- ])
568
+ @pytest.mark.parametrize(
569
+ "prefix,read_only,expected_expr",
570
+ [
571
+ ("", None, 'eval("let x = 5; x + 1")'), # default (no read_only arg)
572
+ ("mw:", False, 'mw:eval("let x = 5; x + 1")'), # explicit False
573
+ ],
574
+ )
519
575
  def test_write_path_unchanged(self, prefix, read_only, expected_expr):
520
576
  """The write path (read_only unset or False) still applies the eval wrap."""
521
577
  bridge = _FakePlaywrightBridge()
@@ -531,7 +587,7 @@ class TestReadOnlyEval:
531
587
 
532
588
 
533
589
  # ═══════════════════════════════════════════════════════════════════════
534
- # stdout hygiene (Change 2 — Camoufox launch pollution)
590
+ # stdout hygiene (Camoufox launch pollution)
535
591
  # ═══════════════════════════════════════════════════════════════════════
536
592
 
537
593
 
@@ -681,7 +737,7 @@ class TestCspSafeReadonlyViaInitScript:
681
737
  # not fire from the isolated world on the affected binary).
682
738
  assert "DOMContentLoaded" in wrapper
683
739
  assert "addEventListener('load'" not in wrapper
684
- assert "addEventListener(\"load\"" not in wrapper
740
+ assert 'addEventListener("load"' not in wrapper
685
741
  # Writes to the known meta tag.
686
742
  assert "__pi-extract" in wrapper
687
743
 
@@ -689,6 +745,7 @@ class TestCspSafeReadonlyViaInitScript:
689
745
  """``do_evaluate(read_only=True)`` with the matching expression reads
690
746
  the stashed meta and returns the urldecoded JSON — no ``page.evaluate``."""
691
747
  from urllib.parse import quote
748
+
692
749
  bridge = _FakePlaywrightBridge()
693
750
  bridge._csp_safe_readonly_via_init_script = True
694
751
  script = "(() => { return JSON.stringify({title: 'x'}); })();"
@@ -699,26 +756,36 @@ class TestCspSafeReadonlyViaInitScript:
699
756
  page = _CspFakePage(meta_content=quote(payload))
700
757
  _bind_session(bridge, page)
701
758
  res = bridge.do_evaluate("t", script, read_only=True)
702
- assert res["success"] == True
759
+ assert res["success"] is True
703
760
  assert res["result"] == payload
704
761
  # The meta was read via the native query_selector (CSP-free).
705
762
  assert page.query_calls == ["meta#__pi-extract"]
706
763
  # page.evaluate was NOT called (CSP would block it).
707
764
  assert page.eval_calls == []
708
765
 
709
- @pytest.mark.parametrize("registered_script,sent_expression,eval_key,expected_result,expect_query", [
710
- # Expression doesn't match the gate → query_selector skipped → eval.
711
- ("REAL_EXTRACTOR;", "other expression", "other", "ok", False),
712
- # Expression matches the gate but meta is absent → eval best-effort.
713
- (
714
- "(() => { return JSON.stringify({a:1}); })();",
715
- "(() => { return JSON.stringify({a:1}); })();",
716
- "(() => { return JSON.stringify({a:1}); })();",
717
- "fallback",
718
- True,
719
- ),
720
- ])
721
- def test_do_evaluate_read_only_falls_through_to_eval(self, registered_script, sent_expression, eval_key, expected_result, expect_query):
766
+ @pytest.mark.parametrize(
767
+ "registered_script,sent_expression,eval_key,expected_result,expect_query",
768
+ [
769
+ # Expression doesn't match the gate → query_selector skipped → eval.
770
+ ("REAL_EXTRACTOR;", "other expression", "other", "ok", False),
771
+ # Expression matches the gate but meta is absent → eval best-effort.
772
+ (
773
+ "(() => { return JSON.stringify({a:1}); })();",
774
+ "(() => { return JSON.stringify({a:1}); })();",
775
+ "(() => { return JSON.stringify({a:1}); })();",
776
+ "fallback",
777
+ True,
778
+ ),
779
+ ],
780
+ )
781
+ def test_do_evaluate_read_only_falls_through_to_eval(
782
+ self,
783
+ registered_script,
784
+ sent_expression,
785
+ eval_key,
786
+ expected_result,
787
+ expect_query,
788
+ ):
722
789
  """A read_only eval that can't be served from the meta (expression
723
790
  doesn't match the gate, or the meta is absent) falls through to
724
791
  ``page.evaluate`` — no silent stale-meta return."""
@@ -730,9 +797,26 @@ class TestCspSafeReadonlyViaInitScript:
730
797
  page.eval_results[eval_key] = expected_result
731
798
  _bind_session(bridge, page)
732
799
  res = bridge.do_evaluate("t", sent_expression, read_only=True)
733
- assert res["success"] == True
800
+ assert res["success"] is True
734
801
  assert res["result"] == expected_result
735
802
  if expect_query:
736
803
  assert page.query_calls == ["meta#__pi-extract"]
737
804
  else:
738
805
  assert page.query_calls == []
806
+
807
+
808
+ class TestSettleDefaultsFromSharedJson:
809
+ """Settle defaults derive from ``browser-data.json`` (navSettle) so the
810
+ TS and Python settles stay in lockstep — no hard-coded copies."""
811
+
812
+ def test_settle_budget_default_matches_shared_json(self):
813
+ from pi_browser_bridge.browser_data import NAV_SETTLE
814
+
815
+ assert PlaywrightBridge._settle_budget_ms == NAV_SETTLE["settleTimeoutMs"]
816
+
817
+ def test_race_window_uses_shared_settle_race_ms(self):
818
+ from pi_browser_bridge import playwright_base as pb
819
+
820
+ source = inspect.getsource(pb.PlaywrightBridge._wait_for_navigation_settle)
821
+ assert 'NAV_SETTLE["settleRaceMs"]' in source
822
+ assert "wait_for_timeout(150)" not in source
@@ -5,33 +5,28 @@ Tests use ``io.StringIO`` to mock stdin/stdout for the I/O functions,
5
5
  while the response builder functions are pure and tested directly.
6
6
  """
7
7
 
8
-
9
8
  import io
10
9
  import json
11
- import sys
12
10
 
13
11
  import pytest
14
12
 
15
13
  from pi_browser_bridge.transport import (
16
- read_request,
17
- write_response,
18
- make_success_response,
19
- make_error_response,
20
- make_parse_error,
21
- make_invalid_request,
22
- make_internal_error,
23
- make_application_error,
24
- PARSE_ERROR,
14
+ APPLICATION_ERROR,
15
+ INVALID_PARAMS,
25
16
  INVALID_REQUEST,
26
17
  METHOD_NOT_FOUND,
27
- INVALID_PARAMS,
28
- INTERNAL_ERROR,
29
- APPLICATION_ERROR,
30
- TIMEOUT_ERROR,
18
+ PARSE_ERROR,
31
19
  SESSION_ERROR,
20
+ TIMEOUT_ERROR,
21
+ make_application_error,
22
+ make_error_response,
23
+ make_invalid_request,
24
+ make_parse_error,
25
+ make_success_response,
26
+ read_request,
27
+ write_response,
32
28
  )
33
29
 
34
-
35
30
  # ═════════════════════════════════════════════════════════════════════
36
31
  # Response builders (pure functions, no I/O)
37
32
  # ═════════════════════════════════════════════════════════════════════
@@ -89,19 +84,6 @@ class TestMakeInvalidRequest:
89
84
  assert "Invalid Request" in resp["error"]["message"]
90
85
 
91
86
 
92
- class TestMakeInternalError:
93
- def test_includes_traceback(self) -> None:
94
- try:
95
- raise RuntimeError("test error")
96
- except RuntimeError as exc:
97
- resp = make_internal_error(1, exc)
98
-
99
- assert resp["error"]["code"] == INTERNAL_ERROR
100
- assert "test error" in resp["error"]["message"]
101
- assert "traceback" in resp["error"]["data"]
102
- assert "RuntimeError" in resp["error"]["data"]["traceback"]
103
-
104
-
105
87
  class TestMakeApplicationError:
106
88
  def test_basic(self) -> None:
107
89
  resp = make_application_error(1, "app error")
@@ -130,7 +112,6 @@ class TestErrorCodes:
130
112
  assert INVALID_REQUEST == -32600
131
113
  assert METHOD_NOT_FOUND == -32601
132
114
  assert INVALID_PARAMS == -32602
133
- assert INTERNAL_ERROR == -32603
134
115
 
135
116
  def test_custom_codes(self) -> None:
136
117
  assert APPLICATION_ERROR == -32000
@@ -138,8 +119,7 @@ class TestErrorCodes:
138
119
  assert SESSION_ERROR == -32002
139
120
 
140
121
  def test_no_overlap(self) -> None:
141
- standard = {PARSE_ERROR, INVALID_REQUEST, METHOD_NOT_FOUND,
142
- INVALID_PARAMS, INTERNAL_ERROR}
122
+ standard = {PARSE_ERROR, INVALID_REQUEST, METHOD_NOT_FOUND, INVALID_PARAMS}
143
123
  custom = {APPLICATION_ERROR, TIMEOUT_ERROR, SESSION_ERROR}
144
124
  assert standard.isdisjoint(custom)
145
125
 
@@ -178,17 +158,13 @@ class TestReadRequest:
178
158
  with pytest.raises(ValueError, match="Failed to parse"):
179
159
  read_request()
180
160
 
181
- def test_missing_jsonrpc_field(
182
- self, monkeypatch: pytest.MonkeyPatch
183
- ) -> None:
161
+ def test_missing_jsonrpc_field(self, monkeypatch: pytest.MonkeyPatch) -> None:
184
162
  request = {"method": "ping", "id": 1}
185
163
  monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(request) + "\n"))
186
164
  with pytest.raises(ValueError, match='jsonrpc.*"2.0"'):
187
165
  read_request()
188
166
 
189
- def test_wrong_jsonrpc_version(
190
- self, monkeypatch: pytest.MonkeyPatch
191
- ) -> None:
167
+ def test_wrong_jsonrpc_version(self, monkeypatch: pytest.MonkeyPatch) -> None:
192
168
  request = {"jsonrpc": "1.0", "method": "ping", "id": 1}
193
169
  monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(request) + "\n"))
194
170
  with pytest.raises(ValueError, match='jsonrpc.*"2.0"'):
@@ -211,9 +187,7 @@ class TestReadRequest:
211
187
  with pytest.raises(ValueError, match="JSON object"):
212
188
  read_request()
213
189
 
214
- def test_works_after_whitespace_line(
215
- self, monkeypatch: pytest.MonkeyPatch
216
- ) -> None:
190
+ def test_works_after_whitespace_line(self, monkeypatch: pytest.MonkeyPatch) -> None:
217
191
  request = {"jsonrpc": "2.0", "method": "browser.navigate", "id": 2}
218
192
  monkeypatch.setattr(
219
193
  "sys.stdin",