fix: prevent browser target and focus surprises
Testing / remote-protocol-compat (0.9.5) (push) Successful in 57s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 1m1s
Testing / test (push) Successful in 1m7s

- Respect the globally selected browser when renaming client aliases.
- Pass the resolved local profile into sync and async local transports so
  BROWSER_CLI_PROFILE is honored consistently.
- Stop tabs.active from explicitly focusing the OS browser window, avoiding
  virtual-desktop jumps during tab activation.
- Make window merging skip audible, unmuted windows so video playback windows
  are not selected as merge targets.
- Bump the Python package and extension manifest versions to 0.12.2.
- Add regression coverage for browser selection and focus-stealing behavior.
This commit is contained in:
2026-06-14 13:00:33 +02:00
parent e1c495d82d
commit 509f1387de
10 changed files with 85 additions and 20 deletions
+11 -1
View File
@@ -49,7 +49,7 @@ def test_clients_rename_uses_global_browser_target_when_set():
result = CliRunner().invoke(main, ["--browser", "old-id", "clients", "rename", "work"])
assert result.exit_code == 0
send_command.assert_called_once_with("clients.rename_profile", {"alias": "work"}, profile=None)
send_command.assert_called_once_with("clients.rename_profile", {"alias": "work"}, profile="old-id")
assert "Restart the browser" not in result.output
def test_clients_rename_rejects_duplicate_alias(tmp_path):
@@ -63,6 +63,16 @@ def test_clients_rename_rejects_duplicate_alias(tmp_path):
assert "Browser alias 'work' already exists" in result.output
send_command.assert_not_called()
def test_clients_rename_duplicate_check_uses_global_browser_target(tmp_path):
registry_path = tmp_path / "registry.json"
registry_path.write_text('{"work": "/tmp/work.sock"}', encoding="utf-8")
with patch("browser_cli.commands.clients.REGISTRY_PATH", registry_path), patch("browser_cli.commands.clients.send_command") as send_command:
result = CliRunner().invoke(main, ["--browser", "work", "clients", "rename", "work"])
assert result.exit_code == 0
send_command.assert_called_once_with("clients.rename_profile", {"alias": "work"}, profile="work")
def test_clients_rename_allows_same_alias_for_same_target(tmp_path):
registry_path = tmp_path / "registry.json"
registry_path.write_text('{"work": "/tmp/work.sock"}', encoding="utf-8")
+38
View File
@@ -132,6 +132,44 @@ def test_send_command_auto_routes_single_remote_target(monkeypatch):
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)
@@ -111,6 +111,15 @@ def test_large_extension_operations_yield_between_batches():
assert "perf.set_profile" in perf
assert "__background" in connection
def test_tab_activation_and_merge_do_not_steal_audible_video_window():
tabs = (ROOT / "extension" / "src" / "commands" / "tabs.ts").read_text()
assert "await chrome.windows.update(tab.windowId, { focused: true });" not in tabs
assert "windowHasAudibleTabs" in tabs
assert "!this.windowHasAudibleTabs(w)" in tabs
assert "skippedAudibleWindows" in tabs
assert "const target = movableWindows.find(w => w.focused) || movableWindows[0];" in tabs
def test_session_autosave_is_debounced_and_non_overlapping():
# The autosave lifecycle moved out of session.ts into a dedicated
# AutoSaveManager (autosave.ts) during the structure refactor; the shared