154 lines
7.5 KiB
Python
154 lines
7.5 KiB
Python
"""Regression tests for issues #907, #908, #909 — model dropdown fixes."""
|
|
import re
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
UI_JS = (REPO_ROOT / "static" / "ui.js").read_text(encoding="utf-8")
|
|
PANELS_JS = (REPO_ROOT / "static" / "panels.js").read_text(encoding="utf-8")
|
|
|
|
|
|
# ── #907: Normalized dedup in _addLiveModelsToSelect ─────────────────────────
|
|
|
|
class TestIssue907LiveModelDedup:
|
|
"""Live-fetched models with @provider: prefix must not duplicate server-injected bare entries."""
|
|
|
|
def test_addLiveModelsToSelect_has_norm_dedup(self):
|
|
# _normId helper and existingNorm set must be present in _addLiveModelsToSelect
|
|
fn_idx = UI_JS.find('function _addLiveModelsToSelect(')
|
|
assert fn_idx != -1, "_addLiveModelsToSelect not found"
|
|
# Find the closing brace of the function (~800 chars is enough)
|
|
fn_body = UI_JS[fn_idx:fn_idx + 2000]
|
|
assert '_normId' in fn_body or 'existingNorm' in fn_body, (
|
|
"_addLiveModelsToSelect must normalise IDs before dedup check (#907)"
|
|
)
|
|
|
|
def test_normId_strips_at_prefix(self):
|
|
# The _normId lambda/function must strip @provider: prefix
|
|
fn_idx = UI_JS.find('function _addLiveModelsToSelect(')
|
|
fn_body = UI_JS[fn_idx:fn_idx + 2000]
|
|
has_at_strip = ("startsWith('@')" in fn_body or "split(':'" in fn_body)
|
|
assert has_at_strip, (
|
|
"_normId in _addLiveModelsToSelect must strip @provider: prefix for dedup (#907)"
|
|
)
|
|
|
|
def test_existingNorm_used_as_guard(self):
|
|
fn_idx = UI_JS.find('function _addLiveModelsToSelect(')
|
|
fn_body = UI_JS[fn_idx:fn_idx + 2000]
|
|
assert 'existingNorm.has(' in fn_body, (
|
|
"_addLiveModelsToSelect must check existingNorm before appending (#907)"
|
|
)
|
|
|
|
def test_normId_handles_multi_colon_ollama_ids(self):
|
|
"""_normId must strip ONLY the first colon so multi-colon Ollama tag IDs
|
|
(e.g. '@ollama-cloud:qwen3-vl:235b-instruct' vs bare 'qwen3-vl:235b-instruct')
|
|
still dedup correctly. JS `split(':',2)[1]` with limit=2 TRUNCATES in JS
|
|
(unlike Python's split), so the naive variant would lose the tag suffix
|
|
and mis-dedup.
|
|
"""
|
|
fn_idx = UI_JS.find('function _addLiveModelsToSelect(')
|
|
assert fn_idx != -1
|
|
fn_body = UI_JS[fn_idx:fn_idx + 2000]
|
|
# The implementation must use indexOf/substring or split().slice(1).join(),
|
|
# not split(':', 2)[1] which truncates the tail.
|
|
good = 'indexOf' in fn_body or "slice(1).join(':')" in fn_body
|
|
assert good, (
|
|
"_normId must strip only the first colon to preserve Ollama multi-colon "
|
|
"tag IDs (e.g. @ollama-cloud:qwen3-vl:235b-instruct). Use "
|
|
"substring(indexOf(':')+1) or split(':').slice(1).join(':') — NOT "
|
|
"split(':', 2)[1] which silently truncates in JS."
|
|
)
|
|
assert "split(':',2)[1]" not in fn_body and "split(':', 2)[1]" not in fn_body, (
|
|
"_normId still uses split(':', 2)[1] which truncates multi-colon IDs in JS; "
|
|
"use indexOf/substring instead."
|
|
)
|
|
|
|
|
|
# ── #908: window._defaultModel updated on settings save ─────────────────────
|
|
|
|
class TestIssue908DefaultModelSync:
|
|
"""window._defaultModel must be updated when the user saves a new default in Preferences."""
|
|
|
|
def test_applySavedSettingsUi_updates_window_defaultModel(self):
|
|
fn_idx = PANELS_JS.find('function _applySavedSettingsUi(')
|
|
assert fn_idx != -1, "_applySavedSettingsUi not found"
|
|
# Find the end of the function (next function definition)
|
|
fn_end = PANELS_JS.find('\nasync function saveSettings(', fn_idx)
|
|
fn_body = PANELS_JS[fn_idx:fn_end]
|
|
assert 'window._defaultModel' in fn_body, (
|
|
"_applySavedSettingsUi must update window._defaultModel so newSession() "
|
|
"uses the newly saved default without a page reload (#908)"
|
|
)
|
|
|
|
def test_defaultModel_update_conditioned_on_body_default_model(self):
|
|
fn_idx = PANELS_JS.find('function _applySavedSettingsUi(')
|
|
fn_end = PANELS_JS.find('\nasync function saveSettings(', fn_idx)
|
|
fn_body = PANELS_JS[fn_idx:fn_end]
|
|
# Must be guarded so we don't clear _defaultModel when body.default_model is absent
|
|
assert "if(body.default_model)" in fn_body or "body.default_model &&" in fn_body, (
|
|
"window._defaultModel assignment must be conditional on body.default_model being set"
|
|
)
|
|
|
|
|
|
# ── #909: Injected default model label quality ───────────────────────────────
|
|
|
|
class TestIssue909InjectedModelLabel:
|
|
"""The server must use a proper label for the injected default model (not raw lowercase ID)."""
|
|
|
|
def test_get_label_for_model_helper_exists(self):
|
|
import api.config as config
|
|
assert hasattr(config, '_get_label_for_model'), (
|
|
"api/config.py must define _get_label_for_model() for the injected default label (#909)"
|
|
)
|
|
|
|
def test_label_helper_capitalizes_bare_id(self):
|
|
from api.config import _get_label_for_model
|
|
label = _get_label_for_model('minimax/minimax-m2.7', [])
|
|
assert label != 'minimax-m2.7', (
|
|
"_get_label_for_model should not return the raw lowercase ID (#909)"
|
|
)
|
|
# Should capitalize: "Minimax M2.7" or similar
|
|
assert label[0].isupper(), "Label should start with an uppercase letter"
|
|
|
|
def test_label_helper_uses_catalog_when_available(self):
|
|
from api.config import _get_label_for_model
|
|
existing_groups = [
|
|
{"provider": "Nous", "models": [
|
|
{"id": "minimax/minimax-m2.7", "label": "Minimax M2.7 (Nous)"}
|
|
]}
|
|
]
|
|
label = _get_label_for_model('minimax/minimax-m2.7', existing_groups)
|
|
assert label == "Minimax M2.7 (Nous)", (
|
|
"_get_label_for_model should prefer catalog label over generated one"
|
|
)
|
|
|
|
def test_label_helper_strips_at_prefix_for_lookup(self):
|
|
from api.config import _get_label_for_model
|
|
existing_groups = [
|
|
{"provider": "Nous", "models": [
|
|
{"id": "minimax/minimax-m2.7", "label": "Minimax M2.7"}
|
|
]}
|
|
]
|
|
# @nous:minimax/minimax-m2.7 should match minimax/minimax-m2.7 in catalog
|
|
label = _get_label_for_model('@nous:minimax/minimax-m2.7', existing_groups)
|
|
assert label == "Minimax M2.7", (
|
|
"_get_label_for_model must strip @provider: prefix before catalog lookup"
|
|
)
|
|
|
|
def test_config_uses_label_helper_not_raw_split(self):
|
|
from pathlib import Path
|
|
config_src = (Path(__file__).resolve().parent.parent / "api" / "config.py").read_text()
|
|
# The raw label-building pattern should be replaced by the helper
|
|
assert "_get_label_for_model" in config_src, (
|
|
"api/config.py must call _get_label_for_model() for injected default model labels (#909)"
|
|
)
|
|
# The old raw pattern should NOT be present in the injection block
|
|
old_pattern = 'default_model.split("/")[-1] if "/" in default_model else default_model'
|
|
label_sections = [
|
|
config_src[i:i+200]
|
|
for i in [m.start() for m in re.finditer(r'label\s*=\s*', config_src)]
|
|
]
|
|
for sec in label_sections:
|
|
assert old_pattern not in sec, (
|
|
"api/config.py still uses raw split-based label for injected default model (#909)"
|
|
)
|