Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions api/updates.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,18 @@ def _check_repo_release(path, name):
current_tag = _current_release_tag(path)
behind = _release_gap(tags, current_tag, latest_tag)

# If behind == 0 but HEAD has moved past the tag (e.g. the agent repo
# keeps committing to master between tagged releases), the release check
# would report "Up to date" even though hundreds of commits are missing.
# Detect this by comparing the short describe output (which includes the
# -N-gSHA suffix when HEAD is past a tag) against the bare tag name.
# When HEAD is ahead of the latest tag, fall through to _check_repo_branch
# so the real commit count is reported instead. See #2653.
if behind == 0:
full_desc, ok = _run_git(['describe', '--tags', '--always'], path)
if ok and full_desc and full_desc != current_tag:
return None

remote_url, _ = _run_git(['remote', 'get-url', 'origin'], path)
remote_url = _normalize_remote_url(remote_url)

Expand Down
111 changes: 111 additions & 0 deletions tests/test_updates.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,114 @@ def fake_git(args, cwd, timeout=10):
assert info.get('stale_check') is not True, (
'fetch with --force should have succeeded, not marked stale'
)


# ---------------------------------------------------------------------------
# #2653 — Update check reports "Up to date" while the repo is hundreds of
# commits past the latest tag (agent cadence bug).
#
# When current_tag == latest_tag (behind==0 from the release check) but HEAD
# has moved past that tag (git describe --tags --always returns a -N-gSHA
# suffix), _check_repo_release must return None so the branch check runs and
# reports the real commit gap.
# ---------------------------------------------------------------------------


def test_check_repo_release_falls_through_when_head_is_past_tag(tmp_path):
"""_check_repo_release returns None when behind==0 but HEAD is past the tag.

Simulates the hermes-agent case: latest tag == current tag (v2026.5.16)
but git describe shows 608 commits past it. The release check must
not report 'Up to date'; it should fall through so the branch check
counts the real gap.
"""
(tmp_path / '.git').mkdir()

def fake_git(args, cwd, timeout=10):
if args == ['tag', '--list', 'v*', '--sort=-v:refname']:
return 'v2026.5.16', True
if args == ['describe', '--tags', '--abbrev=0']:
return 'v2026.5.16', True
# HEAD is 608 commits past the tag — describe includes a suffix.
if args == ['describe', '--tags', '--always']:
return 'v2026.5.16-608-g1d22b9c2d', True
raise AssertionError(f'unexpected git args: {args!r}')

with patch.object(updates, '_run_git', side_effect=fake_git):
result = updates._check_repo_release(tmp_path, 'test-repo')

assert result is None, (
'_check_repo_release should return None when HEAD is past the latest tag '
'so the branch check can report the real commit gap (#2653)'
)


def test_check_repo_release_not_affected_when_head_exactly_on_tag(tmp_path):
"""_check_repo_release works normally when HEAD is exactly on the latest tag."""
(tmp_path / '.git').mkdir()

def fake_git(args, cwd, timeout=10):
if args == ['tag', '--list', 'v*', '--sort=-v:refname']:
return 'v2026.5.16\nv2026.5.10', True
if args == ['describe', '--tags', '--abbrev=0']:
return 'v2026.5.16', True
# No -N-gSHA suffix: HEAD is exactly on the tag.
if args == ['describe', '--tags', '--always']:
return 'v2026.5.16', True
if args == ['remote', 'get-url', 'origin']:
return 'https://github.com/nesquena/hermes-agent.git', True
raise AssertionError(f'unexpected git args: {args!r}')

with patch.object(updates, '_run_git', side_effect=fake_git):
result = updates._check_repo_release(tmp_path, 'agent')

assert result is not None
assert result['behind'] == 0
assert result['current_version'] == 'v2026.5.16'
assert result['latest_version'] == 'v2026.5.16'


def test_check_repo_branch_check_runs_for_post_tag_commits(tmp_path):
"""End-to-end: when HEAD is past latest tag, _check_repo uses branch check.

Mirrors the exact scenario in issue #2653 where Agent: v2026.5.16-593-g...
was displayed alongside 'Up to date' in Settings.
"""
(tmp_path / '.git').mkdir()

def fake_git(args, cwd, timeout=10):
if args == ['fetch', 'origin', '--tags', '--force']:
return '', True
if args == ['tag', '--list', 'v*', '--sort=-v:refname']:
return 'v2026.5.16', True
if args == ['describe', '--tags', '--abbrev=0']:
return 'v2026.5.16', True
# HEAD is 608 commits past the tag.
if args == ['describe', '--tags', '--always']:
return 'v2026.5.16-608-g1d22b9c2d', True
# Branch-check path follows: rev-parse upstream, default branch, rev-list.
if args == ['rev-parse', '--abbrev-ref', '@{upstream}']:
return '', False
if args == ['symbolic-ref', 'refs/remotes/origin/HEAD']:
return 'refs/remotes/origin/master', True
if args[:2] == ['rev-list', '--count']:
return '608', True
# merge-base and short SHA lookups for compare URL
if args[0] == 'merge-base':
return 'abc1234' * 5, True
if args[:2] == ['rev-parse', '--short']:
return 'abc1234', True
if args == ['remote', 'get-url', 'origin']:
return 'https://github.com/nesquena/hermes-agent.git', True
return '', True

with patch.object(updates, '_run_git', side_effect=fake_git):
info = updates._check_repo(tmp_path, 'agent')

assert info is not None
assert info['behind'] == 608, (
f"expected behind=608 (branch check result), got {info['behind']!r} (#2653)"
)
assert info.get('release_based') is not True, (
'post-tag HEAD should use branch check, not release-based check'
)