* 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]>
110 lines
4.6 KiB
Python
110 lines
4.6 KiB
Python
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
MESSAGES_SRC = (ROOT / "static" / "messages.js").read_text()
|
|
|
|
|
|
def test_reattach_path_uses_replay_when_status_reports_journal():
|
|
reattach_pos = MESSAGES_SRC.index("let replayOnly=false;")
|
|
block = MESSAGES_SRC[reattach_pos : reattach_pos + 1200]
|
|
|
|
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():
|
|
reconnect_pos = MESSAGES_SRC.index("setComposerStatus('Reconnecting")
|
|
block = MESSAGES_SRC[reconnect_pos : reconnect_pos + 900]
|
|
|
|
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"
|
|
)
|