"""Public channel resolution is strict, pinned, and independent of name registries.""" from copy import deepcopy import hashlib import json from pathlib import Path import importlib.util import pytest # The HTTP fixture is shared without making tests/ an importable package. _spec = importlib.util.spec_from_file_location("channel_http_fixture", Path(__file__).parents[1] / "scripts/test_release_channels.py") _fixture = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(_fixture) object_server, publisher = _fixture.object_server, _fixture.publisher def seed_manifest(pub, request, objects): from hermes_cli.release_channels import build_prefix, canonical_json key = build_prefix(request["buildId"]) + "build.json" manifest = {"schema": 1, "receiverProtocol": 1, "request": request, "packages": [{"platform": "darwin", "arch": "arm64", "variant": "bundled", "artifact": {"key": build_prefix(request["buildId"]) + "darwin/package.zip", "sha256": "f" * 64, "size": 123}, "version": request["version"], "identity": request["identity"]["appId"], "teamId": "ABCDEFGHIJ", "feed": {"key": build_prefix(request["buildId"]) + "darwin/stable-mac.yml", "channel": "stable"}}]} body = canonical_json(manifest) objects[key] = body head = {"buildId": request["buildId"], "sequence": request["sequence"], "manifestKey": key, "sha256": hashlib.sha256(body).hexdigest()} record_key = f"releases/channels/{request['channel']}.json" record = json.loads(objects[record_key]) record["head"] = head objects[record_key] = canonical_json(record) return manifest, record def test_resolve_verifies_exact_manifest_and_preserves_retirement_constraints(): from hermes_cli.release_channels import ChannelReader, ChannelError, canonical_json with object_server() as (url, objects, headers, requests, faults): pub = publisher(url) for name in ("old-preview", "next-preview"): pub.create(name) request = pub.allocate("next-preview", "a" * 40, "1.2.3") request.update(version="1.2.3", windowsVersion="1.2.3.0", releaseTag="v1.2.3") manifest, record = seed_manifest(pub, request, objects) record["policy"] = "stable-release" objects["releases/channels/next-preview.json"] = canonical_json(record) reader = ChannelReader(url + "/bucket", repository="example/hermes-agent") assert reader.resolve("next-preview").manifest == manifest old = pub._read("old-preview")[0] old.update(state="retired", destination="next-preview", minimumVersion="1.0.0", destinationHead=record["head"], receiverProtocol=1, receiver={"kind": "discontinued"}, lastHead=None) objects["releases/channels/old-preview.json"] = canonical_json(old) resolved = reader.resolve("old-preview") assert resolved.requested == old and resolved.terminal == record assert resolved.requested["minimumVersion"] == "1.0.0" # Retirement pins the qualified destination, not an unqualified later head. newer = dict(request, buildId="f" * 32, sequence=2, sourceVersion="1.3.0", version="1.3.0", windowsVersion="1.3.0.0", releaseTag="v1.3.0") record["nextSequence"] = 3 objects["releases/channels/next-preview.json"] = canonical_json(record) seed_manifest(pub, newer, objects) after_advance = reader.resolve("old-preview") assert after_advance.manifest == manifest assert after_advance.terminal["head"]["buildId"] == newer["buildId"] objects[record["head"]["manifestKey"]] += b" " with pytest.raises(ChannelError, match="SHA256"): reader.resolve("old-preview") @pytest.mark.parametrize("name", ["X", "../x", "two--parts", "a/b", "a%2fb", "a_1", "con", "nul", "x-", "x" * 33, " a", "a\n"]) def test_names_are_validated_without_normalizing(name): from hermes_cli.release_channels import ChannelError, validate_name with pytest.raises(ChannelError): validate_name(name) def test_update_reads_retry_transient_http_and_honor_retry_after(monkeypatch): from email.message import Message from urllib.error import HTTPError from hermes_cli.release_channels import ChannelReader, retrying_reads url = "http://127.0.0.1:12345/releases/fixture.json" headers = Message() headers["Retry-After"] = "7" attempts = [] waits = [] class Response: def __enter__(self): return self def __exit__(self, *_args): return None def geturl(self): return url def read(self, _limit): return b"fixture" def opener(request, timeout): assert timeout == 30 attempts.append(request.full_url) if len(attempts) == 1: raise HTTPError(url, 503, "unavailable", headers, None) return Response() monkeypatch.setattr("pm.network.time.sleep", waits.append) reader = ChannelReader("http://127.0.0.1:12345", opener=opener) with retrying_reads(): assert reader.read_bytes("releases/fixture.json") == b"fixture" assert attempts == [url, url] assert waits == [7.0] def test_passive_reads_and_missing_objects_make_one_attempt(monkeypatch): """Offline looks transient (ENETUNREACH); a passive check must not back off on it.""" import errno from email.message import Message from urllib.error import HTTPError, URLError from hermes_cli.release_channels import ChannelError, ChannelNotFound, ChannelReader, retrying_reads waits = [] monkeypatch.setattr("pm.network.time.sleep", waits.append) def reader(fault, calls): def opener(request, timeout): calls.append(request.full_url) raise fault return ChannelReader("https://releases.example", opener=opener) calls = [] offline = URLError(OSError(errno.ENETUNREACH, "Network is unreachable")) with pytest.raises(ChannelError, match="unavailable"): reader(offline, calls).read_bytes("releases/channels/main.json") assert len(calls) == 1 calls.clear() missing = HTTPError("https://releases.example/x", 404, "missing", Message(), None) with retrying_reads(), pytest.raises(ChannelNotFound): reader(missing, calls).read_bytes("releases/channels/stable.json") assert len(calls) == 1 assert waits == [] def test_reader_rejects_cycles_identity_substitution_and_cross_authority(): from hermes_cli.release_channels import ChannelReader, ChannelError, canonical_json with object_server() as (url, objects, headers, requests, faults): pub = publisher(url) pub.create("alpha") request = pub.allocate("alpha", "a" * 40, "1.2.3") manifest, record = seed_manifest(pub, request, objects) reader = ChannelReader(url + "/bucket", repository="example/hermes-agent") substituted = deepcopy(manifest) substituted["request"]["identity"]["appId"] = "ai.other.identity" raw = canonical_json(substituted) objects[record["head"]["manifestKey"]] = raw record["head"]["sha256"] = hashlib.sha256(raw).hexdigest() objects["releases/channels/alpha.json"] = canonical_json(record) with pytest.raises(ChannelError, match="identity"): reader.resolve("alpha") record["repository"] = "other/hermes-agent" objects["releases/channels/alpha.json"] = canonical_json(record) with pytest.raises(ChannelError, match="authority"): reader.resolve("alpha") record["repository"] = "example/hermes-agent" record.update(state="retired", destination="alpha", minimumVersion="1.0.0", lastHead=record["head"], destinationHead=record["head"], receiverProtocol=1, receiver={"kind": "discontinued"}) objects["releases/channels/alpha.json"] = canonical_json(record) with pytest.raises(ChannelError, match="cycle"): reader.resolve("alpha") def test_legacy_bootstrap_uses_real_archive_keys_and_source_main_has_no_bundle(): from hermes_cli.release_channels import canonical_json, ChannelError with object_server() as (url, objects, headers, requests, faults): pub = publisher(url, verify_build=lambda request, manifest: True) preview = pub.create("temporary") request = pub.allocate("temporary", "a" * 40, "2.0.0") manifest, _ = seed_manifest(pub, request, objects) for name, policy, tag in [("stable", "stable-release", "v2.0.0"), ("canary", "canary-release", "v2.1.0+canary.20260913T000100Z")]: legacy = deepcopy(manifest) legacy["request"].update(channel=name, buildId=("a" if name == "stable" else "b") * 32, releaseTag=tag, version=tag[1:], windowsVersion="2.0.0.0" if name == "stable" else "2.1.0.10") legacy["packages"][0].update(version=tag[1:]) legacy["packages"][0]["artifact"]["key"] = f"releases/tag/{tag}/actual.zip" legacy["packages"][0]["feed"]["key"] = f"releases/tag/{tag}/stable-mac.yml" record = dict(preview, name=name, policy=policy, nextSequence=2, head={"buildId": legacy["request"]["buildId"], "sequence": 1, "manifestKey": "releases/channel-builds/" + legacy["request"]["buildId"] + "/build.json", "sha256": hashlib.sha256(canonical_json(legacy)).hexdigest()}) pub.bootstrap(record, legacy, publish=True) assert pub.reader.resolve(name).manifest == legacy assert pub.request(legacy["request"]["buildId"]) == legacy["request"] from hermes_cli.release_channels import validate_request invalid_versions = ["2.1.0.65536", "65536.1.0.0", "2.1.0.-1", "2.1.0.1.0", "2.1.0.x"] if policy == "stable-release": invalid_versions.append("2.0.0.10") for windows_version in invalid_versions: with pytest.raises(ChannelError, match="Windows version"): validate_request(dict(legacy["request"], windowsVersion=windows_version), policy=policy) with pytest.raises(ChannelError, match="Protected"): pub.promote(legacy["request"]["buildId"]) with pytest.raises(ChannelError, match="Protected"): pub.allocate(name, "a" * 40, "2.0.0") @pytest.mark.parametrize("sequence", [1, 65535, 65536, 2**32 - 1]) def test_sequence_versions_remain_monotonic_at_native_rollover(sequence): from hermes_cli.release_channels import package_versions version, windows = package_versions(sequence) assert version == f"0.0.{sequence}" quad = tuple(map(int, windows.split("."))) assert quad[1] * 65536 + quad[2] == sequence and max(quad) <= 65535 if sequence > 1: assert quad > tuple(map(int, package_versions(sequence - 1)[1].split("."))) @pytest.mark.parametrize("sequence", [0, -1, True, 2**32, "1"]) def test_sequence_exhaustion_never_wraps(sequence): from hermes_cli.release_channels import package_versions, ChannelError with pytest.raises(ChannelError): package_versions(sequence) def test_malformed_record_and_unqualified_retirement_never_resolve(): from hermes_cli.release_channels import ChannelError, canonical_json with object_server() as (url, objects, headers, requests, faults): pub = publisher(url) pub.create("preview") record = pub._read("preview")[0] malformed = dict(record) del malformed["head"] objects["releases/channels/preview.json"] = canonical_json(malformed) with pytest.raises(ChannelError): pub.reader.resolve("preview") objects["releases/channels/preview.json"] = canonical_json(record) pub.create("destination") request = pub.allocate("destination", "a" * 40, "1.0.0") request.update(version="1.0.0", windowsVersion="1.0.0.0", releaseTag="v1.0.0") _, target = seed_manifest(pub, request, objects) target["policy"] = "stable-release" objects["releases/channels/destination.json"] = canonical_json(target) record.update(state="retired", destination="destination", minimumVersion="2.0.0", lastHead=None, destinationHead=target["head"], receiverProtocol=1, receiver={"kind": "discontinued"}) objects["releases/channels/preview.json"] = canonical_json(record) with pytest.raises(ChannelError, match="minimum version"): pub.reader.resolve("preview") @pytest.mark.parametrize("field,value", [("policy", []), ("state", {}), ("revision", True), ("head", []), ("identity", None)]) def test_malformed_wire_types_are_channel_errors(field, value): from hermes_cli.release_channels import validate_record, ChannelError from scripts.releases.channels import preview_identity record = {"schema": 1, "name": "arbitrary", "repository": "example/hermes-agent", "policy": "preview", "state": "active", "revision": 1, "nextSequence": 1, "head": None, "identity": preview_identity("arbitrary", "a" * 16)} record[field] = value with pytest.raises(ChannelError): validate_record(record) def stable_request(version="1.2.3", archive_ref=None): from scripts.releases.channels import preview_identity request = {"schema": 1, "buildId": "a" * 32, "channel": "stable", "sequence": 1, "repository": "example/hermes-agent", "commit": "b" * 40, "sourceVersion": version, "version": version, "windowsVersion": version + ".0", "releaseTag": "v" + version, "identity": preview_identity("stable", "a" * 16), "bundleEnv": {}, "publicBase": "https://releases.example"} if archive_ref is not None: request["archiveRef"] = archive_ref return request def stable_manifest(request, archive_prefix): from hermes_cli.release_channels import build_prefix manifest = {"schema": 1, "receiverProtocol": 1, "request": request, "packages": [ {"platform": "darwin", "arch": "arm64", "variant": "bundled", "identity": request["identity"]["appId"], "version": request["version"], "teamId": "ABCDEFGHIJ", "artifact": {"key": archive_prefix + "Hermes.dmg", "sha256": "d" * 64, "size": 100}, "feed": {"key": archive_prefix + "stable-mac.yml", "channel": "stable"}}]} record = {"schema": 1, "name": request["channel"], "repository": request["repository"], "policy": "stable-release", "state": "active", "revision": 1, "nextSequence": 2, "identity": request["identity"], "head": {"buildId": request["buildId"], "sequence": request["sequence"], "manifestKey": build_prefix(request["buildId"]) + "build.json", "sha256": "a" * 64}} return manifest, record def test_archive_ref_names_the_protected_archive_prefix(): from hermes_cli.release_channels import validate_manifest, validate_request request = stable_request(archive_ref="rc.2-v1.2.3") validate_request(request, policy="stable-release") manifest, record = stable_manifest(request, "releases/tag/rc.2-v1.2.3/") assert validate_manifest(manifest, record, request["publicBase"]) == manifest def test_stable_manifest_without_archive_ref_fails_closed_outside_the_tag_prefix(): from hermes_cli.release_channels import validate_manifest, ChannelError request = stable_request() manifest, record = stable_manifest(request, "releases/tag/rc.2-v1.2.3/") with pytest.raises(ChannelError, match="namespace"): validate_manifest(manifest, record, request["publicBase"]) def test_archive_ref_equal_to_the_release_tag_keeps_the_tag_prefix(): from hermes_cli.release_channels import validate_manifest, validate_request request = stable_request(archive_ref="v1.2.3") validate_request(request, policy="stable-release") manifest, record = stable_manifest(request, "releases/tag/v1.2.3/") assert validate_manifest(manifest, record, request["publicBase"]) == manifest def test_archive_ref_must_name_the_release_version(): from hermes_cli.release_channels import validate_request, ChannelError with pytest.raises(ChannelError, match="(?i)archive ref"): validate_request(stable_request(archive_ref="rc.2-v1.2.4"), policy="stable-release") @pytest.mark.parametrize("ref", ["rc.1-v0.21.5", "rc.12-v1.0.0"]) def test_attempt_ref_shapes_are_attempt_refs(ref): from scripts.releases.versioning import parse_attempt_ref assert parse_attempt_ref(ref) is not None @pytest.mark.parametrize("ref", ["v0.21.5-rc", "v0.21.5-rc.1", "rc.01-v0.21.5", "rc.0-v0.21.5", "rc.1-v2026.9.21", "v0.21.5", "abandoned-rc.1-v0.21.5"]) def test_non_attempt_ref_shapes_are_rejected_as_archive_refs(ref): from hermes_cli.release_channels import validate_request, ChannelError with pytest.raises(ChannelError, match="(?i)archive ref"): validate_request(stable_request(archive_ref=ref), policy="stable-release")