From 21a7564afd401917f2f196b97a26fb7265c7c146 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 15 Apr 2026 07:47:18 +0000 Subject: [PATCH] test: add 22 tests covering batch fixes v0.50.47 (#506-#521) --- tests/test_batch_fixes.py | 199 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 tests/test_batch_fixes.py diff --git a/tests/test_batch_fixes.py b/tests/test_batch_fixes.py new file mode 100644 index 0000000..0a6bc88 --- /dev/null +++ b/tests/test_batch_fixes.py @@ -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 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}" + )