Files
browser-cli/tests/test_client.py
T
daniel156161 1b32410575
Testing / remote-protocol-compat (0.16.0) (push) Successful in 40s
Testing / remote-protocol-compat (0.15.0) (push) Successful in 42s
Testing / test (push) Successful in 49s
Isolate the remote registry in tests
Endpoint resolution consults remembered remotes, so tests that assert the
bare-domain :443 default failed on any machine with an explicit-port remote
remembered for the same host, while passing in CI. That made a real behavior
difference look like a local-only flake.

Isolate the registry for the whole suite instead of patching the two affected
tests, since any test touching endpoint resolution has the same hidden
dependency. Tests that need remembered entries still override the path.

Also replace the hardcoded personal remote host with a documentation domain.
2026-08-09 20:45:56 +02:00

732 lines
28 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("browser-host.example") 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("browser-host.example:443") == "browser-host.example"
assert _normalize_endpoint("browser-host.example") == "browser-host.example"
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("browser-host.example:8765") == "browser-host.example:8765"
def test_resolve_connect_endpoint_adds_443_for_domain():
assert _resolve_connect_endpoint("browser-host.example") == "browser-host.example:443"
assert _resolve_connect_endpoint("browser-host.example:443") == "browser-host.example:443"
assert _resolve_connect_endpoint("browser-host.example:8765") == "browser-host.example: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="browser-host.example:443")
assert result == "ok"
assert sent_to["endpoint"] == "browser-host.example" # 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="browser-host.example")
assert result == "ok"
assert sent_to["endpoint"] == "browser-host.example"
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 = "browser-host.example"
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 == "browser-host.example: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 = "browser-host.example: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 == "browser-host.example:automatisation"
def test_send_command_explicit_key_does_not_persist_remote_key(monkeypatch, tmp_path):
"""--key is a one-shot override; use `browser-cli remote trust` to remember 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)
send_command("tabs.list", remote="host:8765", key=_Path("agent"))
assert used_keys[-1] == "agent"
assert key_for_remote("host:8765") is None
send_command("tabs.list", remote="host:8765")
assert used_keys[-1] is None
# ── 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_with_explicit_remote_lists_all_targets(monkeypatch, tmp_path):
"""`browser-cli --remote host clients` should list all profiles, not auto-route and fail as ambiguous."""
from browser_cli.client import collect_browser_clients
import browser_cli.client.core as core
targets = [
BrowserTarget(
profile="main",
display_name="browser-host.example:main",
socket_path="",
remote="browser-host.example:8765",
browser_name="Chrome",
display_group="browser-host.example",
version="149.0.0.0",
extension_version="0.16.4",
),
BrowserTarget(
profile="work",
display_name="browser-host.example:work",
socket_path="",
remote="browser-host.example:8765",
browser_name="Firefox",
display_group="browser-host.example",
version="151.0",
extension_version="0.16.4",
),
]
monkeypatch.setattr(core, "remote_browser_targets", lambda endpoint, key=None: targets)
monkeypatch.setattr(core, "send_command", lambda *a, **k: pytest.fail("clients.list must not auto-route for cached targets"))
rows = collect_browser_clients(remote="browser-host.example:8765", registry_path=tmp_path / "missing-registry.json")
assert [row["profile"] for row in rows] == ["main", "work"]
assert [row.get("profileGroup") for row in rows] == [None, None]
assert [row["name"] for row in rows] == ["Chrome", "Firefox"]
def test_collect_browser_clients_with_explicit_remote_and_browser_filters_target(monkeypatch, tmp_path):
from browser_cli.client import collect_browser_clients
import browser_cli.client.core as core
targets = [
BrowserTarget("main", "browser-host.example:main", "", remote="browser-host.example:8765", version="1"),
BrowserTarget("work", "browser-host.example:work", "", remote="browser-host.example:8765", version="1"),
]
monkeypatch.setattr(core, "remote_browser_targets", lambda endpoint, key=None: targets)
rows = collect_browser_clients(remote="browser-host.example:8765", browser_alias="work", registry_path=tmp_path / "missing-registry.json")
assert [row["profile"] for row in rows] == ["work"]
assert rows[0].get("profileGroup") is None
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"