Files
daniel156161 6fa931aa36
Testing / remote-protocol-compat (0.9.5) (push) Successful in 56s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 59s
Testing / test (push) Successful in 1m1s
Build & Publish Package / publish (push) Successful in 33s
Package Extension / package-extension (push) Successful in 36s
feat: harden remote serve and reuse connections
- Gate TCP serve commands with safe-by-default policies, per-key allow tokens, per-key rate limiting, and audit labels.
- Reuse authenticated encrypted remote sessions and parallelize/caches multi-browser fanout to reduce repeated handshake roundtrips.
- Increase paged native-host batch size with extension-side byte budgeting to speed large tab listings safely.
- Point install output at public Chrome Web Store / Firefox AMO listings by default, with --dev preserving unpacked workflows.
- Share search-engine metadata between CLI and SDK and bump the package/extension version to 0.16.0.
- Cover the new security, pooling, paging, install, and fanout behavior with expanded Python and extension tests.
2026-06-18 14:24:15 +02:00

685 lines
26 KiB
Python

import asyncio
import json
import socket
import struct
from pathlib import Path
import pytest
from browser_cli.client import (
BrowserNotConnected,
BrowserTarget,
_looks_like_domain,
_normalize_endpoint,
_resolve_connect_endpoint,
active_browser_targets,
display_browser_name,
send_command,
send_command_async,
remote_target_for_alias,
)
from browser_cli.client.targets import resolve_socket
from browser_cli.platform import endpoint_for_alias
from browser_cli.remote.registry import key_for_remote
def _listening_unix_socket(path: Path) -> socket.socket:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(str(path))
sock.listen(1)
return sock
def test_resolve_socket_raises_when_registry_missing(monkeypatch):
monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False)
monkeypatch.setattr("browser_cli.client.targets.REGISTRY_PATH", Path("/nonexistent/browser-cli-registry.json"))
with pytest.raises(BrowserNotConnected, match="Cannot resolve a browser socket automatically"):
resolve_socket()
def test_resolve_socket_uses_only_active_registry_entry(monkeypatch, tmp_path):
monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False)
socket_path = tmp_path / "browser.sock"
listener = _listening_unix_socket(socket_path)
registry_path = tmp_path / "registry.json"
registry_path.write_text(json.dumps({"abc-uuid": str(socket_path)}))
monkeypatch.setattr("browser_cli.client.targets.REGISTRY_PATH", registry_path)
try:
assert resolve_socket() == str(socket_path)
finally:
listener.close()
def test_resolve_socket_raises_when_multiple_active_entries(monkeypatch, tmp_path):
monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False)
first_socket = tmp_path / "one.sock"
second_socket = tmp_path / "two.sock"
first_listener = _listening_unix_socket(first_socket)
second_listener = _listening_unix_socket(second_socket)
registry_path = tmp_path / "registry.json"
registry_path.write_text(json.dumps({"uuid-1": str(first_socket), "uuid-2": str(second_socket)}))
monkeypatch.setattr("browser_cli.client.targets.REGISTRY_PATH", registry_path)
try:
with pytest.raises(BrowserNotConnected, match="Multiple browser instances are active: uuid-1, uuid-2"):
resolve_socket()
finally:
first_listener.close()
second_listener.close()
def test_display_browser_name_uses_uuid_stem_for_default():
assert display_browser_name("default", "/tmp/.browser_cli/550e8400-e29b-41d4-a716-446655440000.sock") == (
"550e8400-e29b-41d4-a716-446655440000"
)
def test_resolve_socket_uses_platform_endpoint_for_explicit_alias(monkeypatch):
monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False)
monkeypatch.setattr("browser_cli.client.targets.REGISTRY_PATH", Path("/nonexistent/browser-cli-registry.json"))
assert resolve_socket("work") == endpoint_for_alias("work")
def test_active_browser_targets_filters_stale_entries(monkeypatch, tmp_path):
active_socket = tmp_path / "work.sock"
listener = _listening_unix_socket(active_socket)
stale_socket = tmp_path / "stale.sock"
stale_listener = _listening_unix_socket(stale_socket)
stale_listener.close()
registry_path = tmp_path / "registry.json"
registry_path.write_text(json.dumps({"work": str(active_socket), "default": str(stale_socket)}))
monkeypatch.setattr("browser_cli.client.targets.REGISTRY_PATH", registry_path)
try:
targets = active_browser_targets(include_remotes=False)
finally:
listener.close()
assert len(targets) == 1
assert targets[0].profile == "work"
assert targets[0].display_name == "work"
def test_active_browser_targets_keeps_windows_registry_entries(monkeypatch, tmp_path):
registry_path = tmp_path / "registry.json"
registry_path.write_text(json.dumps({"work": r"\\.\pipe\browser-cli-work"}))
monkeypatch.setattr("browser_cli.client.targets.REGISTRY_PATH", registry_path)
monkeypatch.setattr("browser_cli.client.targets.is_windows", lambda: True)
targets = active_browser_targets(include_remotes=False)
assert len(targets) == 1
assert targets[0].socket_path == r"\\.\pipe\browser-cli-work"
def test_send_command_auto_routes_single_remote_target(monkeypatch):
monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False)
monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False)
sent = {}
monkeypatch.setattr(
"browser_cli.client.core.remote_browser_targets",
lambda endpoint, key=None: [BrowserTarget("work", "host:work", "", remote=endpoint)],
)
def fake_send_remote(endpoint, msg, private_key=None):
sent.update(msg)
return json.dumps({"success": True, "data": "ok"}).encode("utf-8")
monkeypatch.setattr("browser_cli.client.core._send_remote", fake_send_remote)
assert send_command("tabs.list", remote="host:8765", key=None) == "ok"
assert sent["_route"] == "work"
assert "token" not in sent
def test_send_command_uses_env_profile_for_local_transport(monkeypatch):
monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False)
monkeypatch.setenv("BROWSER_CLI_PROFILE", "work")
seen = {}
def fake_send_local(profile, payload, resolve_socket):
seen["profile"] = profile
seen["payload"] = json.loads(payload)
return json.dumps({"success": True, "data": "ok"}).encode("utf-8")
monkeypatch.setattr("browser_cli.client.targets.is_active_local_profile", lambda profile: True)
monkeypatch.setattr("browser_cli.client.core.local_transport.send_local_sync", fake_send_local)
assert send_command("tabs.list") == "ok"
assert seen["profile"] == "work"
assert seen["payload"]["command"] == "tabs.list"
async def _async_local_profile_result(monkeypatch):
seen = {}
async def fake_send_local(profile, payload, resolve_socket):
seen["profile"] = profile
return json.dumps({"success": True, "data": "ok"}).encode("utf-8")
monkeypatch.setattr("browser_cli.client.targets.is_active_local_profile", lambda profile: True)
monkeypatch.setattr("browser_cli.client.core.local_transport.send_local_async", fake_send_local)
result = await send_command_async("tabs.list")
return result, seen
def test_send_command_async_uses_env_profile_for_local_transport(monkeypatch):
monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False)
monkeypatch.setenv("BROWSER_CLI_PROFILE", "work")
result, seen = asyncio.run(_async_local_profile_result(monkeypatch))
assert result == "ok"
assert seen["profile"] == "work"
def test_send_command_prefers_active_local_profile_over_saved_remote_alias(monkeypatch, tmp_path):
monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False)
monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False)
socket_path = tmp_path / "work.sock"
socket_path.write_text("")
registry_path = tmp_path / "registry.json"
registry_path.write_text(json.dumps({"work": str(socket_path)}), encoding="utf-8")
monkeypatch.setattr("browser_cli.client.targets.REGISTRY_PATH", registry_path)
monkeypatch.setattr(
"browser_cli.client.remote_target_for_alias",
lambda alias: pytest.fail("active local profile must not trigger remote alias discovery"),
)
monkeypatch.setattr("browser_cli.client.targets.is_active_local_profile", lambda profile: True)
payload = json.dumps({"success": True, "data": "local-ok"}).encode("utf-8")
framed = len(payload).to_bytes(4, "little") + payload
class FakeSocket:
def __init__(self, *args, **kwargs):
self.sent = b""
self._response = bytearray(framed)
self.connected_to = None
def __enter__(self):
return self
def __exit__(self, *exc):
return False
def connect(self, path):
self.connected_to = path
def sendall(self, data):
self.sent += data
def recv(self, n):
chunk = bytes(self._response[:n])
del self._response[:n]
return chunk
monkeypatch.setattr("browser_cli.client.targets.socket.socket", lambda *args, **kwargs: FakeSocket())
assert send_command("tabs.list", profile="work") == "local-ok"
def test_send_command_resolves_browser_alias_to_remote_target(monkeypatch):
monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False)
monkeypatch.setenv("BROWSER_CLI_PROFILE", "host:work")
sent = {}
monkeypatch.setattr(
"browser_cli.client.core._remote_browser_targets",
lambda: [BrowserTarget("work", "host:work", "", remote="host:8765")],
)
def fake_send_remote(endpoint, msg, private_key=None):
sent["endpoint"] = endpoint
sent.update(msg)
return json.dumps({"success": True, "data": []}).encode("utf-8")
monkeypatch.setattr("browser_cli.client.core._send_remote", fake_send_remote)
assert send_command("tabs.list") == []
assert sent["endpoint"] == "host:8765"
assert sent["_route"] == "work"
assert "token" not in sent
def test_remote_target_for_alias_accepts_full_endpoint_profile(monkeypatch):
monkeypatch.setattr(
"browser_cli.client.core._remote_browser_targets",
lambda: [BrowserTarget("work", "host:work", "", remote="host:8765")],
)
target = remote_target_for_alias("host:8765:work")
assert target is not None
assert target.profile == "work"
assert target.remote == "host:8765"
def test_remote_target_for_alias_accepts_host_when_only_one_remote_target(monkeypatch):
remote_host = "browser-host.example"
remote_endpoint = f"{remote_host}:8765"
monkeypatch.setattr(
"browser_cli.client.core._remote_browser_targets",
lambda: [BrowserTarget("work", f"{remote_host}:work", "", remote=remote_endpoint)],
)
target = remote_target_for_alias(remote_host)
assert target is not None
assert target.profile == "work"
assert target.remote == remote_endpoint
def test_send_command_resolves_host_alias_to_single_remote_target(monkeypatch):
remote_host = "browser-host.example"
remote_endpoint = f"{remote_host}:8765"
monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False)
monkeypatch.setenv("BROWSER_CLI_PROFILE", remote_host)
sent = {}
monkeypatch.setattr(
"browser_cli.client.core._remote_browser_targets",
lambda: [BrowserTarget("work", f"{remote_host}:work", "", remote=remote_endpoint)],
)
def fake_send_remote(endpoint, msg, private_key=None):
sent["endpoint"] = endpoint
sent.update(msg)
return json.dumps({"success": True, "data": []}).encode("utf-8")
monkeypatch.setattr("browser_cli.client.core._send_remote", fake_send_remote)
assert send_command("tabs.list") == []
assert sent["endpoint"] == remote_endpoint
assert sent["_route"] == "work"
assert "token" not in sent
def test_remote_target_for_alias_reports_ambiguous_host_for_multiple_targets(monkeypatch):
monkeypatch.setattr(
"browser_cli.client.core._remote_browser_targets",
lambda: [
BrowserTarget("main", "host:main", "", remote="host:8765"),
BrowserTarget("work", "host:work", "", remote="host:8765"),
],
)
with pytest.raises(BrowserNotConnected) as exc:
remote_target_for_alias("host")
msg = str(exc.value)
assert "Multiple remote browser instances are active on host: main, work" in msg
assert "browser-cli --remote host:8765 --browser main" in msg
assert "browser-cli --browser host:work" in msg
def test_send_command_reports_ambiguous_remote_browser_alias(monkeypatch):
monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False)
monkeypatch.setenv("BROWSER_CLI_PROFILE", "host")
monkeypatch.setattr("browser_cli.client.targets.REGISTRY_PATH", Path("/nonexistent/browser-cli-registry.json"))
monkeypatch.setattr("browser_cli.client.targets.is_windows", lambda: False)
monkeypatch.setattr(
"browser_cli.client.core._remote_browser_targets",
lambda: [
BrowserTarget("main", "host:main", "", remote="host:8765"),
BrowserTarget("work", "host:work", "", remote="host:8765"),
],
)
with pytest.raises(BrowserNotConnected, match="Multiple remote browser instances are active on host: main, work"):
send_command("tabs.list")
def test_send_command_requires_browser_for_multiple_remote_targets(monkeypatch):
monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False)
monkeypatch.setattr(
"browser_cli.client.core.remote_browser_targets",
lambda endpoint, key=None: [
BrowserTarget("main", "host:main", "", remote=endpoint),
BrowserTarget("furry", "host:furry", "", remote=endpoint),
],
)
with pytest.raises(BrowserNotConnected, match="Multiple remote browser instances are active: main, furry"):
send_command("tabs.list", remote="host:8765")
def test_active_browser_targets_includes_remote_targets(monkeypatch, tmp_path):
remotes_path = tmp_path / "remotes.json"
endpoint = "browser-host.example:8765"
remotes_path.write_text(json.dumps({endpoint: {}}), encoding="utf-8")
monkeypatch.setattr("browser_cli.client.targets.REGISTRY_PATH", tmp_path / "missing-registry.json")
monkeypatch.setattr("browser_cli.remote.registry.REMOTE_REGISTRY_PATH", remotes_path)
def fake_send_command(command, args=None, profile=None, remote=None, key=None):
assert command == "browser-cli.targets"
assert remote == endpoint
return [{"profile": "work", "displayName": "work", "browserName": "Firefox"}]
monkeypatch.setattr("browser_cli.client.core.send_command", fake_send_command)
targets = active_browser_targets()
assert len(targets) == 1
assert targets[0].profile == "work"
assert targets[0].display_name == "browser-host.example:work"
assert targets[0].remote == endpoint
assert targets[0].browser_name == "Firefox"
assert targets[0].display_group == "browser-host.example"
def test_looks_like_domain():
assert _looks_like_domain("browsercli.yiprawr.dev") is True
assert _looks_like_domain("browser-host.example") is True
assert _looks_like_domain("sub.domain.org") is True
assert _looks_like_domain("localhost") is False
assert _looks_like_domain("127.0.0.1") is False
assert _looks_like_domain("203.0.113.100") is False
assert _looks_like_domain("host") is False # no dot
def test_normalize_endpoint_strips_443_for_domains():
assert _normalize_endpoint("browsercli.yiprawr.dev:443") == "browsercli.yiprawr.dev"
assert _normalize_endpoint("browsercli.yiprawr.dev") == "browsercli.yiprawr.dev"
assert _normalize_endpoint("203.0.113.1:443") == "203.0.113.1:443" # IP: keep port
assert _normalize_endpoint("localhost:443") == "localhost:443" # localhost: keep port
assert _normalize_endpoint("host:8765") == "host:8765" # non-443 port: unchanged
assert _normalize_endpoint("browsercli.yiprawr.dev:8765") == "browsercli.yiprawr.dev:8765"
def test_resolve_connect_endpoint_adds_443_for_domain():
assert _resolve_connect_endpoint("browsercli.yiprawr.dev") == "browsercli.yiprawr.dev:443"
assert _resolve_connect_endpoint("browsercli.yiprawr.dev:443") == "browsercli.yiprawr.dev:443"
assert _resolve_connect_endpoint("browsercli.yiprawr.dev:8765") == "browsercli.yiprawr.dev:8765"
assert _resolve_connect_endpoint("host:8765") == "host:8765"
def test_resolve_connect_endpoint_raises_for_bare_non_domain():
with pytest.raises(BrowserNotConnected, match="expected host:port"):
_resolve_connect_endpoint("localhost")
def test_send_command_normalizes_domain_port_443(monkeypatch):
"""--remote domain:443 is normalized; _send_remote gets the portless domain."""
monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False)
monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False)
sent_to = {}
monkeypatch.setattr(
"browser_cli.client.core.remote_browser_targets",
lambda endpoint, key=None: [BrowserTarget("default", f"{endpoint}:default", "", remote=endpoint)],
)
def fake_send_remote(endpoint, msg, private_key=None):
sent_to["endpoint"] = endpoint
return json.dumps({"success": True, "data": "ok"}).encode()
monkeypatch.setattr("browser_cli.client.core._send_remote", fake_send_remote)
result = send_command("tabs.list", remote="browsercli.yiprawr.dev:443")
assert result == "ok"
assert sent_to["endpoint"] == "browsercli.yiprawr.dev" # stored/routed without port
def test_send_command_domain_without_port_defaults_to_443(monkeypatch):
"""--remote domain (no port) is treated as :443."""
monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False)
monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False)
sent_to = {}
monkeypatch.setattr(
"browser_cli.client.core.remote_browser_targets",
lambda endpoint, key=None: [BrowserTarget("default", f"{endpoint}:default", "", remote=endpoint)],
)
def fake_send_remote(endpoint, msg, private_key=None):
sent_to["endpoint"] = endpoint
return json.dumps({"success": True, "data": "ok"}).encode()
monkeypatch.setattr("browser_cli.client.core._send_remote", fake_send_remote)
result = send_command("tabs.list", remote="browsercli.yiprawr.dev")
assert result == "ok"
assert sent_to["endpoint"] == "browsercli.yiprawr.dev"
def test_domain_display_name_omits_port(monkeypatch, tmp_path):
"""Domain endpoints stored without :443 display as 'domain:profile', not 'domain:443:profile'."""
remotes_path = tmp_path / "remotes.json"
endpoint = "browsercli.yiprawr.dev"
remotes_path.write_text(json.dumps({endpoint: {}}), encoding="utf-8")
monkeypatch.setattr("browser_cli.client.targets.REGISTRY_PATH", tmp_path / "missing-registry.json")
monkeypatch.setattr("browser_cli.remote.registry.REMOTE_REGISTRY_PATH", remotes_path)
def fake_send_command(command, args=None, profile=None, remote=None, key=None):
return [{"profile": "automatisation", "displayName": "automatisation"}]
monkeypatch.setattr("browser_cli.client.core.send_command", fake_send_command)
targets = active_browser_targets()
assert len(targets) == 1
assert targets[0].display_name == "browsercli.yiprawr.dev:automatisation"
assert targets[0].remote == endpoint
def test_domain_display_name_backward_compat_with_stored_443(monkeypatch, tmp_path):
"""Old remotes.json with :443 still displays cleanly without the port."""
remotes_path = tmp_path / "remotes.json"
endpoint = "browsercli.yiprawr.dev:443" # old format
remotes_path.write_text(json.dumps({endpoint: {}}), encoding="utf-8")
monkeypatch.setattr("browser_cli.client.targets.REGISTRY_PATH", tmp_path / "missing-registry.json")
monkeypatch.setattr("browser_cli.remote.registry.REMOTE_REGISTRY_PATH", remotes_path)
def fake_send_command(command, args=None, profile=None, remote=None, key=None):
return [{"profile": "automatisation", "displayName": "automatisation"}]
monkeypatch.setattr("browser_cli.client.core.send_command", fake_send_command)
targets = active_browser_targets()
assert len(targets) == 1
assert targets[0].display_name == "browsercli.yiprawr.dev:automatisation"
def test_send_command_auto_saves_and_reuses_key_for_remote(monkeypatch, tmp_path):
"""--key agent is saved on first use; omitting --key on subsequent calls reuses it."""
import json as _json
remotes_path = tmp_path / "remotes.json"
remotes_path.write_text("{}", encoding="utf-8")
monkeypatch.setattr("browser_cli.remote.registry.REMOTE_REGISTRY_PATH", remotes_path)
monkeypatch.setattr("browser_cli.client.targets.REGISTRY_PATH", tmp_path / "missing-registry.json")
monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False)
monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False)
monkeypatch.delenv("BROWSER_CLI_KEY", raising=False)
from pathlib import Path as _Path
used_keys = []
def fake_load_private_key(key_path=None):
used_keys.append(str(key_path) if key_path is not None else None)
return None # no actual key needed for this test
monkeypatch.setattr("browser_cli.client.auth.load_private_key", fake_load_private_key)
monkeypatch.setattr(
"browser_cli.client.core.remote_browser_targets",
lambda endpoint, key=None: [BrowserTarget("default", "host:default", "", remote=endpoint)],
)
def fake_send_remote(endpoint, msg, private_key=None):
return _json.dumps({"success": True, "data": "ok"}).encode()
monkeypatch.setattr("browser_cli.client.core._send_remote", fake_send_remote)
# First call with explicit --key agent
send_command("tabs.list", remote="host:8765", key=_Path("agent"))
assert used_keys[-1] == "agent"
# Key must be persisted now
assert key_for_remote("host:8765") == "agent"
# Second call without --key — should reuse saved "agent"
send_command("tabs.list", remote="host:8765")
assert used_keys[-1] == "agent"
# ── async command transport ──────────────────────────────────────────────────
def test_send_command_async_uses_async_remote_transport(monkeypatch):
monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False)
monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False)
sent = {}
async def fake_send_remote_async(endpoint, msg, private_key=None):
sent["endpoint"] = endpoint
sent.update(msg)
return json.dumps({"success": True, "data": "ok"}).encode()
monkeypatch.setattr("browser_cli.client.core._send_remote_async", fake_send_remote_async)
monkeypatch.setattr("browser_cli.client.core.remote_browser_targets_async", lambda endpoint, key=None: _async_targets(endpoint))
async def run():
return await send_command_async("tabs.list", remote="host:8765", key=None)
async def _async_targets(endpoint):
return [BrowserTarget("work", "host:work", "", remote=endpoint)]
assert asyncio.run(run()) == "ok"
assert sent["endpoint"] == "host:8765"
assert sent["_route"] == "work"
assert sent["user_agent"].startswith("browser-cli/")
def test_send_command_async_local_unix_roundtrip(monkeypatch, tmp_path):
monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False)
monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False)
sock_path = tmp_path / "browser.sock"
seen = []
async def run():
async def handle(reader, writer):
raw_len = await reader.readexactly(4)
msg_len = struct.unpack("<I", raw_len)[0]
payload = await reader.readexactly(msg_len)
seen.append(json.loads(payload))
response = json.dumps({"success": True, "data": "local-ok"}).encode()
writer.write(struct.pack("<I", len(response)) + response)
await writer.drain()
writer.close()
await writer.wait_closed()
server = await asyncio.start_unix_server(handle, path=str(sock_path))
monkeypatch.setattr("browser_cli.client.targets.resolve_socket", lambda profile=None: str(sock_path))
async with server:
result = await send_command_async("tabs.list", profile="work")
server.close()
await server.wait_closed()
return result
assert asyncio.run(run()) == "local-ok"
assert seen[0]["command"] == "tabs.list"
assert seen[0]["args"] == {}
def test_run_concurrent_preserves_order_and_surfaces_exceptions():
"""_run_concurrent mirrors input order and returns exceptions in-slot."""
from browser_cli.client.core import _run_concurrent
async def ok(value):
return value
async def boom():
raise RuntimeError("nope")
results = _run_concurrent([lambda: ok("a"), lambda: boom(), lambda: ok("c")])
assert results[0] == "a"
assert isinstance(results[1], RuntimeError)
assert results[2] == "c"
def test_run_concurrent_falls_back_when_loop_running():
"""Inside a running loop, _run_concurrent stays correct (sequential fallback)."""
from browser_cli.client.core import _run_concurrent
async def ok(value):
return value
async def driver():
# asyncio.run would raise here; _run_concurrent must fall back instead.
return _run_concurrent([lambda: ok(1), lambda: ok(2)])
assert asyncio.run(driver()) == [1, 2]
def test_remote_browser_targets_query_endpoints_concurrently(monkeypatch, tmp_path):
"""Multiple remotes are discovered in parallel, not serially."""
import threading
import time
remotes_path = tmp_path / "remotes.json"
remotes_path.write_text(
json.dumps({"host-a.example:8765": {}, "host-b.example:8765": {}, "host-c.example:8765": {}}),
encoding="utf-8",
)
monkeypatch.setattr("browser_cli.client.targets.REGISTRY_PATH", tmp_path / "missing-registry.json")
monkeypatch.setattr("browser_cli.remote.registry.REMOTE_REGISTRY_PATH", remotes_path)
active = 0
max_active = 0
lock = threading.Lock()
def fake_send_command(command, args=None, profile=None, remote=None, key=None, **kwargs):
nonlocal active, max_active
with lock:
active += 1
max_active = max(max_active, active)
time.sleep(0.1)
with lock:
active -= 1
return [{"profile": "work", "displayName": "work"}]
monkeypatch.setattr("browser_cli.client.core.send_command", fake_send_command)
targets = active_browser_targets()
assert len(targets) == 3
assert max_active >= 2, f"expected concurrent endpoint discovery, peak was {max_active}"
def test_collect_browser_clients_uses_cached_target_version(monkeypatch, tmp_path):
"""A remote target advertising version/extVersion skips the clients.list roundtrip."""
from browser_cli.client import collect_browser_clients
import browser_cli.client.core as core
monkeypatch.setattr("browser_cli.client.targets.REGISTRY_PATH", tmp_path / "missing-registry.json")
cached_target = BrowserTarget(
profile="work",
display_name="host.example:work",
socket_path="",
remote="host.example:8765",
browser_name="Firefox",
display_group="host.example",
version="151.0",
extension_version="0.15.6",
)
monkeypatch.setattr(core, "active_browser_targets", lambda **kw: [cached_target])
calls = []
monkeypatch.setattr(core, "send_command", lambda *a, **k: calls.append((a, k)) or [])
rows = collect_browser_clients(registry_path=tmp_path / "missing-registry.json")
assert calls == [] # no clients.list roundtrip was needed
assert rows == [{
"profile": "host.example:work",
"profileGroup": "host.example",
"name": "Firefox",
"version": "151.0",
"extensionVersion": "0.15.6",
}]
def test_collect_browser_clients_falls_back_when_version_unknown(monkeypatch, tmp_path):
"""An older remote (no advertised version) still triggers a clients.list query."""
from browser_cli.client import collect_browser_clients
import browser_cli.client.core as core
monkeypatch.setattr("browser_cli.client.targets.REGISTRY_PATH", tmp_path / "missing-registry.json")
legacy_target = BrowserTarget(
profile="work",
display_name="host.example:work",
socket_path="",
remote="host.example:8765",
browser_name="Firefox",
display_group="host.example",
)
monkeypatch.setattr(core, "active_browser_targets", lambda **kw: [legacy_target])
calls = []
def fake_send(command, args=None, profile=None, remote=None, key=None, **kw):
calls.append(command)
return [{"name": "Firefox", "version": "151.0", "extensionVersion": "0.15.2"}]
monkeypatch.setattr(core, "send_command", fake_send)
rows = collect_browser_clients(registry_path=tmp_path / "missing-registry.json")
assert calls == ["clients.list"] # fell back to a query
assert rows[0]["version"] == "151.0"