From 3cd18392c9e06bebb2da64173d4cb762e280f6af Mon Sep 17 00:00:00 2001 From: the_og Date: Tue, 7 Jul 2026 20:44:00 -0400 Subject: [PATCH] test: cover __version__, parse_version/is_newer_version, and fetch_latest_release (#19) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All network calls are monkeypatched (urllib.request.urlopen) — no live network in tests. Covers older/newer/equal comparisons, v-prefix, differing-length tuples, malformed input on both sides, and fetch_latest_release success/timeout/network-failure/malformed-response paths. --- tests/test_core.py | 144 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/tests/test_core.py b/tests/test_core.py index aaf2faf..721f42f 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3,6 +3,8 @@ proper test functions with tmp_path/monkeypatch fixtures).""" import json import sys +import urllib.error +import urllib.request import pytest @@ -638,3 +640,145 @@ def test_args_secret_warning_env_not_triggered(): def test_args_secret_warning_empty(): assert c.args_secret_warning({}) is None assert c.args_secret_warning({"args": []}) is None + + +# --------------------------------------------------------------------------- # +# Version / update checking +# --------------------------------------------------------------------------- # +def test_dunder_version_matches_pyproject(): + # Guards against the version drifting out of sync between the two places + # a human might bump it. + assert c.__version__ == "1.1.0" + + +def test_parse_version_basic(): + assert c.parse_version("1.2.3") == (1, 2, 3) + + +def test_parse_version_strips_v_prefix(): + assert c.parse_version("v1.2.3") == (1, 2, 3) + + +def test_parse_version_strips_prerelease_suffix(): + assert c.parse_version("1.2.3-beta.1") == (1, 2, 3) + assert c.parse_version("1.2.3+build5") == (1, 2, 3) + + +def test_parse_version_malformed_returns_empty_tuple(): + assert c.parse_version("not-a-version") == () + assert c.parse_version("") == () + assert c.parse_version(None) == () + + +def test_parse_version_stops_at_first_non_numeric_component(): + assert c.parse_version("1.2.rc1") == (1, 2) + + +def test_is_newer_version_true_when_candidate_is_newer(): + assert c.is_newer_version("1.1.0", "1.2.0") is True + + +def test_is_newer_version_false_when_candidate_is_older(): + assert c.is_newer_version("1.2.0", "1.1.0") is False + + +def test_is_newer_version_false_when_equal(): + assert c.is_newer_version("1.1.0", "1.1.0") is False + + +def test_is_newer_version_equal_with_v_prefix(): + assert c.is_newer_version("1.1.0", "v1.1.0") is False + + +def test_is_newer_version_numeric_not_lexical(): + # A lexical/string compare would say "10.0.0" < "2.0.0" ("1" < "2"). + assert c.is_newer_version("2.0.0", "10.0.0") is True + assert c.is_newer_version("10.0.0", "2.0.0") is False + + +def test_is_newer_version_handles_differing_length(): + assert c.is_newer_version("1.2", "1.2.0") is False + assert c.is_newer_version("1.2.0", "1.2") is False + assert c.is_newer_version("1.2", "1.3") is True + + +def test_is_newer_version_malformed_candidate_is_never_newer(): + assert c.is_newer_version("1.1.0", "not-a-version") is False + assert c.is_newer_version("1.1.0", "") is False + + +def test_is_newer_version_malformed_current_does_not_crash(): + # Shouldn't raise; a valid candidate is reported as newer than "unknown". + assert c.is_newer_version("garbage", "1.0.0") is True + assert c.is_newer_version("garbage", "not-a-version-either") is False + + +class _FakeHTTPResponse: + def __init__(self, data: bytes): + self._data = data + + def read(self): + return self._data + + def __enter__(self): + return self + + def __exit__(self, *exc): + return False + + +def test_fetch_latest_release_ok(monkeypatch): + payload = json.dumps( + { + "tag_name": "v1.2.0", + "html_url": "https://git.avezzano.io/the_og/better-claude-config/releases/tag/v1.2.0", + } + ).encode("utf-8") + monkeypatch.setattr( + urllib.request, "urlopen", lambda req, timeout=None: _FakeHTTPResponse(payload) + ) + result = c.fetch_latest_release() + assert result == { + "version": "v1.2.0", + "url": "https://git.avezzano.io/the_og/better-claude-config/releases/tag/v1.2.0", + } + + +def test_fetch_latest_release_falls_back_to_releases_url_when_no_html_url(monkeypatch): + payload = json.dumps({"tag_name": "v1.2.0"}).encode("utf-8") + monkeypatch.setattr( + urllib.request, "urlopen", lambda req, timeout=None: _FakeHTTPResponse(payload) + ) + result = c.fetch_latest_release() + assert result == {"version": "v1.2.0", "url": c.RELEASES_URL} + + +def test_fetch_latest_release_network_failure_returns_none(monkeypatch): + def boom(req, timeout=None): + raise urllib.error.URLError("no network") + + monkeypatch.setattr(urllib.request, "urlopen", boom) + assert c.fetch_latest_release() is None + + +def test_fetch_latest_release_timeout_returns_none(monkeypatch): + def boom(req, timeout=None): + raise TimeoutError("timed out") + + monkeypatch.setattr(urllib.request, "urlopen", boom) + assert c.fetch_latest_release() is None + + +def test_fetch_latest_release_missing_tag_returns_none(monkeypatch): + payload = json.dumps({"html_url": "https://example.com"}).encode("utf-8") + monkeypatch.setattr( + urllib.request, "urlopen", lambda req, timeout=None: _FakeHTTPResponse(payload) + ) + assert c.fetch_latest_release() is None + + +def test_fetch_latest_release_malformed_json_returns_none(monkeypatch): + monkeypatch.setattr( + urllib.request, "urlopen", lambda req, timeout=None: _FakeHTTPResponse(b"not json") + ) + assert c.fetch_latest_release() is None