Files
2026-07-13 13:12:33 +08:00

211 lines
7.2 KiB
Python

"""CLI surface for `opensquilla uninstall` — flags, guards, confirmation."""
from __future__ import annotations
import json
import os
import subprocess
import sys
from pathlib import Path
import pytest
from typer.testing import CliRunner
from opensquilla.cli import codetask_cmd
from opensquilla.cli.main import app
from opensquilla.uninstall import actions as actions_module
from opensquilla.uninstall import inventory as inventory_module
from opensquilla.uninstall.actions import ActionResult, ExecutionResult
from opensquilla.uninstall.inventory import DataBucket, Inventory
runner = CliRunner()
def _fake_inventory(home: Path) -> Inventory:
state = home / "state"
state.mkdir(parents=True, exist_ok=True)
(home / "config.toml").write_text("x")
return Inventory(
method="pip",
home=home,
state_root=state,
config_path=None,
entrypoints=[],
program_paths=[],
package_uninstall=["pip", "uninstall", "-y", "opensquilla"],
buckets=[
DataBucket("config.toml", home / "config.toml", "config", "config"),
DataBucket("state directory", state, "user-data", "state"),
],
services=[],
receipt=None,
notes=[],
home_recognized=True,
)
def _patch_discover(monkeypatch, home: Path) -> None:
monkeypatch.setattr(inventory_module, "discover", lambda: _fake_inventory(home))
def test_dry_run_json_emits_plan_and_does_nothing(monkeypatch, tmp_path: Path) -> None:
_patch_discover(monkeypatch, tmp_path / "home")
def _boom(*_a, **_k):
raise AssertionError("execute must not run during --dry-run")
monkeypatch.setattr(actions_module, "execute", _boom)
result = runner.invoke(app, ["uninstall", "--dry-run", "--json"])
assert result.exit_code == 0, result.stdout
payload = json.loads(result.stdout)
assert payload["dry_run"] is True
assert payload["plan"]["method"] == "pip"
def test_dry_run_json_subprocess_stdout_stays_machine_readable(tmp_path: Path) -> None:
"""Import-time env loading must not pollute stdout before JSON output."""
home = tmp_path / "home"
env_home = home / ".opensquilla"
env_home.mkdir(parents=True)
(env_home / ".env").write_text("OPENSQUILLA_ENV_JSON_TEST=1\n", encoding="utf-8")
env = os.environ.copy()
env.update(
{
"HOME": str(home),
"USERPROFILE": str(home),
"HTTP_PROXY": "http://127.0.0.1:9",
}
)
env.pop("OPENSQUILLA_TRUST_ENV", None)
completed = subprocess.run(
[sys.executable, "-m", "opensquilla.cli.main", "uninstall", "--dry-run", "--json"],
cwd=Path(__file__).resolve().parents[2],
env=env,
capture_output=True,
text=True,
timeout=30,
check=False,
)
assert completed.returncode == 0, completed.stderr + completed.stdout
assert completed.stdout.lstrip().startswith("{")
payload = json.loads(completed.stdout)
assert payload["dry_run"] is True
def test_non_interactive_without_yes_refuses(monkeypatch, tmp_path: Path) -> None:
_patch_discover(monkeypatch, tmp_path / "home")
def _boom(*_a, **_k):
raise AssertionError("execute must not run without confirmation")
monkeypatch.setattr(actions_module, "execute", _boom)
result = runner.invoke(app, ["uninstall", "--json"])
assert result.exit_code == 2, result.stdout
assert "CONFIRMATION_REQUIRED" in (result.stdout + (result.stderr or ""))
def test_yes_json_executes(monkeypatch, tmp_path: Path) -> None:
_patch_discover(monkeypatch, tmp_path / "home")
captured = {}
def _fake_execute(plan, inventory, **_kwargs):
captured["ran"] = True
return ExecutionResult(
results=[ActionResult("run-package-uninstall", "ok", ok=True)], ok=True
)
monkeypatch.setattr(actions_module, "execute", _fake_execute)
result = runner.invoke(app, ["uninstall", "--yes", "--json"])
assert result.exit_code == 0, result.stdout
assert captured.get("ran") is True
payload = json.loads(result.stdout)
assert payload["ok"] is True
@pytest.mark.parametrize("purge_flag", ["--purge-state", "--purge-config", "--purge-all"])
def test_desktop_profile_routes_data_deletion_to_complete_desktop_cleanup(
monkeypatch,
tmp_path: Path,
purge_flag: str,
) -> None:
monkeypatch.setenv("OPENSQUILLA_PROFILE_KIND", "desktop-primary")
def _boom(*_args, **_kwargs):
raise AssertionError("generic Desktop purge must not inspect or delete partial data")
monkeypatch.setattr(inventory_module, "discover", _boom)
monkeypatch.setattr(actions_module, "execute", _boom)
result = runner.invoke(app, ["uninstall", purge_flag, "--yes", "--json"])
assert result.exit_code == 2, result.stdout
assert "DESKTOP_CLEANUP_REQUIRED" in (result.stdout + (result.stderr or ""))
def test_purge_all_requires_confirmation_phrase(monkeypatch, tmp_path: Path) -> None:
_patch_discover(monkeypatch, tmp_path / "home")
monkeypatch.setattr(codetask_cmd, "_stdin_is_tty", lambda: True) # allow interactive path
def _boom(*_a, **_k):
raise AssertionError("execute must not run on a mismatched phrase")
monkeypatch.setattr(actions_module, "execute", _boom)
result = runner.invoke(app, ["uninstall", "--purge-all"], input="nope\n")
assert result.exit_code == 2, result.stdout
assert "requires confirmation" in (result.stdout + (result.stderr or ""))
def test_yes_purge_all_without_phrase_is_refused(monkeypatch, tmp_path: Path) -> None:
"""`--yes --purge-all` must NOT wipe without the explicit second-factor phrase."""
_patch_discover(monkeypatch, tmp_path / "home")
def _boom(*_a, **_k):
raise AssertionError("execute must not run without the purge-all phrase")
monkeypatch.setattr(actions_module, "execute", _boom)
result = runner.invoke(app, ["uninstall", "--yes", "--purge-all", "--json"])
assert result.exit_code == 2, result.stdout
assert "CONFIRMATION_REQUIRED" in (result.stdout + (result.stderr or ""))
def test_yes_purge_all_with_phrase_executes(monkeypatch, tmp_path: Path) -> None:
_patch_discover(monkeypatch, tmp_path / "home")
captured = {}
def _fake_execute(plan, inventory, **_kwargs):
captured["ran"] = True
return ExecutionResult(results=[], ok=True)
monkeypatch.setattr(actions_module, "execute", _fake_execute)
result = runner.invoke(
app,
["uninstall", "--yes", "--purge-all", "--json", "--confirm-purge-all", "delete everything"],
)
assert result.exit_code == 0, result.stdout
assert captured.get("ran") is True
def test_purge_all_proceeds_on_correct_phrase(monkeypatch, tmp_path: Path) -> None:
_patch_discover(monkeypatch, tmp_path / "home")
monkeypatch.setattr(codetask_cmd, "_stdin_is_tty", lambda: True)
captured = {}
def _fake_execute(plan, inventory, **_kwargs):
captured["ran"] = True
return ExecutionResult(results=[], ok=True)
monkeypatch.setattr(actions_module, "execute", _fake_execute)
result = runner.invoke(app, ["uninstall", "--purge-all"], input="delete everything\n")
assert result.exit_code == 0, result.stdout
assert captured.get("ran") is True