Files
Octop/tests/unit/browser/test_browser_setup.py
2209cdc90f chore(deps): switch runtime packages to octop-* 1.0.0 (#1136)
Replace orcakit-harness-agent / harness-* with octop-harness, octop-gateway, octop-memory, and octop-browser, and align docs, UI copy, and generated paths.

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-25 06:44:24 +08:00

397 lines
14 KiB
Python

"""Tests for browser profile prep / uninstall helpers."""
from __future__ import annotations
import json
import os
from pathlib import Path
from stat import S_IMODE
import pytest
from octop.infra.browser.setup import (
_probe_dir_writable,
_relocated_profiles_root_for_uid,
_runtime_dir_for_uid,
_temp_scope_token,
chrome_source_for_path,
clear_profile_locks,
ensure_chrome_runtime_env,
ensure_profile_writable,
recover_stale_profile,
resolve_browser_display,
uninstall_browser_stream,
)
posix_only = pytest.mark.skipif(os.name != "posix", reason="POSIX-only runtime dirs")
def test_configure_browser_idle_timeout_updates_harness(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from octop_browser.settings import settings as hb_settings
from octop.infra.utils.browser_media import configure_browser_idle_timeout
previous = getattr(hb_settings, "idle_timeout_minutes", None)
try:
configure_browser_idle_timeout(17)
assert os.environ["BROWSER_USE_IDLE_TIMEOUT_MINUTES"] == "17"
assert hb_settings.idle_timeout_minutes == 17.0
finally:
if previous is None:
delattr(hb_settings, "idle_timeout_minutes")
else:
hb_settings.idle_timeout_minutes = previous
monkeypatch.delenv("BROWSER_USE_IDLE_TIMEOUT_MINUTES", raising=False)
def test_clear_profile_locks_removes_singleton_files(tmp_path: Path) -> None:
profile = tmp_path / "default"
profile.mkdir()
(profile / "SingletonLock").write_text("lock")
(profile / "SingletonCookie").write_text("cookie")
expected = {"SingletonLock", "SingletonCookie"}
try:
(profile / "SingletonSocket").symlink_to(tmp_path / "nonexistent-socket")
expected.add("SingletonSocket")
except OSError:
(profile / "SingletonSocket").write_text("socket")
expected.add("SingletonSocket")
cleared = clear_profile_locks(profile)
assert set(cleared) == expected
assert not (profile / "SingletonLock").exists()
assert not (profile / "SingletonCookie").exists()
assert not (profile / "SingletonSocket").exists()
def test_chrome_source_classifies_playwright_and_system(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
from octop.infra.browser import setup as browser_setup
cache = tmp_path / "ms-playwright"
pw_chrome = (
cache / "chromium-123" / "chrome-mac" / "Chromium.app" / "Contents" / "MacOS" / "Chromium"
)
pw_chrome.parent.mkdir(parents=True)
pw_chrome.write_text("x")
monkeypatch.setattr(browser_setup, "_playwright_cache_roots", lambda: [cache])
assert chrome_source_for_path(str(pw_chrome)) == "playwright"
assert chrome_source_for_path("/usr/bin/google-chrome") == "system"
@pytest.mark.asyncio
async def test_uninstall_only_removes_playwright_chromium(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
from octop.infra.browser import setup as browser_setup
cache = tmp_path / "ms-playwright"
cdir = cache / "chromium-999"
cdir.mkdir(parents=True)
(cdir / "chrome").write_text("bin")
harness = tmp_path / ".harness-browser" / "profiles" / "default"
harness.mkdir(parents=True)
(harness / "Preferences").write_text("{}")
async def _close() -> int:
return 0
monkeypatch.setattr(browser_setup, "_playwright_cache_roots", lambda: [cache])
monkeypatch.setattr(browser_setup, "_profiles_root", lambda: harness.parent)
monkeypatch.setattr(browser_setup, "_close_harness_registry", _close)
events: list[dict[str, object]] = []
async for chunk in uninstall_browser_stream():
assert chunk.startswith("data: ")
events.append(json.loads(chunk[6:]))
assert any(e.get("done") and e.get("success") for e in events)
assert not cdir.exists()
assert (harness / "Preferences").exists(), "harness profile must remain"
def test_recover_stale_profile_renames_and_recreates(tmp_path: Path) -> None:
profile = tmp_path / "default"
profile.mkdir()
(profile / "Preferences").write_text("{}")
(profile / "SingletonLock").write_text("stale")
recover_stale_profile(profile)
assert profile.is_dir()
assert not (profile / "Preferences").exists()
stale_dirs = list(tmp_path.glob("default.stale-*"))
assert len(stale_dirs) == 1
assert (stale_dirs[0] / "Preferences").exists()
@posix_only
def test_ensure_chrome_runtime_env_uses_platform_runtime_dir(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("XDG_RUNTIME_DIR", "/run/user/0")
path = ensure_chrome_runtime_env()
# Linux uses /tmp/<uid>; other POSIX (e.g. macOS) uses tempfile + pid.
assert path == _runtime_dir_for_uid()
assert os.environ["XDG_RUNTIME_DIR"] == str(path)
assert path.is_dir()
assert os.access(path, os.W_OK | os.X_OK)
assert S_IMODE(path.stat().st_mode) == 0o700
def test_non_linux_temp_dirs_use_gettempdir_with_stable_token(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Windows must not use a literal /tmp path (drive-root PermissionError)."""
from octop.infra.browser import setup as browser_setup
fake_tmp = tmp_path / "WinTemp"
fake_tmp.mkdir()
monkeypatch.setattr(browser_setup.sys, "platform", "win32")
monkeypatch.setattr(browser_setup.tempfile, "gettempdir", lambda: str(fake_tmp))
runtime = _runtime_dir_for_uid(uid=7)
profiles = _relocated_profiles_root_for_uid(uid=7)
assert runtime == fake_tmp / "runtime-octop-browser-7"
assert profiles == fake_tmp / "octop-browser-profiles-7"
# Same inputs → same paths across "process restarts" (no pid in the name).
assert _runtime_dir_for_uid(uid=7) == runtime
assert _relocated_profiles_root_for_uid(uid=7) == profiles
def test_temp_scope_token_uses_username_when_getuid_missing(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from octop.infra.browser import setup as browser_setup
# Simulate Windows: getuid absent / not callable.
# Real Windows has no os.getuid — setattr(raising=True) would raise.
if hasattr(browser_setup.os, "getuid"):
monkeypatch.setattr(browser_setup.os, "getuid", object())
monkeypatch.setenv("USERNAME", "OctopUser")
monkeypatch.delenv("USER", raising=False)
assert _temp_scope_token() == "OctopUser"
assert _temp_scope_token(uid=3) == "3"
def test_probe_dir_writable_skips_symlink_on_non_linux(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
from octop.infra.browser import setup as browser_setup
monkeypatch.setattr(browser_setup.sys, "platform", "win32")
def _forbid_symlink(self: Path, *args: object, **kwargs: object) -> None:
raise AssertionError("symlink probe must not run on non-Linux")
monkeypatch.setattr(Path, "symlink_to", _forbid_symlink)
target = tmp_path / "profile"
assert _probe_dir_writable(target) is True
assert target.is_dir()
@posix_only
def test_resolve_browser_display_uses_x_socket(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
from octop.infra.browser import setup as browser_setup
monkeypatch.setattr(browser_setup.sys, "platform", "linux")
sock_dir = tmp_path / "X11"
sock_dir.mkdir()
(sock_dir / "X99").touch()
monkeypatch.setattr(
browser_setup,
"_x11_socket_path",
lambda display: (
sock_dir / f"X{display.lstrip(':').split('.')[0]}" if display.startswith(":") else None
),
)
monkeypatch.delenv("DISPLAY", raising=False)
monkeypatch.setattr(
"octop.infra.desktop.setup._display_from_env_file",
lambda: ":99",
)
display = resolve_browser_display()
assert display == ":99"
assert os.environ["DISPLAY"] == ":99"
@posix_only
def test_resolve_browser_display_clears_stale_display(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Stale $DISPLAY without an X socket must not force headed Chrome."""
from octop.infra.browser import setup as browser_setup
monkeypatch.setattr(browser_setup.sys, "platform", "linux")
sock_dir = tmp_path / "X11"
sock_dir.mkdir()
monkeypatch.setattr(
browser_setup,
"_x11_socket_path",
lambda display: (
sock_dir / f"X{display.lstrip(':').split('.')[0]}" if display.startswith(":") else None
),
)
monkeypatch.setenv("DISPLAY", ":0")
monkeypatch.setattr(
"octop.infra.desktop.setup._display_from_env_file",
lambda: None,
)
assert resolve_browser_display() is None
assert "DISPLAY" not in os.environ
def test_ensure_profile_writable_recreates_when_not_writable(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
from octop.infra.browser import setup as browser_setup
profile = tmp_path / "default"
profile.mkdir()
(profile / "Preferences").write_text("{}")
if os.name == "posix":
os.chmod(profile, 0o000)
monkeypatch.setattr(browser_setup, "_under_root_home", lambda _p: False)
try:
result = ensure_profile_writable(profile)
assert result == profile or "octop-browser-profiles" in str(result)
assert _probe_dir_writable(result)
finally:
if profile.exists():
os.chmod(profile, 0o700)
else:
assert ensure_profile_writable(profile) == profile
@posix_only
def test_ensure_profile_writable_relocates_root_home(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
from octop.infra.browser import setup as browser_setup
profile = tmp_path / "under-root" / "default"
profile.mkdir(parents=True)
(profile / "Preferences").write_text("{}")
monkeypatch.setattr(browser_setup.sys, "platform", "linux")
monkeypatch.setattr(browser_setup, "_under_root_home", lambda _p: True)
monkeypatch.setattr(
browser_setup,
"_relocate_profiles_root",
lambda p: tmp_path / "relocated" / p.name,
)
(tmp_path / "relocated" / "default").mkdir(parents=True)
result = ensure_profile_writable(profile)
assert result == tmp_path / "relocated" / "default"
@posix_only
def test_ensure_profile_writable_never_chmods_system_paths(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Regression for #84: profile prep must not chmod / or /tmp."""
from octop.infra.browser import setup as browser_setup
chmod_targets: list[str] = []
real_chmod = os.chmod
def tracking_chmod(path: str | bytes | os.PathLike[str], mode: int) -> None:
text = os.fspath(path)
chmod_targets.append(text)
if text in ("/", "/tmp"):
raise AssertionError(f"must not chmod system path {path}")
return real_chmod(path, mode)
monkeypatch.setattr(os, "chmod", tracking_chmod)
monkeypatch.setattr(browser_setup, "_under_root_home", lambda _p: False)
monkeypatch.setattr(browser_setup, "_probe_dir_writable", lambda _p: True)
profile = Path(f"/tmp/octop-browser-profiles-{os.getuid()}") / "default"
ensure_profile_writable(profile)
# Profile prep no longer chmod/chown at all — recreate/relocate only.
assert chmod_targets == []
def test_octop_browser_profiles_dir_shared(tmp_path: Path) -> None:
from octop.infra.utils.browser_media import (
BROWSER_PROFILES_REL,
octop_browser_profiles_dir,
)
from octop.infra.utils.paths import PathLayout
paths = PathLayout(root=tmp_path / "octop")
dest = octop_browser_profiles_dir(paths)
assert dest == paths.root / BROWSER_PROFILES_REL
assert dest.is_dir()
# Shared root — not under any agent workspace.
assert "agents" not in dest.parts
def test_configure_browser_profiles_dir_sets_env(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
from octop.infra.utils.browser_media import configure_browser_profiles_dir
monkeypatch.delenv("BROWSER_USE_PROFILES_DIR", raising=False)
root = tmp_path / "browser-profiles"
result = configure_browser_profiles_dir(root)
assert result == root.resolve()
assert root.is_dir()
assert os.environ["BROWSER_USE_PROFILES_DIR"] == str(root.resolve())
def test_legacy_profiles_migrated_once(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
from octop.infra.utils import browser_media as media
from octop.infra.utils.paths import PathLayout
# Do not rely on HOME/USERPROFILE — Path.home() differs on Windows.
legacy_root = tmp_path / "legacy-profiles"
legacy = legacy_root / "default"
legacy.mkdir(parents=True)
(legacy / "Preferences").write_text("{}", encoding="utf-8")
monkeypatch.setattr(media, "legacy_harness_profiles_dir", lambda: legacy_root)
paths = PathLayout(root=tmp_path / "octop")
dest = media.octop_browser_profiles_dir(paths)
assert (dest / "default" / "Preferences").read_text(encoding="utf-8") == "{}"
assert not legacy.exists()
# Second call must not fail when legacy is empty/gone.
media.octop_browser_profiles_dir(paths)
assert (dest / "default" / "Preferences").exists()
@posix_only
def test_under_root_home_exempts_octop_home(monkeypatch: pytest.MonkeyPatch) -> None:
from octop.infra.browser import setup as browser_setup
from octop.infra.utils.paths import PathLayout
if not Path("/root").is_dir():
pytest.skip("/root not available")
monkeypatch.setattr(browser_setup.sys, "platform", "linux")
def _fake_from_env() -> PathLayout:
return PathLayout(root=Path("/root/.octop"))
monkeypatch.setattr(
"octop.infra.utils.paths.PathLayout.from_env",
staticmethod(_fake_from_env),
)
assert browser_setup._under_root_home(Path("/root/.octop/browser-profiles/default")) is False
assert browser_setup._under_root_home(Path("/root/.harness-browser/profiles/default")) is True