Files
hermes-webui/tests/test_issue1617_tps_message_header.py
T
2026-05-10 10:31:14 -06:00

141 lines
7.5 KiB
Python

"""Regression coverage for issue #1617: TPS belongs on message headers.
Product decision:
- show live TPS in the assistant message header while streaming when real TPS is available;
- persist/show the final TPS at the end of the turn;
- do not show placeholder or estimated TPS when unavailable.
"""
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
CONFIG_PY = (REPO / "api" / "config.py").read_text(encoding="utf-8")
STREAMING_PY = (REPO / "api" / "streaming.py").read_text(encoding="utf-8")
BOOT_JS = (REPO / "static" / "boot.js").read_text(encoding="utf-8")
INDEX_HTML = (REPO / "static" / "index.html").read_text(encoding="utf-8")
MESSAGES_JS = (REPO / "static" / "messages.js").read_text(encoding="utf-8")
PANELS_JS = (REPO / "static" / "panels.js").read_text(encoding="utf-8")
UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8")
def test_tps_renders_in_message_header_not_global_titlebar():
assert "msg-tps-inline" in UI_JS, "assistant message headers need a TPS chip hook"
assert "msg-tps-inline" in CSS, "TPS header chip needs an explicit CSS hook"
assert "_assistantRoleHtml(tsTitle='', tpsText='')" in UI_JS, (
"assistant role/header rendering should accept the per-message TPS text"
)
assert "_formatTurnTps" in UI_JS, "TPS formatting should be centralized"
assert "_turnTps" in UI_JS, "settled assistant messages should render final TPS from message metadata"
assert "tpsStat" not in MESSAGES_JS, "live TPS must not target the removed/global titlebar chip"
def test_live_metering_updates_only_real_tps_and_never_placeholders():
listener_start = MESSAGES_JS.find("source.addEventListener('metering'")
assert listener_start != -1, "messages.js should listen for metering SSE events"
listener_end = MESSAGES_JS.find("source.addEventListener('apperror'", listener_start)
assert listener_end != -1, "apperror listener should follow metering listener"
listener = MESSAGES_JS[listener_start:listener_end]
assert "_setLiveAssistantTps" in listener, "live metering should update the live assistant header"
assert "tps_available" in listener and "estimated" in listener, (
"live TPS display must check availability and reject estimated readings"
)
assert "0.0 t/s" not in listener, "unavailable TPS should render nothing, not a 0.0 placeholder"
assert "''" not in listener and '""' not in listener, "unavailable TPS should render nothing, not a dash"
assert "high" not in listener.lower() and "low" not in listener.lower(), (
"message-header TPS should not carry global HIGH/LOW titlebar semantics"
)
def test_live_metering_usage_is_provisional_until_done():
listener_start = MESSAGES_JS.find("source.addEventListener('metering'")
assert listener_start != -1, "messages.js should listen for metering SSE events"
listener_end = MESSAGES_JS.find("source.addEventListener('apperror'", listener_start)
assert listener_end != -1, "apperror listener should follow metering listener"
listener = MESSAGES_JS[listener_start:listener_end]
assert "S.lastUsage={...(S.lastUsage||{}),...d.usage}" in listener, (
"live usage should update the transient usage cache for the indicator"
)
assert "_syncCtxIndicator(S.lastUsage)" in listener, (
"live usage should refresh the context indicator"
)
assert "S.session.input_tokens=d.usage.input_tokens" not in listener
assert "S.session.last_prompt_tokens=d.usage.last_prompt_tokens" not in listener
def test_live_prompt_estimate_reanchors_to_fresh_exact_prompt_tokens():
assert "_live_prompt_exact_tokens = [0]" in STREAMING_PY, (
"live prompt estimates need a separate exact-token anchor"
)
assert "_real_prompt_tokens = int(_usage.get('last_prompt_tokens') or 0)" in STREAMING_PY
assert "_real_prompt_tokens != _live_prompt_exact_tokens[0]" in STREAMING_PY
assert "_live_prompt_estimate_tokens[0] = _real_prompt_tokens" in STREAMING_PY
def test_done_payload_persists_final_tps_when_exact_usage_available():
assert "usage['tps']" in STREAMING_PY, "done usage payload should include final exact TPS when available"
assert "output_tokens" in STREAMING_PY and "duration_seconds" in STREAMING_PY, (
"final TPS should be based on exact completion tokens over measured turn duration"
)
assert "d.usage.tps" in MESSAGES_JS, "done handler should read final TPS from the usage payload"
assert "lastAsst._turnTps" in MESSAGES_JS, "done handler should persist final TPS on the last assistant message"
def test_backend_marks_streaming_metering_availability_explicitly():
assert "tps_available" in STREAMING_PY, "metering SSE payloads must explicitly say whether TPS is displayable"
assert "estimated" in STREAMING_PY, "metering SSE payloads must explicitly distinguish estimated readings"
assert "record_token(stream_id, len(STREAM_PARTIAL_TEXT[stream_id]))" not in STREAMING_PY, (
"live TPS must not be derived from streamed character count / byte-size estimates"
)
def test_tps_display_setting_is_default_off_and_persisted():
assert '"show_tps": False' in CONFIG_PY, "TPS display should be disabled by default"
assert '"show_tps"' in CONFIG_PY and "_SETTINGS_BOOL_KEYS" in CONFIG_PY, (
"TPS display should be a persisted boolean WebUI setting"
)
assert "settingsShowTps" in INDEX_HTML, "Preferences needs a user-facing TPS display toggle"
assert "payload.show_tps=showTpsCb.checked" in PANELS_JS, (
"Preferences autosave should persist the TPS display toggle through /api/settings"
)
assert "showTpsCb.checked=!!settings.show_tps" in PANELS_JS, (
"Settings panel should hydrate the TPS toggle from persisted settings"
)
assert "window._showTps=!!s.show_tps" in BOOT_JS, (
"Boot should hydrate show_tps into a runtime flag"
)
assert "window._showTps=false" in BOOT_JS, (
"Boot fallback should keep TPS hidden when settings cannot load"
)
def test_tps_display_hot_applies_when_preferences_autosave():
fn_start = PANELS_JS.find("async function _autosavePreferencesSettings")
assert fn_start != -1, "preferences autosave function should exist"
fn_end = PANELS_JS.find("function _retryPreferencesAutosave", fn_start)
assert fn_end != -1, "retry function should follow preferences autosave"
fn = PANELS_JS[fn_start:fn_end]
assert "payload&&payload.show_tps!==undefined" in fn, (
"TPS preference autosave must detect the show_tps field specifically"
)
assert "window._showTps=!!(saved&&saved.show_tps)" in fn, (
"TPS preference autosave should update the runtime flag from the saved response"
)
assert "clearMessageRenderCache" in fn and "renderMessages" in fn, (
"TPS preference autosave should re-render the open transcript without refresh"
)
def test_tps_header_rendering_respects_display_setting():
assert "function isTpsDisplayEnabled()" in UI_JS, "TPS visibility should be centralized"
assert "return window._showTps===true" in UI_JS, "TPS should only render when explicitly enabled"
assert "const tps=(isTpsDisplayEnabled()&&tpsText)" in UI_JS, (
"settled assistant headers must suppress TPS when the setting is off"
)
assert "isTpsDisplayEnabled()?_formatTurnTps(value):''" in UI_JS, (
"live TPS updates must remove/suppress the chip when the setting is off"
)
assert "isTpsDisplayEnabled()?_formatTurnTps(m._turnTps):''" in UI_JS, (
"reloaded assistant messages must not render persisted TPS while disabled"
)