diff --git a/hermes_cli/model_catalog.py b/hermes_cli/model_catalog.py index 703d958402ca4..2f9979ac085b7 100644 --- a/hermes_cli/model_catalog.py +++ b/hermes_cli/model_catalog.py @@ -46,6 +46,7 @@ import json import logging +import math import time import urllib.error import urllib.request @@ -53,7 +54,7 @@ from typing import Any from hermes_cli import __version__ as _HERMES_VERSION -from utils import atomic_replace +from utils import atomic_replace, is_truthy_value logger = logging.getLogger(__name__) @@ -104,13 +105,26 @@ def _load_catalog_config() -> dict[str, Any]: raw = {} return { - "enabled": bool(raw.get("enabled", True)), + "enabled": is_truthy_value(raw.get("enabled"), default=True), "url": str(raw.get("url") or DEFAULT_CATALOG_URL), - "ttl_hours": float(raw.get("ttl_hours") or DEFAULT_TTL_HOURS), + "ttl_hours": _parse_ttl_hours(raw.get("ttl_hours")), "providers": raw.get("providers") if isinstance(raw.get("providers"), dict) else {}, } +def _parse_ttl_hours(value: Any) -> float: + """Parse ``model_catalog.ttl_hours`` while preserving explicit zero.""" + if value is None or value == "": + return float(DEFAULT_TTL_HOURS) + try: + ttl = float(value) + except (TypeError, ValueError): + return float(DEFAULT_TTL_HOURS) + if not math.isfinite(ttl): + return float(DEFAULT_TTL_HOURS) + return max(0.0, ttl) + + def _cache_path() -> Path: """Return the disk cache path. Import lazily so tests can monkeypatch home.""" from hermes_constants import get_hermes_home diff --git a/tests/hermes_cli/test_model_catalog.py b/tests/hermes_cli/test_model_catalog.py index 43ad6e42cadb8..6ca26bf0bedce 100644 --- a/tests/hermes_cli/test_model_catalog.py +++ b/tests/hermes_cli/test_model_catalog.py @@ -94,6 +94,52 @@ def test_rejects_non_string_model_id(self, isolated_home): assert _validate_manifest(m) is False +class TestConfig: + def test_boolish_disabled_string_is_respected(self, isolated_home): + from hermes_cli import model_catalog + + with patch( + "hermes_cli.config.load_config", + return_value={"model_catalog": {"enabled": "false"}}, + ): + cfg = model_catalog._load_catalog_config() + + assert cfg["enabled"] is False + + def test_ttl_hours_string_and_zero_are_preserved(self, isolated_home): + from hermes_cli import model_catalog + + with patch( + "hermes_cli.config.load_config", + return_value={"model_catalog": {"ttl_hours": "0"}}, + ): + cfg = model_catalog._load_catalog_config() + + assert cfg["ttl_hours"] == 0.0 + + def test_invalid_ttl_hours_falls_back_to_default(self, isolated_home): + from hermes_cli import model_catalog + + with patch( + "hermes_cli.config.load_config", + return_value={"model_catalog": {"ttl_hours": "not-a-number"}}, + ): + cfg = model_catalog._load_catalog_config() + + assert cfg["ttl_hours"] == float(model_catalog.DEFAULT_TTL_HOURS) + + def test_negative_ttl_hours_clamps_to_zero(self, isolated_home): + from hermes_cli import model_catalog + + with patch( + "hermes_cli.config.load_config", + return_value={"model_catalog": {"ttl_hours": "-5"}}, + ): + cfg = model_catalog._load_catalog_config() + + assert cfg["ttl_hours"] == 0.0 + + class TestFetchSuccess: def test_fetch_and_cache_writes_disk(self, isolated_home): from hermes_cli import model_catalog