import hashlib import io import json import platform import time from pathlib import Path import pytest from rich.console import Console from strix.interface import update_check @pytest.fixture(autouse=True) def _isolated_cache(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(update_check, "_CACHE_PATH", tmp_path / "update-check.json") monkeypatch.setattr(update_check, "_background_thread", None) monkeypatch.delenv("STRIX_NO_UPDATE_CHECK", raising=False) for key in ("CI", "GITHUB_ACTIONS", "GITLAB_CI", "JENKINS_URL", "BUILDKITE", "CIRCLECI"): monkeypatch.delenv(key, raising=False) @pytest.mark.parametrize( ("latest", "current", "expected"), [ ("1.2.0", "1.1.0", True), ("1.1.0", "1.1.0", False), ("1.0.9", "1.1.0", False), ("2.0.0", "1.99.99", True), ("1.10.0", "1.9.0", True), ("v1.2.0", "1.1.0", True), ("not-a-version", "1.1.0", False), ("1.2.0", "unknown", False), ], ) def test_is_newer(latest: str, current: str, expected: bool) -> None: assert update_check._is_newer(latest, current) is expected def test_get_available_update_from_cache(monkeypatch: pytest.MonkeyPatch) -> None: update_check._CACHE_PATH.write_text( json.dumps({"latest_version": "9.9.9", "checked_at": time.time()}) ) monkeypatch.setattr(update_check, "get_version", lambda: "1.0.0") assert update_check.get_available_update() == "9.9.9" def test_get_available_update_up_to_date(monkeypatch: pytest.MonkeyPatch) -> None: update_check._CACHE_PATH.write_text( json.dumps({"latest_version": "1.0.0", "checked_at": time.time()}) ) monkeypatch.setattr(update_check, "get_version", lambda: "1.0.0") assert update_check.get_available_update() is None def test_get_available_update_disabled_by_env(monkeypatch: pytest.MonkeyPatch) -> None: update_check._CACHE_PATH.write_text( json.dumps({"latest_version": "9.9.9", "checked_at": time.time()}) ) monkeypatch.setattr(update_check, "get_version", lambda: "1.0.0") monkeypatch.setenv("STRIX_NO_UPDATE_CHECK", "1") assert update_check.get_available_update() is None def test_get_available_update_disabled_in_ci(monkeypatch: pytest.MonkeyPatch) -> None: update_check._CACHE_PATH.write_text( json.dumps({"latest_version": "9.9.9", "checked_at": time.time()}) ) monkeypatch.setattr(update_check, "get_version", lambda: "1.0.0") monkeypatch.setenv("CI", "true") assert update_check.get_available_update() is None def test_get_available_update_corrupt_cache(monkeypatch: pytest.MonkeyPatch) -> None: update_check._CACHE_PATH.write_text("{not json") monkeypatch.setattr(update_check, "get_version", lambda: "1.0.0") assert update_check.get_available_update() is None def test_background_check_skipped_when_fresh(monkeypatch: pytest.MonkeyPatch) -> None: update_check._CACHE_PATH.write_text( json.dumps({"latest_version": "1.0.0", "checked_at": time.time()}) ) called = False def fake_refresh() -> None: nonlocal called called = True monkeypatch.setattr(update_check, "_refresh_cache", fake_refresh) update_check.start_background_check() assert update_check._background_thread is None assert called is False def test_background_check_runs_when_stale(monkeypatch: pytest.MonkeyPatch) -> None: update_check._CACHE_PATH.write_text( json.dumps({"latest_version": "1.0.0", "checked_at": time.time() - 2 * 24 * 60 * 60}) ) monkeypatch.setattr(update_check, "_fetch_latest_version", lambda: "1.2.3") update_check.start_background_check() assert update_check._background_thread is not None update_check._background_thread.join(timeout=5) cache = json.loads(update_check._CACHE_PATH.read_text()) assert cache["latest_version"] == "1.2.3" def test_skipped_version_suppresses_update(monkeypatch: pytest.MonkeyPatch) -> None: update_check._CACHE_PATH.write_text( json.dumps({"latest_version": "9.9.9", "checked_at": time.time()}) ) monkeypatch.setattr(update_check, "get_version", lambda: "1.0.0") update_check.skip_version("9.9.9") assert update_check.get_available_update() is None assert update_check.get_available_update(respect_skip=False) == "9.9.9" def test_newer_release_overrides_skipped_version(monkeypatch: pytest.MonkeyPatch) -> None: update_check._CACHE_PATH.write_text( json.dumps( {"latest_version": "9.9.10", "checked_at": time.time(), "skipped_version": "9.9.9"} ) ) monkeypatch.setattr(update_check, "get_version", lambda: "1.0.0") assert update_check.get_available_update() == "9.9.10" def test_write_cache_preserves_existing_fields() -> None: update_check.skip_version("9.9.9") update_check._write_cache(latest_version="1.2.3", checked_at=123.0) cache = json.loads(update_check._CACHE_PATH.read_text()) assert cache == {"latest_version": "1.2.3", "checked_at": 123.0, "skipped_version": "9.9.9"} def test_get_upgrade_command_all_methods() -> None: assert update_check.get_upgrade_command("binary") == "strix --update" assert update_check.get_upgrade_command("pipx") == "pipx upgrade strix-agent" assert update_check.get_upgrade_command("uv") == "uv tool upgrade strix-agent" assert update_check.get_upgrade_command("pip") == "pip install --upgrade strix-agent" def test_self_update_non_binary_prints_command(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(update_check, "is_binary_install", lambda: False) buffer = io.StringIO() assert update_check.self_update(Console(file=buffer)) is False assert "upgrade" in buffer.getvalue() def test_self_update_already_latest(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(update_check, "is_binary_install", lambda: True) monkeypatch.setattr(update_check, "_fetch_latest_version", lambda: "1.0.0") monkeypatch.setattr(update_check, "get_version", lambda: "1.0.0") assert update_check.self_update() is True def test_sha256_file(tmp_path: Path) -> None: path = tmp_path / "blob" path.write_bytes(b"strix") assert update_check._sha256_file(path) == hashlib.sha256(b"strix").hexdigest() def test_release_target(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(platform, "system", lambda: "Linux") monkeypatch.setattr(platform, "machine", lambda: "x86_64") assert update_check._release_target() == "linux-x86_64" monkeypatch.setattr(platform, "system", lambda: "Darwin") monkeypatch.setattr(platform, "machine", lambda: "arm64") assert update_check._release_target() == "macos-arm64" monkeypatch.setattr(platform, "machine", lambda: "riscv64") assert update_check._release_target() is None