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
This commit is contained in:
nesquena-hermes
2026-05-01 17:57:34 +00:00
parent 101d02a3e9
commit 0f594ec714
3 changed files with 151 additions and 0 deletions

View File

@@ -2,6 +2,11 @@
## [Unreleased]
## [v0.50.256] — 2026-05-01
### Fixed
- **TTS speaker icon and four other Lucide icons rendered invisibly** (#1413, closes #1413) — `static/icons.js::LI_PATHS` was missing five icon names that `static/*.js` calls `li('NAME', ...)` with. The `li()` helper logs `console.warn('li(): unknown icon NAME')` and returns an empty string when the name isn't registered, so the host element renders with `display:flex` and a click handler but no glyph. Five missing entries added: (1) `volume-2` — TTS speaker button on every assistant message (`ui.js:3376`); regression from #499, surfaced after #1411 (v0.50.255) fixed the CSS specificity collision and made the empty button visible-but-empty. Reported by @AvidFuturist via Telegram. (2) `chevron-up` — queue pill chevron (`ui.js:2178`); had a `▲` ASCII fallback but only when `li` itself was undefined, not when it returned `''`. (3) `hash`, (4) `cpu`, (5) `dollar-sign` — Insights panel stat cards (`panels.js:883-885`); fresh regression from #1405 (v0.50.255). New regression test `test_issue1413_li_path_coverage.py` walks every `li('NAME', ...)` call across `static/*.js` and asserts each `NAME` is registered in `LI_PATHS` — guards the entire class of bug, not just the five fixed here. (`static/icons.js`, `tests/test_issue1413_li_path_coverage.py`) — fixes #1413, reported by @AvidFuturist via Telegram
## [v0.50.255] — 2026-05-01
### Added

View File

@@ -62,6 +62,14 @@ const LI_PATHS = {
'clipboard-list': '<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1" ry="1"/><line x1="9" y1="12" x2="15" y2="12"/><line x1="9" y1="16" x2="12" y2="16"/>',
'map': '<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/>',
'git-branch': '<line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>',
// Audio / TTS
'volume-2': '<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/>',
// Queue pill chevron (ui.js queue indicator)
'chevron-up': '<polyline points="18 15 12 9 6 15"/>',
// Insights panel stat cards (panels.js)
'hash': '<line x1="4" y1="9" x2="20" y2="9"/><line x1="4" y1="15" x2="20" y2="15"/><line x1="10" y1="3" x2="8" y2="21"/><line x1="16" y1="3" x2="14" y2="21"/>',
'cpu': '<rect x="4" y="4" width="16" height="16" rx="2" ry="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/>',
'dollar-sign': '<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>',
};
/**

View File

@@ -0,0 +1,138 @@
"""
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."
)