9b8cefcd72
- Add Firefox as an install target with native messaging manifest support. - Generate Firefox-specific extension packages with Gecko metadata and AMO-compatible manifest transforms. - Keep tab group commands available in Firefox through dynamic tab group API helpers. - Avoid Firefox linter warnings for static tab group API references and direct eval tokens. - Add Firefox packaging and installer regression coverage. - Bump the package and extension version to 0.15.1.
715 lines
28 KiB
Python
715 lines
28 KiB
Python
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
import os
|
|
import sys
|
|
|
|
from click.testing import CliRunner
|
|
from unittest.mock import patch
|
|
|
|
from browser_cli.cli import main, _project_version
|
|
from browser_cli.client import BrowserTarget
|
|
from browser_cli.markdown import _clean_markdown_output, _convert_html_to_markdown
|
|
|
|
def _expected_version() -> str:
|
|
pyproject = Path(__file__).resolve().parent.parent / "pyproject.toml"
|
|
for line in pyproject.read_text(encoding="utf-8").splitlines():
|
|
if line.startswith("version = "):
|
|
return line.split('"')[1]
|
|
raise AssertionError("version not found in pyproject.toml")
|
|
|
|
def test_short_version_option():
|
|
result = CliRunner().invoke(main, ["-V"])
|
|
assert result.exit_code == 0
|
|
assert result.output.strip() == _expected_version()
|
|
|
|
def test_long_version_option():
|
|
result = CliRunner().invoke(main, ["--version"])
|
|
assert result.exit_code == 0
|
|
assert result.output.strip() == _expected_version()
|
|
|
|
def test_project_version_falls_back_to_installed_package_metadata():
|
|
with patch("browser_cli.cli.Path.read_text", side_effect=OSError), patch(
|
|
"browser_cli.cli.package_version", return_value="9.9.9"
|
|
):
|
|
assert _project_version() == "9.9.9"
|
|
|
|
def test_clients_rename_uses_command_level_browser_target():
|
|
with patch("browser_cli.commands.clients.REGISTRY_PATH", Path("/nonexistent/browser-cli-registry.json")), patch(
|
|
"browser_cli.commands.clients.send_command"
|
|
) as send_command:
|
|
result = CliRunner().invoke(main, ["clients", "rename", "--browser", "old-id", "work"])
|
|
|
|
assert result.exit_code == 0
|
|
send_command.assert_called_once_with("clients.rename_profile", {"alias": "work"}, profile="old-id")
|
|
|
|
def test_clients_rename_uses_global_browser_target_when_set():
|
|
with patch("browser_cli.commands.clients.REGISTRY_PATH", Path("/nonexistent/browser-cli-registry.json")), patch(
|
|
"browser_cli.commands.clients.send_command"
|
|
) as send_command:
|
|
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="old-id")
|
|
assert "Restart the browser" not in result.output
|
|
|
|
def test_clients_rename_rejects_duplicate_alias(tmp_path):
|
|
registry_path = tmp_path / "registry.json"
|
|
registry_path.write_text('{"work": "/tmp/work.sock", "old-id": "/tmp/old-id.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, ["clients", "rename", "--browser", "old-id", "work"])
|
|
|
|
assert result.exit_code != 0
|
|
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")
|
|
|
|
with patch("browser_cli.commands.clients.REGISTRY_PATH", registry_path), patch("browser_cli.commands.clients.send_command") as send_command:
|
|
result = CliRunner().invoke(main, ["clients", "rename", "--browser", "work", "work"])
|
|
|
|
assert result.exit_code == 0
|
|
send_command.assert_called_once_with("clients.rename_profile", {"alias": "work"}, profile="work")
|
|
|
|
def test_install_help_lists_supported_browsers():
|
|
result = CliRunner().invoke(main, ["install", "--help"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "[chrome|chromium|brave|edge|vivaldi|firefox]" in result.output
|
|
|
|
def test_install_writes_testing_and_webstore_allowed_origins(tmp_path):
|
|
manifests = []
|
|
|
|
def fake_install_manifest(_browser, _host_exe, manifest):
|
|
manifests.append(manifest)
|
|
return [tmp_path / "com.browsercli.host.json"]
|
|
|
|
with patch("browser_cli.commands.install.native_host_exe", return_value=tmp_path / "browser-cli-native-host"), patch(
|
|
"browser_cli.commands.install.write_native_host_exe"
|
|
), patch("browser_cli.commands.install._install_manifest", side_effect=fake_install_manifest):
|
|
result = CliRunner().invoke(main, ["install", "brave"])
|
|
|
|
assert result.exit_code == 0
|
|
assert manifests == [
|
|
{
|
|
"name": "com.browsercli.host",
|
|
"description": "browser-cli native messaging host",
|
|
"path": str(tmp_path / "browser-cli-native-host"),
|
|
"type": "stdio",
|
|
"allowed_origins": [
|
|
"chrome-extension://bfpmkhngkjnfhabmfckgeohlilokodkg/",
|
|
"chrome-extension://hekaebjhbhhdbmakimmaklbblbmccahp/",
|
|
],
|
|
}
|
|
]
|
|
assert "Testing extension ID" in result.output
|
|
assert "Chrome Web Store extension ID" in result.output
|
|
|
|
def test_install_writes_firefox_allowed_extensions(tmp_path):
|
|
manifests = []
|
|
|
|
def fake_install_manifest(_browser, _host_exe, manifest):
|
|
manifests.append(manifest)
|
|
return [tmp_path / "com.browsercli.host.json"]
|
|
|
|
with patch("browser_cli.commands.install.native_host_exe", return_value=tmp_path / "browser-cli-native-host"), patch(
|
|
"browser_cli.commands.install.write_native_host_exe"
|
|
), patch("browser_cli.commands.install._install_manifest", side_effect=fake_install_manifest):
|
|
result = CliRunner().invoke(main, ["install", "firefox"])
|
|
|
|
assert result.exit_code == 0
|
|
assert manifests == [
|
|
{
|
|
"name": "com.browsercli.host",
|
|
"description": "browser-cli native messaging host",
|
|
"path": str(tmp_path / "browser-cli-native-host"),
|
|
"type": "stdio",
|
|
"allowed_extensions": ["browser-cli@yiprawr.dev"],
|
|
}
|
|
]
|
|
assert "about:debugging#/runtime/this-firefox" in result.output
|
|
assert "Firefox extension ID" in result.output
|
|
|
|
def test_install_windows_registers_native_host(tmp_path):
|
|
writes = []
|
|
|
|
class FakeKey:
|
|
def __init__(self, path):
|
|
self.path = path
|
|
def __enter__(self):
|
|
return self
|
|
def __exit__(self, _exc_type, _exc, _tb):
|
|
return False
|
|
|
|
fake_winreg = SimpleNamespace(
|
|
HKEY_CURRENT_USER="HKCU",
|
|
KEY_WRITE=0x20006,
|
|
KEY_WOW64_32KEY=0x0200,
|
|
KEY_WOW64_64KEY=0x0100,
|
|
REG_SZ=1,
|
|
CreateKeyEx=lambda _root, path, _reserved, _access: FakeKey(path),
|
|
SetValueEx=lambda key, name, _reserved, _reg_type, value: writes.append((key.path, name, value)),
|
|
)
|
|
|
|
host_exe = tmp_path / "browser-cli-native-host.exe"
|
|
with patch("browser_cli.commands.install.is_windows", return_value=True), patch(
|
|
"browser_cli.commands.install.native_host_exe", return_value=host_exe
|
|
), patch("browser_cli.commands.install.write_native_host_exe"), patch(
|
|
"browser_cli.commands.install.Path.write_text"
|
|
), patch.dict(sys.modules, {"winreg": fake_winreg}):
|
|
result = CliRunner().invoke(main, ["install", "edge"])
|
|
|
|
assert result.exit_code == 0
|
|
assert any("Software\\Microsoft\\Edge\\NativeMessagingHosts\\com.browsercli.host" in path for path, _, _ in writes)
|
|
|
|
def test_write_native_host_exe_unix(tmp_path):
|
|
from browser_cli.commands.install import write_native_host_exe
|
|
|
|
host = tmp_path / "libexec" / "browser-cli-native-host"
|
|
with patch("browser_cli.commands.install.is_windows", return_value=False):
|
|
write_native_host_exe(host)
|
|
|
|
assert host.exists()
|
|
content = host.read_text()
|
|
assert content.startswith(f"#!{sys.executable}")
|
|
assert "from browser_cli.native.host import main" in content
|
|
assert host.stat().st_mode & 0o111 # executable bit set
|
|
|
|
def test_write_native_host_exe_windows(tmp_path):
|
|
from browser_cli.commands.install import write_native_host_exe
|
|
|
|
host = tmp_path / "libexec" / "browser-cli-native-host.cmd"
|
|
with patch("browser_cli.commands.install.is_windows", return_value=True):
|
|
write_native_host_exe(host)
|
|
|
|
assert host.exists()
|
|
content = host.read_text(encoding="utf-8")
|
|
assert "@echo off" in content
|
|
assert "browser_cli.native.host" in content
|
|
|
|
def test_clients_exits_cleanly_when_registry_is_missing():
|
|
with patch("browser_cli.commands.clients.REGISTRY_PATH", Path("/nonexistent/browser-cli-registry.json")), patch(
|
|
"browser_cli.commands.clients.active_browser_targets", return_value=[]
|
|
):
|
|
result = CliRunner().invoke(main, ["clients"])
|
|
|
|
assert result.exit_code == 1
|
|
assert "No browser clients found" in result.output
|
|
|
|
def test_clients_without_remote_shows_saved_remotes_without_pq_warning(tmp_path):
|
|
from browser_cli.client import BrowserTarget
|
|
|
|
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")
|
|
|
|
def fake_send_command(command, args=None, profile=None, remote=None, key=None, suppress_pq_warning=False):
|
|
assert command == "clients.list"
|
|
if remote:
|
|
assert suppress_pq_warning is True
|
|
assert profile == "work"
|
|
return [{"profile": "work", "name": "Remote Chrome", "version": "1", "extensionVersion": "0.12.0"}]
|
|
assert profile == "main"
|
|
return [{"profile": "main", "name": "Chrome", "version": "1", "extensionVersion": "0.12.0"}]
|
|
|
|
def fake_active_browser_targets(suppress_pq_warning=False):
|
|
assert suppress_pq_warning is True
|
|
return [remote_target]
|
|
|
|
with patch("browser_cli.commands.clients.REGISTRY_PATH", registry_path), patch(
|
|
"browser_cli.commands.clients.send_command", side_effect=fake_send_command
|
|
), patch("browser_cli.commands.clients.active_browser_targets", side_effect=fake_active_browser_targets) as active_targets:
|
|
result = CliRunner().invoke(main, ["clients"])
|
|
|
|
assert result.exit_code == 0
|
|
active_targets.assert_called_once_with(suppress_pq_warning=True)
|
|
assert "Chrome" in result.output
|
|
assert "Remote Chrome" in result.output
|
|
assert "post-quantum" not in result.output
|
|
|
|
def test_clients_reads_registry_with_trailing_garbage(tmp_path):
|
|
registry_path = tmp_path / "registry.json"
|
|
registry_path.write_text('{"main": "/tmp/.browser_cli/main.sock"}"}', encoding="utf-8")
|
|
|
|
def fake_send_command(command, args=None, profile=None, remote=None, key=None):
|
|
assert command == "clients.list"
|
|
assert profile == "main"
|
|
return [{"profile": "main", "name": "Chrome", "version": "1", "extensionVersion": "0.8.2"}]
|
|
|
|
with patch("browser_cli.commands.clients.REGISTRY_PATH", registry_path), patch(
|
|
"browser_cli.commands.clients.send_command", side_effect=fake_send_command
|
|
), patch("browser_cli.commands.clients.active_browser_targets", return_value=[]):
|
|
result = CliRunner().invoke(main, ["clients"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "main" in result.output
|
|
assert "0.8.2" in result.output
|
|
|
|
def test_clients_remote_uses_remote_endpoint_without_local_registry():
|
|
def fake_send_command(command, args=None, profile=None, remote=None, key=None):
|
|
assert command == "clients.list"
|
|
assert profile is None
|
|
assert remote == "127.0.0.1:8765"
|
|
return [{"name": "Chrome", "version": "1", "extensionVersion": "2.3.4"}]
|
|
|
|
with patch.dict(os.environ, {}, clear=True), patch(
|
|
"browser_cli.commands.clients.REGISTRY_PATH", Path("/nonexistent/browser-cli-registry.json")
|
|
), patch("browser_cli.commands.clients.send_command", side_effect=fake_send_command) as send_command:
|
|
result = CliRunner().invoke(main, ["--remote", "127.0.0.1:8765", "clients"])
|
|
|
|
assert result.exit_code == 0
|
|
send_command.assert_called_once()
|
|
assert "remote" in result.output
|
|
assert "Chrome" in result.output
|
|
assert "2.3.4" in result.output
|
|
|
|
def test_clients_remote_respects_global_browser_route():
|
|
with patch.dict(os.environ, {}, clear=True), patch("browser_cli.commands.clients.send_command", return_value=[]) as send_command:
|
|
result = CliRunner().invoke(main, ["--remote", "127.0.0.1:8765", "--browser", "work", "clients"])
|
|
|
|
assert result.exit_code == 1
|
|
send_command.assert_called_once_with("clients.list", profile="work", remote="127.0.0.1:8765", key=None)
|
|
|
|
def test_clients_browser_alias_resolves_to_remote():
|
|
"""--browser <host> without --remote resolves the alias, fetches all targets from that remote,
|
|
and shows only clients from that host (not local profiles)."""
|
|
from browser_cli.client import BrowserTarget
|
|
|
|
resolved_target = BrowserTarget(
|
|
profile="automatisation",
|
|
display_name="browser-host.example:automatisation",
|
|
socket_path="",
|
|
remote="browser-host.example:8765",
|
|
)
|
|
all_remote_targets = [resolved_target]
|
|
|
|
def fake_send_command(command, args=None, profile=None, remote=None, key=None):
|
|
assert command == "clients.list"
|
|
assert profile == "automatisation"
|
|
assert remote == "browser-host.example:8765"
|
|
return [{"name": "Chrome", "version": "147.0.0.0", "extensionVersion": "0.8.5"}]
|
|
|
|
with patch.dict(os.environ, {}, clear=True), patch(
|
|
"browser_cli.commands.clients.remote_target_for_alias", return_value=resolved_target
|
|
), patch(
|
|
"browser_cli.commands.clients.remote_browser_targets", return_value=all_remote_targets
|
|
), patch("browser_cli.commands.clients.send_command", side_effect=fake_send_command) as send_command:
|
|
result = CliRunner().invoke(main, ["--browser", "browser-host.example", "clients"])
|
|
|
|
assert result.exit_code == 0
|
|
send_command.assert_called_once()
|
|
assert "Chrome" in result.output
|
|
assert "0.8.5" in result.output
|
|
|
|
def test_clients_shows_named_profile_and_uses_socket_uuid_for_default(tmp_path):
|
|
registry_path = tmp_path / "registry.json"
|
|
default_socket = tmp_path / "550e8400-e29b-41d4-a716-446655440000.sock"
|
|
work_socket = tmp_path / "work.sock"
|
|
registry_path.write_text(
|
|
'{"default": "%s", "work": "%s"}' % (default_socket, work_socket),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
responses = {
|
|
"default": [{"profile": "default", "name": "Chrome", "version": "1", "extensionVersion": "2.3.4"}],
|
|
"work": [{"profile": "default", "name": "Chrome", "version": "1", "extensionVersion": "2.3.4"}],
|
|
}
|
|
|
|
def fake_send_command(command, args=None, profile=None, remote=None, key=None):
|
|
assert command == "clients.list"
|
|
return responses[profile]
|
|
|
|
with patch("browser_cli.commands.clients.REGISTRY_PATH", registry_path), patch(
|
|
"browser_cli.commands.clients.send_command", side_effect=fake_send_command
|
|
), patch("browser_cli.commands.clients.active_browser_targets", return_value=[]):
|
|
result = CliRunner().invoke(main, ["clients"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "550e8400-e29b-41d4-a716-446655440000" in result.output
|
|
assert "work" in result.output
|
|
assert "Extension Version" in result.output
|
|
assert "2.3.4" in result.output
|
|
|
|
def test_tabs_list_multi_browser_shows_browser_column():
|
|
def fake_send_command(command, args=None, profile=None):
|
|
assert command == "tabs.list"
|
|
return [{"id": 1 if profile == "default" else 2, "windowId": 1, "active": True, "title": profile, "url": "https://example.com"}]
|
|
|
|
with patch(
|
|
"browser_cli.active_browser_targets",
|
|
return_value=[
|
|
BrowserTarget("default", "550e8400-e29b-41d4-a716-446655440000", "/tmp/default.sock"),
|
|
BrowserTarget("work", "work", "/tmp/work.sock"),
|
|
],
|
|
), patch("browser_cli.send_command", side_effect=fake_send_command):
|
|
result = CliRunner().invoke(main, ["tabs", "list"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Browser" in result.output
|
|
assert "550e8400-e29b-41d4-a716-446655440000" 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",
|
|
side_effect=AssertionError("local targets should not be used for explicit remote"),
|
|
), patch(
|
|
"browser_cli.remote_browser_targets",
|
|
return_value=[BrowserTarget("work", "remote-host:work", "", remote="remote-host:8765")],
|
|
), patch(
|
|
"browser_cli.send_command",
|
|
return_value=[{"id": 1, "windowId": 1, "active": True, "title": "Remote", "url": "https://example.com"}],
|
|
) as send_command:
|
|
result = CliRunner().invoke(main, ["--remote", "remote-host:8765", "tabs", "list"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "remote-host:work" in result.output
|
|
assert "Remote" in result.output
|
|
send_command.assert_called_once_with("tabs.list", {}, profile="work", remote="remote-host:8765", key=None)
|
|
|
|
def test_tabs_list_with_explicit_browser_does_not_show_browser_column():
|
|
with patch(
|
|
"browser_cli.active_browser_targets",
|
|
return_value=[
|
|
BrowserTarget("default", "uuid-1", "/tmp/default.sock"),
|
|
BrowserTarget("work", "work", "/tmp/work.sock"),
|
|
],
|
|
), patch(
|
|
"browser_cli.send_command",
|
|
return_value=[{"id": 1, "windowId": 1, "active": True, "title": "Example", "url": "https://example.com"}],
|
|
) as send_command:
|
|
result = CliRunner().invoke(main, ["--browser", "work", "tabs", "list"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Browser" not in result.output
|
|
send_command.assert_called_once_with("tabs.list", {}, profile="work", remote=None, key=None)
|
|
|
|
def test_tabs_count_multi_browser_shows_total():
|
|
counts = {"default": 3, "work": 4}
|
|
|
|
def fake_send_command(command, args=None, profile=None):
|
|
assert command == "tabs.count"
|
|
assert args == {"pattern": "github"}
|
|
return counts[profile]
|
|
|
|
with patch(
|
|
"browser_cli.active_browser_targets",
|
|
return_value=[
|
|
BrowserTarget("default", "uuid-1", "/tmp/default.sock"),
|
|
BrowserTarget("work", "work", "/tmp/work.sock"),
|
|
],
|
|
), patch("browser_cli.send_command", side_effect=fake_send_command):
|
|
result = CliRunner().invoke(main, ["tabs", "count", "github"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Browser" in result.output
|
|
assert "Total" in result.output
|
|
assert "7" in result.output
|
|
|
|
def test_group_count_multi_browser_shows_total():
|
|
counts = {"default": 1, "work": 2}
|
|
|
|
def fake_send_command(command, args=None, profile=None):
|
|
assert command == "group.count"
|
|
return counts[profile]
|
|
|
|
with patch(
|
|
"browser_cli.active_browser_targets",
|
|
return_value=[
|
|
BrowserTarget("default", "uuid-1", "/tmp/default.sock"),
|
|
BrowserTarget("work", "work", "/tmp/work.sock"),
|
|
],
|
|
), patch("browser_cli.send_command", side_effect=fake_send_command):
|
|
result = CliRunner().invoke(main, ["groups", "count"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Browser" in result.output
|
|
assert "Total" in result.output
|
|
assert "3" in result.output
|
|
|
|
def test_group_list_leaves_unnamed_group_cell_empty():
|
|
with patch(
|
|
"browser_cli.send_command",
|
|
return_value=[{"id": 42, "title": "", "color": "grey", "collapsed": False, "tabCount": 1}],
|
|
):
|
|
result = CliRunner().invoke(main, ["groups", "list"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "(unnamed)" not in result.output
|
|
assert "42" in result.output
|
|
assert "grey" in result.output
|
|
|
|
def test_tabs_move_accepts_right_short_alias():
|
|
with patch("browser_cli.send_command") as send_command:
|
|
result = CliRunner().invoke(main, ["tabs", "move", "12", "-r"])
|
|
|
|
assert result.exit_code == 0
|
|
send_command.assert_called_once_with(
|
|
"tabs.move",
|
|
{"tabId": 12, "forward": True, "backward": False, "groupId": None, "windowId": None, "index": None},
|
|
profile=None, remote=None, key=None,
|
|
)
|
|
|
|
def test_groups_move_accepts_left_short_alias():
|
|
with patch("browser_cli.send_command") as send_command:
|
|
result = CliRunner().invoke(main, ["groups", "move", "research", "-l"])
|
|
|
|
assert result.exit_code == 0
|
|
send_command.assert_called_once_with(
|
|
"group.move", {"group": "research", "forward": False, "backward": True}, profile=None, remote=None, key=None
|
|
)
|
|
|
|
def test_windows_list_multi_browser_shows_browser_column():
|
|
def fake_send_command(command, args=None, profile=None):
|
|
assert command == "windows.list"
|
|
return [{"id": 1, "alias": profile, "focused": True, "tabCount": 2, "state": "normal"}]
|
|
|
|
with patch(
|
|
"browser_cli.active_browser_targets",
|
|
return_value=[
|
|
BrowserTarget("default", "uuid-1", "/tmp/default.sock"),
|
|
BrowserTarget("work", "work", "/tmp/work.sock"),
|
|
],
|
|
), patch("browser_cli.send_command", side_effect=fake_send_command):
|
|
result = CliRunner().invoke(main, ["windows", "list"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Browser" in result.output
|
|
assert "Focused" not in result.output
|
|
assert "uuid-1" 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"
|
|
return [{"name": f"{profile}-session", "tabs": 2, "savedAt": 1712707200000}]
|
|
|
|
with patch(
|
|
"browser_cli.active_browser_targets",
|
|
return_value=[
|
|
BrowserTarget("default", "uuid-1", "/tmp/default.sock"),
|
|
BrowserTarget("work", "work", "/tmp/work.sock"),
|
|
],
|
|
), patch("browser_cli.send_command", side_effect=fake_send_command):
|
|
result = CliRunner().invoke(main, ["session", "list"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Browser" 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_with_explicit_browser_does_not_show_browser_column():
|
|
with patch(
|
|
"browser_cli.active_browser_targets",
|
|
return_value=[
|
|
BrowserTarget("default", "uuid-1", "/tmp/default.sock"),
|
|
BrowserTarget("work", "work", "/tmp/work.sock"),
|
|
],
|
|
), patch(
|
|
"browser_cli.send_command",
|
|
return_value=[{"name": "work-session", "tabs": 2, "savedAt": 1712707200000}],
|
|
) as send_command:
|
|
result = CliRunner().invoke(main, ["--browser", "work", "session", "list"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Browser" not in result.output
|
|
send_command.assert_called_once_with("session.list", {}, profile="work", remote=None, key=None)
|
|
|
|
def test_windows_open_passes_url():
|
|
with patch("browser_cli.send_command", return_value={"id": 7}) as send_command:
|
|
result = CliRunner().invoke(main, ["windows", "open", "https://example.com"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "https://example.com" in result.output
|
|
send_command.assert_called_once_with("windows.open", {"url": "https://example.com"}, profile=None, remote=None, key=None)
|
|
|
|
def test_extract_markdown_command():
|
|
with patch("browser_cli.send_command", return_value="# Title") as send_command:
|
|
result = CliRunner().invoke(main, ["extract", "markdown"])
|
|
|
|
assert result.exit_code == 0
|
|
assert result.output == "# Title\n"
|
|
send_command.assert_called_once_with("extract.markdown", {"selector": None}, profile=None, remote=None, key=None)
|
|
|
|
def test_extract_markdown_command_with_selector():
|
|
with patch("browser_cli.send_command", return_value="## Post") as send_command:
|
|
result = CliRunner().invoke(main, ["extract", "markdown", "--selector", "article"])
|
|
|
|
assert result.exit_code == 0
|
|
assert result.output == "## Post\n"
|
|
send_command.assert_called_once_with("extract.markdown", {"selector": "article"}, profile=None, remote=None, key=None)
|
|
|
|
def test_clean_markdown_output_removes_escaped_underscores_and_dashes():
|
|
assert _clean_markdown_output(r"hello\_world \- item") == "hello_world - item"
|
|
|
|
def test_clean_markdown_output_trims_useless_whitespace():
|
|
raw = " # Title \n\n\n paragraph with space \n next line\t \n"
|
|
assert _clean_markdown_output(raw) == "# Title\n\nparagraph with space\nnext line"
|
|
|
|
def test_clean_markdown_output_repairs_empty_table_header_rows():
|
|
raw = (
|
|
"| | | |\n"
|
|
"| --- | --- | --- |\n"
|
|
"| Bereich | Plan | Ist |\n"
|
|
"| A | B | C |\n"
|
|
)
|
|
assert _clean_markdown_output(raw) == (
|
|
"| Bereich | Plan | Ist |\n"
|
|
"| --- | --- | --- |\n"
|
|
"| A | B | C |"
|
|
)
|
|
|
|
def test_clean_markdown_output_preserves_graph_code_blocks():
|
|
raw = "```\n\nA\n │\n ▼\nB\n\n```"
|
|
assert _clean_markdown_output(raw) == "```\nA\n │\n ▼\nB\n```"
|
|
|
|
def test_clean_markdown_output_renders_code_block_list_branches():
|
|
raw = "```\nPlattformen\n- Omnifact\n- Open WebUI + Ollama\n- Le Chat\n```"
|
|
assert _clean_markdown_output(raw) == (
|
|
"```\n"
|
|
"Plattformen\n"
|
|
"├ Omnifact\n"
|
|
"├ Open WebUI + Ollama\n"
|
|
"└ Le Chat\n"
|
|
"```"
|
|
)
|
|
|
|
def test_clean_markdown_output_unflattens_graph_code_blocks():
|
|
raw = (
|
|
"```\n"
|
|
"Golden Set │ ▼Promptfoo(Testausführung) │ ▼UpTrain(Qualitätsbewertung) │ "
|
|
"▼Langfuse(Logging / Observability) │ ▼Plattformen├ Omnifact├ Open WebUI + Ollama└ Le Chat\n"
|
|
"```"
|
|
)
|
|
assert _clean_markdown_output(raw) == (
|
|
"```\n"
|
|
"Golden Set\n"
|
|
" │\n"
|
|
" ▼\n"
|
|
"Promptfoo\n"
|
|
"(Testausführung)\n"
|
|
" │\n"
|
|
" ▼\n"
|
|
"UpTrain\n"
|
|
"(Qualitätsbewertung)\n"
|
|
" │\n"
|
|
" ▼\n"
|
|
"Langfuse\n"
|
|
"(Logging / Observability)\n"
|
|
" │\n"
|
|
" ▼\n"
|
|
"Plattformen\n"
|
|
"├ Omnifact\n"
|
|
"├ Open WebUI + Ollama\n"
|
|
"└ Le Chat\n"
|
|
"```"
|
|
)
|
|
|
|
def test_extract_markdown_command_repairs_malformed_tables_and_code_blocks():
|
|
raw = (
|
|
"| | | |\n"
|
|
"| --- | --- | --- |\n"
|
|
"| Bereich | Plan | Ist |\n"
|
|
"| Eval-Stack | Testumgebung | funktionsfähig |\n\n"
|
|
"```\n"
|
|
"Golden Set │ ▼Promptfoo(Testausführung) │ ▼Plattformen├ Omnifact└ Le Chat\n"
|
|
"```"
|
|
)
|
|
with patch("browser_cli.send_command", return_value=raw):
|
|
result = CliRunner().invoke(main, ["extract", "markdown"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "| Bereich | Plan | Ist |" in result.output
|
|
assert "| | | |" not in result.output
|
|
assert "Golden Set\n │\n ▼\nPromptfoo\n(Testausführung)" in result.output
|
|
assert "├ Omnifact" in result.output
|
|
assert "└ Le Chat" in result.output
|
|
|
|
def test_convert_html_to_markdown_normalizes_blank_table_header_rows():
|
|
html = """
|
|
<main>
|
|
<table>
|
|
<tr><td></td><td></td><td></td><td></td></tr>
|
|
<tr><td>Risiko</td><td>Beschreibung</td><td>Auswirkung</td><td>Gegenmaßnahme</td></tr>
|
|
<tr><td>Datenschutz</td><td>X</td><td>Y</td><td>Z</td></tr>
|
|
</table>
|
|
</main>
|
|
"""
|
|
markdown = _convert_html_to_markdown(html)
|
|
assert "| Risiko | Beschreibung | Auswirkung | Gegenmaßnahme |" in markdown
|
|
assert "| | | | |" not in markdown
|
|
|
|
def test_convert_html_to_markdown_preserves_codemirror_graph_blocks():
|
|
html = """
|
|
<main>
|
|
<h1>Teil 5 - Eval-Stack Architektur</h1>
|
|
<div class="cm-editor" data-is-code-block-view="true" contenteditable="false">
|
|
<div class="cm-line">Golden Set</div>
|
|
<div class="cm-line"> │</div>
|
|
<div class="cm-line"> ▼</div>
|
|
<div class="cm-line">Promptfoo</div>
|
|
<div class="cm-line">(Testausführung)</div>
|
|
<div class="cm-line"> │</div>
|
|
<div class="cm-line"> ▼</div>
|
|
<div class="cm-line">Plattformen</div>
|
|
<div class="cm-line">- Omnifact</div>
|
|
<div class="cm-line">- Open WebUI + Ollama</div>
|
|
<div class="cm-line">- Le Chat</div>
|
|
</div>
|
|
</main>
|
|
"""
|
|
markdown = _convert_html_to_markdown(html)
|
|
assert "```\nGolden Set\n │\n ▼\nPromptfoo" in markdown
|
|
assert "├ Omnifact" in markdown
|
|
assert "└ Le Chat" in markdown
|
|
|
|
def test_convert_html_to_markdown_indents_multiline_list_items():
|
|
html = """
|
|
<main>
|
|
<h2>2. <strong>Zielarchitektur</strong></h2>
|
|
<ul>
|
|
<li>
|
|
<p>Unternehmensdaten → RAG → KI-Orchestrierung →<br>Local LLMs / API Modelle / Spezialmodelle</p>
|
|
</li>
|
|
</ul>
|
|
</main>
|
|
"""
|
|
markdown = _convert_html_to_markdown(html)
|
|
assert (
|
|
"- Unternehmensdaten → RAG → KI-Orchestrierung →\n"
|
|
" Local LLMs / API Modelle / Spezialmodelle"
|
|
) in markdown
|
|
|
|
def test_tabs_list_multi_browser_queries_remote_target():
|
|
endpoint = "browser-host.example:8765"
|
|
remote_target = BrowserTarget(
|
|
"work",
|
|
"browser-host.example:work",
|
|
"",
|
|
remote=endpoint,
|
|
)
|
|
|
|
with patch("browser_cli.active_browser_targets", return_value=[remote_target, BrowserTarget("local", "local", "/tmp/local.sock")]), patch(
|
|
"browser_cli.send_command",
|
|
return_value=[{"id": 1, "windowId": 1, "active": True, "title": "Remote", "url": "https://example.com"}],
|
|
) as send_command:
|
|
result = CliRunner().invoke(main, ["tabs", "list"])
|
|
|
|
assert result.exit_code == 0
|
|
send_command.assert_any_call("tabs.list", {}, profile="work", remote=endpoint, key=None)
|
|
assert "browser-host.example:work" in result.output
|