import json from pathlib import Path from types import SimpleNamespace import pytest from browser_cli.native import host as native_host from browser_cli import framing, local_transport from browser_cli.native import local_server, protocol as native_protocol def _raise_system_exit(code: int): raise SystemExit(code) def test_cleanup_removes_socket_and_registry_entry(monkeypatch, tmp_path): alias = "work" socket_path = tmp_path / "work.sock" socket_path.write_text("") registry_path = tmp_path / "registry.json" registry_path.write_text(json.dumps({alias: str(socket_path), "other": str(tmp_path / "other.sock")})) monkeypatch.setattr(native_host, "REGISTRY_PATH", registry_path) monkeypatch.setattr(native_host, "_socket_path_for", lambda alias: str(socket_path)) monkeypatch.setattr(native_host, "is_windows", lambda: False) native_host._cleanup(alias) assert not socket_path.exists() assert json.loads(registry_path.read_text()) == {"other": str(tmp_path / "other.sock")} def test_stdin_reader_cleans_up_on_eof(monkeypatch): cleaned = [] monkeypatch.setattr(native_host.protocol, "read_native_message", lambda stream: None) monkeypatch.setattr(native_host, "_cleanup", cleaned.append) monkeypatch.setattr(native_host.os, "_exit", _raise_system_exit) monkeypatch.setattr(native_host.sys, "stdin", SimpleNamespace(buffer=object())) with pytest.raises(SystemExit, match="0"): native_host.stdin_reader("work") assert cleaned == ["work"] def test_cleanup_windows_skips_socket_unlink(monkeypatch, tmp_path): registry_path = tmp_path / "registry.json" registry_path.write_text(json.dumps({"work": r"\\.\pipe\browser-cli-work"})) monkeypatch.setattr(native_host, "REGISTRY_PATH", registry_path) monkeypatch.setattr(native_host, "is_windows", lambda: True) native_host._cleanup("work") assert json.loads(registry_path.read_text()) == {} def test_stdin_reader_cleans_up_on_bye(monkeypatch): cleaned = [] messages = iter([{"type": "bye"}]) monkeypatch.setattr(native_host.protocol, "read_native_message", lambda stream: next(messages)) monkeypatch.setattr(native_host, "_cleanup", cleaned.append) monkeypatch.setattr(native_host.os, "_exit", _raise_system_exit) monkeypatch.setattr(native_host.sys, "stdin", SimpleNamespace(buffer=object())) with pytest.raises(SystemExit, match="0"): native_host.stdin_reader("work") assert cleaned == ["work"] def test_stdin_reader_routes_response_messages(monkeypatch): response_queue = native_host.queue.Queue() native_host.PENDING["msg-1"] = response_queue messages = iter([{"type": "hello"}, {"id": "msg-1", "success": True}, None]) monkeypatch.setattr(native_host.protocol, "read_native_message", lambda stream: next(messages)) monkeypatch.setattr(native_host, "_cleanup", lambda alias: None) monkeypatch.setattr(native_host.os, "_exit", _raise_system_exit) monkeypatch.setattr(native_host.sys, "stdin", SimpleNamespace(buffer=object())) with pytest.raises(SystemExit, match="0"): native_host.stdin_reader("work") assert response_queue.get_nowait() == {"id": "msg-1", "success": True} native_host.PENDING.clear() def test_collect_paged_browser_command_accumulates_pages(monkeypatch): calls = [] pages = iter([ {"success": True, "data": {"__browserCliPage": True, "items": [1, 2], "total": 3, "nextOffset": 2}}, {"success": True, "data": {"__browserCliPage": True, "items": [3], "total": 3, "nextOffset": None}}, ]) def fake_send(cmd): calls.append(cmd) return next(pages) monkeypatch.setattr(native_host, "PAGE_SIZE", 2) monkeypatch.setattr(native_host, "_send_browser_command", fake_send) result = native_host._collect_paged_browser_command({"id": "orig", "command": "tabs.list", "args": {"foo": "bar"}}) assert result == {"id": "orig", "success": True, "data": [1, 2, 3], "pageSize": 2, "total": 3} assert [call["args"]["__page"] for call in calls] == [ {"offset": 0, "limit": 2}, {"offset": 2, "limit": 2}, ] assert all(call["args"]["foo"] == "bar" for call in calls) assert all(call["id"] != "orig" for call in calls) def test_collect_paged_browser_command_passes_through_non_paged_response(monkeypatch): monkeypatch.setattr(native_host, "_send_browser_command", lambda cmd: {"id": cmd["id"], "success": True, "data": {"value": 1}}) result = native_host._collect_paged_browser_command({"id": "orig", "command": "tabs.list", "args": {}}) assert result == {"id": "orig", "success": True, "data": {"value": 1}} def test_handle_browser_command_pages_known_list_commands(monkeypatch): seen = [] monkeypatch.setattr(native_host, "_collect_paged_browser_command", lambda cmd: seen.append(cmd) or {"success": True, "data": []}) result = native_host._handle_browser_command({"id": "orig", "command": "tabs.list", "args": {}}) assert result == {"success": True, "data": []} assert seen[0]["command"] == "tabs.list" def test_handle_browser_command_sends_non_pageable_directly(monkeypatch): seen = [] monkeypatch.setattr(native_host, "_send_browser_command", lambda cmd: seen.append(cmd) or {"success": True, "data": "ok"}) result = native_host._handle_browser_command({"id": "x", "command": "navigate.open", "args": {}}) assert result == {"success": True, "data": "ok"} assert seen[0]["command"] == "navigate.open" # --------------------------------------------------------------------------- # _read_exact_stream # --------------------------------------------------------------------------- def test_read_exact_stream_full_read(): """Returns the exact bytes when stream delivers them in one shot.""" import io stream = io.BytesIO(b"hello") assert native_protocol.read_exact_stream(stream, 5) == b"hello" def test_read_exact_stream_partial_chunks(): """Accumulates multiple short chunks until n bytes are read.""" import io class _ChunkyStream: def __init__(self, data, chunk_size): self._data = data self._pos = 0 self._chunk_size = chunk_size def read(self, n): end = min(self._pos + self._chunk_size, len(self._data)) chunk = self._data[self._pos:end] self._pos = end return chunk stream = _ChunkyStream(b"abcdefgh", 3) assert native_protocol.read_exact_stream(stream, 8) == b"abcdefgh" def test_read_exact_stream_eof_returns_none(): """Returns None if stream is exhausted before n bytes are delivered.""" import io stream = io.BytesIO(b"ab") # only 2 bytes, asking for 4 assert native_protocol.read_exact_stream(stream, 4) is None def test_read_exact_stream_immediate_eof(): """Returns None on an empty stream.""" import io stream = io.BytesIO(b"") assert native_protocol.read_exact_stream(stream, 1) is None # --------------------------------------------------------------------------- # write_native_message / read_native_message round-trip # --------------------------------------------------------------------------- def test_write_and_read_native_message_roundtrip(): """write_native_message followed by read_native_message recovers the original dict.""" import io buf = io.BytesIO() msg = {"id": "abc", "command": "tabs.list", "args": {}} native_protocol.write_native_message(buf, msg) buf.seek(0) result = native_protocol.read_native_message(buf) assert result == msg def test_read_native_message_eof_at_length_prefix(): """Returns None when the stream is empty (no length prefix).""" import io stream = io.BytesIO(b"") assert native_protocol.read_native_message(stream) is None def test_read_native_message_eof_at_body(): """Returns None when the body is truncated after reading the length prefix.""" import io import struct # Write a 10-byte length prefix but only 5 bytes of body buf = struct.pack("= 5 # stopped at/just past the cap, not unbounded def test_collect_paged_browser_command_invalid_items(monkeypatch): """If items is not a list the command returns an error dict.""" monkeypatch.setattr( native_host, "_send_browser_command", lambda cmd: { "id": cmd["id"], "success": True, "data": {"__browserCliPage": True, "items": "not-a-list", "total": 1, "nextOffset": None}, }, ) result = native_host._collect_paged_browser_command({"id": "bad", "command": "tabs.list", "args": {}}) assert result["success"] is False assert "invalid paged response" in result["error"] # --------------------------------------------------------------------------- # _resolve_profile_alias # --------------------------------------------------------------------------- def test_resolve_profile_alias_uses_hello_alias(): alias = native_host._resolve_profile_alias({"type": "hello", "alias": "brave-work"}) assert alias == "brave-work" def test_resolve_profile_alias_no_hello_returns_uuid(): alias = native_host._resolve_profile_alias(None) import uuid uuid.UUID(alias) # raises ValueError if not a valid UUID def test_resolve_profile_alias_default_alias_returns_uuid(): from browser_cli.platform import DEFAULT_ALIAS alias = native_host._resolve_profile_alias({"type": "hello", "alias": DEFAULT_ALIAS}) import uuid uuid.UUID(alias) def test_resolve_profile_alias_non_hello_type_returns_uuid(): alias = native_host._resolve_profile_alias({"type": "bye", "alias": "some"}) import uuid uuid.UUID(alias) # --------------------------------------------------------------------------- # asyncio Unix-socket server path # --------------------------------------------------------------------------- def test_async_recv_all_and_send_all_roundtrip(): """local_transport async framing mirrors the sync length-prefixed socket framing.""" import asyncio async def run(): async def handle(reader, writer): payload = await local_transport.async_recv_all(reader) await local_transport.async_send_all(writer, payload + b"-reply") writer.close() await writer.wait_closed() server = await asyncio.start_server(handle, "127.0.0.1", 0) host, port = server.sockets[0].getsockname() async with server: reader, writer = await asyncio.open_connection(host, port) await local_transport.async_send_all(writer, b"hello") assert await local_transport.async_recv_all(reader) == b"hello-reply" writer.close() await writer.wait_closed() asyncio.run(run()) def test_async_socket_server_handles_cli_request(monkeypatch, tmp_path): """Unix CLI socket server accepts requests concurrently via asyncio.""" import asyncio import struct async def read_frame(reader): raw_len = await reader.readexactly(4) msg_len = struct.unpack("