diff --git a/omnigent/update_check.py b/omnigent/update_check.py index 1d0b803e..234dc825 100644 --- a/omnigent/update_check.py +++ b/omnigent/update_check.py @@ -296,7 +296,7 @@ def _run_installed_wheel_check() -> None: cache is not None and cache.kind == "wheel" and cache.latest_version - and _is_newer(cache.latest_version, info.package_version) + and _should_notify_release(cache.latest_version, info.package_version) and cache.latest_version != cache.last_notified_version ): _print_pypi_notice(info.package_version, cache.latest_version) @@ -339,6 +339,31 @@ def _is_newer(latest: str, current: str) -> bool: return latest != current and bool(latest) +def _should_notify_release(latest: str, current: str) -> bool: + """Return whether the passive update notice should report *latest*. + + A development build is already on its corresponding release line, so + the notice stays quiet for that line's final release. Later releases and + post-releases still produce a notice. + """ + from packaging.version import InvalidVersion, parse + + try: + latest_version = parse(latest) + current_version = parse(current) + except InvalidVersion: + return _is_newer(latest, current) + + if ( + current_version.is_devrelease + and latest_version.epoch == current_version.epoch + and latest_version.release == current_version.release + and not latest_version.is_postrelease + ): + return False + return latest_version > current_version + + def _resolve_index_url() -> str: """Resolve the package index to query, honoring uv/pip config. diff --git a/tests/cli/test_update_check.py b/tests/cli/test_update_check.py index 67098d03..6b3d0e14 100644 --- a/tests/cli/test_update_check.py +++ b/tests/cli/test_update_check.py @@ -598,6 +598,7 @@ def _write_fake_dist_info( direct_url: dict[str, object] | None = None, uv_cache: dict[str, object] | None = None, dir_mtime_epoch: float | None = None, + version: str = "0.1.0", ) -> importlib.metadata.PathDistribution: """Build a real ``.dist-info/`` on disk and return a PathDistribution. @@ -619,11 +620,14 @@ def _write_fake_dist_info( :param dir_mtime_epoch: When provided, ``os.utime`` is used to backdate the dist-info dir's mtime to this Unix timestamp — this is the fallback signal when ``uv_cache.json`` is absent. + :param version: Installed package version written to ``METADATA``. :returns: A ``PathDistribution`` constructed against the dir. """ - dist_info = tmp_path / "omnigent-0.1.0.dist-info" + dist_info = tmp_path / f"omnigent-{version}.dist-info" dist_info.mkdir() - (dist_info / "METADATA").write_text("Metadata-Version: 2.1\nName: omnigent\nVersion: 0.1.0\n") + (dist_info / "METADATA").write_text( + f"Metadata-Version: 2.1\nName: omnigent\nVersion: {version}\n" + ) if installer is not None: (dist_info / "INSTALLER").write_text(installer + "\n") if direct_url is not None: @@ -1002,6 +1006,30 @@ def test_wheel_check_no_nag_when_up_to_date( assert capsys.readouterr().err == "" +def test_wheel_check_no_nag_for_matching_dev_release( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """A dev build from the latest release line is already current.""" + monkeypatch.delenv("OMNIGENT_NO_UPDATE_CHECK", raising=False) + _point_cache_at(tmp_path, monkeypatch) + _write_cache( + _CacheEntry( + last_check_epoch=time.time(), + commits_behind=0, + kind="wheel", + latest_version="0.9.0", + ) + ) + dist = _write_fake_dist_info(tmp_path, installer="uv", version="0.9.0.dev0") + monkeypatch.setattr("omnigent.update_check._get_distribution", lambda: dist) + + _run_installed_wheel_check() + + assert capsys.readouterr().err == "" + + def test_wheel_check_nags_when_newer_release_available( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -1234,6 +1262,15 @@ def test_is_newer_tolerates_garbage() -> None: assert _is_newer("", "0.1.0") is False +def test_should_notify_release_treats_dev_build_as_current_release() -> None: + """Matching finals stay quiet without hiding later release lines.""" + from omnigent.update_check import _should_notify_release + + assert _should_notify_release("0.9.0", "0.9.0.dev0") is False + assert _should_notify_release("0.9.1", "0.9.0.dev0") is True + assert _should_notify_release("0.9.0.post1", "0.9.0.dev0") is True + + class _FakeResp: """Minimal httpx.Response stand-in for the Simple-API parser."""