diff --git a/strix/interface/tui/app.py b/strix/interface/tui/app.py index cc5a36c4..14bc6cb1 100644 --- a/strix/interface/tui/app.py +++ b/strix/interface/tui/app.py @@ -15,6 +15,7 @@ from typing import TYPE_CHECKING, Any, ClassVar if TYPE_CHECKING: + from pygments.token import _TokenType from textual.timer import Timer from rich.align import Align @@ -352,7 +353,7 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc] if not token_value: continue color = None - tt = token_type + tt: _TokenType | None = token_type while tt: if tt in colors: color = colors[tt] diff --git a/strix/interface/update_check.py b/strix/interface/update_check.py index 71159267..53ce7e9b 100644 --- a/strix/interface/update_check.py +++ b/strix/interface/update_check.py @@ -1,7 +1,7 @@ """Update notifications and self-update for the strix CLI. Follows the pattern used by tools like gh, uv, and pip: a background, -rate-limited (once per 24h) check against the release source, a cached +rate-limited (once per hour) check against the release source, a cached result in ``~/.strix``, a non-intrusive notice with the upgrade command for the detected install method, and a ``strix --update`` self-update path for the standalone binary install. @@ -37,8 +37,9 @@ logger = logging.getLogger(__name__) GITHUB_REPO = "usestrix/strix" PYPI_PACKAGE = "strix-agent" -CHECK_INTERVAL_SECONDS = 24 * 60 * 60 +CHECK_INTERVAL_SECONDS = 60 * 60 REQUEST_TIMEOUT_SECONDS = 5 +PROMPT_JOIN_TIMEOUT_SECONDS = 3.0 _CACHE_PATH = Path.home() / ".strix" / "update-check.json" @@ -175,7 +176,7 @@ def _refresh_cache() -> None: def start_background_check() -> None: - """Refresh the cached latest-version info in a daemon thread (at most once per 24h).""" + """Refresh the cached latest-version info in a daemon thread (at most once per hour).""" global _background_thread # noqa: PLW0603 if _is_disabled(): return @@ -187,12 +188,16 @@ def start_background_check() -> None: _background_thread.start() -def get_available_update(*, respect_skip: bool = True) -> str | None: +def get_available_update( + *, + respect_skip: bool = True, + join_timeout: float = 0.2, +) -> str | None: """Return the newer version from the cache, or None if up to date / unknown.""" if _is_disabled(): return None if _background_thread is not None: - _background_thread.join(timeout=0.2) + _background_thread.join(timeout=join_timeout) cache = _read_cache() latest = cache.get("latest_version") current = get_version() @@ -239,7 +244,7 @@ def prompt_update_if_available(console: Console) -> bool: Returns True if strix was updated (caller should re-exec / exit). """ - latest = get_available_update() + latest = get_available_update(join_timeout=PROMPT_JOIN_TIMEOUT_SECONDS) if not latest or not sys.stdin.isatty() or not sys.stdout.isatty(): return False console.print() diff --git a/tests/test_update_check.py b/tests/test_update_check.py index 3583f393..d452e41c 100644 --- a/tests/test_update_check.py +++ b/tests/test_update_check.py @@ -132,6 +132,27 @@ def test_write_cache_preserves_existing_fields() -> None: assert cache == {"latest_version": "1.2.3", "checked_at": 123.0, "skipped_version": "9.9.9"} +def test_prompt_join_waits_for_fresh_fetch(monkeypatch: pytest.MonkeyPatch) -> None: + update_check._CACHE_PATH.write_text( + json.dumps({"latest_version": "1.0.0", "checked_at": time.time() - 2 * 60 * 60}) + ) + monkeypatch.setattr(update_check, "get_version", lambda: "1.0.0") + + def slow_fetch() -> str: + time.sleep(0.5) + return "9.9.9" + + monkeypatch.setattr(update_check, "_fetch_latest_version", slow_fetch) + update_check.start_background_check() + assert update_check.get_available_update(join_timeout=0.0) is None + assert ( + update_check.get_available_update( + join_timeout=update_check.PROMPT_JOIN_TIMEOUT_SECONDS, + ) + == "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"