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 #1118 — api() retries on network errors (stale keep-alive after idle)."""
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _src() -> str:
|
|
|
|
|
with open("static/workspace.js") as f:
|
|
|
|
|
return f.read()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestApiRetryOnNetworkError:
|
|
|
|
|
"""The api() function in workspace.js should retry on fetch TypeError (network failure)."""
|
|
|
|
|
|
|
|
|
|
def test_api_function_has_retry_loop(self):
|
|
|
|
|
"""api() must contain a retry loop (for/while with attempt counter)."""
|
|
|
|
|
src = _src()
|
|
|
|
|
assert re.search(r'for\s*\(\s*let\s+\w+\s*=\s*0\s*;', src), \
|
|
|
|
|
"api() must have a for loop with attempt counter"
|
|
|
|
|
|
|
|
|
|
def test_api_retries_on_typeerror(self):
|
|
|
|
|
"""api() must retry when fetch throws TypeError (network failure)."""
|
|
|
|
|
src = _src()
|
|
|
|
|
# Find the api function body
|
|
|
|
|
m = re.search(r'async function api\(path,opts.*?\n\}(?!\n[\s]*function)', src, re.DOTALL)
|
|
|
|
|
assert m, "api() function must exist"
|
|
|
|
|
body = m.group(0)
|
|
|
|
|
assert 'TypeError' in body, \
|
|
|
|
|
"api() must check for TypeError to detect network failures"
|
|
|
|
|
assert 'attempt' in body, \
|
|
|
|
|
"api() must track attempt count for retry logic"
|
|
|
|
|
|
|
|
|
|
def test_api_does_not_retry_http_errors(self):
|
|
|
|
|
"""api() must NOT retry on HTTP error responses (4xx/5xx) — only network failures."""
|
|
|
|
|
src = _src()
|
|
|
|
|
m = re.search(r'async function api\(path,opts.*?\n\}(?!\n[\s]*function)', src, re.DOTALL)
|
|
|
|
|
assert m, "api() function must exist"
|
|
|
|
|
body = m.group(0)
|
|
|
|
|
# The retry should be in catch block (after res.ok check), not in the ok path
|
|
|
|
|
# HTTP errors throw Error (not TypeError), so only TypeError triggers retry
|
|
|
|
|
assert 'e instanceof TypeError' in body, \
|
|
|
|
|
"api() retry must be limited to TypeError (network errors), not all errors"
|
|
|
|
|
|
|
|
|
|
def test_api_max_3_attempts(self):
|
|
|
|
|
"""api() must limit retries (max 3 attempts total = 1 initial + 2 retries)."""
|
|
|
|
|
src = _src()
|
|
|
|
|
m = re.search(r'async function api\(path,opts.*?\n\}(?!\n[\s]*function)', src, re.DOTALL)
|
|
|
|
|
assert m, "api() function must exist"
|
|
|
|
|
body = m.group(0)
|
|
|
|
|
# Should have attempt < 2 (0, 1, 2 = 3 attempts max)
|
|
|
|
|
assert re.search(r'attempt\s*<\s*2', body), \
|
|
|
|
|
"api() must limit to 3 attempts max (attempt < 2)"
|
|
|
|
|
|
|
|
|
|
def test_api_preserves_401_redirect(self):
|
2026-05-04 06:57:52 +00:00
|
|
|
"""api() must still redirect to login on 401 without escaping subpath mounts."""
|
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
|
|
|
src = _src()
|
|
|
|
|
assert "res.status===401" in src, \
|
|
|
|
|
"api() must still check for 401 status"
|
2026-05-04 06:57:52 +00:00
|
|
|
assert "login?next=" in src, \
|
|
|
|
|
"api() must still redirect to login on 401"
|
|
|
|
|
assert "/login?next=" not in src, \
|
|
|
|
|
"api() must not escape subpath mounts by redirecting to root /login"
|
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
|
|
|
|
|
|
|
|
def test_api_preserves_error_parsing(self):
|
|
|
|
|
"""api() must still parse JSON error bodies for non-200 responses."""
|
|
|
|
|
src = _src()
|
|
|
|
|
assert "JSON.parse(text)" in src, \
|
|
|
|
|
"api() must still parse JSON error responses"
|
|
|
|
|
assert "res.json()" in src, \
|
|
|
|
|
"api() must still parse JSON success responses"
|