557 lines
26 KiB
Python
557 lines
26 KiB
Python
"""Regression tests for #1567 — Nous Portal picker capacity + endpoint symmetry.
|
||
|
||
Two issues addressed in one PR:
|
||
|
||
1. **Endpoint disagreement (the bug):** The Settings → Providers card and the
|
||
model picker dropdown returned different Nous catalogs because their
|
||
detection paths differ. ``api/providers.py:get_providers`` iterates ALL
|
||
OAuth providers regardless of `list_available_providers().authenticated`.
|
||
``api/config.py:_build_available_models_uncached`` only includes providers
|
||
in ``detected_providers``, which is gated on
|
||
``list_available_providers().authenticated``. On some hermes_cli versions
|
||
that flag disagrees with ``get_auth_status(<id>).logged_in``. Result: the
|
||
providers card shows the live catalog (e.g. 396 models) and the picker
|
||
shows nothing or the stale 4-entry static fallback.
|
||
|
||
2. **UX cap (the design concern):** Even with the disagreement fixed, dumping
|
||
a 397-model dropdown into the picker would be unusable. We cap the
|
||
dropdown at ~15 featured entries (deterministic vendor-priority sample,
|
||
sticky for the user's currently-selected model) and return the full
|
||
catalog under ``extra_models`` so /model autocomplete and the dynamic
|
||
label map still cover everything.
|
||
|
||
Tests in this file pin both invariants.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
import types
|
||
|
||
import api.config as config
|
||
import api.profiles as profiles
|
||
|
||
|
||
# Big catalog matches the shape of an enterprise Nous Portal account.
|
||
# Volume distribution mirrors what we saw on Nathan's machine (~30 models)
|
||
# extrapolated up to ~400 with the same vendor mix Deor reported.
|
||
_BIG_CATALOG_VENDORS = {
|
||
"anthropic": 8, "openai": 30, "google": 12, "moonshotai": 5, "z-ai": 15,
|
||
"minimax": 10, "qwen": 80, "x-ai": 8, "deepseek": 20, "stepfun": 10,
|
||
"xiaomi": 6, "tencent": 12, "nvidia": 25, "arcee-ai": 8,
|
||
"meta-llama": 50, "mistralai": 40, "cohere": 25, "databricks": 15, "lambda-ai": 18,
|
||
}
|
||
|
||
|
||
def _build_big_catalog() -> list[str]:
|
||
out = []
|
||
for v, n in _BIG_CATALOG_VENDORS.items():
|
||
for i in range(n):
|
||
out.append(f"{v}/model-{v}-{i:02d}")
|
||
return out
|
||
|
||
|
||
def _install_fake_hermes_cli(
|
||
monkeypatch,
|
||
*,
|
||
nous_ids: list[str] | None = None,
|
||
raise_on_lookup: bool = False,
|
||
list_authenticated: bool = True,
|
||
auth_status_logged_in: bool = True,
|
||
):
|
||
"""Install fake ``hermes_cli`` modules with controllable Nous behavior.
|
||
|
||
The two flags ``list_authenticated`` and ``auth_status_logged_in`` model
|
||
the divergence between ``hermes_cli.models.list_available_providers()``
|
||
and ``hermes_cli.auth.get_auth_status()`` that #1567 calls out as a
|
||
real-world pattern on some hermes_cli versions.
|
||
"""
|
||
fake_pkg = types.ModuleType("hermes_cli")
|
||
fake_pkg.__path__ = []
|
||
|
||
fake_models = types.ModuleType("hermes_cli.models")
|
||
fake_models.list_available_providers = lambda: [
|
||
{"id": "nous", "label": "Nous Portal", "aliases": [], "authenticated": list_authenticated},
|
||
]
|
||
if raise_on_lookup:
|
||
def _raise(_pid):
|
||
raise RuntimeError("simulated hermes_cli failure")
|
||
fake_models.provider_model_ids = _raise
|
||
else:
|
||
ids = list(nous_ids) if nous_ids is not None else []
|
||
fake_models.provider_model_ids = lambda pid: ids if pid == "nous" else []
|
||
|
||
fake_auth = types.ModuleType("hermes_cli.auth")
|
||
|
||
def _get_auth_status(pid):
|
||
if pid == "nous":
|
||
return {"logged_in": auth_status_logged_in, "key_source": "oauth"}
|
||
return {}
|
||
|
||
fake_auth.get_auth_status = _get_auth_status
|
||
|
||
monkeypatch.setitem(sys.modules, "hermes_cli", fake_pkg)
|
||
monkeypatch.setitem(sys.modules, "hermes_cli.models", fake_models)
|
||
monkeypatch.setitem(sys.modules, "hermes_cli.auth", fake_auth)
|
||
monkeypatch.delitem(sys.modules, "agent.credential_pool", raising=False)
|
||
monkeypatch.delitem(sys.modules, "agent", raising=False)
|
||
|
||
config.invalidate_models_cache()
|
||
|
||
|
||
def _swap_in_test_config(extra_cfg):
|
||
old_cfg = dict(config.cfg)
|
||
old_mtime = config._cfg_mtime
|
||
config.cfg.clear()
|
||
config.cfg["model"] = {}
|
||
config.cfg.update(extra_cfg)
|
||
try:
|
||
config._cfg_mtime = config.Path(config._get_config_path()).stat().st_mtime
|
||
except Exception:
|
||
config._cfg_mtime = 0.0
|
||
|
||
def _restore():
|
||
config.cfg.clear()
|
||
config.cfg.update(old_cfg)
|
||
config._cfg_mtime = old_mtime
|
||
|
||
return _restore
|
||
|
||
|
||
def _scrub_provider_env(monkeypatch):
|
||
"""Drop every provider env var so detection doesn't leak unrelated keys."""
|
||
for var in (
|
||
"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GOOGLE_API_KEY", "GEMINI_API_KEY",
|
||
"DEEPSEEK_API_KEY", "XAI_API_KEY", "GROQ_API_KEY",
|
||
"MISTRAL_API_KEY", "OPENROUTER_API_KEY",
|
||
"OLLAMA_CLOUD_API_KEY", "OLLAMA_API_KEY",
|
||
"GLM_API_KEY", "KIMI_API_KEY", "MOONSHOT_API_KEY",
|
||
"MINIMAX_API_KEY", "MINIMAX_CN_API_KEY",
|
||
"XIAOMI_API_KEY",
|
||
"OPENCODE_ZEN_API_KEY", "OPENCODE_GO_API_KEY",
|
||
"NOUS_API_KEY", "NVIDIA_API_KEY", "LM_API_KEY", "LMSTUDIO_API_KEY",
|
||
):
|
||
monkeypatch.delenv(var, raising=False)
|
||
|
||
|
||
# ────────────────────────────────────────────────────────────────────────
|
||
# Section 1 — _build_nous_featured_set helper invariants
|
||
# ────────────────────────────────────────────────────────────────────────
|
||
|
||
|
||
class TestBuildNousFeaturedSet:
|
||
"""Unit tests for the deterministic featured-vs-extras split helper."""
|
||
|
||
def test_small_catalog_is_no_op(self):
|
||
from api.config import _build_nous_featured_set, _NOUS_FEATURED_THRESHOLD
|
||
# 20 entries — below the threshold, helper should return the input
|
||
# untouched and an empty extras list.
|
||
catalog = [f"vendor/model-{i:02d}" for i in range(20)]
|
||
assert len(catalog) <= _NOUS_FEATURED_THRESHOLD
|
||
featured, extras = _build_nous_featured_set(catalog)
|
||
assert featured == catalog
|
||
assert extras == []
|
||
|
||
def test_large_catalog_is_capped_to_target(self):
|
||
from api.config import _build_nous_featured_set, _NOUS_FEATURED_TARGET
|
||
catalog = _build_big_catalog()
|
||
assert len(catalog) > 100, "test fixture should produce a large catalog"
|
||
featured, extras = _build_nous_featured_set(catalog)
|
||
assert len(featured) == _NOUS_FEATURED_TARGET, (
|
||
f"Large catalog should produce exactly _NOUS_FEATURED_TARGET "
|
||
f"featured entries, got {len(featured)}."
|
||
)
|
||
assert len(extras) == len(catalog) - _NOUS_FEATURED_TARGET
|
||
|
||
def test_featured_and_extras_are_disjoint_and_complete(self):
|
||
from api.config import _build_nous_featured_set
|
||
catalog = _build_big_catalog()
|
||
featured, extras = _build_nous_featured_set(catalog)
|
||
assert set(featured) & set(extras) == set(), (
|
||
"featured and extras must be disjoint — every model belongs to "
|
||
"exactly one bucket."
|
||
)
|
||
assert set(featured) | set(extras) == set(catalog), (
|
||
"featured ∪ extras must equal the input catalog — no model "
|
||
"should be silently dropped."
|
||
)
|
||
|
||
def test_priority_vendors_get_picked_first(self):
|
||
from api.config import _build_nous_featured_set, _NOUS_VENDOR_PRIORITY
|
||
catalog = _build_big_catalog()
|
||
featured, _ = _build_nous_featured_set(catalog)
|
||
# Every priority vendor with ≥1 entry in the catalog must appear in
|
||
# featured (round-robin guarantee until we hit the slot budget).
|
||
featured_vendors = {m.split("/", 1)[0] for m in featured}
|
||
for v in _NOUS_VENDOR_PRIORITY:
|
||
if v in _BIG_CATALOG_VENDORS:
|
||
assert v in featured_vendors, (
|
||
f"Priority vendor {v!r} missing from featured set — "
|
||
f"round-robin guarantee violated."
|
||
)
|
||
|
||
def test_sticky_selection_is_preserved(self):
|
||
from api.config import _build_nous_featured_set
|
||
catalog = _build_big_catalog()
|
||
# Pick a model from a leftover (non-priority) vendor that wouldn't
|
||
# normally make the featured cut.
|
||
sticky = "lambda-ai/model-lambda-ai-15"
|
||
assert sticky in catalog
|
||
featured, extras = _build_nous_featured_set(catalog, selected_model_id=sticky)
|
||
assert sticky in featured, (
|
||
f"Sticky-selected model {sticky!r} must appear in featured — "
|
||
f"otherwise the user's choice gets orphaned out of the dropdown "
|
||
f"after a refresh."
|
||
)
|
||
assert sticky not in extras
|
||
|
||
def test_sticky_selection_handles_at_nous_prefix(self):
|
||
from api.config import _build_nous_featured_set
|
||
catalog = _build_big_catalog()
|
||
# The frontend stores selections as @nous:vendor/model — helper must
|
||
# strip the prefix to match against the bare-id catalog.
|
||
sticky_with_prefix = "@nous:lambda-ai/model-lambda-ai-15"
|
||
bare = "lambda-ai/model-lambda-ai-15"
|
||
featured, _ = _build_nous_featured_set(catalog, selected_model_id=sticky_with_prefix)
|
||
assert bare in featured
|
||
|
||
def test_curated_static_flagships_are_preserved(self):
|
||
from api.config import _build_nous_featured_set, _PROVIDER_MODELS
|
||
# Build a catalog that contains all the curated static IDs so the
|
||
# rule-2 path fires.
|
||
static_ids = []
|
||
for entry in _PROVIDER_MODELS.get("nous", []):
|
||
sid = entry["id"]
|
||
if sid.startswith("@nous:"):
|
||
sid = sid[len("@nous:"):]
|
||
static_ids.append(sid)
|
||
catalog = static_ids + [f"filler-vendor/filler-{i:03d}" for i in range(100)]
|
||
featured, _ = _build_nous_featured_set(catalog)
|
||
for sid in static_ids:
|
||
assert sid in featured, (
|
||
f"Curated static flagship {sid!r} dropped from featured set."
|
||
)
|
||
|
||
def test_empty_catalog_returns_empty(self):
|
||
from api.config import _build_nous_featured_set
|
||
f, e = _build_nous_featured_set([])
|
||
assert f == [] and e == []
|
||
|
||
def test_deterministic_across_calls(self):
|
||
from api.config import _build_nous_featured_set
|
||
catalog = _build_big_catalog()
|
||
f1, e1 = _build_nous_featured_set(catalog)
|
||
f2, e2 = _build_nous_featured_set(catalog)
|
||
assert f1 == f2 and e1 == e2, (
|
||
"Featured set must be deterministic — random/seeded selection "
|
||
"would cause cache thrash and dropdown flicker on every reload."
|
||
)
|
||
|
||
|
||
# ────────────────────────────────────────────────────────────────────────
|
||
# Section 2 — End-to-end /api/models behaviour with the cap applied
|
||
# ────────────────────────────────────────────────────────────────────────
|
||
|
||
|
||
class TestApiModelsLargeCatalog:
|
||
"""Wired-up test exercising the dispatch branch at config.py:2243."""
|
||
|
||
def test_picker_caps_large_catalog_and_exposes_extras(self, monkeypatch, tmp_path):
|
||
_scrub_provider_env(monkeypatch)
|
||
catalog = _build_big_catalog()
|
||
_install_fake_hermes_cli(monkeypatch, nous_ids=catalog)
|
||
monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: tmp_path)
|
||
|
||
restore = _swap_in_test_config({"model": {"provider": "nous"}})
|
||
try:
|
||
data = config.get_available_models()
|
||
nous_groups = [g for g in data["groups"] if g["provider_id"] == "nous"]
|
||
assert len(nous_groups) == 1
|
||
grp = nous_groups[0]
|
||
from api.config import _NOUS_FEATURED_TARGET
|
||
assert len(grp["models"]) == _NOUS_FEATURED_TARGET, (
|
||
f"Picker should render {_NOUS_FEATURED_TARGET} featured entries "
|
||
f"on a {len(catalog)}-model catalog, got {len(grp['models'])}."
|
||
)
|
||
assert "extra_models" in grp, (
|
||
"Capped Nous group must include 'extra_models' so /model "
|
||
"autocomplete and the label map cover the full catalog."
|
||
)
|
||
assert len(grp["extra_models"]) == len(catalog) - _NOUS_FEATURED_TARGET
|
||
# Optgroup label is decorated with the truncation count so the user
|
||
# knows the dropdown is intentionally trimmed.
|
||
assert f"{_NOUS_FEATURED_TARGET} of {len(catalog)}" in grp["provider"], (
|
||
f"Provider label should include '({_NOUS_FEATURED_TARGET} of "
|
||
f"{len(catalog)})' for capped catalogs, got {grp['provider']!r}."
|
||
)
|
||
finally:
|
||
restore()
|
||
|
||
def test_picker_does_not_cap_small_catalog(self, monkeypatch, tmp_path):
|
||
_scrub_provider_env(monkeypatch)
|
||
# 20 models — below threshold, should pass through with no extras.
|
||
small_catalog = [f"vendor-{i % 4}/model-{i:02d}" for i in range(20)]
|
||
_install_fake_hermes_cli(monkeypatch, nous_ids=small_catalog)
|
||
monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: tmp_path)
|
||
|
||
restore = _swap_in_test_config({"model": {"provider": "nous"}})
|
||
try:
|
||
data = config.get_available_models()
|
||
grp = next(g for g in data["groups"] if g["provider_id"] == "nous")
|
||
assert len(grp["models"]) == 20
|
||
assert "extra_models" not in grp or grp["extra_models"] == []
|
||
assert "of " not in grp["provider"], (
|
||
"Optgroup label should NOT include a truncation count when no "
|
||
"trimming happened, got " + repr(grp["provider"])
|
||
)
|
||
finally:
|
||
restore()
|
||
|
||
|
||
# ────────────────────────────────────────────────────────────────────────
|
||
# Section 3 — Auth-detection symmetry (#1567 part 1)
|
||
# ────────────────────────────────────────────────────────────────────────
|
||
|
||
|
||
class TestNousDetectionSymmetry:
|
||
"""The picker must include Nous whenever the providers card would —
|
||
fixes the asymmetric-detection bug at the heart of #1567."""
|
||
|
||
def test_picker_includes_nous_when_get_auth_status_logged_in(self, monkeypatch, tmp_path):
|
||
"""list_available_providers() reports authenticated=False but
|
||
get_auth_status('nous').logged_in=True. Picker must still show Nous."""
|
||
_scrub_provider_env(monkeypatch)
|
||
catalog = ["anthropic/claude-opus-4.7", "openai/gpt-5.5"]
|
||
_install_fake_hermes_cli(
|
||
monkeypatch,
|
||
nous_ids=catalog,
|
||
list_authenticated=False, # primary detection path says NO
|
||
auth_status_logged_in=True, # secondary detection path says YES
|
||
)
|
||
monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: tmp_path)
|
||
|
||
restore = _swap_in_test_config({"model": {"provider": "nous"}})
|
||
try:
|
||
data = config.get_available_models()
|
||
nous_groups = [g for g in data["groups"] if g["provider_id"] == "nous"]
|
||
assert nous_groups, (
|
||
"Picker must include Nous group when get_auth_status reports "
|
||
"logged_in=True, even if list_available_providers disagrees. "
|
||
"This is the asymmetric-detection bug from #1567."
|
||
)
|
||
assert len(nous_groups[0]["models"]) == 2
|
||
finally:
|
||
restore()
|
||
|
||
def test_picker_omits_nous_when_both_auth_signals_false(self, monkeypatch, tmp_path):
|
||
"""When neither signal reports authenticated, Nous should NOT appear.
|
||
Previously the static 4-entry list could leak in via the fallback path
|
||
even for unauthenticated users — that fallback is now scoped to the
|
||
hermes_cli-unavailable case only."""
|
||
_scrub_provider_env(monkeypatch)
|
||
_install_fake_hermes_cli(
|
||
monkeypatch,
|
||
nous_ids=[], # no live catalog (also no auth)
|
||
list_authenticated=False,
|
||
auth_status_logged_in=False,
|
||
)
|
||
monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: tmp_path)
|
||
|
||
restore = _swap_in_test_config({"model": {"provider": "anthropic"}})
|
||
try:
|
||
# Active provider is anthropic, not nous — so detected_providers
|
||
# only includes nous if the new auth-symmetry check fires.
|
||
data = config.get_available_models()
|
||
nous_groups = [g for g in data["groups"] if g["provider_id"] == "nous"]
|
||
assert not nous_groups, (
|
||
"Nous must NOT appear in picker when neither auth signal "
|
||
"reports authenticated. Got: " + str(nous_groups)
|
||
)
|
||
finally:
|
||
restore()
|
||
|
||
|
||
# ────────────────────────────────────────────────────────────────────────
|
||
# Section 4 — Live-fetch-empty handling (#1567 part 2)
|
||
# ────────────────────────────────────────────────────────────────────────
|
||
|
||
|
||
class TestNousLiveFetchEmpty:
|
||
"""When authenticated but live-fetch returns [] (transient hermes_cli
|
||
state, OAuth refresh in flight), DON'T fall back to the stale 4-entry
|
||
static list — that creates the providers-card-vs-picker disagreement
|
||
that #1567 reports. Omit the group entirely instead."""
|
||
|
||
def test_authenticated_empty_catalog_omits_nous_group(self, monkeypatch, tmp_path):
|
||
_scrub_provider_env(monkeypatch)
|
||
_install_fake_hermes_cli(
|
||
monkeypatch,
|
||
nous_ids=[], # live-fetch returns empty list (no exception)
|
||
auth_status_logged_in=True, # but user IS authenticated
|
||
)
|
||
monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: tmp_path)
|
||
|
||
restore = _swap_in_test_config({"model": {"provider": "nous"}})
|
||
try:
|
||
data = config.get_available_models()
|
||
nous_groups = [g for g in data["groups"] if g["provider_id"] == "nous"]
|
||
assert not nous_groups, (
|
||
"Authenticated user with empty live-fetch should NOT see "
|
||
"the stale 4-entry static list — that's exactly the "
|
||
"providers-card-vs-picker disagreement #1567 reports. "
|
||
"Omit the Nous group entirely; it'll re-populate on the "
|
||
"next cache rebuild when the live-fetch returns something."
|
||
)
|
||
finally:
|
||
restore()
|
||
|
||
def test_hermes_cli_unavailable_falls_back_to_static_4(self, monkeypatch, tmp_path):
|
||
"""When hermes_cli is unavailable (raises) — distinct from returning [] —
|
||
we DO fall back to the static 4-entry list so the picker isn't empty
|
||
in that degraded environment. This preserves pre-#1538 behavior for
|
||
test envs without hermes_cli."""
|
||
_scrub_provider_env(monkeypatch)
|
||
_install_fake_hermes_cli(
|
||
monkeypatch,
|
||
raise_on_lookup=True,
|
||
auth_status_logged_in=True,
|
||
)
|
||
monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: tmp_path)
|
||
|
||
restore = _swap_in_test_config({"model": {"provider": "nous"}})
|
||
try:
|
||
data = config.get_available_models()
|
||
nous_groups = [g for g in data["groups"] if g["provider_id"] == "nous"]
|
||
assert nous_groups, (
|
||
"When hermes_cli raises, Nous group MUST still appear with "
|
||
"the curated static fallback so the picker isn't empty in "
|
||
"test envs that lack the agent package."
|
||
)
|
||
assert len(nous_groups[0]["models"]) == 4, (
|
||
"Static fallback should expose the curated 4-entry list "
|
||
"from _PROVIDER_MODELS['nous']."
|
||
)
|
||
finally:
|
||
restore()
|
||
|
||
|
||
# ────────────────────────────────────────────────────────────────────────
|
||
# Section 5 — Providers card ↔ picker symmetry
|
||
# ────────────────────────────────────────────────────────────────────────
|
||
|
||
|
||
class TestProvidersCardPickerSymmetry:
|
||
"""Both endpoints must report the same featured set + total count for
|
||
Nous Portal. This is the load-bearing invariant that ends the visual
|
||
disagreement #1567 reports."""
|
||
|
||
def test_providers_card_and_picker_agree_on_featured_set(self, monkeypatch, tmp_path):
|
||
_scrub_provider_env(monkeypatch)
|
||
catalog = _build_big_catalog()
|
||
_install_fake_hermes_cli(monkeypatch, nous_ids=catalog)
|
||
monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: tmp_path)
|
||
|
||
restore = _swap_in_test_config({"model": {"provider": "nous"}})
|
||
try:
|
||
from api.providers import get_providers
|
||
from api.config import _NOUS_FEATURED_TARGET
|
||
|
||
providers = {p["id"]: p for p in get_providers()["providers"]}
|
||
picker = config.get_available_models()
|
||
picker_nous = next(g for g in picker["groups"] if g["provider_id"] == "nous")
|
||
|
||
card = providers["nous"]
|
||
# Both render exactly _NOUS_FEATURED_TARGET visible models.
|
||
assert len(card["models"]) == _NOUS_FEATURED_TARGET
|
||
assert len(picker_nous["models"]) == _NOUS_FEATURED_TARGET
|
||
|
||
# Both report the full catalog size somewhere.
|
||
assert card["models_total"] == len(catalog), (
|
||
f"Providers card models_total should match live catalog size, "
|
||
f"got {card['models_total']} vs catalog {len(catalog)}."
|
||
)
|
||
picker_total = len(picker_nous.get("models", [])) + len(
|
||
picker_nous.get("extra_models", [])
|
||
)
|
||
assert picker_total == len(catalog), (
|
||
f"Picker featured + extras must equal live catalog size, "
|
||
f"got {picker_total} vs {len(catalog)}."
|
||
)
|
||
|
||
# And they pick THE SAME featured set (not e.g. one's first-15
|
||
# and another's last-15).
|
||
card_ids = [m["id"] for m in card["models"]]
|
||
picker_ids = [m["id"] for m in picker_nous["models"]]
|
||
assert card_ids == picker_ids, (
|
||
f"Providers card and picker must show the SAME featured "
|
||
f"set so users see consistent labels in both places. "
|
||
f"Card: {card_ids}\nPicker: {picker_ids}"
|
||
)
|
||
finally:
|
||
restore()
|
||
|
||
|
||
# ────────────────────────────────────────────────────────────────────────
|
||
# Section 6 — Frontend contract (static-source assertions)
|
||
# ────────────────────────────────────────────────────────────────────────
|
||
|
||
|
||
class TestFrontendExtrasContract:
|
||
"""Pin the JS-side contract: dropdown reads `models`, slash command and
|
||
label map ALSO read `extra_models`. Without this, a model from the
|
||
catalog tail gets a bare-ID label or is invisible to /model autocomplete."""
|
||
|
||
def test_ui_js_hydrates_dynamic_labels_from_extra_models(self):
|
||
from pathlib import Path
|
||
src = (Path(__file__).resolve().parent.parent / "static" / "ui.js").read_text(encoding="utf-8")
|
||
# Find the populateModelDropdown function and check it consumes
|
||
# extra_models. Use a windowed substring search so the test stays
|
||
# robust against minor refactors of surrounding code.
|
||
idx = src.find("async function populateModelDropdown")
|
||
assert idx != -1
|
||
body = src[idx : idx + 3000]
|
||
assert "extra_models" in body, (
|
||
"populateModelDropdown must hydrate _dynamicModelLabels from "
|
||
"g.extra_models so a model selected outside the featured set "
|
||
"still gets a proper label. Without this, /model audio-lines "
|
||
"→ 'audio-lines' bare-ID display. (#1567)"
|
||
)
|
||
|
||
def test_commands_js_loads_slash_args_from_extra_models(self):
|
||
from pathlib import Path
|
||
src = (Path(__file__).resolve().parent.parent / "static" / "commands.js").read_text(encoding="utf-8")
|
||
idx = src.find("async function _loadSlashModelSubArgs")
|
||
assert idx != -1
|
||
body = src[idx : idx + 1500]
|
||
assert "extra_models" in body, (
|
||
"_loadSlashModelSubArgs must iterate group.extra_models so /model "
|
||
"autocomplete covers the full catalog, not just the dropdown's "
|
||
"featured subset. The slash command exists precisely so power "
|
||
"users can reach any model by typing its name. (#1567)"
|
||
)
|
||
|
||
def test_panels_js_uses_models_total_for_count(self):
|
||
from pathlib import Path
|
||
src = (Path(__file__).resolve().parent.parent / "static" / "panels.js").read_text(encoding="utf-8")
|
||
idx = src.find("function _buildProviderCard")
|
||
assert idx != -1
|
||
body = src[idx : idx + 1500]
|
||
assert "models_total" in body, (
|
||
"Provider card header should use p.models_total (full catalog "
|
||
"size) for the count, not p.models.length (which is now the "
|
||
"trimmed featured-set size). Without this, the header text says "
|
||
"'15 models' instead of '396 models' for capped catalogs. (#1567)"
|
||
)
|
||
|
||
def test_panels_js_renders_more_disclosure_pill(self):
|
||
from pathlib import Path
|
||
src = (Path(__file__).resolve().parent.parent / "static" / "panels.js").read_text(encoding="utf-8")
|
||
# The "+N more" disclosure must reference the difference between
|
||
# rendered count and total count somewhere in the providers-card
|
||
# rendering path.
|
||
assert "provider-card-model-tag-more" in src, (
|
||
"Provider card must render a '+N more' disclosure pill when "
|
||
"len(models) < models_total, so users know the dropdown is "
|
||
"intentionally capped and the rest is reachable via /model."
|
||
)
|