Files
hermes-agent/tests/hermes_cli/test_catalog_json.py
emozilla 47703c53e5 fix(local-models): keep older apps on their packaged catalog
Older apps refresh catalog.json from main and ignore fields they do not know. Given this
catalog, main's code recommends Flash Next on an RTX Spark and launches it without its MTP head,
with the 26.8 GiB per-layer table loaded and its QSA compute buffer unpriced. Catalog schema 2
makes those apps skip the refresh and keep the catalog they shipped with; they read schema 2
once they update.
2026-10-06 16:08:23 -04:00

122 lines
4.2 KiB
Python

"""The pulled catalog: packaged JSON is the offline truth, a GitHub fetch
swaps entries in memory only, and min_engine gates day-0 models.
Nothing here touches disk beyond the packaged file — the design constraint
is that a git checkout must never see a dirty tracked catalog.json."""
from __future__ import annotations
import dataclasses
import io
import json
import urllib.request
import pytest
import hermes_cli.local_runtime.catalog as cat
@pytest.fixture(autouse=True)
def _reset_refresh_state(monkeypatch):
"""Each test starts outside the TTL window with the packaged catalog."""
monkeypatch.setattr(cat, "_last_refresh_attempt", 0.0)
packaged = cat._packaged_catalog()
monkeypatch.setattr(cat, "CATALOG", packaged)
yield
def _doc_from(entries):
"""A fetchable catalog document built by mutating the packaged JSON."""
from importlib.resources import files
doc = json.loads(files("hermes_cli.local_runtime")
.joinpath("catalog.json").read_text(encoding="utf-8"))
doc["models"] = entries(doc["models"])
return doc
def _fetch_returns(monkeypatch, doc):
body = json.dumps(doc).encode()
class R(io.BytesIO):
def __enter__(self):
return self
def __exit__(self, *a):
return False
monkeypatch.setattr(urllib.request, "urlopen",
lambda *a, **k: R(body))
def test_packaged_json_round_trips_the_catalog():
"""The packaged JSON must produce a complete, selection-ready catalog:
every entry carries estimator inputs and at least one variant, and the
known invariants (best-first ordering, Q4 floor) hold — the same
contract the literals obeyed."""
assert len(cat.CATALOG) >= 4
for e in cat.CATALOG:
assert e.variants and e.n_ctx_train > 0 and e.per_layer_f16 >= 0
sizes = [v.size_bytes for v in e.variants]
assert sizes == sorted(sizes, reverse=True), f"{e.id} not best-first"
def test_refresh_swaps_in_memory_only(monkeypatch, tmp_path):
"""A fetched catalog replaces CATALOG in memory; the packaged file on
disk is untouched (checkout stays clean)."""
from importlib.resources import files
packaged_path = files("hermes_cli.local_runtime").joinpath("catalog.json")
before = packaged_path.read_text(encoding="utf-8")
def add_day0(models):
day0 = dict(models[0])
day0.update(id="day0-model", display_name="Day 0",
description="new", min_engine="b99999")
return models + [day0]
_fetch_returns(monkeypatch, _doc_from(add_day0))
assert cat.refresh_catalog(force=True) is True
assert "day0-model" in {e.id for e in cat.CATALOG}
assert cat.catalog_by_id()["day0-model"].min_engine == "b99999"
assert packaged_path.read_text(encoding="utf-8") == before
def test_refresh_failure_keeps_current_catalog(monkeypatch):
def boom(*a, **k):
raise OSError("offline")
monkeypatch.setattr(urllib.request, "urlopen", boom)
ids_before = [e.id for e in cat.CATALOG]
assert cat.refresh_catalog(force=True) is False
assert [e.id for e in cat.CATALOG] == ids_before
def test_refresh_rejects_wrong_schema(monkeypatch):
doc = _doc_from(lambda m: m)
doc["schema_version"] = cat._SCHEMA_VERSION + 1
_fetch_returns(monkeypatch, doc)
ids_before = [e.id for e in cat.CATALOG]
assert cat.refresh_catalog(force=True) is False
assert [e.id for e in cat.CATALOG] == ids_before
def test_loader_ignores_unknown_fields():
doc = _doc_from(lambda m: m)
doc["models"][0]["future_field"] = {"anything": True}
entries = cat._load_catalog(doc)
assert entries[0].id == doc["models"][0]["id"]
def test_min_engine_gate(monkeypatch):
from hermes_cli.web_routers.local_models import _engine_too_old
from hermes_cli.local_runtime.binaries import Engine
from pathlib import Path
monkeypatch.setattr("hermes_cli.local_runtime.binaries.installed_engine",
lambda *args, **kwargs: Engine("cpu", "b10362", Path("unused")))
assert _engine_too_old("") is False, "no requirement, no gate"
assert _engine_too_old("b10000") is False, "installed engine suffices"
assert _engine_too_old("b10363") is True, "newer requirement gates"