161 lines
6.3 KiB
Python
161 lines
6.3 KiB
Python
"""Install-method detection, data-bucket cartography, and service discovery."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from opensquilla.uninstall import inventory
|
|
from opensquilla.uninstall.inventory import (
|
|
METHOD_DESKTOP,
|
|
METHOD_DOCKER,
|
|
METHOD_PIP,
|
|
METHOD_PIPX,
|
|
METHOD_PORTABLE,
|
|
METHOD_SOURCE,
|
|
METHOD_UNKNOWN,
|
|
METHOD_UV_TOOL,
|
|
DataBucket,
|
|
build_data_buckets,
|
|
)
|
|
|
|
|
|
def _isolate_detection(monkeypatch) -> None:
|
|
"""Neutralize ambient signals so each detection test is deterministic."""
|
|
for var in (
|
|
"OPENSQUILLA_INSTALL_METHOD",
|
|
"OPENSQUILLA_DESKTOP",
|
|
"OPENSQUILLA_RUNNING_IN_CONTAINER",
|
|
):
|
|
monkeypatch.delenv(var, raising=False)
|
|
monkeypatch.setattr(inventory, "_docker_image_install", lambda: False)
|
|
monkeypatch.setattr(inventory, "_portable_venv_dir", lambda: None)
|
|
monkeypatch.setattr(inventory, "_is_editable_install", lambda: False)
|
|
monkeypatch.setattr(inventory, "_venv_ancestry", lambda: None)
|
|
monkeypatch.setattr(inventory, "_has_distribution", lambda: True)
|
|
|
|
|
|
def test_detect_docker_image_layout(monkeypatch) -> None:
|
|
_isolate_detection(monkeypatch)
|
|
monkeypatch.setattr(inventory, "_docker_image_install", lambda: True)
|
|
monkeypatch.setattr(inventory, "_is_editable_install", lambda: True) # ignored under docker
|
|
assert inventory.detect_install_method() == METHOD_DOCKER
|
|
|
|
|
|
def test_running_in_container_env_is_docker(monkeypatch) -> None:
|
|
_isolate_detection(monkeypatch)
|
|
monkeypatch.setenv("OPENSQUILLA_RUNNING_IN_CONTAINER", "1")
|
|
assert inventory.detect_install_method() == METHOD_DOCKER
|
|
|
|
|
|
def test_pip_install_in_plain_container_is_not_docker(monkeypatch) -> None:
|
|
# A devcontainer/CI pip install (has /.dockerenv but normal home, no signal)
|
|
# must NOT be misdetected as docker — otherwise removal is wrongly refused.
|
|
_isolate_detection(monkeypatch) # _docker_image_install -> False, has dist -> True
|
|
assert inventory.detect_install_method() == METHOD_PIP
|
|
|
|
|
|
def test_detect_desktop_env(monkeypatch) -> None:
|
|
_isolate_detection(monkeypatch)
|
|
monkeypatch.setenv("OPENSQUILLA_DESKTOP", "1")
|
|
assert inventory.detect_install_method() == METHOD_DESKTOP
|
|
|
|
|
|
def test_detect_portable_marker(monkeypatch) -> None:
|
|
_isolate_detection(monkeypatch)
|
|
monkeypatch.setattr(inventory, "_portable_venv_dir", lambda: Path("/x/.venv-abc"))
|
|
assert inventory.detect_install_method() == METHOD_PORTABLE
|
|
|
|
|
|
def test_detect_source_editable(monkeypatch) -> None:
|
|
_isolate_detection(monkeypatch)
|
|
monkeypatch.setattr(inventory, "_is_editable_install", lambda: True)
|
|
assert inventory.detect_install_method() == METHOD_SOURCE
|
|
|
|
|
|
def test_detect_uv_then_pipx_then_pip(monkeypatch) -> None:
|
|
_isolate_detection(monkeypatch)
|
|
monkeypatch.setattr(inventory, "_venv_ancestry", lambda: METHOD_UV_TOOL)
|
|
assert inventory.detect_install_method() == METHOD_UV_TOOL
|
|
monkeypatch.setattr(inventory, "_venv_ancestry", lambda: METHOD_PIPX)
|
|
assert inventory.detect_install_method() == METHOD_PIPX
|
|
monkeypatch.setattr(inventory, "_venv_ancestry", lambda: None)
|
|
assert inventory.detect_install_method() == METHOD_PIP
|
|
|
|
|
|
def test_detect_unknown_when_no_distribution(monkeypatch) -> None:
|
|
_isolate_detection(monkeypatch)
|
|
monkeypatch.setattr(inventory, "_has_distribution", lambda: False)
|
|
assert inventory.detect_install_method() == METHOD_UNKNOWN
|
|
|
|
|
|
def test_detect_explicit_source_env_maps_to_source(monkeypatch) -> None:
|
|
_isolate_detection(monkeypatch)
|
|
monkeypatch.setattr(inventory, "_has_distribution", lambda: False)
|
|
monkeypatch.setenv("OPENSQUILLA_INSTALL_METHOD", "source")
|
|
assert inventory.detect_install_method() == METHOD_SOURCE
|
|
|
|
|
|
def test_databucket_existing_paths_includes_wal_sidecars(tmp_path: Path) -> None:
|
|
db = tmp_path / "sessions.db"
|
|
db.write_text("x")
|
|
(tmp_path / "sessions.db-wal").write_text("w")
|
|
(tmp_path / "sessions.db-shm").write_text("s")
|
|
bucket = DataBucket("sessions", db, "user-data", "state", sidecars=("-wal", "-shm"))
|
|
found = {p.name for p in bucket.existing_paths()}
|
|
assert found == {"sessions.db", "sessions.db-wal", "sessions.db-shm"}
|
|
|
|
|
|
def test_databucket_glob(tmp_path: Path) -> None:
|
|
(tmp_path / "config.toml.backup.1").write_text("a")
|
|
(tmp_path / "config.toml.backup.2").write_text("b")
|
|
bucket = DataBucket("backups", tmp_path / "config.toml.backup.*", "config", "config", glob=True)
|
|
assert len(bucket.existing_paths()) == 2
|
|
|
|
|
|
def test_build_buckets_in_home_and_relocated(monkeypatch, tmp_path: Path) -> None:
|
|
home = tmp_path / "home"
|
|
(home / "state").mkdir(parents=True)
|
|
(home / "config.toml").write_text("x")
|
|
outside = tmp_path / "elsewhere" / "scheduler.db"
|
|
outside.parent.mkdir(parents=True)
|
|
outside.write_text("s")
|
|
monkeypatch.setenv("OPENSQUILLA_SCHEDULER_DB", str(outside))
|
|
|
|
buckets = build_data_buckets(home, config=None)
|
|
by_name = {b.name: b for b in buckets}
|
|
assert by_name["config.toml"].purge_flag == "config"
|
|
assert by_name["state directory"].purge_flag == "state"
|
|
sched = by_name["scheduler DB"]
|
|
assert sched.outside_home is True
|
|
assert sched.sidecars == ("-wal", "-shm")
|
|
|
|
|
|
def test_build_buckets_in_home_relocation_is_auto_purged(monkeypatch, tmp_path: Path) -> None:
|
|
home = tmp_path / "home"
|
|
(home / "state").mkdir(parents=True)
|
|
inhome = home / "relocated" / "scheduler.db"
|
|
inhome.parent.mkdir(parents=True)
|
|
inhome.write_text("s")
|
|
monkeypatch.setenv("OPENSQUILLA_SCHEDULER_DB", str(inhome))
|
|
|
|
buckets = build_data_buckets(home, config=None)
|
|
sched = next(b for b in buckets if b.name == "scheduler DB")
|
|
# A relocation that still lands under the home is auto-purged, not orphaned.
|
|
assert sched.outside_home is False
|
|
assert sched.purge_flag == "state"
|
|
|
|
|
|
def test_detect_services_reads_unit_files(monkeypatch, tmp_path: Path) -> None:
|
|
monkeypatch.setenv("HOME", str(tmp_path))
|
|
plist = tmp_path / "Library/LaunchAgents/ai.opensquilla.gateway.plist"
|
|
plist.parent.mkdir(parents=True)
|
|
plist.write_text("<plist/>")
|
|
unit = tmp_path / ".config/systemd/user/opensquilla.service"
|
|
unit.parent.mkdir(parents=True)
|
|
unit.write_text("[Unit]")
|
|
|
|
services = inventory.detect_services()
|
|
platforms = {s.platform for s in services}
|
|
assert "launchd" in platforms
|
|
assert "systemd" in platforms
|