from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SESSIONS_JS = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8") UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8") def _load_session_inflight_branch() -> str: start = SESSIONS_JS.find("if(INFLIGHT[sid]){") assert start != -1, "loadSession INFLIGHT branch not found" end = SESSIONS_JS.find("}else{", start) assert end != -1, "loadSession INFLIGHT branch end not found" return SESSIONS_JS[start:end] def test_load_session_inflight_reattach_merges_pending_user_message_before_render(): """#2341: Reattaching to an active stream must show the initiating user turn. A reload or session switch can hydrate a running turn from INFLIGHT while the backend still carries the user prompt only as pending_user_message. Without merging that pending row before renderMessages(), the user sees assistant thinking/tool activity without the prompt that started it. """ block = _load_session_inflight_branch() merge_pos = block.find("_mergePendingSessionMessage") # #3326 added an optional {preserveScroll} arg to the INFLIGHT-branch render # call; match the call form rather than the bare `renderMessages();`. render_pos = block.find("renderMessages(") assert merge_pos != -1, ( "loadSession's INFLIGHT reattach branch must merge pending_user_message " "into S.messages before rendering the running turn" ) assert render_pos != -1, "INFLIGHT branch render call not found" assert merge_pos < render_pos, ( "The pending user row must be present before renderMessages() rebuilds " "the active transcript" ) assert "restoreLiveTurnHtmlForSession(sid)" in block, ( "Session restore may keep a live DOM snapshot instead of always " "recreating a fresh Thinking row after renderMessages()" ) assert "INFLIGHT[sid].messages=S.messages;" in block, ( "After merging the pending user row, the INFLIGHT cache should be updated " "so later session switches keep the same visible turn" ) assert "messages.findIndex(m=>m&&m.role==='assistant'&&m._live)" in SESSIONS_JS assert "messages.splice(liveAssistantIdx,0,pendingMsg)" in SESSIONS_JS def test_pending_user_message_dedup_checks_current_message_array(): """#2341: Pending merge must not duplicate an already-visible user row.""" assert "function getPendingSessionMessage(session, messagesOverride=null)" in UI_JS helper_start = UI_JS.find("function getPendingSessionMessage(session, messagesOverride=null)") assert helper_start != -1, "getPendingSessionMessage helper not found" helper_end = UI_JS.find("async function checkInflightOnBoot", helper_start) assert helper_end != -1, "getPendingSessionMessage helper end not found" helper = UI_JS[helper_start:helper_end] assert "messagesOverride" in helper assert "Array.isArray(messagesOverride)?messagesOverride" in helper.replace(" ", ""), ( "Pending-message dedup must inspect the current S.messages/INFLIGHT " "array, not only session.messages from the metadata response" ) assert "_pendingCurrentTailUserMessage(messages)" in helper, ( "Pending-message dedup must inspect only the current tail user row, " "not a historical same-text row" ) assert "sameCurrentTurn" in helper and "return null;" in helper, ( "Pending-message merge must still suppress duplicates when the current " "tail user row already matches pending_user_message" ) assert "[...messages].reverse().find" not in helper, ( "Pending-message dedup must not reverse-scan historical user rows" )