mirror of
https://github.com/TencentCloud/Octop.git
synced 2026-10-16 05:39:20 +08:00
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>
397 lines
14 KiB
Python
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
|