mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-14 15:07:45 +00:00
0ad95cb16a
release: v0.50.241
Batch release of 4 PRs:
- #1290 (@nickgiulioni1) — Inline audio/video media editor with playback
speed controls and HTTP byte-range streaming. PDF/media previews in
workspace file browser. Composer tray inline players for audio/video.
(Rebased from #1232.)
- #1287 (@renatomott) — Configured model badges (Primary / Fallback N) in
the model picker, carried through to the composer chip. Persists through
on-disk model cache.
- #1289 (@franksong2702) — Appearance autosave for theme/skin/font-size in
Settings; inline Saving / Saved / Failed status. Font size now persists
to config.yaml. Refs #1003.
- #1294 (@franksong2702) — Normalize agent session source metadata
(raw_source / session_source / source_label) through /api/sessions and
gateway watcher SSE snapshots. Existing source_tag / is_cli_session
fields preserved. Refs #1013.
Tests: 3254 passed, 2 skipped, 3 xpassed (was 3199 before this release).
Independently reviewed and approved by nesquena (commit d1738f6).
248 lines
9.9 KiB
Python
248 lines
9.9 KiB
Python
"""Tests for the batch of fixes from PRs #506-#521 (v0.50.47).
|
|
|
|
Covers:
|
|
- /root workspace unblocking (#510/#521)
|
|
- Attached-files split guard (#521)
|
|
- custom_providers model visibility (#515/#519)
|
|
- Cron skill cache invalidation (#507/#508)
|
|
- System (auto) theme (#504/#506/#509/#514)
|
|
"""
|
|
|
|
import pathlib
|
|
import re
|
|
|
|
REPO = pathlib.Path(__file__).parent.parent
|
|
|
|
|
|
def read(rel):
|
|
return (REPO / rel).read_text()
|
|
|
|
|
|
# ── Group A: /root workspace ──────────────────────────────────────────────────
|
|
|
|
class TestRootWorkspaceUnblocked:
|
|
|
|
def test_root_not_in_blocked_system_roots(self):
|
|
src = read("api/workspace.py")
|
|
assert "Path('/root')" not in src, (
|
|
"/root must not be in _BLOCKED_SYSTEM_ROOTS — "
|
|
"breaks deployments where Hermes runs as root"
|
|
)
|
|
|
|
def test_etc_still_blocked(self):
|
|
"""Sanity: other dangerous paths remain blocked.
|
|
|
|
After the macOS symlink fix, blocked roots are listed as bare strings
|
|
in a tuple and ``_workspace_blocked_roots()`` materialises both the
|
|
literal and resolved-canonical Path forms. Assert the source still
|
|
names ``/etc`` and ``/proc`` as blocked roots.
|
|
"""
|
|
src = read("api/workspace.py")
|
|
assert "'/etc'" in src or 'Path("/etc")' in src or "Path('/etc')" in src
|
|
assert "'/proc'" in src or 'Path("/proc")' in src or "Path('/proc')" in src
|
|
|
|
def test_split_guard_present(self):
|
|
src = read("api/streaming.py")
|
|
assert "'\\n\\n[Attached files:' in msg_text" in src, (
|
|
"base_text split must guard against missing '[Attached files:' "
|
|
"to avoid empty-string on plain messages"
|
|
)
|
|
|
|
|
|
# ── Group B: custom_providers visibility ─────────────────────────────────────
|
|
|
|
class TestCustomProvidersVisibility:
|
|
|
|
def test_has_custom_providers_variable_present(self):
|
|
src = read("api/config.py")
|
|
assert "_has_custom_providers" in src, (
|
|
"_has_custom_providers variable must exist in get_available_models()"
|
|
)
|
|
|
|
def test_discard_custom_conditional_on_no_custom_providers(self):
|
|
src = read("api/config.py")
|
|
assert "not _has_custom_providers" in src, (
|
|
"detected_providers.discard('custom') must be gated on "
|
|
"'not _has_custom_providers'"
|
|
)
|
|
|
|
def test_custom_providers_isinstance_check(self):
|
|
src = read("api/config.py")
|
|
assert "isinstance(_custom_providers_cfg, list)" in src, (
|
|
"_has_custom_providers must check isinstance(..., list)"
|
|
)
|
|
|
|
|
|
# ── Group C: cron skill cache ─────────────────────────────────────────────────
|
|
|
|
class TestCronSkillCacheInvalidation:
|
|
|
|
def _panels_src(self):
|
|
return read("static/panels.js")
|
|
|
|
def test_cache_busted_on_form_open(self):
|
|
src = self._panels_src()
|
|
# toggleCronForm should set cache to null unconditionally
|
|
# openCronCreate() opens the task create form (renamed from toggleCronForm
|
|
# in the main-view refactor). It must null the skills cache before fetching.
|
|
m = re.search(
|
|
r'function openCronCreate\(\)\{.*?_cronSkillsCache\s*=\s*null',
|
|
src, re.DOTALL
|
|
)
|
|
assert m, (
|
|
"openCronCreate must unconditionally null _cronSkillsCache "
|
|
"before fetching skills"
|
|
)
|
|
|
|
def test_cache_not_guarded_by_if_on_open(self):
|
|
src = self._panels_src()
|
|
# openCronCreate must not gate the fetch behind an if(!_cronSkillsCache) guard.
|
|
m = re.search(
|
|
r'function openCronCreate\(\)\{.*?\}',
|
|
src, re.DOTALL
|
|
)
|
|
assert m, "openCronCreate definition not found"
|
|
assert "if(!_cronSkillsCache)" not in m.group(0), (
|
|
"openCronCreate should not use 'if(!_cronSkillsCache)' guard — "
|
|
"cache must always be busted on open"
|
|
)
|
|
|
|
def test_cache_busted_on_skill_save(self):
|
|
src = self._panels_src()
|
|
# saveSkillForm() is the handler invoked on skill save (renamed from
|
|
# submitSkillSave in the main-view refactor; the old name still aliases it).
|
|
m = re.search(
|
|
r'async function saveSkillForm\(\).*?_skillsData\s*=\s*null.*?_cronSkillsCache\s*=\s*null',
|
|
src, re.DOTALL
|
|
)
|
|
assert m, (
|
|
"_cronSkillsCache must be set to null in saveSkillForm() "
|
|
"right after _skillsData = null"
|
|
)
|
|
|
|
|
|
# ── Group D: System (auto) theme ──────────────────────────────────────────────
|
|
|
|
class TestSystemTheme:
|
|
|
|
def test_apply_theme_helper_in_boot_js(self):
|
|
src = read("static/boot.js")
|
|
assert "function _applyTheme(" in src, (
|
|
"_applyTheme helper function must be defined in boot.js"
|
|
)
|
|
|
|
def test_apply_theme_resolves_system(self):
|
|
src = read("static/boot.js")
|
|
assert "normalized.theme==='system'" in src or "=== 'system'" in src, (
|
|
"_applyTheme must branch on 'system' to resolve via matchMedia"
|
|
)
|
|
|
|
def test_apply_theme_uses_matchmedia(self):
|
|
src = read("static/boot.js")
|
|
assert "prefers-color-scheme" in src, (
|
|
"_applyTheme must use matchMedia('(prefers-color-scheme:dark)')"
|
|
)
|
|
|
|
def test_load_settings_calls_apply_theme(self):
|
|
src = read("static/boot.js")
|
|
assert "_applyTheme(appearance.theme)" in src, (
|
|
"loadSettings must call _applyTheme() instead of direct data-theme assignment"
|
|
)
|
|
|
|
def test_system_option_in_theme_picker(self):
|
|
html = read("static/index.html")
|
|
assert "_pickTheme('system')" in html, (
|
|
"Theme picker must include a system theme button"
|
|
)
|
|
assert ">System<" in html, (
|
|
"Theme picker must show 'System' label"
|
|
)
|
|
|
|
def test_theme_picker_uses_pick_theme(self):
|
|
html = read("static/index.html")
|
|
assert "_pickTheme(" in html, (
|
|
"Theme buttons must call _pickTheme()"
|
|
)
|
|
|
|
def test_flicker_script_resolves_system(self):
|
|
html = read("static/index.html")
|
|
# The head flicker-prevention IIFE must handle 'system'
|
|
assert "==='system'" in html or "=== 'system'" in html, (
|
|
"Flicker-prevention head script must resolve 'system' before setting data-theme"
|
|
)
|
|
assert "legacy={slate:['dark','slate']" in html, (
|
|
"Flicker-prevention head script must normalize legacy theme names on first paint"
|
|
)
|
|
|
|
def test_system_in_commands_themes_list(self):
|
|
src = read("static/commands.js")
|
|
assert "'system'" in src, (
|
|
"/theme command must include 'system' in the valid themes array"
|
|
)
|
|
|
|
def test_commands_uses_apply_theme(self):
|
|
src = read("static/commands.js")
|
|
assert "_applyTheme(appearance.theme)" in src, (
|
|
"cmdTheme must call _applyTheme() with the normalized canonical theme"
|
|
)
|
|
|
|
def test_commands_accept_legacy_theme_aliases(self):
|
|
src = read("static/commands.js")
|
|
assert "const legacyThemes=Object.keys(_LEGACY_THEME_MAP||{});" in src, (
|
|
"cmdTheme must accept legacy theme aliases and map them onto canonical appearance values"
|
|
)
|
|
|
|
def test_panels_reverts_via_apply_theme(self):
|
|
src = read("static/panels.js")
|
|
block = re.search(r"function _revertSettingsPreview\(\)\{.*?\n\}", src, re.DOTALL)
|
|
assert block, "_revertSettingsPreview() should be present"
|
|
assert "_applyTheme(" not in block.group(0), (
|
|
"_revertSettingsPreview must no longer call _applyTheme() since Appearance now autosaves"
|
|
)
|
|
|
|
def test_system_theme_apply_path_uses_apply_theme(self):
|
|
src = read("static/boot.js")
|
|
assert "_applyTheme(appearance.theme)" in src, (
|
|
"System theme still must be activated through _applyTheme() in boot/theme application"
|
|
)
|
|
|
|
def test_panels_saves_system_string_not_resolved(self):
|
|
src = read("static/panels.js")
|
|
assert "localStorage.getItem('hermes-theme')" in src, (
|
|
"_settingsThemeOnOpen must read from localStorage to preserve "
|
|
"the 'system' string, not the resolved 'dark'/'light'"
|
|
)
|
|
|
|
def test_i18n_cmd_theme_includes_system_english(self):
|
|
src = read("static/i18n.js")
|
|
assert "system/dark/light" in src, (
|
|
"English cmd_theme i18n key must include 'system' in the theme list"
|
|
)
|
|
|
|
def test_i18n_cmd_theme_all_locales(self):
|
|
src = read("static/i18n.js")
|
|
count = src.count("system/dark/light")
|
|
assert count >= 5, (
|
|
f"cmd_theme description should mention 'system' in all 5 locales; "
|
|
f"found {count}"
|
|
)
|
|
|
|
def test_theme_listener_cleanup_uses_stable_handler(self):
|
|
src = read("static/boot.js")
|
|
assert "_systemThemeMq&&_onSystemThemeChange" in src, (
|
|
"_applyTheme must track the active OS-theme listener so it can be removed cleanly"
|
|
)
|
|
assert "removeEventListener('change',_onSystemThemeChange)" in src, (
|
|
"_applyTheme must remove the previous OS-theme listener before adding a new one"
|
|
)
|
|
|
|
def test_panels_hydrates_appearance_before_models_fetch(self):
|
|
src = read("static/panels.js")
|
|
skin_idx = src.index("const skinVal=(settings.skin||'default').toLowerCase();")
|
|
# models is now declared as let models=null before the try block
|
|
models_idx = src.index("models=await api('/api/models');")
|
|
assert skin_idx < models_idx, (
|
|
"loadSettingsPanel must hydrate theme/skin before awaiting /api/models, "
|
|
"otherwise a slow model fetch can clobber an in-progress skin selection"
|
|
)
|