Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
136 lines
6.4 KiB
Python
136 lines
6.4 KiB
Python
"""Offline protocol/error regressions for the live browser helper."""
|
|
import importlib.machinery
|
|
import importlib.util
|
|
from pathlib import Path
|
|
import struct
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import unittest
|
|
from unittest.mock import Mock, patch
|
|
|
|
loader = importlib.machinery.SourceFileLoader("page_browser", str(Path(__file__).with_name("rakazo-page-browser")))
|
|
spec = importlib.util.spec_from_loader(loader.name, loader)
|
|
helper = importlib.util.module_from_spec(spec)
|
|
loader.exec_module(helper)
|
|
|
|
|
|
class PageBrowserTest(unittest.TestCase):
|
|
def test_closed_stdin_cancels_helper_without_a_browser(self):
|
|
with subprocess.Popen([sys.executable, str(Path(__file__).with_name("rakazo-page-browser")), "snapshot", "{}"],
|
|
env={**os.environ, "RAKAZO_BROWSER_WATCH_STDIN": "1", "RAKAZO_CDP_PORT": "0"},
|
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as process:
|
|
process.stdin.close()
|
|
self.assertEqual(process.wait(timeout=3), 130)
|
|
|
|
def test_closed_extended_frame_does_not_spin(self):
|
|
for partial in (b"\x81\x7e", b"\x81\x7f", b"\x81\x80"):
|
|
with self.subTest(partial=partial):
|
|
client = helper.CdpClient("ws://127.0.0.1:9222/test")
|
|
client._sock = Mock()
|
|
client._sock.recv.return_value = b""
|
|
client._buf.extend(partial)
|
|
with self.assertRaisesRegex(RuntimeError, "closed"):
|
|
client._read_frame()
|
|
|
|
def test_bounds_frame_before_reading_payload(self):
|
|
client = helper.CdpClient("ws://127.0.0.1:9222/test")
|
|
client._sock = Mock()
|
|
client._buf.extend(b"\x81\x7f" + struct.pack("!Q", 10_000_000))
|
|
with self.assertRaisesRegex(RuntimeError, "too large"):
|
|
client._read_frame()
|
|
client._sock.recv.assert_not_called()
|
|
|
|
def test_rejects_url_credentials_before_navigation(self):
|
|
client = Mock()
|
|
for url in ("http://example:fake-password@example.test", "https://example:fake-password@example.test"):
|
|
with self.assertRaisesRegex(RuntimeError, "credentials"):
|
|
helper.navigate(client, "session", url)
|
|
client.call.assert_not_called()
|
|
|
|
def test_rejects_navigation_error(self):
|
|
client = Mock()
|
|
client.call.return_value = {"errorText": "net::ERR_NAME_NOT_RESOLVED"}
|
|
with self.assertRaisesRegex(RuntimeError, "ERR_NAME_NOT_RESOLVED"):
|
|
helper.navigate(client, "session", "https://example.test")
|
|
|
|
def test_preserves_completed_actions_when_next_response_is_lost(self):
|
|
with patch.object(helper, "ensure_helpers"), patch.object(helper, "eval_json") as evaluate:
|
|
evaluate.side_effect = [True, {}, True, TimeoutError("lost response")]
|
|
result = helper.act(Mock(), "session", [
|
|
{"kind": "click", "ref": "first"}, {"kind": "click", "ref": "second"},
|
|
])
|
|
self.assertEqual(result["completed"], 1)
|
|
self.assertTrue(result["uncertain"])
|
|
self.assertFalse(result["ok"])
|
|
|
|
def test_rejects_missing_text_before_any_action(self):
|
|
with patch.object(helper, "ensure_helpers") as ensure:
|
|
with self.assertRaisesRegex(RuntimeError, "Text"):
|
|
helper.act(Mock(), "session", [{"kind": "fill", "ref": "first"}])
|
|
ensure.assert_not_called()
|
|
|
|
def test_discovery_cannot_leave_loopback_endpoint(self):
|
|
with patch.object(helper, "http_get_json", return_value={"webSocketDebuggerUrl": "ws://example.test:9222/session"}):
|
|
with self.assertRaisesRegex(RuntimeError, "Unexpected CDP"):
|
|
helper.discover_ws_url(9222)
|
|
|
|
def test_ensure_world_reuses_named_isolated_context(self):
|
|
client = helper.CdpClient("ws://127.0.0.1:9222/test")
|
|
calls = []
|
|
|
|
def fake_call(method, params=None, *, session_id=None, timeout_s=30.0):
|
|
calls.append(method)
|
|
if method == "Page.getFrameTree":
|
|
return {"frameTree": {"frame": {"id": "frame-1"}}}
|
|
if method == "Runtime.enable":
|
|
client._handle_event({
|
|
"method": "Runtime.executionContextCreated",
|
|
"sessionId": session_id,
|
|
"params": {
|
|
"context": {
|
|
"id": 42,
|
|
"name": helper.WORLD_NAME,
|
|
"auxData": {"frameId": "frame-1", "isDefault": False},
|
|
}
|
|
},
|
|
})
|
|
return {}
|
|
raise AssertionError(f"unexpected CDP call: {method}")
|
|
|
|
client.call = fake_call # type: ignore[method-assign]
|
|
helper.ensure_world(client, "session-a")
|
|
self.assertEqual(client.context_id, 42)
|
|
self.assertNotIn("Page.createIsolatedWorld", calls)
|
|
|
|
def test_context_lookup_rejects_default_foreign_and_unbound_contexts(self):
|
|
client = helper.CdpClient("ws://127.0.0.1:9222/test")
|
|
for aux, session in (({"frameId": "frame-1", "isDefault": True}, "session-a"),
|
|
({"frameId": "other-frame", "isDefault": False}, "session-a"),
|
|
({"frameId": "frame-1", "isDefault": False}, "session-b"),
|
|
({}, "session-a")):
|
|
client._handle_event({"method": "Runtime.executionContextCreated", "sessionId": session,
|
|
"params": {"context": {"id": 42, "name": helper.WORLD_NAME, "auxData": aux}}})
|
|
self.assertIsNone(client.find_world_context(helper.WORLD_NAME, "frame-1", "session-a"))
|
|
|
|
def test_ensure_world_creates_when_missing(self):
|
|
client = helper.CdpClient("ws://127.0.0.1:9222/test")
|
|
|
|
def fake_call(method, params=None, *, session_id=None, timeout_s=30.0):
|
|
if method == "Page.getFrameTree":
|
|
return {"frameTree": {"frame": {"id": "frame-1"}}}
|
|
if method == "Runtime.enable":
|
|
return {}
|
|
if method == "Page.createIsolatedWorld":
|
|
self.assertEqual(params["worldName"], helper.WORLD_NAME)
|
|
return {"executionContextId": 7}
|
|
raise AssertionError(f"unexpected CDP call: {method}")
|
|
|
|
client.call = fake_call # type: ignore[method-assign]
|
|
helper.ensure_world(client, "session-a")
|
|
self.assertEqual(client.context_id, 7)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|