feat: group multi-browser output by source
Testing / remote-protocol-compat (0.9.3) (push) Successful in 52s
Testing / test (push) Successful in 1m2s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 1m0s
Package Extension / package-extension (push) Successful in 1m11s
Build & Publish Package / publish (push) Successful in 1m7s

- Add browser source grouping metadata to SDK-created tabs, groups,
  list results, and aggregate count results.
- Render grouped local/remote browser tables consistently for clients,
  tabs, groups, windows, sessions, and remote status output.
- Document remote control, auth, HTTP gateway usage, and the refreshed
  project structure in the README.
- Add coverage for grouped output and BrowserCounts browser_groups.
- Bump the Python package, extension manifest, and lockfile to 0.15.6.
- Add a just publish helper for building and publishing release artifacts.
This commit is contained in:
2026-06-18 00:52:04 +02:00
parent 479a0f1964
commit 8dece7800f
19 changed files with 540 additions and 270 deletions
+35 -6
View File
@@ -559,12 +559,37 @@ class TestTabs:
mock_send.side_effect = [3, 4]
result = b.tabs.count("github")
assert result == BrowserCounts(total=7, by_browser={"uuid-1": 3, "work": 4})
assert result == BrowserCounts(
total=7,
by_browser={"uuid-1": 3, "work": 4},
browser_groups={"uuid-1": "local", "work": "local"},
)
assert mock_send.call_args_list == [
call("tabs.count", {"pattern": "github"}, profile="default"),
call("tabs.count", {"pattern": "github"}, profile="work"),
]
def test_tabs_count_multi_browser_keeps_remote_display_groups(self, b, mock_send):
with patch(
"browser_cli.active_browser_targets",
return_value=[
BrowserTarget("main", "browser-host.example:main", "", remote="browser-host.example:8765", display_group="browser-host.example"),
BrowserTarget("work", "browser-host.example:work", "", remote="browser-host.example:8765", display_group="browser-host.example"),
],
):
mock_send.side_effect = [1, 2]
result = b.tabs.count()
assert result == BrowserCounts(
total=3,
by_browser={"browser-host.example:main": 1, "browser-host.example:work": 2},
browser_groups={"browser-host.example:main": "browser-host.example", "browser-host.example:work": "browser-host.example"},
)
assert mock_send.call_args_list == [
call("tabs.count", {"pattern": None}, profile="main", remote="browser-host.example:8765", key=None),
call("tabs.count", {"pattern": None}, profile="work", remote="browser-host.example:8765", key=None),
]
def test_tabs_query(self, b, mock_send):
mock_send.return_value = [TAB_DATA]
result = b.tabs.query("example")
@@ -713,7 +738,11 @@ class TestGroups:
mock_send.side_effect = [2, 5]
result = b.groups.count()
assert result == BrowserCounts(total=7, by_browser={"uuid-1": 2, "work": 5})
assert result == BrowserCounts(
total=7,
by_browser={"uuid-1": 2, "work": 5},
browser_groups={"uuid-1": "local", "work": "local"},
)
def test_group_query(self, b, mock_send):
mock_send.return_value = [GROUP_DATA]
@@ -772,8 +801,8 @@ class TestWindows:
result = b.windows.list()
assert result == [
{"id": 1, "tabCount": 2, "state": "normal", "browser": "uuid-1"},
{"id": 2, "tabCount": 3, "state": "maximized", "browser": "work"},
{"id": 1, "tabCount": 2, "state": "normal", "browser": "uuid-1", "browserGroup": "local"},
{"id": 2, "tabCount": 3, "state": "maximized", "browser": "work", "browserGroup": "local"},
]
def test_windows_open_without_url(self, b, mock_send):
@@ -907,8 +936,8 @@ class TestSession:
result = b.session.list()
assert result == [
{"name": "first", "tabs": 2, "savedAt": 1712707200000, "browser": "uuid-1"},
{"name": "second", "tabs": 5, "savedAt": 1712707300000, "browser": "work"},
{"name": "first", "tabs": 2, "savedAt": 1712707200000, "browser": "uuid-1", "browserGroup": "local"},
{"name": "second", "tabs": 5, "savedAt": 1712707300000, "browser": "work", "browserGroup": "local"},
]
assert mock_send.call_args_list == [
call("session.list", {}, profile="default"),
+135 -1
View File
@@ -217,7 +217,13 @@ def test_clients_without_remote_shows_saved_remotes_without_pq_warning(tmp_path)
registry_path = tmp_path / "registry.json"
registry_path.write_text('{"main": "/tmp/.browser_cli/main.sock"}', encoding="utf-8")
remote_target = BrowserTarget("work", "browser-host.example:work", "", remote="browser-host.example:8765")
remote_target = BrowserTarget(
"work",
"browser-host.example:work",
"",
remote="browser-host.example:8765",
display_group="browser-host.example",
)
def fake_send_command(command, args=None, profile=None, remote=None, key=None, suppress_pq_warning=False):
assert command == "clients.list"
@@ -239,7 +245,11 @@ def test_clients_without_remote_shows_saved_remotes_without_pq_warning(tmp_path)
assert result.exit_code == 0
active_targets.assert_called_once_with(suppress_pq_warning=True)
assert "local" in result.output
assert "Chrome" in result.output
assert "browser-host.example" in result.output
assert "browser-host.example:work" not in result.output
assert "work" in result.output
assert "Remote Chrome" in result.output
assert "post-quantum" not in result.output
@@ -362,9 +372,33 @@ def test_tabs_list_multi_browser_shows_browser_column():
assert result.exit_code == 0
assert "Browser" in result.output
assert "local" in result.output
assert "550e8400-e29b-41d4-a716-446655440000" in result.output
assert "work" in result.output
def test_tabs_list_unscoped_groups_remote_targets_by_host():
targets = [
BrowserTarget("main", "browser-host.example:main", "", remote="browser-host.example:8765", display_group="browser-host.example"),
BrowserTarget("work", "browser-host.example:work", "", remote="browser-host.example:8765", display_group="browser-host.example"),
]
def fake_send_command(command, args=None, profile=None, remote=None, key=None):
assert command == "tabs.list"
assert remote == "browser-host.example:8765"
return [{"id": 1, "windowId": 1, "active": True, "title": profile, "url": "https://example.com"}]
with patch("browser_cli.active_browser_targets", return_value=targets), patch(
"browser_cli.send_command", side_effect=fake_send_command
):
result = CliRunner().invoke(main, ["tabs", "list"])
assert result.exit_code == 0
assert "browser-host.example" in result.output
assert "browser-host.example:main" not in result.output
assert "browser-host.example:work" not in result.output
assert "main" in result.output
assert "work" in result.output
def test_tabs_list_with_remote_uses_only_remote_targets():
with patch(
"browser_cli.active_browser_targets",
@@ -495,9 +529,36 @@ def test_tabs_count_multi_browser_shows_total():
assert result.exit_code == 0
assert "Browser" in result.output
assert "local" in result.output
assert "Total" in result.output
assert "7" in result.output
def test_tabs_count_unscoped_groups_remote_targets_by_host():
counts = {"main": 1, "work": 2}
def fake_send_command(command, args=None, profile=None, remote=None, key=None):
assert command == "tabs.count"
assert remote == "browser-host.example:8765"
return counts[profile]
with patch(
"browser_cli.active_browser_targets",
return_value=[
BrowserTarget("main", "browser-host.example:main", "", remote="browser-host.example:8765", display_group="browser-host.example"),
BrowserTarget("work", "browser-host.example:work", "", remote="browser-host.example:8765", display_group="browser-host.example"),
],
), patch("browser_cli.send_command", side_effect=fake_send_command):
result = CliRunner().invoke(main, ["tabs", "count"])
assert result.exit_code == 0
assert "browser-host.example" in result.output
assert "browser-host.example:main" not in result.output
assert "browser-host.example:work" not in result.output
assert "main" in result.output
assert "work" in result.output
assert "Total" in result.output
assert "3" in result.output
def test_group_count_multi_browser_shows_total():
counts = {"default": 1, "work": 2}
@@ -519,6 +580,29 @@ def test_group_count_multi_browser_shows_total():
assert "Total" in result.output
assert "3" in result.output
def test_groups_list_unscoped_groups_remote_targets_by_host():
targets = [
BrowserTarget("main", "browser-host.example:main", "", remote="browser-host.example:8765", display_group="browser-host.example"),
BrowserTarget("work", "browser-host.example:work", "", remote="browser-host.example:8765", display_group="browser-host.example"),
]
def fake_send_command(command, args=None, profile=None, remote=None, key=None):
assert command == "group.list"
assert remote == "browser-host.example:8765"
return [{"id": 1, "title": profile, "color": "blue", "collapsed": False, "tabCount": 2}]
with patch("browser_cli.active_browser_targets", return_value=targets), patch(
"browser_cli.send_command", side_effect=fake_send_command
):
result = CliRunner().invoke(main, ["groups", "list"])
assert result.exit_code == 0
assert "browser-host.example" in result.output
assert "browser-host.example:main" not in result.output
assert "browser-host.example:work" not in result.output
assert "main" in result.output
assert "work" in result.output
def test_group_list_leaves_unnamed_group_cell_empty():
with patch(
"browser_cli.send_command",
@@ -567,10 +651,34 @@ def test_windows_list_multi_browser_shows_browser_column():
assert result.exit_code == 0
assert "Browser" in result.output
assert "local" in result.output
assert "Focused" not in result.output
assert "uuid-1" in result.output
assert "work" in result.output
def test_windows_list_unscoped_groups_remote_targets_by_host():
targets = [
BrowserTarget("main", "browser-host.example:main", "", remote="browser-host.example:8765", display_group="browser-host.example"),
BrowserTarget("work", "browser-host.example:work", "", remote="browser-host.example:8765", display_group="browser-host.example"),
]
def fake_send_command(command, args=None, profile=None, remote=None, key=None):
assert command == "windows.list"
assert remote == "browser-host.example:8765"
return [{"id": 1, "alias": profile, "focused": True, "tabCount": 2, "state": "normal"}]
with patch("browser_cli.active_browser_targets", return_value=targets), patch(
"browser_cli.send_command", side_effect=fake_send_command
):
result = CliRunner().invoke(main, ["windows", "list"])
assert result.exit_code == 0
assert "browser-host.example" in result.output
assert "browser-host.example:main" not in result.output
assert "browser-host.example:work" not in result.output
assert "main" in result.output
assert "work" in result.output
def test_session_list_multi_browser_shows_browser_column():
def fake_send_command(command, args=None, profile=None):
assert command == "session.list"
@@ -587,11 +695,37 @@ def test_session_list_multi_browser_shows_browser_column():
assert result.exit_code == 0
assert "Browser" in result.output
assert "local" in result.output
assert "uuid-1" in result.output
assert "work" in result.output
assert "default-session" in result.output
assert "work-session" in result.output
def test_session_list_unscoped_groups_remote_targets_by_host():
targets = [
BrowserTarget("main", "browser-host.example:main", "", remote="browser-host.example:8765", display_group="browser-host.example"),
BrowserTarget("work", "browser-host.example:work", "", remote="browser-host.example:8765", display_group="browser-host.example"),
]
def fake_send_command(command, args=None, profile=None, remote=None, key=None):
assert command == "session.list"
assert remote == "browser-host.example:8765"
return [{"name": f"{profile}-session", "tabs": 2, "savedAt": 1712707200000}]
with patch("browser_cli.active_browser_targets", return_value=targets), patch(
"browser_cli.send_command", side_effect=fake_send_command
):
result = CliRunner().invoke(main, ["session", "list"])
assert result.exit_code == 0
assert "browser-host.example" in result.output
assert "browser-host.example:main" not in result.output
assert "browser-host.example:work" not in result.output
assert "main" in result.output
assert "work" in result.output
assert "main-session" in result.output
assert "work-session" in result.output
def test_session_list_with_explicit_browser_does_not_show_browser_column():
with patch(
"browser_cli.active_browser_targets",