import json from pathlib import Path import pytest from browser_cli.client import ( BrowserNotConnected, BrowserTarget, _resolve_socket, active_browser_targets, display_browser_name, save_remote_token, send_command, remote_target_for_alias, token_for_remote, ) from browser_cli.platform import endpoint_for_alias def test_resolve_socket_raises_when_registry_missing(monkeypatch): monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False) monkeypatch.setattr("browser_cli.client.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" socket_path.write_text("") registry_path = tmp_path / "registry.json" registry_path.write_text(json.dumps({"abc-uuid": str(socket_path)})) monkeypatch.setattr("browser_cli.client.REGISTRY_PATH", registry_path) assert _resolve_socket() == str(socket_path) 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_socket.write_text("") second_socket.write_text("") 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.REGISTRY_PATH", registry_path) with pytest.raises(BrowserNotConnected, match="Multiple browser instances are active: uuid-1, uuid-2"): _resolve_socket() 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.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" active_socket.write_text("") stale_socket = tmp_path / "stale.sock" 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.REGISTRY_PATH", registry_path) targets = active_browser_targets(include_remotes=False) 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.REGISTRY_PATH", registry_path) monkeypatch.setattr("browser_cli.client.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_save_remote_token_persists_per_endpoint(monkeypatch, tmp_path): remotes_path = tmp_path / "remotes.json" monkeypatch.setattr("browser_cli.client.REMOTE_REGISTRY_PATH", remotes_path) endpoint = "browser-host.example:8765" save_remote_token(endpoint, "secret-token") assert token_for_remote(endpoint) == "secret-token" assert json.loads(remotes_path.read_text(encoding="utf-8")) == { endpoint: {"token": "secret-token"} } def test_send_command_auto_routes_single_remote_target(monkeypatch): monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False) monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False) monkeypatch.delenv("BROWSER_CLI_TOKEN", raising=False) sent = {} monkeypatch.setattr( "browser_cli.client.remote_browser_targets", lambda endpoint, token=None: [BrowserTarget("work", "host:work", "", remote=endpoint, token=token)], ) def fake_send_remote(endpoint, framed): payload_len = int.from_bytes(framed[:4], "little") msg = json.loads(framed[4:4 + payload_len]) sent.update(msg) return json.dumps({"success": True, "data": "ok"}).encode("utf-8") monkeypatch.setattr("browser_cli.client._send_remote", fake_send_remote) assert send_command("tabs.list", remote="host:8765", token="secret") == "ok" assert sent["_route"] == "work" assert sent["token"] == "secret" def test_send_command_resolves_browser_alias_to_remote_target(monkeypatch): monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False) monkeypatch.delenv("BROWSER_CLI_TOKEN", raising=False) monkeypatch.setenv("BROWSER_CLI_PROFILE", "host:work") sent = {} monkeypatch.setattr( "browser_cli.client._remote_browser_targets", lambda: [BrowserTarget("work", "host:work", "", remote="host:8765", token="secret")], ) def fake_send_remote(endpoint, framed): payload_len = int.from_bytes(framed[:4], "little") msg = json.loads(framed[4:4 + payload_len]) sent["endpoint"] = endpoint sent.update(msg) return json.dumps({"success": True, "data": []}).encode("utf-8") monkeypatch.setattr("browser_cli.client._send_remote", fake_send_remote) assert send_command("tabs.list") == [] assert sent["endpoint"] == "host:8765" assert sent["_route"] == "work" assert sent["token"] == "secret" def test_remote_target_for_alias_accepts_full_endpoint_profile(monkeypatch): monkeypatch.setattr( "browser_cli.client._remote_browser_targets", lambda: [BrowserTarget("work", "host:work", "", remote="host:8765", token="secret")], ) target = remote_target_for_alias("host:8765:work") assert target is not None assert target.profile == "work" assert target.remote == "host:8765" def test_send_command_requires_browser_for_multiple_remote_targets(monkeypatch): monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False) monkeypatch.setattr( "browser_cli.client.remote_browser_targets", lambda endpoint, token=None: [ BrowserTarget("main", "host:main", "", remote=endpoint, token=token), BrowserTarget("furry", "host:furry", "", remote=endpoint, token=token), ], ) with pytest.raises(BrowserNotConnected, match="Multiple remote browser instances are active: main, furry"): send_command("tabs.list", remote="host:8765", token="secret") 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: {"token": "secret-token"}}), encoding="utf-8") monkeypatch.setattr("browser_cli.client.REGISTRY_PATH", tmp_path / "missing-registry.json") monkeypatch.setattr("browser_cli.client.REMOTE_REGISTRY_PATH", remotes_path) def fake_send_command(command, args=None, profile=None, remote=None, token=None): assert command == "browser-cli.targets" assert remote == endpoint assert token == "secret-token" return [{"profile": "work", "displayName": "work"}] monkeypatch.setattr("browser_cli.client.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].token == "secret-token"