Files
hermes-webui/tests/test_turn_duration_display.py
T
Sanjay Santhanam 14fac05dc9 fix(streaming): use truthy-check for _pending_started_at fallback
Switch the per-turn duration fallback from `is not None` to a truthy check so
None, missing-attr, and an explicit 0 all uniformly fall back to time.time().

Without this, a 0 timestamp (e.g. via a buggy migration or manual file edit)
would yield `time.time() - 0` ≈ wall-clock-since-epoch, displaying nonsense
like 'Done in 56 years 4 months ...'. In practice pending_started_at is always
set via int(time.time()) so this is a hardening fix, not a live-bug fix.

Also drop the brittle source-string assertion in the regression test that
pinned the literal expression. The behavioural test
test_done_handler_persists_duration_on_last_assistant_message already proves
the duration field is set; pinning the source line broke twice during the
v0.50.290 release pipeline alone (Opus tightening + maintainer revert).

Fixes #1595

Signed-off-by: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com>
2026-05-03 23:21:19 -07:00

64 lines
2.9 KiB
Python

"""Regression tests for per-turn response duration in WebUI.
The WebUI should expose how long an agent turn took, using backend timing so
reload/reconnect does not lose the measurement.
"""
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
STREAMING_PY = (REPO / "api" / "streaming.py").read_text(encoding="utf-8")
MESSAGES_JS = (REPO / "static" / "messages.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_streaming_done_payload_includes_backend_turn_duration():
assert "duration_seconds" in STREAMING_PY, (
"api/streaming.py should include a backend-measured duration_seconds "
"field in the done usage payload."
)
assert "pending_started_at" in STREAMING_PY and "time.time()" in STREAMING_PY, (
"Turn duration should be measured from the persisted pending_started_at "
"start time, not only from browser-local state."
)
assert "recovered/legacy flows" in STREAMING_PY, (
"The missing-start fallback should be documented so it is not mistaken "
"for the primary timing path."
)
assert "_turnDuration" in STREAMING_PY, (
"The measured duration should be persisted on the assistant message so "
"it survives reload after the SSE stream settles."
)
def test_done_handler_persists_duration_on_last_assistant_message():
assert "d.usage.duration_seconds" in MESSAGES_JS, (
"static/messages.js should read duration_seconds from the done usage payload."
)
assert "lastAsst._turnDuration" in MESSAGES_JS, (
"The done handler should attach the duration to the last assistant message "
"so renderMessages() can display it after the live stream settles."
)
def test_ui_formats_and_renders_turn_duration_in_footer_and_activity_summary():
assert "function _formatTurnDuration" in UI_JS, (
"ui.js should centralize duration formatting for footer and compact activity display."
)
assert "msg-duration-inline" in UI_JS and "Done in" in UI_JS, (
"Expanded/non-activity display should show a subtle footer chip like 'Done in 42s'."
)
assert "tool-call-group-duration" in UI_JS, (
"Compact tool activity summary should have a dedicated duration span at the end of the line."
)
assert "data-turn-duration" in UI_JS, (
"Activity groups need a stable data-turn-duration hook so settled duration can update the summary."
)
assert "compactActivityForMessage" in UI_JS, (
"When compact activity is present, duration should live on the Activity row "
"instead of being duplicated in the assistant footer."
)
assert ".msg-duration-inline" in CSS and ".tool-call-group-duration" in CSS, (
"Duration UI should have explicit CSS hooks for the footer chip and compact activity summary."
)