Files
Thireus ☠ 2d48f4c306 ui: give the renderMessages probes the block helper
Four upstream tests read renderMessages out of ui.js and run a slice of it against a fake segment object. Since the message block goes in through _insertSegmentBlock, three of them stopped at "ReferenceError: _insertSegmentBlock is not defined" and the fourth still pinned the old seg.insertAdjacentHTML line, which took twelve of the fifteen test shards red on this branch and nowhere else.

test_status_command_card.py defines the helper in its own harness, one line that does what the real helper does without a browser. test_compaction_current_summary_fallback.py and test_anchor_fallback_ownership.py evaluate the real helper from ui.js alongside the functions they already evaluate; their DOM shims return no template content, so it takes its insertAdjacentHTML fallback exactly as before. test_sprint49.py pins the new call, and its point is unchanged: the footer still rides in the same block as the body.

A new case in tests/test_issue2051_duplicate_assistant_answer.py pins why the helper exists, which answers a review note: the block is parsed into a template and the nodes are MOVED, so a repeated call adds nothing, and insertAdjacentHTML survives only as the fallback. All three insertions go through it.

tests/test_anchor_fallback_ownership.py, tests/test_compaction_current_summary_fallback.py, tests/test_sprint49.py, tests/test_status_command_card.py: 34 passed. The three #2051 files: 12 passed.
2026-09-21 19:28:49 +01:00

150 lines
6.7 KiB
Python

"""Tests for sprint 49 timestamp footer polish — v0.50.97.
Covers:
- #680: assistant messages now render footer timestamps, not just user messages
- messages from prior days render a fuller date+time string in the footer
- timestamp/action footer stays attached to visible response segments only
- user and assistant footer chrome is hover-only by default
- last assistant turn keeps cumulative usage visible and reveals time/actions on hover
- unchanged historical messages preserve their original timestamps across turns
"""
import pathlib
import re
from api.streaming import _restore_reasoning_metadata, _sanitize_messages_for_api
REPO = pathlib.Path(__file__).parent.parent
UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
UI_CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8")
STREAMING_PY = (REPO / "api" / "streaming.py").read_text(encoding="utf-8")
def test_footer_timestamp_is_not_limited_to_user_messages():
assert "const timeHtml = tsTime ?" in UI_JS
assert "isUser && tsTime" not in UI_JS, (
"Timestamp footer should no longer be gated to user messages only"
)
def test_footer_timestamp_uses_richer_format_for_older_messages():
assert "function _formatMessageFooterTimestamp(tsVal)" in UI_JS
assert "month:'short'" in UI_JS or 'month: "short"' in UI_JS
assert "day:'numeric'" in UI_JS or 'day: "numeric"' in UI_JS
assert "hour:'numeric'" in UI_JS or 'hour: "numeric"' in UI_JS
assert "minute:'2-digit'" in UI_JS or 'minute: "2-digit"' in UI_JS
def test_timestamp_footer_stays_on_visible_response_segments():
assert "if(hasVisibleBody){" in UI_JS
# #2051: the block goes in through _insertSegmentBlock() rather than
# seg.insertAdjacentHTML(), so a wrapped DOM API cannot render it twice. The footer
# still rides in the same block, which is what this test is about.
assert '_insertSegmentBlock(seg, `${filesHtml}<div class="msg-body">${bodyHtml}</div>${footHtml}`);' in UI_JS, (
"Footer timestamp should stay attached to visible response segments"
)
assert "assistantThinking.set(rawIdx, thinkingText);" in UI_JS, (
"Thinking-only assistant segments should preserve thinking for the shared activity dropdown without rendering a footer"
)
assert "seg.classList.add('assistant-segment-anchor');" in UI_JS, (
"Empty assistant anchor segments should stay footerless while anchoring activity metadata"
)
def test_footer_chrome_is_hover_only_for_user_and_assistant_messages():
assert ".msg-row[data-role=\"user\"] .msg-foot {\n opacity: 0;" in UI_CSS
assert ".msg-row[data-role=\"user\"]:hover .msg-foot," in UI_CSS
assert ".msg-row[data-role=\"assistant\"] .msg-foot," in UI_CSS
assert ".assistant-turn .msg-foot {" in UI_CSS
assert ".assistant-turn:hover .msg-foot," in UI_CSS
def test_last_assistant_keeps_usage_visible_and_reveals_time_and_actions_on_hover():
assert "usage.className='msg-usage-inline';" in UI_JS
assert "targetFoot.classList.add('msg-foot-with-usage');" in UI_JS
assert (
"targetFoot.insertBefore(usage, targetFoot.firstChild);" in UI_JS
or "targetFoot.insertBefore(fragments[i], targetFoot.firstChild);" in UI_JS
or "targetFoot.insertBefore(fragments[i], firstChild);" in UI_JS
)
assert ".assistant-turn .msg-foot-with-usage," in UI_CSS
assert ".msg-row[data-role=\"assistant\"] .msg-foot-with-usage {\n opacity: 1;" in UI_CSS
assert ".msg-foot-with-usage .msg-time,\n.msg-foot-with-usage .msg-actions {\n opacity: 0;" in UI_CSS
assert ".assistant-turn:hover .msg-foot-with-usage .msg-time," in UI_CSS
def test_restore_reasoning_metadata_preserves_existing_timestamps():
assert "def _restore_reasoning_metadata(previous_messages, updated_messages):" in STREAMING_PY
assert "if prev_msg.get('timestamp') and not cur_msg.get('timestamp'):" in STREAMING_PY
assert "cur_msg['timestamp'] = prev_msg['timestamp']" in STREAMING_PY
assert "elif prev_msg.get('_ts') and not cur_msg.get('_ts') and not cur_msg.get('timestamp'):" in STREAMING_PY
assert "cur_msg['_ts'] = prev_msg['_ts']" in STREAMING_PY
def test_restore_reasoning_metadata_preserves_timestamp_on_reload_for_unchanged_messages():
previous_messages = [
{"role": "user", "content": "hello", "timestamp": 1713500000},
{"role": "assistant", "content": "world", "timestamp": 1713500060},
]
updated_messages = [
{"role": "user", "content": "hello"},
{"role": "assistant", "content": "world"},
]
restored = _restore_reasoning_metadata(previous_messages, updated_messages)
assert restored[0]["timestamp"] == 1713500000
assert restored[1]["timestamp"] == 1713500060
def test_restore_reasoning_metadata_does_not_preserve_timestamp_for_changed_messages():
previous_messages = [
{"role": "user", "content": "hello", "timestamp": 1713500000},
{"role": "assistant", "content": "old answer", "timestamp": 1713500060},
]
updated_messages = [
{"role": "user", "content": "hello"},
{"role": "assistant", "content": "new answer"},
]
restored = _restore_reasoning_metadata(previous_messages, updated_messages)
assert restored[0]["timestamp"] == 1713500000
assert "timestamp" not in restored[1]
def test_sanitize_messages_for_api_drops_reasoning_only_display_entries():
messages = [
{"role": "user", "content": "hello"},
{"role": "assistant", "content": "", "reasoning": "hidden chain", "_partial_tool_calls": [{"name": "read_file"}]},
{"role": "assistant", "content": "visible answer", "reasoning": "display metadata"},
{"role": "assistant", "content": [{"type": "reasoning", "text": "hidden"}]},
]
sanitized = _sanitize_messages_for_api(messages)
assert sanitized == [
{"role": "user", "content": "hello"},
{"role": "assistant", "content": "visible answer"},
]
def test_restore_reasoning_metadata_does_not_reinsert_reasoning_only_display_entries():
previous_messages = [
{"role": "user", "content": "hello", "timestamp": 1713500000},
{"role": "assistant", "content": "", "reasoning": "old hidden thought", "timestamp": 1713500001},
{"role": "assistant", "content": "visible answer", "reasoning": "answer thought", "timestamp": 1713500060},
]
updated_messages = [
{"role": "user", "content": "hello"},
{"role": "assistant", "content": "visible answer"},
]
restored = _restore_reasoning_metadata(previous_messages, updated_messages)
assert len(restored) == 2
assert restored[1]["content"] == "visible answer"
assert restored[1]["reasoning"] == "answer thought"
assert restored[1]["timestamp"] == 1713500060