diff --git a/scripts/setup.sh b/scripts/setup.sh index bba0dcc..ab671b0 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -94,7 +94,14 @@ blockfile, plugin_yaml, port = sys.argv[1], sys.argv[2], sys.argv[3] with open(plugin_yaml) as fh: manifest = yaml.safe_load(fh) or {} -models = manifest.get("models") or ["auto"] +models = manifest.get("models") +if not isinstance(models, list) or not models: + # Keep in sync with backend.STATIC_MODELS. setup.sh cannot import + # backend.py (it depends on `requests`), so the fallback catalog is + # duplicated here deliberately. A missing, empty, or non-list `models:` + # in a custom plugin.yaml is treated as "use the built-in catalog", the + # same way backend.list_models() falls back to STATIC_MODELS. + models = ["auto", "claude-sonnet-4.5", "claude-sonnet-4", "claude-haiku-4.5"] # Coerce all model identifiers to str — YAML 1.1 may parse numeric-looking # values as int/float (e.g. "4.0" as float). The model catalog must be # exact strings; backend.list_models() already does this coercion for the diff --git a/tests/test_coverage_gaps.py b/tests/test_coverage_gaps.py index 3d6ca21..50c8096 100644 --- a/tests/test_coverage_gaps.py +++ b/tests/test_coverage_gaps.py @@ -197,9 +197,16 @@ def _make_hermes_cli_mock(initial_config=None): import types saved = [copy.deepcopy(initial_config or {})] + save_calls = [] fake_cfg = types.ModuleType("hermes_cli.config") fake_cfg.load_config = lambda: copy.deepcopy(saved[0]) - fake_cfg.save_config = lambda c: saved.__setitem__(0, copy.deepcopy(c)) + + def _save_config(config): + save_calls.append(copy.deepcopy(config)) + saved[0] = copy.deepcopy(config) + + fake_cfg.save_config = _save_config + fake_cfg.save_calls = save_calls fake_hermes = types.ModuleType("hermes_cli") fake_hermes.config = fake_cfg return fake_hermes, fake_cfg, saved @@ -256,8 +263,39 @@ def test_provider_register_noop_when_already_current(monkeypatch): monkeypatch.setitem(sys.modules, "hermes_cli.config", fake_cfg) result = _provider.register_provider(8088) assert result is True - # No write: save_config must not have been called, so the stored config - # is unchanged (comment-preserving idempotency). + # No write: save_config must not have been called at all, otherwise the + # rebuild would round-trip config.yaml and strip its comments. Assert the + # call count, not just the stored value (which would be equal either way). + assert fake_cfg.save_calls == [] + assert saved[0] == cfg + + +def test_provider_register_noop_cross_writer_key(monkeypatch): + """The redaction sentinel ("***") and the canonical keyless value + ("no-key-required") must compare equal, so a config written by setup.sh + (api_key: no-key-required) is not rewritten on first plugin load.""" + import sys + + import _provider + from _provider import _declared_models + + models = [str(m) for m in _declared_models()] + existing = { + "name": "AWS Builder", + "transport": "openai_chat", + "base_url": "http://localhost:8088/v1", + "model": models[0], + "discover_models": False, + "api_key": "no-key-required", # as setup.sh writes it + "models": {m: {} for m in models}, + } + cfg = {"providers": {_provider.PROVIDER_SLUG: existing}} + fake_hermes, fake_cfg, saved = _make_hermes_cli_mock(cfg) + monkeypatch.setitem(sys.modules, "hermes_cli", fake_hermes) + monkeypatch.setitem(sys.modules, "hermes_cli.config", fake_cfg) + result = _provider.register_provider(8088) + assert result is True + assert fake_cfg.save_calls == [] assert saved[0] == cfg diff --git a/tests/test_setup.py b/tests/test_setup.py index 484e717..3e27476 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -47,3 +47,19 @@ def test_setup_default_model_keeps_auto_when_declared(tmp_path, monkeypatch): """When `auto` is declared first (the shipped default), model stays auto.""" out = _generate("models:\n - auto\n - claude-sonnet-4.5\n", tmp_path, monkeypatch) assert 'model: "auto"' in out + + +def test_setup_models_fallback_matches_static_models(tmp_path, monkeypatch): + """A manifest without `models:` must fall back to the full built-in + catalog (matching backend.STATIC_MODELS), not a single `auto`.""" + out = _generate("name: AWS Builder\n", tmp_path, monkeypatch) + assert "claude-sonnet-4.5" in out + assert "claude-haiku-4.5" in out + + +def test_setup_models_scalar_is_not_iterated(tmp_path, monkeypatch): + """A scalar `models: auto` must not be iterated character-by-character + (which would produce `model: "a"`); it falls back to the built-in catalog.""" + out = _generate("models: auto\n", tmp_path, monkeypatch) + assert 'model: "auto"' in out + assert 'model: "a"' not in out