Files
hermes-agent/tests/hermes_cli/test_browser_connect_default_chromium.py
ethernet 082f89ceaf fix(registries): normalize scope keys on write paths; repair windows test gaps
The scope-normalization sweep only fixed the read paths
(list_providers/get_provider/registry_generation) in most registries;
the write paths (register/snapshot/restore) still stored under the raw
scope string while reads resolved through hermes_home_key — so a
scoped registration was written under one key and read under another
(agent/terminal_env_registry.py test_scoped_registration_isolated
failed on both Linux and Windows CI).

- add hermes_constants.normalize_scope(scope): hermes_home_key for
  non-None, None preserved for the process-global layer — one named
  function instead of the expression repeated at ~20 call sites
- apply it on both sides of all nine scoped registries
  (browser, terminal_env, image_gen, transcription, tts, video_gen,
  web_search, secret_sources, dashboard_auth, tools.registry)
- agent/deadline: MAX_SAFE_TIMEOUT_S on win32 leaves 60s headroom under
  the DWORD-ms ceiling so margin-adding consumers (human-wait +60s)
  stay platform-safe; tools/approval fail-closed fallback mirrors it
- tools/approval: strip shell escapes everywhere except inside
  drive-anchored paths (the Windows shell is bash, so r\m still spells
  rm; only C:\... backslashes must survive deobfuscation)
- consolidate the duplicated _rmtree_force into hermes_cli/fs_utils
  and repoint both callers
- hermes_cli/kanban + disk-cleanup: shlex.split(posix=False) keeps
  wrapping quotes in tokens; strip them before dispatch/matching
- test gating: SIGKILL durability conformance cells are linux_only
  (TerminateProcess is not SIGKILL), macOS TCC anchor symlink-tree
  classes are linux_only, Linux chromium profile tests resolve the
  fixture home via expanduser patch + posix-joined expecteds so they
  run identically on every host
2026-08-31 17:52:31 -04:00

187 lines
7.6 KiB
Python

"""Default-Chromium detection and profile-dir resolution (hermes_cli.browser_connect).
These exercise the parsers with real command output shapes instead of
patching the detectors themselves, so a change in what macOS / xdg report is
caught here rather than in a user's browser session.
"""
from unittest.mock import patch
import pytest
import posixpath
import hermes_cli.browser_connect as bc
def _ls_dump(*entries: str) -> str:
return "(\n" + ",\n".join(entries) + "\n)\n"
def _handler(scheme: str, bundle: str) -> str:
return (
" {\n"
" LSHandlerPreferredVersions = {\n"
' LSHandlerRoleAll = "-";\n'
" };\n"
f' LSHandlerRoleAll = "{bundle}";\n'
f" LSHandlerURLScheme = {scheme};\n"
" }"
)
def _content_type_handler(uti: str, bundle: str) -> str:
return (
" {\n"
f' LSHandlerContentType = "{uti}";\n'
f' LSHandlerRoleViewer = "{bundle}";\n'
" }"
)
class TestLaunchServicesHttpsHandler:
def test_https_entry_wins_over_other_schemes(self):
dump = _ls_dump(
_handler("ftp", "com.google.chrome"),
_handler("https", "com.apple.safari"),
)
assert bc._launchservices_https_handler(dump) == "com.apple.safari"
def test_content_type_registration_is_not_an_https_handler(self):
dump = _ls_dump(_content_type_handler("public.html", "com.google.chrome"))
assert bc._launchservices_https_handler(dump) is None
def test_no_entries_means_no_recorded_handler(self):
assert bc._launchservices_https_handler("(\n)\n") is None
assert bc._launchservices_https_handler("") is None
def test_nested_dictionary_does_not_split_the_entry(self):
dump = _ls_dump(_handler("https", "com.microsoft.edgemac"))
assert bc._launchservices_https_handler(dump) == "com.microsoft.edgemac"
class TestDetectDefaultDarwin:
def _run_with(self, dump: str):
class _Proc:
stdout = dump
return patch.object(bc.subprocess, "run", return_value=_Proc())
def test_chrome_as_https_handler(self):
with self._run_with(_ls_dump(_handler("https", "com.google.chrome"))):
assert bc._detect_default_darwin() == "chrome"
def test_safari_default_with_chrome_installed_fails_closed(self):
"""The old fallback returned the first installed Chromium app; a
non-Chromium default must resolve to None even when Chrome exists."""
dump = _ls_dump(
_handler("https", "com.apple.safari"),
_handler("ftp", "com.google.chrome"),
)
with self._run_with(dump), \
patch.object(bc, "chromium_executable", return_value="/Applications/Google Chrome.app/x"):
assert bc._detect_default_darwin() is None
def test_no_handler_recorded_fails_closed(self):
with self._run_with("(\n)\n"), \
patch.object(bc, "chromium_executable", return_value="/Applications/Google Chrome.app/x"):
assert bc._detect_default_darwin() is None
def test_firefox_default_fails_closed(self):
with self._run_with(_ls_dump(_handler("https", "org.mozilla.firefox"))):
assert bc._detect_default_darwin() is None
def test_reader_failure_fails_closed(self):
with patch.object(bc.subprocess, "run", side_effect=OSError("no defaults")):
assert bc._detect_default_darwin() is None
@pytest.mark.parametrize(
"bundle,expected",
[
("com.google.Chrome", "chrome"),
("com.brave.Browser", "brave"),
("com.brave.Browser.origin", "brave-origin"),
("com.microsoft.edgemac", "edge"),
("org.chromium.Chromium", "chromium"),
("com.brave.Browser.origin.beta", bc.UNSUPPORTED_CHANNEL),
("com.brave.Browser.origin.nightly", bc.UNSUPPORTED_CHANNEL),
],
)
def test_bundle_map(self, bundle, expected):
with self._run_with(_ls_dump(_handler("https", bundle))):
assert bc._detect_default_darwin() == expected
class TestDetectDefaultLinux:
def _run_with(self, output: str):
class _Proc:
stdout = output
return patch.object(bc.subprocess, "run", return_value=_Proc())
@pytest.mark.parametrize(
"desktop,expected",
[
("google-chrome.desktop", "chrome"),
("com.google.Chrome.desktop", "chrome"),
("chromium_chromium.desktop", "chromium"),
("org.chromium.Chromium.desktop", "chromium"),
("brave-browser.desktop", "brave"),
("com.brave.Browser.desktop", "brave"),
("brave-origin.desktop", "brave-origin"),
("brave-origin-beta.desktop", bc.UNSUPPORTED_CHANNEL),
("brave-origin-nightly.desktop", bc.UNSUPPORTED_CHANNEL),
("microsoft-edge.desktop", "edge"),
("com.microsoft.Edge.desktop", "edge"),
("firefox.desktop", None),
("org.mozilla.firefox.desktop", None),
("", None),
],
)
def test_xdg_desktop_names(self, desktop, expected):
with self._run_with(desktop + "\n"):
assert bc._detect_default_linux() == expected
def test_missing_xdg_settings_fails_closed(self):
with patch.object(bc.subprocess, "run", side_effect=FileNotFoundError("xdg-settings")):
assert bc._detect_default_linux() is None
class TestLinuxProfileDir:
def _env(self, monkeypatch, home):
# The code under test resolves the user's home via
# os.path.expanduser("~"), which reads HOME on POSIX but USERPROFILE
# on Windows — patch expanduser to a POSIX-FORM home so the
# Linux-target path resolution is exercised identically on every
# host (posixpath.join only inserts '/' between components; a
# backslash-drive home would leak host separators into the result).
monkeypatch.setattr(bc.os.path, "expanduser", lambda _p: home.as_posix())
monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
def test_native_path_when_nothing_exists(self, tmp_path, monkeypatch):
self._env(monkeypatch, tmp_path)
assert bc.real_profile_data_dir("chromium", "Linux") == posixpath.join(tmp_path.as_posix(), ".config", "chromium")
def test_snap_chromium_profile_is_found(self, tmp_path, monkeypatch):
self._env(monkeypatch, tmp_path)
snap = tmp_path / "snap" / "chromium" / "common" / "chromium"
snap.mkdir(parents=True)
assert bc.real_profile_data_dir("chromium", "Linux") == snap.as_posix()
def test_flatpak_chrome_profile_is_found(self, tmp_path, monkeypatch):
self._env(monkeypatch, tmp_path)
flatpak = tmp_path / ".var" / "app" / "com.google.Chrome" / "config" / "google-chrome"
flatpak.mkdir(parents=True)
assert bc.real_profile_data_dir("chrome", "Linux") == flatpak.as_posix()
def test_native_profile_wins_when_present(self, tmp_path, monkeypatch):
self._env(monkeypatch, tmp_path)
native = tmp_path / ".config" / "BraveSoftware" / "Brave-Browser"
native.mkdir(parents=True)
(tmp_path / ".var" / "app" / "com.brave.Browser" / "config" / "BraveSoftware" / "Brave-Browser").mkdir(parents=True)
assert bc.real_profile_data_dir("brave", "Linux") == native.as_posix()
def test_xdg_config_home_is_honoured(self, tmp_path, monkeypatch):
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setenv("XDG_CONFIG_HOME", "/home/t/.config")
assert bc.real_profile_data_dir("edge", "Linux") == "/home/t/.config/microsoft-edge"