mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-15 20:20:24 +00:00
0b0e2a3213
* stage-3712: model picker show-all overflow + fix per-row count leak (#3691) Self-rebased rodboev's #3712 onto master. FIX (Nathan-caught): the per-row provider chip stripped the '(N of M)' overflow count — that count belongs on the group heading only, and was reading as nonsense post-expand (row 30 showing '15 of 30'). Updated test to assert heading-carries-count + rows-do-not. Co-authored-by: rodboev <rodboev@users.noreply.github.com> * stage-3712: + dedupe overflow expand (Codex SILENT) — reuse existing injected option Codex caught: select hidden overflow model from search → reopen → Show all could duplicate the row (_appendOverflowOptionsToGroup blindly appended every extra model without checking the select for an already-injected option). Now moves/reuses an existing same-value option instead of re-creating it. * stage-3712: + fix expanded-heading double-count (Opus) — strip decoration, count rendered rows Opus caught: post-expand the heading showed 'Nous (2 of 4) (4)' (appended its own count onto the already-decorated backend label) and used modelCount (incl hoisted- configured) vs rendered rows. Now strips the '(N of M)' suffix when expanded + counts groupRows.length. Test asserts no double count on out[expanded]. * stage-3712: + include extra_models in Settings/Cron/Profile/Aux native selectors (Codex CORE) Codex caught: the server overflow split moves models>15 into extra_models for ALL providers, but the 4 native <select> builders in panels.js (settingsModel:6690, cronFormModel:1112, profileFormModel:5784, _auxProviders:8287) read g.models only — silently dropping the overflow tail for large providers. Now concat models+extra_models at all 4 sites. Test guard added. * Release PG: model picker show-all overflow for large provider catalogs (#3691) Self-rebased rodboev's #3712 onto v0.51.445; full browser-tested (desktop+mobile, before/after) + Nathan screenshot-approved. 4 gate-caught issues fixed inline: per-row count leak (Nathan), overflow-expand dedup (Codex), heading double-count (Opus), and native Settings/Cron/Profile/Aux selectors dropping the overflow tail (Codex CORE). Codex SAFE + Opus SHIP + 41 picker tests green. Co-authored-by: rodboev <rodboev@users.noreply.github.com> --------- Co-authored-by: nesquena-hermes <agent@nesquena-hermes> Co-authored-by: rodboev <rodboev@users.noreply.github.com>
134 lines
4.7 KiB
Python
134 lines
4.7 KiB
Python
"""Regression tests for /api/models/live backend TTL caching."""
|
|
|
|
import sys
|
|
import types
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
def _install_provider_model_ids(monkeypatch, fn):
|
|
hermes_cli = types.ModuleType("hermes_cli")
|
|
hermes_cli.__path__ = []
|
|
models = types.ModuleType("hermes_cli.models")
|
|
models.provider_model_ids = fn
|
|
monkeypatch.setitem(sys.modules, "hermes_cli", hermes_cli)
|
|
monkeypatch.setitem(sys.modules, "hermes_cli.models", models)
|
|
|
|
|
|
def _patch_live_models_basics(monkeypatch, routes, profile="default"):
|
|
import api.config as config
|
|
import api.profiles as profiles
|
|
|
|
routes._clear_live_models_cache()
|
|
monkeypatch.setattr(routes, "j", lambda _handler, payload, status=200, extra_headers=None: payload)
|
|
monkeypatch.setattr(config, "get_config", lambda: {"model": {"provider": "openai"}})
|
|
monkeypatch.setattr(config, "_resolve_provider_alias", lambda provider: provider)
|
|
monkeypatch.setattr(profiles, "get_active_profile_name", lambda: profile)
|
|
|
|
|
|
def test_live_models_cache_hits_within_ttl(monkeypatch):
|
|
import api.routes as routes
|
|
|
|
calls = []
|
|
|
|
def provider_model_ids(provider):
|
|
calls.append(provider)
|
|
return ["openai/gpt-test"]
|
|
|
|
_install_provider_model_ids(monkeypatch, provider_model_ids)
|
|
_patch_live_models_basics(monkeypatch, routes)
|
|
|
|
parsed = urlparse("/api/models/live?provider=openai")
|
|
first = routes._handle_live_models(object(), parsed)
|
|
second = routes._handle_live_models(object(), parsed)
|
|
|
|
assert calls == ["openai"]
|
|
assert first == second
|
|
assert first["models"] == [{"id": "openai/gpt-test", "label": "GPT Test"}]
|
|
|
|
|
|
def test_live_models_cache_expires(monkeypatch):
|
|
import api.routes as routes
|
|
|
|
now = [1000.0]
|
|
calls = []
|
|
|
|
def provider_model_ids(provider):
|
|
calls.append(provider)
|
|
return [f"{provider}/model-{len(calls)}"]
|
|
|
|
_install_provider_model_ids(monkeypatch, provider_model_ids)
|
|
_patch_live_models_basics(monkeypatch, routes)
|
|
monkeypatch.setattr(routes.time, "monotonic", lambda: now[0])
|
|
|
|
parsed = urlparse("/api/models/live?provider=openai")
|
|
first = routes._handle_live_models(object(), parsed)
|
|
now[0] += routes._LIVE_MODELS_CACHE_TTL + 1
|
|
second = routes._handle_live_models(object(), parsed)
|
|
|
|
assert calls == ["openai", "openai"]
|
|
assert first["models"][0]["id"] == "openai/model-1"
|
|
assert second["models"][0]["id"] == "openai/model-2"
|
|
|
|
|
|
def test_live_models_cache_is_profile_scoped(monkeypatch):
|
|
import api.routes as routes
|
|
import api.profiles as profiles
|
|
|
|
active_profile = ["default"]
|
|
calls = []
|
|
|
|
def provider_model_ids(provider):
|
|
calls.append((active_profile[0], provider))
|
|
return [f"{provider}/{active_profile[0]}-model"]
|
|
|
|
_install_provider_model_ids(monkeypatch, provider_model_ids)
|
|
_patch_live_models_basics(monkeypatch, routes)
|
|
monkeypatch.setattr(profiles, "get_active_profile_name", lambda: active_profile[0])
|
|
|
|
parsed = urlparse("/api/models/live?provider=openai")
|
|
default_payload = routes._handle_live_models(object(), parsed)
|
|
active_profile[0] = "research"
|
|
research_payload = routes._handle_live_models(object(), parsed)
|
|
again_payload = routes._handle_live_models(object(), parsed)
|
|
|
|
assert calls == [("default", "openai"), ("research", "openai")]
|
|
assert default_payload["models"][0]["id"] == "openai/default-model"
|
|
assert research_payload["models"][0]["id"] == "openai/research-model"
|
|
assert again_payload == research_payload
|
|
|
|
|
|
def test_live_models_cache_returns_deep_copies(monkeypatch):
|
|
import api.routes as routes
|
|
|
|
_install_provider_model_ids(monkeypatch, lambda provider: ["openai/gpt-test"])
|
|
_patch_live_models_basics(monkeypatch, routes)
|
|
|
|
parsed = urlparse("/api/models/live?provider=openai")
|
|
first = routes._handle_live_models(object(), parsed)
|
|
first["models"].clear()
|
|
first["provider"] = "mutated"
|
|
|
|
second = routes._handle_live_models(object(), parsed)
|
|
|
|
assert second["provider"] == "openai"
|
|
assert second["models"] == [{"id": "openai/gpt-test", "label": "GPT Test"}]
|
|
|
|
|
|
def test_live_models_endpoint_respects_picker_visibility_budget(monkeypatch):
|
|
import api.config as config
|
|
import api.routes as routes
|
|
|
|
_install_provider_model_ids(
|
|
monkeypatch,
|
|
lambda provider: [f"{provider}/model-{idx}" for idx in range(40)],
|
|
)
|
|
_patch_live_models_basics(monkeypatch, routes)
|
|
|
|
parsed = urlparse("/api/models/live?provider=openai")
|
|
payload = routes._handle_live_models(object(), parsed)
|
|
|
|
assert payload["count"] == config._MODEL_PICKER_VISIBLE_TARGET
|
|
assert [m["id"] for m in payload["models"]] == [
|
|
f"openai/model-{idx}" for idx in range(config._MODEL_PICKER_VISIBLE_TARGET)
|
|
]
|