import json import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[1] MESSAGES_SRC = (ROOT / "static" / "messages.js").read_text(encoding="utf-8") SESSIONS_SRC = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8") UI_SRC = (ROOT / "static" / "ui.js").read_text(encoding="utf-8") def _function_body(src: str, signature: str) -> str: start = src.index(signature) brace = src.index("{", start) depth = 0 for idx in range(brace, len(src)): char = src[idx] if char == "{": depth += 1 elif char == "}": depth -= 1 if depth == 0: return src[start : idx + 1] raise AssertionError(f"could not extract function body for {signature!r}") def _optional_function_body(src: str, signature: str) -> str: """Extract a helper if present, else return "". Used for helpers whose ABSENCE is itself the regression under test: the probe must still run so the behavioral assertion reports the duplicate, rather than dying with a substring-not-found extraction error that says nothing about observable behavior. """ try: return _function_body(src, signature) except (ValueError, AssertionError): return "" def _run_session_identity_probe() -> dict: prompt = "same submitted prompt\nwith a second line" workspace_prompt = f"[Workspace::v1: /tmp/hermes-webui]\n{prompt}" legacy_workspace_prompt = f"[Workspace: /tmp/hermes-webui]\n{prompt}" attached_prompt = f"{prompt}\n\n[Attached files: /tmp/a.txt]" forced_prompt = ( "[USER OVERRIDE] You MUST follow the skill 'hermes-webui-coordinator' " "content provided below before responding to the next message.\n\n" "[FORCED SKILL CONTEXT: hermes-webui-coordinator]\n" "skill body that should not make a second user bubble\n" "[/FORCED SKILL CONTEXT]\n\n" f"{prompt}" ) helpers = "\n".join( [ _function_body(UI_SRC, "function _stripWorkspaceDisplayPrefix"), _function_body(SESSIONS_SRC, "function _messageComparableText"), _function_body(SESSIONS_SRC, "function _stripAttachedFilesMarker"), _function_body(SESSIONS_SRC, "function _stripForcedSkillEnvelope"), _function_body(SESSIONS_SRC, "function _normalizeUserTranscriptText"), _function_body(SESSIONS_SRC, "function _sameTranscriptMessage"), _function_body(SESSIONS_SRC, "function _currentTailUserMessage"), _function_body(SESSIONS_SRC, "function _hasCurrentTailUserDuplicate"), _function_body(SESSIONS_SRC, "function _inflightHasVisibleLiveState"), ] ) script = f""" {helpers} const plain = {{role:'user', content:{json.dumps(prompt)}}}; const workspace = {{role:'user', content:{json.dumps(workspace_prompt)}}}; const legacyWorkspace = {{role:'user', content:{json.dumps(legacy_workspace_prompt)}}}; const attached = {{role:'user', content:{json.dumps(attached_prompt)}}}; const forced = {{role:'user', content:{json.dumps(forced_prompt)}}}; const different = {{role:'user', content:'a different submitted prompt'}}; process.stdout.write(JSON.stringify({{ workspaceDedupe: _sameTranscriptMessage(plain, workspace), legacyWorkspaceDedupe: _sameTranscriptMessage(plain, legacyWorkspace), attachedDedupe: _sameTranscriptMessage(plain, attached), forcedSkillDedupe: _sameTranscriptMessage(plain, forced), differentUserNotDedupe: !_sameTranscriptMessage(plain, different), roleMismatchNotDedupe: !_sameTranscriptMessage(plain, {{role:'assistant', content:{json.dumps(prompt)}}}), userOnlyInflightVisible: _inflightHasVisibleLiveState({{messages:[plain]}}), emptyUserOnlyInflightNotVisible: !_inflightHasVisibleLiveState({{messages:[{{role:'user', content:' '}}]}}), }})); """ proc = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) return json.loads(proc.stdout) def _run_current_turn_scope_probe() -> dict: prompt = "repeat me" historical_workspace_prompt = f"[Workspace::v1: /tmp/old]\n{prompt}" current_workspace_prompt = f"[Workspace::v1: /tmp/current]\n{prompt}" helpers = "\n".join( [ _function_body(UI_SRC, "function _stripWorkspaceDisplayPrefix"), _function_body(UI_SRC, "function msgContent"), _function_body(UI_SRC, "function _isContextCompactionText"), _function_body(UI_SRC, "function _isContextCompactionMessage"), _function_body(SESSIONS_SRC, "function _messageComparableText"), _function_body(SESSIONS_SRC, "function _stripAttachedFilesMarker"), _function_body(SESSIONS_SRC, "function _stripForcedSkillEnvelope"), _function_body(SESSIONS_SRC, "function _normalizeUserTranscriptText"), _function_body(SESSIONS_SRC, "function _sameTranscriptMessage"), _function_body(SESSIONS_SRC, "function _currentTailUserMessage"), _function_body(SESSIONS_SRC, "function _hasCurrentTailUserDuplicate"), _function_body(SESSIONS_SRC, "function _mergePendingSessionMessage"), _function_body(SESSIONS_SRC, "function _mergeInflightTailMessages"), ] ) script = f""" {helpers} function getPendingSessionMessage(session, messages){{ const text=String(session&&session.pending_user_message||'').trim(); if(!text) return null; return {{ role:'user', content:text, _ts:session.pending_started_at||10, _pending:true, }}; }} const historical = {{role:'user', content:{json.dumps(historical_workspace_prompt)}, _ts:1}}; const historicalAnswer = {{role:'assistant', content:'done', _ts:2}}; const liveAssistant = {{role:'assistant', content:'working', _live:true, _ts:4}}; const pendingSession = {{pending_user_message:{json.dumps(prompt)}, pending_started_at:3}}; const pendingAfterHistory = [historical, historicalAnswer]; const insertedAfterHistory = _mergePendingSessionMessage(pendingSession, pendingAfterHistory); const pendingBeforeLive = [historical, historicalAnswer, liveAssistant]; const insertedBeforeLive = _mergePendingSessionMessage(pendingSession, pendingBeforeLive); const optimisticCurrent = {{role:'user', content:{json.dumps(current_workspace_prompt)}, _ts:3}}; const pendingWithCurrent = [historical, historicalAnswer, optimisticCurrent, liveAssistant]; const insertedWithCurrent = _mergePendingSessionMessage(pendingSession, pendingWithCurrent); const inflightAfterHistory = _mergeInflightTailMessages( [historical, historicalAnswer], [{{role:'user', content:{json.dumps(prompt)}, _ts:3}}, liveAssistant] ); const inflightWithCurrent = _mergeInflightTailMessages( [historical, historicalAnswer, optimisticCurrent], [{{role:'user', content:{json.dumps(prompt)}, _ts:3}}, liveAssistant] ); const compaction = {{ role:'user', content:'[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted.', _ts:3.5, }}; const compactionBase = [historical, historicalAnswer, optimisticCurrent, compaction]; const compactionCandidate = {{role:'user', content:{json.dumps(prompt)}, _ts:3}}; const compactionCurrentTail = _currentTailUserMessage(compactionBase); const compactionTailDuplicate = _hasCurrentTailUserDuplicate(compactionBase, compactionCandidate); const compactionMerged = _mergeInflightTailMessages( compactionBase, [compactionCandidate, liveAssistant] ); const insertedAfterCompaction = _mergePendingSessionMessage(pendingSession, compactionMerged); const compactionPromptCount = compactionMerged.filter( m=>m&&m.role==='user'&&m._ts===3&&_normalizeUserTranscriptText(m.content)==={json.dumps(prompt)} ).length; const compactionMarkerRetained = compactionMerged.some(m=>_isContextCompactionMessage(m)); const compactionLiveAssistantRetained = compactionMerged.some( m=>m&&m.role==='assistant'&&m._live&&m.content==='working' ); const completedBoundaryDedupe = _hasCurrentTailUserDuplicate( [historical, historicalAnswer, compaction], {{role:'user', content:{json.dumps(prompt)}, _ts:3}} ); const distinctCompletedTurnPromptCount = inflightAfterHistory.filter( m=>m&&m.role==='user'&&_normalizeUserTranscriptText(m.content)==={json.dumps(prompt)} ).length; process.stdout.write(JSON.stringify({{ insertedAfterHistory, pendingAfterHistoryRoles: pendingAfterHistory.map(m=>m.role), insertedBeforeLive, pendingBeforeLiveRoles: pendingBeforeLive.map(m=>m.role), insertedWithCurrent, pendingWithCurrentRoles: pendingWithCurrent.map(m=>m.role), inflightAfterHistoryRoles: inflightAfterHistory.map(m=>m.role), inflightWithCurrentRoles: inflightWithCurrent.map(m=>m.role), insertedAfterCompaction, compactionCurrentTailContent: compactionCurrentTail&&compactionCurrentTail.content, compactionTailDuplicate, compactionPromptCount, compactionMarkerRetained, compactionLiveAssistantRetained, completedBoundaryDedupe, distinctCompletedTurnPromptCount, }})); """ proc = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) return json.loads(proc.stdout) def _run_pending_session_message_probe() -> dict: prompt = "repeat me" historical_workspace_prompt = f"[Workspace::v1: /tmp/old]\n{prompt}" current_workspace_prompt = f"[Workspace::v1: /tmp/current]\n{prompt}" helpers = "\n".join( [ _function_body(UI_SRC, "function _stripWorkspaceDisplayPrefix"), _function_body(UI_SRC, "function msgContent"), _function_body(SESSIONS_SRC, "function _messageComparableText"), _function_body(SESSIONS_SRC, "function _stripAttachedFilesMarker"), _function_body(SESSIONS_SRC, "function _stripForcedSkillEnvelope"), _function_body(SESSIONS_SRC, "function _normalizeUserTranscriptText"), _function_body(SESSIONS_SRC, "function _sameTranscriptMessage"), _function_body(UI_SRC, "function _pendingCurrentTailUserMessage"), _optional_function_body(UI_SRC, "function _messageTimestampSeconds"), _optional_function_body(UI_SRC, "function _activeTurnTokenMatches"), _optional_function_body(UI_SRC, "function _pendingActiveTurnUserMessage"), _function_body(UI_SRC, "function _isContextCompactionText"), _function_body(UI_SRC, "function _isContextCompactionMessage"), _function_body(UI_SRC, "function getPendingSessionMessage"), ] ) script = f""" const _PENDING_ACTIVE_TURN_TS_EPSILON=1e-6; {helpers} const prompt = {json.dumps(prompt)}; const historical = {{role:'user', content:prompt, _ts:1}}; const historicalWorkspace = {{role:'user', content:{json.dumps(historical_workspace_prompt)}, _ts:1}}; const historicalAnswer = {{role:'assistant', content:'done', _ts:2}}; const currentTail = {{role:'user', content:prompt, _ts:3}}; const currentWorkspaceTail = {{role:'user', content:{json.dumps(current_workspace_prompt)}, _ts:3}}; const currentTailForCompaction = {{role:'user', content:prompt, _ts:3}}; const liveAssistant = {{role:'assistant', content:'working', _live:true, _ts:4}}; const attachments = [{{name:'note.txt', path:'note.txt', mime:'text/plain'}}]; const compactionMarker = {{ role:'user', content:'[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted.', _ts:3.5, }}; const repeatedPromptTurnOne = {{role:'user', content:prompt, _ts:1}}; const repeatedPromptAnswerOne = {{role:'assistant', content:'done', _ts:2}}; const repeatedPromptTurnTwo = {{role:'user', content:prompt, _ts:3}}; const repeatedPromptAnswerTwo = {{role:'assistant', content:'done', _ts:4}}; const repeatedCompletedBase = [ repeatedPromptTurnOne, repeatedPromptAnswerOne, repeatedPromptTurnTwo, repeatedPromptAnswerTwo, ]; const fromHistoricalSameText = getPendingSessionMessage( {{pending_user_message:prompt, pending_started_at:3}}, [historical, historicalAnswer] ); const fromHistoricalWorkspace = getPendingSessionMessage( {{pending_user_message:prompt, pending_started_at:3}}, [historicalWorkspace, historicalAnswer] ); const exactCurrentMessages = [historical, historicalAnswer, currentTail]; const exactCurrentResult = getPendingSessionMessage( {{pending_user_message:prompt, pending_started_at:4, pending_attachments:attachments}}, exactCurrentMessages ); const workspaceCurrentResult = getPendingSessionMessage( {{pending_user_message:prompt, pending_started_at:4}}, [historical, historicalAnswer, currentWorkspaceTail] ); const liveAfterCurrentResult = getPendingSessionMessage( {{pending_user_message:prompt, pending_started_at:4}}, [historical, historicalAnswer, currentWorkspaceTail, liveAssistant] ); const differentTailResult = getPendingSessionMessage( {{pending_user_message:prompt, pending_started_at:4}}, [historical, historicalAnswer, {{role:'user', content:'different prompt', _ts:3}}] ); const compactionTailResult = getPendingSessionMessage( {{pending_user_message:prompt, pending_started_at:4, pending_attachments:attachments}}, [historical, historicalAnswer, currentTailForCompaction, compactionMarker] ); const repeatedCompletedResult = getPendingSessionMessage( {{pending_user_message:prompt, pending_started_at:5}}, repeatedCompletedBase ); const repeatedCompletedMessages = repeatedCompletedResult ? [...repeatedCompletedBase, repeatedCompletedResult] : repeatedCompletedBase; const repeatedCompletedPromptCount = repeatedCompletedMessages.filter( m=>m&&m.role==='user'&&_normalizeUserTranscriptText(m.content)===prompt ).length; const compactionCurrentTail = _pendingCurrentTailUserMessage([historical, historicalAnswer, currentTailForCompaction, compactionMarker]); // Mid-run reload: the server already reconciled the active turn's user row into // the transcript AND this turn's assistant/tool output follows it, while // pending_user_message is still set. The strict tail scan cannot see the user // row (it stops at the completed assistant), so without the pending_started_at // fallback the prompt is materialized a second time -> duplicate user bubble. const midRunStartedAt = 500; const midRunTranscript = [ {{role:'user', content:'earlier question', timestamp:100}}, {{role:'assistant', content:'earlier answer', timestamp:200}}, {{role:'user', content:prompt, timestamp:midRunStartedAt}}, {{role:'assistant', content:'', timestamp:midRunStartedAt+15}}, {{role:'tool', content:'{{"ok":true}}', timestamp:midRunStartedAt+16}}, {{role:'assistant', content:'partial answer', timestamp:midRunStartedAt+60}}, ]; const midRunResult = getPendingSessionMessage( {{pending_user_message:prompt, pending_started_at:midRunStartedAt}}, midRunTranscript ); const midRunWorkspaceTranscript = [ {{role:'user', content:'earlier question', timestamp:100}}, {{role:'assistant', content:'earlier answer', timestamp:200}}, {{role:'user', content:{json.dumps(current_workspace_prompt)}, timestamp:midRunStartedAt}}, {{role:'assistant', content:'partial answer', timestamp:midRunStartedAt+30}}, ]; const midRunWorkspaceResult = getPendingSessionMessage( {{pending_user_message:prompt, pending_started_at:midRunStartedAt}}, midRunWorkspaceTranscript ); // A same-text row from an OLDER turn must never be adopted: only the row whose // timestamp matches pending_started_at identifies the active turn. const staleSameTextTranscript = [ {{role:'user', content:prompt, timestamp:midRunStartedAt-600}}, {{role:'assistant', content:'old answer', timestamp:midRunStartedAt-500}}, ]; const staleSameTextResult = getPendingSessionMessage( {{pending_user_message:prompt, pending_started_at:midRunStartedAt}}, staleSameTextTranscript ); // Legacy/absent pending_started_at disables the fallback rather than guessing. const noStartedAtResult = getPendingSessionMessage( {{pending_user_message:prompt}}, midRunTranscript ); // Regression (review round 2): two completed identical-text turns whose // started_at differ by ~1s. The old 1.5s tolerance adopted the EARLIER row as // the "active turn", hiding the second (still-pending) turn and copying its // attachments onto the old row. With the precision-only epsilon the second turn // is materialized and the first row stays untouched. const rapidRepeatTurnOne = {{role:'user', content:prompt, timestamp:100}}; const rapidRepeatAnswerOne = {{role:'assistant', content:'done', timestamp:101}}; const rapidRepeatSecondAttachments = [{{name:'second.txt', path:'second.txt', mime:'text/plain'}}]; const rapidRepeatResult = getPendingSessionMessage( {{pending_user_message:prompt, pending_started_at:101, pending_attachments:rapidRepeatSecondAttachments}}, [rapidRepeatTurnOne, rapidRepeatAnswerOne] ); // Exact token identity: the eager-checkpoint row carries _active_turn_token // built from the same stream_id + started_at as the session — adopted even // though its timestamp (499.9) is NOT within the epsilon of pending_started_at. const tokenIdentityTranscript = [ {{role:'user', content:'earlier question', timestamp:100}}, {{role:'assistant', content:'earlier answer', timestamp:200}}, {{role:'user', content:prompt, timestamp:499.9, _active_turn_token:'stream-abc:500'}}, {{role:'assistant', content:'partial', timestamp:560}}, ]; const tokenIdentityResult = getPendingSessionMessage( {{pending_user_message:prompt, pending_started_at:midRunStartedAt, active_stream_id:'stream-abc'}}, tokenIdentityTranscript ); process.stdout.write(JSON.stringify({{ historicalSameTextSurvives: !!fromHistoricalSameText && fromHistoricalSameText.content===prompt && fromHistoricalSameText._pending===true, historicalWorkspaceSurvives: !!fromHistoricalWorkspace && fromHistoricalWorkspace.content===prompt && fromHistoricalWorkspace._pending===true, exactCurrentTailDedupe: exactCurrentResult===null, exactCurrentTailAttachmentsCopied: Array.isArray(currentTail.attachments) && currentTail.attachments[0].name==='note.txt', workspaceCurrentTailDedupe: workspaceCurrentResult===null, liveAfterCurrentTailDedupe: liveAfterCurrentResult===null, differentCurrentTailSurvives: !!differentTailResult && differentTailResult.content===prompt && differentTailResult._pending===true, compactionBoundaryDedupe: compactionTailResult===null, compactionBoundaryCurrentTail: compactionCurrentTail&&compactionCurrentTail.role==='user'&&compactionCurrentTail.content===prompt, compactionCurrentTailAttachmentsCopied: Array.isArray(currentTailForCompaction.attachments) && currentTailForCompaction.attachments[0].name==='note.txt', repeatedCompletedPromptsRemainValid: repeatedCompletedPromptCount===3, midRunReloadDedupe: midRunResult===null, midRunWorkspaceReloadDedupe: midRunWorkspaceResult===null, staleSameTextStillMaterializes: !!staleSameTextResult && staleSameTextResult._pending===true, legacyNoStartedAtStillMaterializes: !!noStartedAtResult && noStartedAtResult._pending===true, rapidRepeatSecondTurnReturned: !!rapidRepeatResult && rapidRepeatResult._pending===true && rapidRepeatResult.content===prompt && Array.isArray(rapidRepeatResult.attachments) && rapidRepeatResult.attachments[0].name==='second.txt', rapidRepeatFirstRowKeptClean: !Array.isArray(rapidRepeatTurnOne.attachments), tokenIdentityDedupe: tokenIdentityResult===null, isContextCompactionText: _isContextCompactionText(compactionMarker.content), isContextCompactionMessage: _isContextCompactionMessage(compactionMarker), }})); """ proc = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) return json.loads(proc.stdout) def test_reattach_path_uses_replay_when_status_reports_journal(): reattach_pos = MESSAGES_SRC.index("let replayOnly=false;") # Window widened to 2200: the SSE-recovery follow-restore fix (the # _wasFollowingAtReconnectDead guard + its sticky-unpin check) inserted lines # into the reconnect-dead cleanup block between this anchor and the # replay-params assertion below, pushing the target string past the old slice. block = MESSAGES_SRC[reattach_pos : reattach_pos + 2200] assert "st.replay_available" in block assert "replayOnly=true" in block assert "(reconnecting||replayOnly)?_runJournalReplayParams():''" in block assert "_clearOwnerInflightState()" in block def test_error_reconnect_path_can_restore_from_journal(): # Anchor on the reconnect block's stable entry point rather than the exact # composer-status string: the first status was changed to a template literal # `Reconnecting… (1/${_retryDelays.length})` (staged-probe counter), so the # old single-quoted "setComposerStatus('Reconnecting" anchor no longer exists. reconnect_pos = MESSAGES_SRC.index("_reconnectAttempted=true;") block = MESSAGES_SRC[reconnect_pos : reconnect_pos + 1100] assert "st.active" in block assert "st.replay_available" in block assert "Restoring stream" in block assert "_runJournalReplayParams()" in block def test_frontend_replay_cursor_uses_eventsource_last_event_id(): cursor_pos = MESSAGES_SRC.index("function _rememberRunJournalCursor") block = MESSAGES_SRC[cursor_pos : cursor_pos + 1000] assert "e.lastEventId" in block assert "lastIndexOf(':')" in block assert "_lastRunJournalSeq=seq" in block assert "source.addEventListener(_runJournalEventName,_rememberRunJournalCursor)" in MESSAGES_SRC assert "after_seq=${encodeURIComponent(String(_runJournalReplayAfterSeq()))}" in MESSAGES_SRC assert "after_seq=0" not in MESSAGES_SRC def test_replayed_long_task_events_enter_the_same_live_timeline_handlers(): """Run-journal replay must not grow a parallel long-task renderer. The run-state consistency contract depends on replayed journal events flowing through the same EventSource handlers as live streams. Otherwise a live long task can render as Thinking -> progress text -> tool cards, while the same journaled event sequence replays as a flattened or reordered scene. """ wire_pos = MESSAGES_SRC.index("function _wireSSE(source)") wire_block = MESSAGES_SRC[wire_pos : MESSAGES_SRC.index("async function _restoreSettledSession", wire_pos)] replay_events = [ "reasoning", "interim_assistant", "tool", "tool_complete", "compressing", "compressed", "metering", "done", "apperror", ] for event_name in replay_events: assert f"source.addEventListener('{event_name}'" in wire_block, ( f"{event_name} must be handled by the shared live/replay SSE pipeline" ) thinking_helper = MESSAGES_SRC[ MESSAGES_SRC.index("function _updateLiveThinkingCard") : MESSAGES_SRC.index("// Split a content string", MESSAGES_SRC.index("function _updateLiveThinkingCard")) ] assert "_updateLiveThinkingCard(" in wire_block, "reasoning replay should use the live Thinking card path" assert "updateThinking(text, opts)" in thinking_helper and "appendThinking(text, opts)" in thinking_helper, ( "the shared Thinking helper should still route replay/live reasoning into the Worklog Thinking card path" ) assert "appendLiveToolCard(tc" in wire_block, "tool replay should use live tool-card rendering" # Compression replay must dispatch through setCompressionUi(...). The handler # body may build the state object inline (`setCompressionUi({...})`) or hoist # it into a `state` variable first (`setCompressionUi(state)`) — both forms # use the same compression-card path, so accept either. Pinning the literal # `{` after the open-paren was over-specific and broke in v0.51.76 when # PR #2347 hoisted the state object to share it with `appendLiveCompressionCard`. assert ("setCompressionUi({" in wire_block) or ("setCompressionUi(state)" in wire_block), ( "compression replay should use the compression card path" ) assert "_runJournalReplayParams()" in MESSAGES_SRC, "replay attachments should enter _wireSSE via EventSource" def test_run_journal_cursor_tracks_every_long_task_timeline_event(): """Every user-visible long-task event needs cursor tracking for parity replay.""" cursor_loop_pos = MESSAGES_SRC.index("for(const _runJournalEventName of [") cursor_loop = MESSAGES_SRC[cursor_loop_pos : MESSAGES_SRC.index("]", cursor_loop_pos)] timeline_events = [ "token", "interim_assistant", "reasoning", "tool", "tool_complete", "compressing", "compressed", "metering", "done", "apperror", "cancel", ] for event_name in timeline_events: assert f"'{event_name}'" in cursor_loop, ( f"{event_name} must advance the replay cursor to avoid duplicate timeline replay" ) def test_server_runtime_journal_snapshot_restores_structured_inflight_state(): helper_pos = SESSIONS_SRC.index("function _serverLiveSnapshotToolId") helper_block = SESSIONS_SRC[helper_pos : helper_pos + 3600] load_pos = SESSIONS_SRC.index("async function loadSession") load_end = SESSIONS_SRC.index("// ── Handoff hint logic", load_pos) load_block = SESSIONS_SRC[load_pos:load_end] assert "runtime_journal_snapshot" in load_block assert "_serverLiveSnapshotInflight(S.session.runtime_journal_snapshot" in load_block assert "!_inflightHasVisibleLiveState(INFLIGHT[sid])" in load_block assert "journalSnapshot:true" in helper_block assert "lastRunJournalSeq" in helper_block assert "last_assistant_text" in helper_block assert "activity_burst_anchors" in helper_block for key in ("tid", "id", "tool_call_id", "tool_use_id", "call_id"): assert key in helper_block def test_active_reload_keeps_user_only_inflight_visible_until_pending_dedupe(): """A just-submitted user row is visible live state before first assistant text. On an active first-turn reload, the sidecar can still have messages=[] while pending_user_message and the submitted turn journal record the same prompt. The browser must not discard the user-only optimistic INFLIGHT entry as a cursor-only snapshot before pending/live replay reconciliation runs. """ result = _run_session_identity_probe() assert result["userOnlyInflightVisible"] is True assert result["emptyUserOnlyInflightNotVisible"] is True def test_pending_user_merge_dedupes_user_turn_variants_by_behavior(): """Pending user rows and replayed/checkpointed user rows share one turn. Execute the same JavaScript helpers the browser uses so the regression test catches regex/order/trim mistakes, not just identifier wiring. """ result = _run_session_identity_probe() assert result["workspaceDedupe"] is True assert result["legacyWorkspaceDedupe"] is True assert result["attachedDedupe"] is True assert result["forcedSkillDedupe"] is True assert result["differentUserNotDedupe"] is True assert result["roleMismatchNotDedupe"] is True def test_user_turn_dedupe_is_scoped_to_current_turn_by_behavior(): result = _run_current_turn_scope_probe() assert result["insertedAfterHistory"] is True assert result["pendingAfterHistoryRoles"] == ["user", "assistant", "user"] assert result["insertedBeforeLive"] is True assert result["pendingBeforeLiveRoles"] == ["user", "assistant", "user", "assistant"] assert result["insertedWithCurrent"] is False assert result["pendingWithCurrentRoles"] == ["user", "assistant", "user", "assistant"] assert result["inflightAfterHistoryRoles"] == ["user", "assistant", "user", "assistant"] assert result["inflightWithCurrentRoles"] == ["user", "assistant", "user", "assistant"] assert result["insertedAfterCompaction"] is False assert result["compactionCurrentTailContent"] == "[Workspace::v1: /tmp/current]\nrepeat me" assert result["compactionTailDuplicate"] is True assert result["compactionPromptCount"] == 1 assert result["compactionMarkerRetained"] is True assert result["compactionLiveAssistantRetained"] is True assert result["completedBoundaryDedupe"] is False assert result["distinctCompletedTurnPromptCount"] == 2 def test_get_pending_session_message_keeps_deferred_repeat_prompt_by_behavior(): """Deferred active reload must not hide a current repeat prompt. In the default deferred save mode, chat start persists only pending_user_message before the worker appends the display row. If an older turn has the same visible user text, getPendingSessionMessage still has to return the current pending row; downstream merge code then dedupes only against the current tail. """ result = _run_pending_session_message_probe() assert result["historicalSameTextSurvives"] is True assert result["historicalWorkspaceSurvives"] is True assert result["exactCurrentTailDedupe"] is True assert result["exactCurrentTailAttachmentsCopied"] is True assert result["workspaceCurrentTailDedupe"] is True assert result["liveAfterCurrentTailDedupe"] is True assert result["differentCurrentTailSurvives"] is True assert result["compactionBoundaryDedupe"] is True assert result["compactionBoundaryCurrentTail"] is True assert result["compactionCurrentTailAttachmentsCopied"] is True assert result["repeatedCompletedPromptsRemainValid"] is True # #6649 follow-up: mid-run reload must not duplicate the active turn's user row assert result["midRunReloadDedupe"] is True assert result["midRunWorkspaceReloadDedupe"] is True assert result["staleSameTextStillMaterializes"] is True assert result["legacyNoStartedAtStillMaterializes"] is True # #6670 review round 2: ~1s-apart identical turns must not be collapsed by # the active-turn fallback (no over-dedup, no attachment mis-attachment). assert result["rapidRepeatSecondTurnReturned"] is True assert result["rapidRepeatFirstRowKeptClean"] is True assert result["tokenIdentityDedupe"] is True assert result["isContextCompactionText"] is True assert result["isContextCompactionMessage"] is True def test_live_tool_matching_uses_the_same_aliases_as_live_card_dedup(): live_tid_pos = MESSAGES_SRC.index("function _liveToolTid") live_tid_block = MESSAGES_SRC[live_tid_pos : live_tid_pos + 450] find_pos = MESSAGES_SRC.index("function _findPendingLiveToolCallIndex") find_block = MESSAGES_SRC[find_pos : find_pos + 900] upsert_pos = MESSAGES_SRC.index("function upsertLiveToolCall") upsert_block = MESSAGES_SRC[upsert_pos : upsert_pos + 600] for key in ("tid", "id", "tool_call_id", "tool_use_id", "call_id"): assert key in live_tid_block assert key in find_block assert key in upsert_block