v0.50.223: model picker, idle retry, drag-drop, CSP, clipboard copy (#1127)
* fix(#604): model picker shows all configured providers
Two fixes to ensure the model picker surface every provider a user has
configured:
1. Added env var detection for XAI_API_KEY (→ x-ai) and MISTRAL_API_KEY
(→ mistralai). Previously these providers were only detectable via
hermes auth or credential pool, not via environment variables.
2. Added config.yaml providers section scanning. Users who configure
providers in config.yaml (e.g. providers.anthropic.api_key) without
setting the corresponding env var will now see those providers in the
model picker. Only providers with known model catalogs are added.
- Added 12 regression tests
* fix(#1112): allow Google Fonts in CSP style-src and font-src
Mermaid themes inject @import for fonts.googleapis.com at render time.
CSP style-src blocked these requests, causing console violations.
- Add https://fonts.googleapis.com to style-src (CSS stylesheets)
- Add https://fonts.gstatic.com to font-src (WOFF2/WOFF font files)
- Add 3 regression tests + verify existing CSP tests still pass
* fix(#1118): retry api() calls on network errors after long idle
After a long idle period, the browser's TCP keep-alive connection to the
server can become stale. The next fetch() throws a TypeError (network
failure), causing 'Failed to load session' instead of transparently
reconnecting.
- Added retry loop in api() (workspace.js): up to 3 attempts
- Only retries on TypeError (network failures), NOT on HTTP errors (4xx/5xx)
- 401 redirects still fire immediately
- Added 6 regression tests
* feat(#1116): composer placeholder reflects active profile name
When a named profile is active (not 'default'), the composer placeholder
and title bar show the profile name (capitalised) instead of the global
bot_name. Falls back to bot_name/'Hermes' for the default profile.
- boot.js: applyBotName() checks S.activeProfile before _botName
- panels.js: switchToProfile() calls applyBotName() after switch
- Added 5 regression tests
* feat(#1097): drag and drop workspace files into chat composer
Files and folders in the workspace file tree are now draggable.
Dropping them into the composer inserts @path reference at cursor
position. OS file drag-and-drop (attach files) still works.
- ui.js: _renderTreeItems sets draggable + dragstart with ws-path
- panels.js: drop handler checks for application/ws-path first,
inserts @path with smart spacing and cursor positioning
- Added 9 regression tests
* fix(#1096): copy buttons work — add clipboard-write Permissions-Policy
Copy buttons on messages and code blocks were silently failing because
the Permissions-Policy header did not include clipboard-write=(self).
Firefox blocks navigator.clipboard.writeText() without explicit permission.
- api/helpers.py: add clipboard-write=(self) to Permissions-Policy
- ui.js: _copyText now catches clipboard API errors and falls back
to execCommand('copy'). _fallbackCopy extracted as separate function
with proper focus() call and visible-but-hidden positioning (not -9999px)
- Added 8 regression tests
* chore: CHANGELOG for v0.50.223
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 22:29:02 +00:00
|
|
|
"""Tests for #604 — model picker shows all configured providers."""
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _src() -> str:
|
|
|
|
|
with open("api/config.py") as f:
|
|
|
|
|
return f.read()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_provider_models_keys() -> set:
|
|
|
|
|
"""Extract top-level provider keys from _PROVIDER_MODELS dict."""
|
|
|
|
|
with open("api/config.py") as f:
|
|
|
|
|
lines = f.readlines()
|
|
|
|
|
keys = []
|
|
|
|
|
in_dict = False
|
|
|
|
|
for line in lines:
|
|
|
|
|
if "_PROVIDER_MODELS = {" in line:
|
|
|
|
|
in_dict = True
|
|
|
|
|
continue
|
|
|
|
|
if in_dict:
|
|
|
|
|
m = re.match(r'^ "([^"]+)":\s*\[', line)
|
|
|
|
|
if m:
|
|
|
|
|
keys.append(m.group(1))
|
|
|
|
|
if re.match(r'^\}', line):
|
|
|
|
|
break
|
|
|
|
|
return set(keys)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_PROVIDER_MODELS_KEYS = _get_provider_models_keys()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestProviderDetectionEnvVars:
|
|
|
|
|
"""All known env vars should map to valid provider IDs."""
|
|
|
|
|
|
|
|
|
|
# Providers that exist but aren't in _PROVIDER_MODELS (use special handling)
|
|
|
|
|
_SPECIAL_PROVIDERS = {"openrouter", "ollama-cloud", "custom", "ollama", "lmstudio", "local"}
|
|
|
|
|
|
|
|
|
|
def test_xai_env_maps_to_xai_provider(self):
|
|
|
|
|
"""XAI_API_KEY should add 'x-ai' (not 'xai')."""
|
|
|
|
|
src = _src()
|
|
|
|
|
assert re.search(r'XAI_API_KEY.*?add\("x-ai"\)', src, re.DOTALL), \
|
|
|
|
|
"XAI_API_KEY must map to provider 'x-ai'"
|
|
|
|
|
|
|
|
|
|
def test_mistral_env_maps_to_mistralai_provider(self):
|
|
|
|
|
"""MISTRAL_API_KEY should add 'mistralai' (not 'mistral')."""
|
|
|
|
|
src = _src()
|
|
|
|
|
assert re.search(r'MISTRAL_API_KEY.*?add\("mistralai"\)', src, re.DOTALL), \
|
|
|
|
|
"MISTRAL_API_KEY must map to provider 'mistralai'"
|
|
|
|
|
|
|
|
|
|
def test_all_provider_env_vars_map_to_known_providers(self):
|
|
|
|
|
"""Every detected_provider.add() call should reference a known provider."""
|
|
|
|
|
src = _src()
|
|
|
|
|
fn = re.search(r'def _build_available_models_uncached', src)
|
|
|
|
|
fn_block = src[fn.start():fn.start() + 10000]
|
|
|
|
|
adds = re.findall(r'detected_providers\.add\("([^"]+)"\)', fn_block)
|
|
|
|
|
unknown = [p for p in adds if p not in _PROVIDER_MODELS_KEYS and p not in self._SPECIAL_PROVIDERS]
|
|
|
|
|
assert not unknown, \
|
|
|
|
|
f"Unknown provider IDs in env var detection: {unknown}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestConfigProvidersDetection:
|
|
|
|
|
"""Providers listed in config.yaml providers section should be detected."""
|
|
|
|
|
|
|
|
|
|
def test_cfg_providers_detection_exists(self):
|
|
|
|
|
"""_build_available_models must scan cfg['providers'] for known providers."""
|
|
|
|
|
src = _src()
|
|
|
|
|
assert "cfg.get(\"providers\", {})" in src, \
|
|
|
|
|
"Must read cfg['providers']"
|
|
|
|
|
assert "_cfg_providers" in src, \
|
|
|
|
|
"Must use _cfg_providers variable"
|
|
|
|
|
|
|
|
|
|
def test_cfg_providers_only_adds_known(self):
|
|
|
|
|
"""Only providers in _PROVIDER_MODELS should be added from config."""
|
|
|
|
|
src = _src()
|
|
|
|
|
# Find the config providers detection block
|
|
|
|
|
m = re.search(r'Also detect providers explicitly listed', src)
|
|
|
|
|
assert m, "Comment about config.yaml providers detection must exist"
|
|
|
|
|
block = src[m.start():m.start() + 500]
|
|
|
|
|
assert "_PROVIDER_MODELS" in block, \
|
|
|
|
|
"Config providers detection must check against _PROVIDER_MODELS"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestProviderModelsCompleteness:
|
|
|
|
|
"""Verify _PROVIDER_MODELS has expected providers."""
|
|
|
|
|
|
|
|
|
|
def test_has_anthropic(self):
|
|
|
|
|
assert "anthropic" in _PROVIDER_MODELS_KEYS
|
|
|
|
|
|
|
|
|
|
def test_has_openai(self):
|
|
|
|
|
assert "openai" in _PROVIDER_MODELS_KEYS
|
|
|
|
|
|
|
|
|
|
def test_has_google(self):
|
|
|
|
|
assert "google" in _PROVIDER_MODELS_KEYS
|
|
|
|
|
|
|
|
|
|
def test_has_deepseek(self):
|
|
|
|
|
assert "deepseek" in _PROVIDER_MODELS_KEYS
|
|
|
|
|
|
|
|
|
|
def test_has_xai(self):
|
|
|
|
|
assert "x-ai" in _PROVIDER_MODELS_KEYS
|
|
|
|
|
|
|
|
|
|
def test_has_mistralai(self):
|
|
|
|
|
assert "mistralai" in _PROVIDER_MODELS_KEYS
|
|
|
|
|
|
|
|
|
|
def test_has_openrouter(self):
|
|
|
|
|
# openrouter uses _FALLBACK_MODELS, not _PROVIDER_MODELS
|
|
|
|
|
pass # intentionally no assertion
|
2026-04-29 07:50:32 +00:00
|
|
|
|
|
|
|
|
def test_has_minimax_cn(self):
|
|
|
|
|
assert "minimax-cn" in _PROVIDER_MODELS_KEYS
|