fix(clients): flatten scoped remote output
Testing / remote-protocol-compat (0.15.0) (push) Successful in 49s
Testing / remote-protocol-compat (0.16.0) (push) Successful in 52s
Testing / test (push) Successful in 49s

- Render explicit --remote clients results with profile-only labels.

- Suppress remote host group headers for scoped client queries.

- Keep global and mixed client listings grouped by host.

- Update client and CLI tests for scoped remote rendering.

- Bump browser-cli and extension versions to 0.16.5.
This commit is contained in:
2026-06-26 09:12:44 +02:00
parent 6270d8c956
commit 937c6a1ce0
6 changed files with 52 additions and 24 deletions
+11 -8
View File
@@ -151,21 +151,25 @@ def active_browser_targets(*, include_remotes: bool = True, key=None, suppress_p
targets.extend(_remote_browser_targets(key=key, suppress_pq_warning=suppress_pq_warning)) targets.extend(_remote_browser_targets(key=key, suppress_pq_warning=suppress_pq_warning))
return targets return targets
def _cached_client_row(target: BrowserTarget) -> dict | None: def _cached_client_row(target: BrowserTarget, *, scoped: bool = False) -> dict | None:
"""Build a clients row from a target's discovery data, skipping a roundtrip. """Build a clients row from a target's discovery data, skipping a roundtrip.
Returns None when the remote didn't advertise its version (older serve), so Returns None when the remote didn't advertise its version (older serve), so
callers fall back to an explicit ``clients.list`` query. callers fall back to an explicit ``clients.list`` query. When *scoped* is
true, the caller already selected one remote host, so render profile-only
labels instead of adding a host group header.
""" """
if target.version is None and target.extension_version is None: if target.version is None and target.extension_version is None:
return None return None
return { row = {
"profile": target.display_name, "profile": target.profile if scoped else target.display_name,
"profileGroup": target.display_group,
"name": target.browser_name or "", "name": target.browser_name or "",
"version": target.version or "", "version": target.version or "",
"extensionVersion": target.extension_version or "", "extensionVersion": target.extension_version or "",
} }
if target.display_group and not scoped:
row["profileGroup"] = target.display_group
return row
def _rows_from_result(result, label: str, profile_group: str | None) -> list[dict]: def _rows_from_result(result, label: str, profile_group: str | None) -> list[dict]:
rows = [] rows = []
@@ -249,18 +253,17 @@ def collect_browser_clients(
if targets: if targets:
uncached = [] uncached = []
for target in targets: for target in targets:
cached = _cached_client_row(target) cached = _cached_client_row(target, scoped=True)
if cached is not None: if cached is not None:
rows.append(cached) rows.append(cached)
else: else:
uncached.append(target) uncached.append(target)
results = _run_concurrent([ results = _run_concurrent([
(lambda t=t: _client_rows_async( (lambda t=t: _client_rows_async(
t.display_name, t.profile,
profile=t.profile, profile=t.profile,
remote=remote, remote=remote,
key=key, key=key,
profile_group=t.display_group,
)) ))
for t in uncached for t in uncached
]) ])
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "browser-cli", "name": "browser-cli",
"version": "0.16.4", "version": "0.16.5",
"description": "Control your browser from the terminal or Python SDK", "description": "Control your browser from the terminal or Python SDK",
"browser_specific_settings": { "browser_specific_settings": {
"gecko": { "gecko": {
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "real-browser-cli" name = "real-browser-cli"
version = "0.16.4" version = "0.16.5"
description = "Control your real running browser from the terminal or Python SDK" description = "Control your real running browser from the terminal or Python SDK"
readme = "README.md" readme = "README.md"
license = { file = "LICENSE" } license = { file = "LICENSE" }
+34 -11
View File
@@ -294,29 +294,52 @@ def test_clients_reads_registry_with_trailing_garbage(tmp_path):
assert "0.8.2" in result.output assert "0.8.2" in result.output
def test_clients_remote_uses_remote_endpoint_without_local_registry(): def test_clients_remote_uses_remote_endpoint_without_local_registry():
def fake_send_command(command, args=None, profile=None, remote=None, key=None): target = BrowserTarget(
assert command == "clients.list" profile="work",
assert profile is None display_name="127.0.0.1:work",
assert remote == "127.0.0.1:8765" socket_path="",
return [{"name": "Chrome", "version": "1", "extensionVersion": "2.3.4"}] remote="127.0.0.1:8765",
browser_name="Chrome",
display_group="127.0.0.1",
version="1",
extension_version="2.3.4",
)
with patch.dict(os.environ, {}, clear=True), patch( with patch.dict(os.environ, {}, clear=True), patch(
"browser_cli.commands.clients.REGISTRY_PATH", Path("/nonexistent/browser-cli-registry.json") "browser_cli.commands.clients.REGISTRY_PATH", Path("/nonexistent/browser-cli-registry.json")
), patch("browser_cli.client.core.send_command", side_effect=fake_send_command) as send_command: ), patch("browser_cli.client.core.remote_browser_targets", return_value=[target]), patch(
"browser_cli.client.core.send_command"
) as send_command:
result = CliRunner().invoke(main, ["--remote", "127.0.0.1:8765", "clients"]) result = CliRunner().invoke(main, ["--remote", "127.0.0.1:8765", "clients"])
assert result.exit_code == 0 assert result.exit_code == 0
send_command.assert_called_once() send_command.assert_not_called()
assert "remote" in result.output assert "work" in result.output
assert "127.0.0.1" not in result.output
assert "Chrome" in result.output assert "Chrome" in result.output
assert "2.3.4" in result.output assert "2.3.4" in result.output
def test_clients_remote_respects_global_browser_route(): def test_clients_remote_respects_global_browser_route():
with patch.dict(os.environ, {}, clear=True), patch("browser_cli.client.core.send_command", return_value=[]) as send_command: target = BrowserTarget(
profile="work",
display_name="127.0.0.1:work",
socket_path="",
remote="127.0.0.1:8765",
browser_name="Chrome",
display_group="127.0.0.1",
version="1",
extension_version="2.3.4",
)
with patch.dict(os.environ, {}, clear=True), patch(
"browser_cli.client.core.remote_browser_targets", return_value=[target]
), patch("browser_cli.client.core.send_command") as send_command:
result = CliRunner().invoke(main, ["--remote", "127.0.0.1:8765", "--browser", "work", "clients"]) result = CliRunner().invoke(main, ["--remote", "127.0.0.1:8765", "--browser", "work", "clients"])
assert result.exit_code == 1 assert result.exit_code == 0
send_command.assert_called_once_with("clients.list", profile="work", remote="127.0.0.1:8765", key=None) send_command.assert_not_called()
assert "work" in result.output
assert "127.0.0.1" not in result.output
def test_clients_browser_alias_resolves_to_remote(): def test_clients_browser_alias_resolves_to_remote():
"""--browser <host> without --remote resolves the alias, fetches all targets from that remote, """--browser <host> without --remote resolves the alias, fetches all targets from that remote,
+4 -2
View File
@@ -681,7 +681,8 @@ def test_collect_browser_clients_with_explicit_remote_lists_all_targets(monkeypa
rows = collect_browser_clients(remote="browser-host.example:8765", registry_path=tmp_path / "missing-registry.json") rows = collect_browser_clients(remote="browser-host.example:8765", registry_path=tmp_path / "missing-registry.json")
assert [row["profile"] for row in rows] == ["browser-host.example:main", "browser-host.example:work"] 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"] assert [row["name"] for row in rows] == ["Chrome", "Firefox"]
def test_collect_browser_clients_with_explicit_remote_and_browser_filters_target(monkeypatch, tmp_path): def test_collect_browser_clients_with_explicit_remote_and_browser_filters_target(monkeypatch, tmp_path):
@@ -696,7 +697,8 @@ def test_collect_browser_clients_with_explicit_remote_and_browser_filters_target
rows = collect_browser_clients(remote="browser-host.example:8765", browser_alias="work", registry_path=tmp_path / "missing-registry.json") 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] == ["browser-host.example:work"] 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): 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.""" """An older remote (no advertised version) still triggers a clients.list query."""
Generated
+1 -1
View File
@@ -474,7 +474,7 @@ wheels = [
[[package]] [[package]]
name = "real-browser-cli" name = "real-browser-cli"
version = "0.16.4" version = "0.16.5"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "click" }, { name = "click" },