"""Tests for the stable update channel (tag-tracking) in hermes_cli/update_cmd.py.""" from unittest.mock import patch from hermes_cli.update_cmd import ( _latest_release_tag_from_ls_remote, _parse_release_tag, _stable_channel_active, ) class TestParseReleaseTag: def test_final_releases_parse(self): assert _parse_release_tag("v0.17.0") == (0, 17, 0) assert _parse_release_tag("v10.2.33") == (10, 2, 33) assert _parse_release_tag(" v1.2.3 ") == (1, 2, 3) def test_prereleases_and_garbage_rejected(self): for tag in ("v1.2.3-rc1", "v1.2.3-beta.1", "v1.2", "1.2.3", "release-1", "vv1.2.3", ""): assert _parse_release_tag(tag) is None, tag def test_calver_tags_rejected(self): """Historical CalVer tags (v2026.7.20) must not win a numeric sort. The major component is capped at three digits, the same rule as _SEMVER_TAG_RE in scripts/write_install_stamp.py and latestReleaseFromLsRemote in apps/desktop. A four-digit year would rank above every SemVer release forever. """ assert _parse_release_tag("v2026.7.20") is None assert _parse_release_tag("v1000.0.0") is None assert _parse_release_tag("v999.0.0") == (999, 0, 0) def test_numeric_ordering_not_lexicographic(self): """v0.10.0 must sort above v0.9.0 — the whole point of tuple parsing.""" newer, older = _parse_release_tag("v0.10.0"), _parse_release_tag("v0.9.0") assert newer is not None and older is not None assert newer > older class TestLatestReleaseTagFromLsRemote: def test_picks_newest_final_release(self): output = ( "aaa1\trefs/tags/v0.9.0\n" "bbb2\trefs/tags/v0.10.0\n" "ccc3\trefs/tags/v0.10.1-rc1\n" "ddd4\trefs/tags/some-other-tag\n" ) tag, sha = _latest_release_tag_from_ls_remote(output) assert tag == "v0.10.0" assert sha == "bbb2" def test_peeled_sha_wins_for_annotated_tags(self): output = ( "tagobj\trefs/tags/v1.0.0\n" "commitsha\trefs/tags/v1.0.0^{}\n" ) tag, sha = _latest_release_tag_from_ls_remote(output) assert tag == "v1.0.0" assert sha == "commitsha" def test_no_release_tags(self): assert _latest_release_tag_from_ls_remote("aaa\trefs/tags/nightly\n") == (None, None) assert _latest_release_tag_from_ls_remote("") == (None, None) def test_malformed_lines_ignored(self): output = "garbage line no tab\naaa\trefs/heads/main\nbbb\trefs/tags/v2.0.0\n" assert _latest_release_tag_from_ls_remote(output) == ("v2.0.0", "bbb") class _Args: def __init__(self, branch=None): self.branch = branch class TestStableChannelActive: def test_explicit_branch_always_wins(self): """--branch means main-style behavior regardless of channel config.""" assert _stable_channel_active(_Args(branch="bb/gui")) is False def test_config_stable_activates(self): with patch("hermes_cli.config.load_config", return_value={"update": {"channel": "stable"}}): assert _stable_channel_active(_Args()) is True def test_default_config_stays_main(self): with patch("hermes_cli.config.load_config", return_value={"update": {"channel": "auto"}}): assert _stable_channel_active(_Args()) is False def test_config_failure_defaults_to_main(self): with patch("hermes_cli.config.load_config", side_effect=RuntimeError("boom")): assert _stable_channel_active(_Args()) is False