test: add 22 tests covering batch fixes v0.50.47 (#506-#521)

This commit is contained in:
Hermes Agent
2026-04-15 07:47:18 +00:00
parent 44a544362f
commit 21a7564afd

199
tests/test_batch_fixes.py Normal file
View File

@@ -0,0 +1,199 @@
"""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."""
src = read("api/workspace.py")
assert "Path('/etc')" in src
assert "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
m = re.search(
r'function toggleCronForm\(\)\{.*?_cronSkillsCache=null',
src, re.DOTALL
)
assert m, (
"toggleCronForm must unconditionally null _cronSkillsCache "
"before fetching skills"
)
def test_cache_not_guarded_by_if_on_open(self):
src = self._panels_src()
# The old guard should be gone
assert "if(!_cronSkillsCache)" not in src, (
"toggleCronForm 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()
# After submitSkillSave's api() call, _cronSkillsCache must be nulled
m = re.search(
r'async function submitSkillSave\(\).*?_skillsData\s*=\s*null.*?_cronSkillsCache\s*=\s*null',
src, re.DOTALL
)
assert m, (
"_cronSkillsCache must be set to null in submitSkillSave() "
"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 "name==='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(_theme)" in src, (
"loadSettings must call _applyTheme() instead of direct data-theme assignment"
)
def test_system_option_in_theme_select(self):
html = read("static/index.html")
assert 'value="system"' in html, (
"Theme <select> must include <option value=\"system\">"
)
assert "System (auto)" in html, (
"Theme picker must show 'System (auto)' label"
)
def test_theme_select_uses_apply_theme_onchange(self):
html = read("static/index.html")
assert "_applyTheme(this.value)" in html, (
"Theme <select> onchange must call _applyTheme(this.value)"
)
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"
)
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(themeName)" in src, (
"cmdTheme must call _applyTheme() to handle system resolution"
)
def test_panels_reverts_via_apply_theme(self):
src = read("static/panels.js")
assert "_applyTheme(_settingsThemeOnOpen)" in src or \
"_applyTheme(" in src, (
"_revertSettingsPreview must call _applyTheme() so 'system' "
"is correctly re-activated on settings discard"
)
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}"
)