Files
hermes-webui/tests/test_issue1413_li_path_coverage.py
T
nesquena-hermes 0f594ec714 fix: register 5 missing Lucide icons (TTS speaker + queue chevron + insights cards) (#1413)
The li() helper in static/icons.js logs console.warn and returns ''
when an icon name is not in LI_PATHS. Five icon names referenced by
static/*.js were never registered, so their host elements rendered as
empty 0-size buttons / containers despite display:flex.

Five missing icons added:

  - 'volume-2'    — TTS speaker on every assistant message
                    (ui.js:3376; regression from #499; surfaced after
                    #1411 fixed CSS specificity in v0.50.255)
  - 'chevron-up'  — queue pill chevron (ui.js:2178; the '▲' fallback
                    only fired when li was undefined, not when it
                    returned '')
  - 'hash'        — Insights 'Messages' stat card (panels.js:883)
  - 'cpu'         — Insights 'Tokens' stat card (panels.js:884)
  - 'dollar-sign' — Insights 'Cost' stat card (panels.js:885)

The Insights icons are a fresh regression from #1405 (v0.50.255).

Adds tests/test_issue1413_li_path_coverage.py — three tests:

  1. Walk every li('NAME', ...) call across static/*.js, assert NAME
     is registered in LI_PATHS. Prevents the entire class of bug.
  2. Pin the five icons added by this fix so removal gets a clear
     error message.
  3. Pin the warn+empty-string contract of li() so the diagnostic
     story in the test docstring stays accurate.

Reported by @AvidFuturist via Telegram, 2026-05-01.

Fixes #1413
2026-05-01 17:57:34 +00:00

139 lines
5.6 KiB
Python

"""
Regression test for #1413 — every li('NAME', ...) call in static/*.js must
reference an icon name registered in LI_PATHS in static/icons.js.
History
-------
- v0.50.255 #1411 fixed the CSS specificity collision on .msg-tts-btn (#1409),
making the TTS speaker button no longer display:none. But the button still
rendered empty because li('volume-2', 13) hit the unknown-icon branch and
returned ''. Reported by @AvidFuturist 2026-05-01.
- Audit at fix time also found 'chevron-up' (queue pill in ui.js), 'hash'
/ 'cpu' / 'dollar-sign' (insights panel stat cards in panels.js, shipped
in v0.50.255 #1405) silently failing the same way. All five added in this
fix.
Why this guard matters
----------------------
li() returns the empty string and only console.warns when an icon name is
missing. The button or container is then visually empty but the DOM, CSS,
and click handler all still work — so manual QA passes, automated DOM tests
pass, and the regression ships. Walking the call sites at test time is the
only cheap way to catch this entire class of bug.
What the test does
------------------
1. Parse static/icons.js to extract every key in the LI_PATHS object literal.
2. Walk every other static/*.js file and collect every li('NAME', ...) call.
3. Assert each NAME is registered.
If this test fires, fix it by adding the missing icon to LI_PATHS. Lucide
SVG paths are at https://lucide.dev/icons/<name>.
"""
from __future__ import annotations
import re
from pathlib import Path
import pytest
STATIC_DIR = Path(__file__).parent.parent / "static"
def _load_registered_icons() -> set[str]:
"""Return the set of icon names registered in LI_PATHS in icons.js."""
icons_js = (STATIC_DIR / "icons.js").read_text(encoding="utf-8")
# Match `const LI_PATHS = { ... };` (multiline, dotall)
obj_match = re.search(
r"const\s+LI_PATHS\s*=\s*\{(.+?)^\};",
icons_js,
re.DOTALL | re.MULTILINE,
)
assert obj_match, "Could not locate LI_PATHS object literal in icons.js"
body = obj_match.group(1)
# Each key is a single-quoted identifier (allowed: letters, digits, hyphen,
# underscore) followed by a colon. Comments are stripped first to avoid
# picking up example names from inline `//` comments.
body_no_comments = re.sub(r"//[^\n]*", "", body)
return set(re.findall(r"'([\w\-]+)'\s*:", body_no_comments))
def _collect_li_call_sites() -> list[tuple[str, int, str]]:
"""Return [(file_name, line_number, icon_name), ...] for every li() call
in static JS files (excluding icons.js which defines li itself)."""
pattern = re.compile(r"\bli\(\s*['\"]([\w\-]+)['\"]")
sites: list[tuple[str, int, str]] = []
for path in sorted(STATIC_DIR.glob("*.js")):
if path.name == "icons.js":
continue
for lineno, line in enumerate(
path.read_text(encoding="utf-8").splitlines(), start=1
):
for match in pattern.finditer(line):
sites.append((path.name, lineno, match.group(1)))
return sites
def test_all_li_calls_reference_registered_icons() -> None:
"""Every li('NAME', ...) in static/*.js must have NAME in LI_PATHS."""
registered = _load_registered_icons()
assert len(registered) > 20, (
"Sanity check: parsed only %d LI_PATHS keys — parser may be broken."
% len(registered)
)
call_sites = _collect_li_call_sites()
assert len(call_sites) > 10, (
"Sanity check: parser found only %d li() call sites across static/*.js"
% len(call_sites)
)
missing: dict[str, list[str]] = {}
for file_name, lineno, icon in call_sites:
if icon not in registered:
missing.setdefault(icon, []).append(f"{file_name}:{lineno}")
if missing:
report = "\n".join(
f" {icon!r} — referenced from: {', '.join(sites)}"
for icon, sites in sorted(missing.items())
)
pytest.fail(
"li() called with icon name(s) not registered in LI_PATHS in "
"static/icons.js. The button/container will render empty in "
"production. Add the Lucide SVG path to LI_PATHS for each:\n"
+ report
)
def test_specific_icons_present_after_1413_fix() -> None:
"""Pin the five icons added by the #1413 fix so regressions get a clear
error message (not just a generic "missing icon" failure)."""
registered = _load_registered_icons()
for icon in ("volume-2", "chevron-up", "hash", "cpu", "dollar-sign"):
assert icon in registered, (
f"Icon {icon!r} was added to LI_PATHS by the #1413 fix and must "
f"not be removed. Re-adding required: see static/icons.js."
)
def test_li_helper_warns_on_unknown_icon() -> None:
"""Assert the li() helper still uses the warn+empty-string fallback shape
we relied on when diagnosing #1413. If this contract changes (e.g. li
starts returning a placeholder glyph instead of ''), we lose the
deterministic "invisible button" symptom and these tests need updating."""
icons_js = (STATIC_DIR / "icons.js").read_text(encoding="utf-8")
# The helper body has the shape:
# const p = LI_PATHS[name];
# if (!p) { console.warn('li(): unknown icon', name); return ''; }
assert "console.warn('li(): unknown icon'" in icons_js, (
"li() helper no longer uses console.warn fallback — update the "
"#1413 audit story in this test file to match the new contract."
)
assert "return '';" in icons_js, (
"li() helper no longer returns empty string on unknown icon — "
"update the #1413 audit story in this test file."
)