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
package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py
CHANGED
|
@@ -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(
|
|
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(
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
|
326
|
+
assert not bridge._fingerprint_managed_context
|
|
324
327
|
assert bridge._eval_prefix == ""
|
|
325
|
-
assert bridge._scroll_via_wheel
|
|
326
|
-
assert bridge._skip_default_viewport
|
|
327
|
-
assert bridge._skip_networkidle
|
|
328
|
-
assert bridge._wrap_mw_eval_in_eval
|
|
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(
|
|
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(
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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"]
|
|
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
|
-
|
|
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"]
|
|
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 =
|
|
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"]
|
|
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(
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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"]
|
|
524
|
+
assert q["fingerprint_managed_context"] is False
|
|
477
525
|
assert q["eval_prefix"] == ""
|
|
478
|
-
assert q["scroll_via_wheel"]
|
|
479
|
-
assert q["skip_default_viewport"]
|
|
480
|
-
assert q["skip_networkidle"]
|
|
481
|
-
assert q["wrap_mw_eval_in_eval"]
|
|
482
|
-
assert q["csp_safe_readonly_via_init_script"]
|
|
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(
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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(
|
|
516
|
-
|
|
517
|
-
|
|
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 (
|
|
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
|
|
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"]
|
|
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(
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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"]
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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",
|