Files
hermes-agent/tests/hermes_cli/test_source_release_channels.py

247 lines
12 KiB
Python

"""Release-channel checks and updates against actual repositories and HTTP feeds."""
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
from threading import Thread
from types import SimpleNamespace
import subprocess
import urllib.request
from urllib.parse import urlsplit
import pytest
from hermes_cli import main, update_cmd
from hermes_cli.source_releases import resolve_source_release
from hermes_cli.update_channel import set_install_channel
def git(root, *args):
return subprocess.run(
["git", *args], cwd=root, check=True, capture_output=True, text=True,
).stdout.strip()
@pytest.fixture(params=["utf-8", "utf-8-sig"])
def releases(tmp_path, monkeypatch, request):
origin = tmp_path / "origin"
origin.mkdir()
git(origin, "init", "-b", "main")
git(origin, "config", "user.name", "Release Fixture")
git(origin, "config", "user.email", "fixture@example.invalid")
git(origin, "config", "commit.gpgsign", "false")
git(origin, "config", "tag.gpgsign", "false")
commits = []
for label in ("old", "stable", "canary", "unpublished"):
(origin / "content.txt").write_text(label, encoding="utf-8")
git(origin, "add", ".")
git(origin, "commit", "-m", label)
commits.append(git(origin, "rev-parse", "HEAD"))
tags = {"stable": "v1.2.3", "canary": "v1.2.3+canary.20260911T125822Z"}
git(origin, "tag", "-a", tags["stable"], commits[1], "-m", "stable")
git(origin, "tag", "-a", tags["canary"], commits[2], "-m", "canary")
git(origin, "tag", "v99.0.0", commits[3])
git(origin, "tag", "v99.0.1+canary.20260912T125822Z", commits[3])
checkout = tmp_path / "checkout"
git(tmp_path, "clone", str(origin), str(checkout))
git(checkout, "config", "user.name", "Release Fixture")
git(checkout, "config", "user.email", "fixture@example.invalid")
git(checkout, "config", "commit.gpgsign", "false")
git(checkout, "checkout", "--detach", commits[0])
monkeypatch.setattr(main, "PROJECT_ROOT", checkout)
monkeypatch.setenv("HERMES_INSTALL_ROOT", str(checkout))
monkeypatch.delenv("HERMES_MANAGED", raising=False)
responses = {}
requests = []
for channel, tag in tags.items():
responses[f"/releases/{channel}/index.html"] = (
f'<meta name="hermes-build" content="{tag}">'
)
responses[f"/repos/NousResearch/hermes-agent/releases/tags/{tag}"] = {
"tag_name": tag, "draft": False, "prerelease": channel == "canary",
}
responses[f"/repos/NousResearch/hermes-agent/commits/{tag}"] = {
"sha": commits[1 if channel == "stable" else 2],
}
responses["/releases/stable/release-candidates.json"] = {
"tag": tags["stable"], "commit": commits[1],
}
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
requests.append(self.path)
data = responses.get(self.path)
self.send_response(404 if data is None else 200)
self.end_headers()
if data is not None:
self.wfile.write((data if isinstance(data, str) else json.dumps(data)).encode(request.param))
def log_message(self, format, *args):
pass
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
thread = Thread(target=server.serve_forever, daemon=True)
thread.start()
open_url = urllib.request.urlopen
def local_urlopen(request, *args, **kwargs):
url = request.full_url if isinstance(request, urllib.request.Request) else request
parsed = urlsplit(url)
return open_url(f"http://127.0.0.1:{server.server_port}{parsed.path}"
+ (f"?{parsed.query}" if parsed.query else ""), *args, **kwargs)
monkeypatch.setattr(urllib.request, "urlopen", local_urlopen)
# Legacy pointer tests below retain their HTTP/tag boundary. New CLI callers
# consume the protocol reader, whose complete schema is tested independently.
from hermes_cli import source_releases
def resolve_channel(name, repository):
record = {"name": name, "repository": repository, "state": "active",
"policy": "source-branch" if name == "main" else "preview"}
manifest = None
if name == "main":
record["delivery"] = {"kind": "source-branch", "branch": "main"}
else:
manifest = {"request": {"commit": commits[1 if name == "stable" else 2],
"sourceVersion": tags[name].removeprefix("v"), "buildId": "legacy-fixture"}}
return SimpleNamespace(requested=record, terminal=record, manifest=manifest)
monkeypatch.setattr(source_releases, "_resolve_channel", resolve_channel)
yield SimpleNamespace(root=checkout, origin=origin, commits=commits,
tags=tags, responses=responses, requests=requests)
server.shutdown()
server.server_close()
thread.join()
@pytest.mark.parametrize("git_cmd", [["git"], None], ids=["git", "no-git"])
def test_stable_resolution_uses_promoted_pointer_not_highest_tag(releases, git_cmd):
assert resolve_source_release("stable", git_cmd, releases.root) == (
releases.tags["stable"], releases.commits[1],
)
assert releases.requests
@pytest.mark.parametrize("channel", ["stable", "canary"])
@pytest.mark.parametrize("start", ["old", "ahead", "local"])
def test_source_check_and_apply_land_on_selected_release(releases, monkeypatch, capsys, channel, start):
if start != "old":
git(releases.root, "checkout", "-b", "my-work", releases.commits[3])
if start == "local":
(releases.root / "my-work.txt").write_text("committed local work\n")
git(releases.root, "add", ".")
git(releases.root, "commit", "-m", "local work")
(releases.root / "notes.txt").write_text("uncommitted notes\n")
branch_sha = git(releases.root, "rev-parse", "HEAD")
set_install_channel(channel, releases.root)
before = git(releases.root, "rev-parse", "HEAD")
update_cmd._cmd_update_check()
assert releases.tags[channel] in capsys.readouterr().out
assert git(releases.root, "rev-parse", "HEAD") == before
# Exercise the real selection/fetch/checkout path, not dependency installation
# or live service management. No host OS is simulated.
opts = update_cmd._UpdateOptions(
pre_update_version=None, gw_input_fn=None,
assume_yes=True, keep_stash=False, switch_branch=False, discard_local_changes=False,
)
monkeypatch.setattr(update_cmd, "_resolve_update_options", lambda *_: opts)
monkeypatch.setattr(update_cmd, "_begin_update_receipt_and_plan", lambda *_: None)
monkeypatch.setattr(main, "_run_pre_update_backup", lambda *_: None)
monkeypatch.setattr(main, "_pause_windows_gateways_for_update", lambda: None)
monkeypatch.setattr(update_cmd, "_prepare_git_command", lambda: (False, ["git"], False))
applied = []
monkeypatch.setattr(update_cmd, "_complete_source_update", lambda request: applied.append(request))
args = SimpleNamespace(branch=None, channel=None, force_venv=True)
update_cmd._cmd_update_impl(args, False)
expected = releases.commits[1 if channel == "stable" else 2]
assert len(applied) == 1
assert applied[0]["expected_sha"] == expected
assert applied[0]["source"] == str(releases.root.resolve())
assert git(releases.root, "rev-parse", "HEAD") == expected
if start != "old":
assert git(releases.root, "rev-parse", "my-work") == branch_sha
if start == "local":
assert (releases.root / "notes.txt").read_text() == "uncommitted notes\n"
update_cmd._cmd_update_check()
assert "Up to date with" in capsys.readouterr().out
def test_source_check_honors_transient_channel_without_rewriting_record(releases, capsys):
set_install_channel("stable", releases.root)
args = SimpleNamespace(branch=None, channel="canary", check=True)
main.cmd_update(args)
assert releases.tags["canary"] in capsys.readouterr().out
update_cmd._cmd_update_check()
assert releases.tags["stable"] in capsys.readouterr().out
def test_fork_origin_uses_its_own_published_release_not_the_official_pointer(releases):
url = "https://github.com/Fixture/hermes-agent.git"
git(releases.root, "config", "remote.origin.url", url)
git(releases.root, "config", f"url.{releases.origin}.insteadOf", url)
tag = releases.tags["stable"]
git(releases.origin, "tag", "-f", tag, releases.commits[3])
releases.responses["/repos/Fixture/hermes-agent/releases/latest"] = {
"tag_name": tag, "draft": False, "prerelease": False,
}
releases.responses[f"/repos/Fixture/hermes-agent/commits/{tag}"] = {"sha": releases.commits[3]}
assert resolve_source_release("stable", ["git"], releases.root) == (tag, releases.commits[3])
assert not any(path.startswith("/releases/") for path in releases.requests)
def test_zip_fallback_keeps_selected_repository_and_commit(releases, monkeypatch):
from hermes_cli import update_cmd_zip
seen = []
monkeypatch.setattr(update_cmd_zip, "_abort_zip_update_if_dirty_tree", lambda: None)
class DownloadBoundary(Exception):
pass
def download(branch, url):
seen.append(url)
raise DownloadBoundary
monkeypatch.setattr(update_cmd_zip, "_download_and_swap_zip", download)
with pytest.raises(DownloadBoundary):
update_cmd_zip._update_via_zip(
SimpleNamespace(branch=None), target_sha=releases.commits[2],
target_repository="Fixture/hermes-agent", completion_request={})
assert seen == [f"https://github.com/Fixture/hermes-agent/archive/{releases.commits[2]}.zip"]
@pytest.mark.parametrize("git_cmd", [["git"], None], ids=["git", "no-git"])
def test_selected_draft_never_falls_back_to_other_tags(releases, git_cmd):
releases.responses[f"/repos/NousResearch/hermes-agent/releases/tags/{releases.tags['stable']}"]["draft"] = True
assert resolve_source_release("stable", git_cmd, releases.root) == (None, None)
assert not any("/tags?" in path for path in releases.requests)
def test_main_check_still_uses_branch_without_release_requests(releases, capsys):
set_install_channel("main", releases.root)
update_cmd._cmd_update_check()
assert "behind origin/main" in capsys.readouterr().out
assert releases.requests == []
@pytest.mark.parametrize("channel", ["stable", "canary"])
def test_origin_tag_cannot_substitute_a_fork_commit(releases, channel):
git(releases.origin, "tag", "-f", releases.tags[channel], releases.commits[3])
assert resolve_source_release(channel, ["git"], releases.root) == (None, None)
# Without git, the same selection remains pinned to the official commit.
assert resolve_source_release(channel) == (
releases.tags[channel], releases.commits[1 if channel == "stable" else 2],
)
@pytest.mark.parametrize("channel", ["stable", "canary"])
def test_missing_pointers_fall_back_only_to_published_releases(releases, channel):
releases.responses.pop("/releases/stable/release-candidates.json")
releases.responses.pop(f"/releases/{channel}/index.html")
published = releases.responses[f"/repos/NousResearch/hermes-agent/releases/tags/{releases.tags[channel]}"]
releases.responses["/repos/NousResearch/hermes-agent/releases/latest"] = published
releases.responses["/repos/NousResearch/hermes-agent/releases?per_page=100&page=1"] = [
{"tag_name": "v99.0.1+canary.20260912T125822Z", "draft": True, "prerelease": True},
published,
]
assert resolve_source_release(channel, ["git"], releases.root) == (
releases.tags[channel], releases.commits[1 if channel == "stable" else 2],
)
assert not any("/tags?" in path for path in releases.requests)
assert f"/repos/NousResearch/hermes-agent/releases/tags/{releases.tags[channel]}" not in releases.requests