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:
@@ -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
|
||||
|
||||
@@ -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"/>',
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
138
tests/test_issue1413_li_path_coverage.py
Normal file
138
tests/test_issue1413_li_path_coverage.py
Normal 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."
|
||||
)
|
||||
Reference in New Issue
Block a user