HERMES-GIELIE/tests/test_turn_duration_display.py

141 lines
6.9 KiB
Python
Raw Normal View History

2026-05-04 03:14:49 +00:00
"""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")
ROUTES_PY = (REPO / "api" / "routes.py").read_text(encoding="utf-8")
2026-05-04 03:14:49 +00:00
UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
I18N_JS = (REPO / "static" / "i18n.js").read_text(encoding="utf-8")
2026-05-04 03:14:49 +00:00
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."
)
2026-05-04 04:12:07 +00:00
assert "recovered/legacy flows" in STREAMING_PY, (
"The missing-start fallback should be documented so it is not mistaken "
"for the primary timing path."
)
2026-05-04 03:14:49 +00:00
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, (
[HELD — independent review pending] Release v0.51.294 — stage-3401 (live-to-final redesign #3401 + 4 deep-review fixes) (#3741) * Harden interrupted recovery control filtering * Redesign live-to-final assistant replies * Fix live activity anchor test fixture * Fix CI lint issues for live reply tests * Strengthen live progress prompt contract * Recover PR #3401 refresh on origin/master * Repair live-to-final refresh regressions * Fix live worklog refresh regressions * Show live footer timer on initial stream start * Restore live stream shell after reload * Preserve per-frame live SSE replay cursors * Preserve reasoning as Worklog Thinking cards * Quiet Worklog Thinking card styling * Align Worklog Thinking card styling * Scope live Worklog Thinking cards by segment * Suppress exact duplicate settled Thinking * Close #3401 merge review test gaps * fix(#3401): resolve 4 deep-review regressions (inline-think, reconnect-dup, neon skin, busy-gate worklog) Deep review (Codex diff-vs-master + live-browser drive) of the live-to-final refactor surfaced 4 regressions vs master that the rewritten suite no longer guarded: 1. Inline <think>…</think>answer reasoning vanished — _assistantReasoningPayloadText used $-anchored regexes so a leading think block + visible answer extracted nothing and the Thinking card never rendered. Removed the 3 $ anchors to match the (non-anchored) display stripper. Live: inline-think thinking-only turn now renders. 2. (CORE) reconnect/reload duplicated the live reply — _rememberRunJournalCursor advanced a closure-local seq but never wrote INFLIGHT[activeSid].lastRunJournalSeq, so a reload replayed the journal from after_seq=0 over restored lastAssistantText. Now mirrors the cursor onto INFLIGHT + schedules a throttled persist. 3. Neon skin silently broke — PR deleted the :root[data-skin="neon"] CSS but left Neon in the picker. Restored the neon CSS block from master. 4. Settled tool-worklog rebuild gated purely on !S.busy — dropped every prior settled turn's worklog when renderMessages re-ran during an active stream (switch-back to an in-progress session). Restored master's !S.busy || (S.toolCalls && S.toolCalls.length). Live: busy re-render now preserves tool cards (4→4, was 4→0). Live-verified all 4 + confirmed #3709/#3592 invariants still hold (1 thinking card, none below footer; distinct siblings preserved). + tests/test_issue3401_deep_review_fixes.py (7). * test(#3401): realign 3 stale source-shape assertions to the deep-review fixes Fix commit changed two source literals that existing stage tests scanned for: - test_live_activity_timeline.py (x2): split anchor 'if(!S.busy){' → the restored 'if(!S.busy || (S.toolCalls&&S.toolCalls.length)){' guard (fix 4). - test_run_journal_frontend_static.py: 'after_seq=0' not in source — fix 2's comment contained that literal; rephrased the comment to 'the zero floor (after_seq of 0)'. Intent of all three assertions unchanged; only the matched string updated. No code behavior change. * docs(changelog): v0.51.294 — Release JJ (stage-3401, #3401 live-to-final redesign) --------- Co-authored-by: Frank Song <franksong2702@gmail.com> Co-authored-by: Nathan-Hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 19:12:37 +00:00
"The spec Activity summary needs a stable data-turn-duration hook so settled duration can update its summary."
2026-05-04 03:14:49 +00:00
)
[HELD — independent review pending] Release v0.51.294 — stage-3401 (live-to-final redesign #3401 + 4 deep-review fixes) (#3741) * Harden interrupted recovery control filtering * Redesign live-to-final assistant replies * Fix live activity anchor test fixture * Fix CI lint issues for live reply tests * Strengthen live progress prompt contract * Recover PR #3401 refresh on origin/master * Repair live-to-final refresh regressions * Fix live worklog refresh regressions * Show live footer timer on initial stream start * Restore live stream shell after reload * Preserve per-frame live SSE replay cursors * Preserve reasoning as Worklog Thinking cards * Quiet Worklog Thinking card styling * Align Worklog Thinking card styling * Scope live Worklog Thinking cards by segment * Suppress exact duplicate settled Thinking * Close #3401 merge review test gaps * fix(#3401): resolve 4 deep-review regressions (inline-think, reconnect-dup, neon skin, busy-gate worklog) Deep review (Codex diff-vs-master + live-browser drive) of the live-to-final refactor surfaced 4 regressions vs master that the rewritten suite no longer guarded: 1. Inline <think>…</think>answer reasoning vanished — _assistantReasoningPayloadText used $-anchored regexes so a leading think block + visible answer extracted nothing and the Thinking card never rendered. Removed the 3 $ anchors to match the (non-anchored) display stripper. Live: inline-think thinking-only turn now renders. 2. (CORE) reconnect/reload duplicated the live reply — _rememberRunJournalCursor advanced a closure-local seq but never wrote INFLIGHT[activeSid].lastRunJournalSeq, so a reload replayed the journal from after_seq=0 over restored lastAssistantText. Now mirrors the cursor onto INFLIGHT + schedules a throttled persist. 3. Neon skin silently broke — PR deleted the :root[data-skin="neon"] CSS but left Neon in the picker. Restored the neon CSS block from master. 4. Settled tool-worklog rebuild gated purely on !S.busy — dropped every prior settled turn's worklog when renderMessages re-ran during an active stream (switch-back to an in-progress session). Restored master's !S.busy || (S.toolCalls && S.toolCalls.length). Live: busy re-render now preserves tool cards (4→4, was 4→0). Live-verified all 4 + confirmed #3709/#3592 invariants still hold (1 thinking card, none below footer; distinct siblings preserved). + tests/test_issue3401_deep_review_fixes.py (7). * test(#3401): realign 3 stale source-shape assertions to the deep-review fixes Fix commit changed two source literals that existing stage tests scanned for: - test_live_activity_timeline.py (x2): split anchor 'if(!S.busy){' → the restored 'if(!S.busy || (S.toolCalls&&S.toolCalls.length)){' guard (fix 4). - test_run_journal_frontend_static.py: 'after_seq=0' not in source — fix 2's comment contained that literal; rephrased the comment to 'the zero floor (after_seq of 0)'. Intent of all three assertions unchanged; only the matched string updated. No code behavior change. * docs(changelog): v0.51.294 — Release JJ (stage-3401, #3401 live-to-final redesign) --------- Co-authored-by: Frank Song <franksong2702@gmail.com> Co-authored-by: Nathan-Hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 19:12:37 +00:00
assert "turnDuration:includeTurnDuration?_turnDurationForAnchor(anchorRow):undefined" in UI_JS, (
"Settled compact activity should put turn duration on the first spec Activity row, "
"not resurrect the legacy top Run Activity."
)
assert "compactWorklogForMessage" in UI_JS, (
"When folded Worklog detail is present, duration should live on the Worklog row "
2026-05-04 03:14:49 +00:00
"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."
)
def test_active_compact_activity_elapsed_timer_uses_persisted_start_time():
assert '"pending_started_at": s.pending_started_at' in ROUTES_PY, (
"/api/chat/start should return the persisted pending_started_at timestamp "
"so the live timer starts from backend/session truth."
)
assert "startData.pending_started_at" in MESSAGES_JS, (
"send() should copy chat-start pending_started_at into S.session before "
"attaching the live stream."
)
[HELD — independent review pending] Release v0.51.294 — stage-3401 (live-to-final redesign #3401 + 4 deep-review fixes) (#3741) * Harden interrupted recovery control filtering * Redesign live-to-final assistant replies * Fix live activity anchor test fixture * Fix CI lint issues for live reply tests * Strengthen live progress prompt contract * Recover PR #3401 refresh on origin/master * Repair live-to-final refresh regressions * Fix live worklog refresh regressions * Show live footer timer on initial stream start * Restore live stream shell after reload * Preserve per-frame live SSE replay cursors * Preserve reasoning as Worklog Thinking cards * Quiet Worklog Thinking card styling * Align Worklog Thinking card styling * Scope live Worklog Thinking cards by segment * Suppress exact duplicate settled Thinking * Close #3401 merge review test gaps * fix(#3401): resolve 4 deep-review regressions (inline-think, reconnect-dup, neon skin, busy-gate worklog) Deep review (Codex diff-vs-master + live-browser drive) of the live-to-final refactor surfaced 4 regressions vs master that the rewritten suite no longer guarded: 1. Inline <think>…</think>answer reasoning vanished — _assistantReasoningPayloadText used $-anchored regexes so a leading think block + visible answer extracted nothing and the Thinking card never rendered. Removed the 3 $ anchors to match the (non-anchored) display stripper. Live: inline-think thinking-only turn now renders. 2. (CORE) reconnect/reload duplicated the live reply — _rememberRunJournalCursor advanced a closure-local seq but never wrote INFLIGHT[activeSid].lastRunJournalSeq, so a reload replayed the journal from after_seq=0 over restored lastAssistantText. Now mirrors the cursor onto INFLIGHT + schedules a throttled persist. 3. Neon skin silently broke — PR deleted the :root[data-skin="neon"] CSS but left Neon in the picker. Restored the neon CSS block from master. 4. Settled tool-worklog rebuild gated purely on !S.busy — dropped every prior settled turn's worklog when renderMessages re-ran during an active stream (switch-back to an in-progress session). Restored master's !S.busy || (S.toolCalls && S.toolCalls.length). Live: busy re-render now preserves tool cards (4→4, was 4→0). Live-verified all 4 + confirmed #3709/#3592 invariants still hold (1 thinking card, none below footer; distinct siblings preserved). + tests/test_issue3401_deep_review_fixes.py (7). * test(#3401): realign 3 stale source-shape assertions to the deep-review fixes Fix commit changed two source literals that existing stage tests scanned for: - test_live_activity_timeline.py (x2): split anchor 'if(!S.busy){' → the restored 'if(!S.busy || (S.toolCalls&&S.toolCalls.length)){' guard (fix 4). - test_run_journal_frontend_static.py: 'after_seq=0' not in source — fix 2's comment contained that literal; rephrased the comment to 'the zero floor (after_seq of 0)'. Intent of all three assertions unchanged; only the matched string updated. No code behavior change. * docs(changelog): v0.51.294 — Release JJ (stage-3401, #3401 live-to-final redesign) --------- Co-authored-by: Frank Song <franksong2702@gmail.com> Co-authored-by: Nathan-Hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 19:12:37 +00:00
assert "showLiveRunStatus(activeSid,{startedAt:_startedAt});" in MESSAGES_JS, (
"The first chat-start path should show the bottom live footer timer as soon "
"as stream_id and pending_started_at are known; reconnect should not be the "
"only path that restores it."
)
assert "function _processedElapsedLabel" in UI_JS and "t('processed_elapsed',text)" in UI_JS, (
"Compact Worklog should present the running timer as the stable processed-time anchor."
)
assert "data-turn-started-at" in UI_JS and "data-active-turn-elapsed" in UI_JS, (
"Live compact Activity groups need stable start-time and active-elapsed "
"hooks for browser QA and reconnect/rerender safety."
)
assert "_activityProcessedElapsedLabel(group)" in UI_JS, (
"The in-progress Activity summary should own the live elapsed label "
"instead of relying on the bottom live footer."
)
assert "setInterval" in UI_JS and "_clearActivityElapsedTimer" in UI_JS, (
"The active elapsed label should tick while running and clear its interval "
"on terminal/error/session-switch cleanup paths."
)
[HELD — independent review pending] Release v0.51.294 — stage-3401 (live-to-final redesign #3401 + 4 deep-review fixes) (#3741) * Harden interrupted recovery control filtering * Redesign live-to-final assistant replies * Fix live activity anchor test fixture * Fix CI lint issues for live reply tests * Strengthen live progress prompt contract * Recover PR #3401 refresh on origin/master * Repair live-to-final refresh regressions * Fix live worklog refresh regressions * Show live footer timer on initial stream start * Restore live stream shell after reload * Preserve per-frame live SSE replay cursors * Preserve reasoning as Worklog Thinking cards * Quiet Worklog Thinking card styling * Align Worklog Thinking card styling * Scope live Worklog Thinking cards by segment * Suppress exact duplicate settled Thinking * Close #3401 merge review test gaps * fix(#3401): resolve 4 deep-review regressions (inline-think, reconnect-dup, neon skin, busy-gate worklog) Deep review (Codex diff-vs-master + live-browser drive) of the live-to-final refactor surfaced 4 regressions vs master that the rewritten suite no longer guarded: 1. Inline <think>…</think>answer reasoning vanished — _assistantReasoningPayloadText used $-anchored regexes so a leading think block + visible answer extracted nothing and the Thinking card never rendered. Removed the 3 $ anchors to match the (non-anchored) display stripper. Live: inline-think thinking-only turn now renders. 2. (CORE) reconnect/reload duplicated the live reply — _rememberRunJournalCursor advanced a closure-local seq but never wrote INFLIGHT[activeSid].lastRunJournalSeq, so a reload replayed the journal from after_seq=0 over restored lastAssistantText. Now mirrors the cursor onto INFLIGHT + schedules a throttled persist. 3. Neon skin silently broke — PR deleted the :root[data-skin="neon"] CSS but left Neon in the picker. Restored the neon CSS block from master. 4. Settled tool-worklog rebuild gated purely on !S.busy — dropped every prior settled turn's worklog when renderMessages re-ran during an active stream (switch-back to an in-progress session). Restored master's !S.busy || (S.toolCalls && S.toolCalls.length). Live: busy re-render now preserves tool cards (4→4, was 4→0). Live-verified all 4 + confirmed #3709/#3592 invariants still hold (1 thinking card, none below footer; distinct siblings preserved). + tests/test_issue3401_deep_review_fixes.py (7). * test(#3401): realign 3 stale source-shape assertions to the deep-review fixes Fix commit changed two source literals that existing stage tests scanned for: - test_live_activity_timeline.py (x2): split anchor 'if(!S.busy){' → the restored 'if(!S.busy || (S.toolCalls&&S.toolCalls.length)){' guard (fix 4). - test_run_journal_frontend_static.py: 'after_seq=0' not in source — fix 2's comment contained that literal; rephrased the comment to 'the zero floor (after_seq of 0)'. Intent of all three assertions unchanged; only the matched string updated. No code behavior change. * docs(changelog): v0.51.294 — Release JJ (stage-3401, #3401 live-to-final redesign) --------- Co-authored-by: Frank Song <franksong2702@gmail.com> Co-authored-by: Nathan-Hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 19:12:37 +00:00
def test_live_footer_timer_is_re_synced_after_message_rerender():
assert "function _syncLiveRunStatusAfterRender()" in UI_JS, (
"renderMessages() needs a dedicated helper so the live footer timer "
"can be restored after DOM rebuilds."
)
assert "_syncLiveRunStatusAfterRender();" in UI_JS, (
"renderMessages() should call the live-status sync helper after it "
"rebuilds msgInner."
)
assert "showLiveRunStatus(sid,{startedAt,tokens:_liveRunStatusTokens});" in UI_JS, (
"If the timer node was torn down during a rerender, the helper should "
"recreate it for the active session."
)
def test_compact_worklog_hides_bottom_live_footer_timer():
show = UI_JS.split("function showLiveRunStatus", 1)[1].split("function _renderLiveRunStatusContent", 1)[0]
sync = UI_JS.split("function _syncLiveRunStatusAfterRender", 1)[1].split("function hideLiveRunStatus", 1)[0]
assert "isCompactWorklogMode" in show
assert "if(el){el.hidden=true;el.innerHTML='';}" in show
assert "isCompactWorklogMode" in sync
assert "if(el){el.hidden=true;el.innerHTML='';}" in sync
def test_processed_elapsed_anchor_is_i18n_driven():
assert "function _i18nProcessedElapsed(prefix, duration)" in I18N_JS
assert "function _i18nProcessedElapsedEn(duration)" in I18N_JS
assert "return _i18nProcessedElapsed('Processed', duration);" in I18N_JS
assert "function _i18nProcessedElapsedZh(duration)" in I18N_JS
assert "return _i18nProcessedElapsed('已处理', duration);" in I18N_JS
assert "function _i18nProcessedElapsedZhHant(duration)" in I18N_JS
assert "return _i18nProcessedElapsed('已處理', duration);" in I18N_JS
assert "processed_elapsed: _i18nProcessedElapsedEn" in I18N_JS
assert "processed_elapsed: _i18nProcessedElapsedZh" in I18N_JS
assert "processed_elapsed: _i18nProcessedElapsedZhHant" in I18N_JS
assert "t('processed_elapsed','')" in UI_JS
assert "`已处理 ${" not in UI_JS