From 558fcb61466af84d4f8e0728d01ca9abc0ca2832 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 16 Jul 2026 06:42:09 -0700 Subject: [PATCH] test(dashboard): cover theme bootstrap CSS render + _serve_index injection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Server-side coverage for the critical-CSS shim (PR #36024 salvage): - user theme → style block emitted with ONLY real bundle variable names (--background-base/--midground-base from layerVars(), --theme-font-sans/--theme-base-size from typographyVars()/index.css), and an html,body rule expressed via those vars so runtime theme switches never leave a stale canvas/font - built-in / unknown / non-string active theme → no block - malformed theme YAML and load_config() exceptions → no crash, index still serves - breakout attempt in a theme value stays escaped - mount_spa integration: block present in for user themes, absent for built-ins --- tests/hermes_cli/test_web_server.py | 183 ++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) diff --git a/tests/hermes_cli/test_web_server.py b/tests/hermes_cli/test_web_server.py index 5cbdeffe461..691d3796a25 100644 --- a/tests/hermes_cli/test_web_server.py +++ b/tests/hermes_cli/test_web_server.py @@ -6130,6 +6130,189 @@ class TestDiscoverUserThemes: assert len(results) == 1 # only the valid one +class TestThemeBootstrapCSS: + """Tests for _render_active_theme_bootstrap_css() and its injection + into index.html via _serve_index() — the critical-CSS shim that kills + the default-teal first-paint flash for user YAML themes.""" + + @staticmethod + def _write_theme(hermes_home, name="ocean"): + themes_dir = hermes_home / "dashboard-themes" + themes_dir.mkdir(exist_ok=True) + (themes_dir / f"{name}.yaml").write_text( + f"name: {name}\n" + "label: Ocean\n" + "palette:\n" + " background:\n" + " hex: \"#0a1628\"\n" + " midground:\n" + " hex: \"#dbe4f0\"\n" + "typography:\n" + " fontSans: \"Inter, sans-serif\"\n" + " baseSize: \"17px\"\n", + encoding="utf-8", + ) + + def test_user_theme_renders_bundle_vars(self, tmp_path, monkeypatch): + """Active user theme → style block with ONLY variable names the + bundle actually consumes (layerVars/typographyVars tokens).""" + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + self._write_theme(tmp_path) + from hermes_cli import web_server + monkeypatch.setattr( + web_server, "load_config", lambda: {"dashboard": {"theme": "ocean"}} + ) + css = web_server._render_active_theme_bootstrap_css() + assert css.startswith('") + # Real bundle tokens (web/src/themes/context.tsx + index.css). + assert "--background-base:#0a1628;" in css + assert "--midground-base:#dbe4f0;" in css + assert "--theme-font-sans:Inter, sans-serif;" in css + assert "--theme-base-size:17px;" in css + # Names that do NOT exist in the bundle must not be emitted. + for bogus in ("--color-background", "--color-midground", + "--font-sans:", "--font-base-size"): + assert bogus not in css + # Canvas rule flows through the variables (never goes stale when + # applyTheme() rewrites them as inline styles at runtime). + assert "html,body{background-color:var(--background-base);" in css + assert "font-family:var(--theme-font-sans);" in css + assert "font-size:var(--theme-base-size);" in css + # No baked literal values in the html,body rule. + assert "#0a1628" not in css.split("html,body")[1] + + def test_builtin_theme_renders_nothing(self, tmp_path, monkeypatch): + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + from hermes_cli import web_server + for builtin in ("default", "midnight", "cyberpunk"): + monkeypatch.setattr( + web_server, "load_config", + lambda b=builtin: {"dashboard": {"theme": b}}, + ) + assert web_server._render_active_theme_bootstrap_css() == "" + + def test_unknown_theme_renders_nothing(self, tmp_path, monkeypatch): + """Configured theme has no YAML on disk → empty string, no crash.""" + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + from hermes_cli import web_server + monkeypatch.setattr( + web_server, "load_config", lambda: {"dashboard": {"theme": "ghost"}} + ) + assert web_server._render_active_theme_bootstrap_css() == "" + + def test_non_string_theme_renders_nothing(self, tmp_path, monkeypatch): + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + from hermes_cli import web_server + monkeypatch.setattr( + web_server, "load_config", lambda: {"dashboard": {"theme": 42}} + ) + assert web_server._render_active_theme_bootstrap_css() == "" + + def test_malformed_theme_yaml_no_crash(self, tmp_path, monkeypatch): + """A garbage YAML for the active theme name must not crash — the + discover helper skips it, so no style block is emitted.""" + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + themes_dir = tmp_path / "dashboard-themes" + themes_dir.mkdir() + (themes_dir / "broken.yaml").write_text( + "::: not valid yaml :::\n\tindent wrong", encoding="utf-8" + ) + from hermes_cli import web_server + monkeypatch.setattr( + web_server, "load_config", lambda: {"dashboard": {"theme": "broken"}} + ) + assert web_server._render_active_theme_bootstrap_css() == "" + + def test_load_config_exception_no_crash(self, monkeypatch): + from hermes_cli import web_server + + def boom(): + raise RuntimeError("config unreadable") + + monkeypatch.setattr(web_server, "load_config", boom) + assert web_server._render_active_theme_bootstrap_css() == "" + + def test_style_escape_defends_style_breakout(self, tmp_path, monkeypatch): + """`` in a theme value cannot break out of the block.""" + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + themes_dir = tmp_path / "dashboard-themes" + themes_dir.mkdir() + (themes_dir / "sneaky.yaml").write_text( + "name: sneaky\n" + "typography:\n" + " fontSans: ''\n", + encoding="utf-8", + ) + from hermes_cli import web_server + monkeypatch.setattr( + web_server, "load_config", lambda: {"dashboard": {"theme": "sneaky"}} + ) + css = web_server._render_active_theme_bootstrap_css() + assert css.count("") == 1 # only the legitimate closer + assert "<\\/style>" in css # payload was escaped, not emitted raw + + @staticmethod + def _mount_spa_client(tmp_path, monkeypatch): + from fastapi import FastAPI + from starlette.testclient import TestClient + import hermes_cli.web_server as ws + + dist = tmp_path / "web_dist" + (dist / "assets").mkdir(parents=True) + (dist / "index.html").write_text( + "tSPA", + encoding="utf-8", + ) + monkeypatch.setattr(ws, "WEB_DIST", dist) + spa_app = FastAPI() + ws.mount_spa(spa_app) + return TestClient(spa_app) + + def test_serve_index_injects_bootstrap_for_user_theme(self, tmp_path, monkeypatch): + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + self._write_theme(tmp_path) + import hermes_cli.web_server as ws + monkeypatch.setattr( + ws, "load_config", lambda: {"dashboard": {"theme": "ocean"}} + ) + client = self._mount_spa_client(tmp_path, monkeypatch) + resp = client.get("/chat") + assert resp.status_code == 200 + assert '