Merge commit 'refs/pull/6048/head' of github.com:nesquena/hermes-webui into stage/6048
This commit is contained in:
commit
4f5f197f9a
6 changed files with 819 additions and 23 deletions
|
|
@ -2380,8 +2380,11 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
|||
burstId:_currentActivityBurstId,
|
||||
};
|
||||
}
|
||||
function _updateLiveThinkingCard(text){
|
||||
const opts=_liveThinkingPlacement();
|
||||
function _updateLiveThinkingCard(text, options){
|
||||
const opts={
|
||||
..._liveThinkingPlacement(),
|
||||
...((options&&typeof options==='object')?options:{}),
|
||||
};
|
||||
if(typeof updateThinking==='function') updateThinking(text, opts);
|
||||
else appendThinking(text, opts);
|
||||
}
|
||||
|
|
@ -2625,7 +2628,10 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
|||
// in-closure SSE handlers). Explicit terminal-path calls above just expire it
|
||||
// sooner; this guarantees window._liveAnchorRegistries can't grow unbounded.
|
||||
_scheduleAnchorRegistryCleanup(600000);
|
||||
function _applyToAnchor(sourceEventType, rawEventData, sseEvent){
|
||||
// Applying an event and painting it are separate outcomes. Reasoning uses the
|
||||
// optional holder to decide whether a temporary visible fallback is needed.
|
||||
function _applyToAnchor(sourceEventType, rawEventData, sseEvent, renderOutcome){
|
||||
if(renderOutcome&&typeof renderOutcome==='object') renderOutcome.rendered=false;
|
||||
if(!_anchorRegistry||!_anchorApi||typeof _anchorApi.applyAssistantTurnAnchorSourceEvent!=='function') return null;
|
||||
const raw=(rawEventData&&typeof rawEventData==='object')?rawEventData:{};
|
||||
const eventId=(sseEvent&&sseEvent.lastEventId)||raw.event_id||raw.lastEventId||raw.last_event_id||'';
|
||||
|
|
@ -2649,7 +2655,8 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
|||
sourceEvent,
|
||||
{session_id:activeSid,stream_id:streamId}
|
||||
);
|
||||
_renderAnchorLiveScene();
|
||||
const rendered=_renderAnchorLiveScene();
|
||||
if(renderOutcome&&typeof renderOutcome==='object') renderOutcome.rendered=rendered;
|
||||
return result;
|
||||
}catch(err){
|
||||
if(!_anchorShadowWarned&&typeof console!=='undefined'&&console.warn){
|
||||
|
|
@ -3785,19 +3792,24 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
|||
}
|
||||
function _upsertAnchorReasoning(text, options={}){
|
||||
const clean=String(text||'').trim();
|
||||
if(!clean||!_anchorRegistry||window._showThinking===false) return null;
|
||||
const placement=_liveThinkingPlacement();
|
||||
const segmentSeq=Number(options.segmentSeq||placement.segmentSeq||_anchorSegmentSeq());
|
||||
const localId=String(options.localId||`live-reasoning:${streamId}:${segmentSeq}`);
|
||||
if(options&&typeof options==='object'){
|
||||
options.anchorReasoningLocalId=localId;
|
||||
options.segmentSeq=segmentSeq;
|
||||
if(options.burstId===undefined) options.burstId=_currentActivityBurstId;
|
||||
}
|
||||
if(!clean||!_anchorRegistry||window._showThinking===false) return null;
|
||||
const existing=_findAnchorActivityEventByLocalId(localId,'reasoning');
|
||||
if(existing){
|
||||
const replaced=_replaceAnchorActivityEventByLocalId(localId,'reasoning',{
|
||||
status:options.sealed?'completed':'running',
|
||||
payload:{text:clean,activitySegmentSeq:segmentSeq,activityBurstId:_currentActivityBurstId},
|
||||
});
|
||||
_renderAnchorLiveScene();
|
||||
return replaced;
|
||||
return _renderAnchorLiveScene()?replaced:null;
|
||||
}
|
||||
const renderOutcome={rendered:false};
|
||||
_applyToAnchor('reasoning',{
|
||||
text:clean,
|
||||
local_id:localId,
|
||||
|
|
@ -3805,8 +3817,8 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
|||
status:options.sealed?'completed':'running',
|
||||
activitySegmentSeq:segmentSeq,
|
||||
activityBurstId:_currentActivityBurstId,
|
||||
},null);
|
||||
return _findAnchorActivityEventByLocalId(localId,'reasoning');
|
||||
},null,renderOutcome);
|
||||
return renderOutcome.rendered?_findAnchorActivityEventByLocalId(localId,'reasoning'):null;
|
||||
}
|
||||
function _compactVisibleEchoText(value){
|
||||
return String(value||'').replace(/\s+/g,'');
|
||||
|
|
@ -5340,16 +5352,23 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
|||
|
||||
source.addEventListener('reasoning',e=>{
|
||||
if(_terminalStateReached||_streamFinalized) return;
|
||||
if(!_ownsActiveStreamOrBackground()) return;
|
||||
const d=JSON.parse(e.data);
|
||||
const text=d.text||'';
|
||||
reasoningText += text;
|
||||
liveReasoningText += text;
|
||||
if(d.text&&S.session&&S.session.session_id===activeSid) _completeAutomaticCompressionOnLiveProgress(activeSid);
|
||||
syncInflightAssistantMessage();
|
||||
if(text&&S.session&&S.session.session_id===activeSid){
|
||||
if(text&&S.session&&S.session.session_id===activeSid&&S.activeStreamId===streamId){
|
||||
const liveThinkingText=_liveThinkingText();
|
||||
if(!_upsertAnchorReasoning(liveThinkingText)){
|
||||
_updateLiveThinkingCard(liveThinkingText);
|
||||
const anchorReasoningFallback={};
|
||||
if(!_upsertAnchorReasoning(liveThinkingText, anchorReasoningFallback)){
|
||||
_updateLiveThinkingCard(liveThinkingText,{
|
||||
...anchorReasoningFallback,
|
||||
anchorRenderFallback:true,
|
||||
sessionId:activeSid,
|
||||
streamId,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
95
static/ui.js
95
static/ui.js
|
|
@ -12084,6 +12084,7 @@ function _anchorSceneNodeForRow(row, opts){
|
|||
if(!node) return null;
|
||||
node.setAttribute('data-anchor-scene-row','1');
|
||||
node.setAttribute('data-anchor-row-id',String(row.row_id||row.local_id||''));
|
||||
if(row.local_id) node.setAttribute('data-anchor-local-id',String(row.local_id));
|
||||
node.setAttribute('data-anchor-row-role',String(row.role||'activity'));
|
||||
node.setAttribute('data-anchor-source-event-type',String(row.source_event_type||''));
|
||||
return node;
|
||||
|
|
@ -12153,6 +12154,7 @@ function _anchorSceneTransparentNodeForRow(row, opts){
|
|||
if(settled) node.setAttribute('data-anchor-settled-scene-row','1');
|
||||
if(live) node.setAttribute('data-anchor-live-scene-row','1');
|
||||
node.setAttribute('data-anchor-row-id',String(row.row_id||row.local_id||''));
|
||||
if(row.local_id) node.setAttribute('data-anchor-local-id',String(row.local_id));
|
||||
node.setAttribute('data-anchor-row-role',String(row.role||'activity'));
|
||||
node.setAttribute('data-anchor-source-event-type',String(row.source_event_type||''));
|
||||
if(opts&&opts.streamId) node.setAttribute('data-anchor-stream-id',String(opts.streamId));
|
||||
|
|
@ -12344,6 +12346,81 @@ function _prepareLiveAnchorScrollRebuildGuard(scrollSnapshot){
|
|||
},
|
||||
};
|
||||
}
|
||||
function _resetMismatchedLiveAssistantTurnForSession(turn, sessionId){
|
||||
const sid=String(sessionId||'');
|
||||
if(!turn||!sid||!turn.dataset) return false;
|
||||
const existingSid=String(turn.dataset.sessionId||'');
|
||||
if(!existingSid||existingSid===sid) return false;
|
||||
const blocks=typeof _assistantTurnBlocks==='function' ? _assistantTurnBlocks(turn) : turn;
|
||||
if(blocks){
|
||||
try{
|
||||
blocks.innerHTML='';
|
||||
}catch(_){
|
||||
while(blocks.firstChild) blocks.removeChild(blocks.firstChild);
|
||||
}
|
||||
}
|
||||
turn.dataset.sessionId=sid;
|
||||
return true;
|
||||
}
|
||||
function _liveAnchorReasoningRowForFallback(turn, opts){
|
||||
opts=opts||{};
|
||||
const blocks=typeof _assistantTurnBlocks==='function' ? _assistantTurnBlocks(turn) : turn;
|
||||
if(!turn||!blocks||!blocks.querySelectorAll) return null;
|
||||
const streamId=String(opts.streamId||S.activeStreamId||'');
|
||||
const sessionId=String(opts.sessionId||(S.session&&S.session.session_id)||'');
|
||||
const localId=String(opts.anchorReasoningLocalId||opts.localId||'').trim();
|
||||
if(!localId) return null;
|
||||
const rows=blocks.querySelectorAll(
|
||||
'[data-anchor-scene-row="1"][data-anchor-local-id]'
|
||||
);
|
||||
for(const row of Array.from(rows)){
|
||||
const anchorLocalId=String(row.getAttribute&&row.getAttribute('data-anchor-local-id')||'');
|
||||
if(anchorLocalId!==localId) continue;
|
||||
const rowRole=String(row.getAttribute&&row.getAttribute('data-anchor-row-role')||'');
|
||||
const rowSource=String(row.getAttribute&&row.getAttribute('data-anchor-source-event-type')||'');
|
||||
if(rowRole!=='thinking'&&rowSource!=='reasoning') continue;
|
||||
const rowStreamId=String(row.getAttribute&&row.getAttribute('data-anchor-stream-id')||'');
|
||||
if(rowStreamId&&streamId&&rowStreamId!==streamId) continue;
|
||||
const rowSessionId=String(row.getAttribute&&row.getAttribute('data-session-id')||'');
|
||||
if(rowSessionId&&sessionId&&rowSessionId!==sessionId) continue;
|
||||
return row;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function _updateLiveAnchorReasoningRowForFallback(turn, text, opts){
|
||||
const clean=_sanitizeThinkingDisplayText(text);
|
||||
if(!clean||window._showThinking===false) return false;
|
||||
const row=_liveAnchorReasoningRowForFallback(turn, opts);
|
||||
if(!row) return false;
|
||||
if(row.classList&&row.classList.contains('wl-reason')){
|
||||
if(typeof _renderWorklogReasonInto==='function') _renderWorklogReasonInto(row, clean);
|
||||
else row.textContent=clean;
|
||||
const group=row.closest&&row.closest('.tool-worklog-group,.tool-call-group,.live-worklog');
|
||||
if(group&&typeof _syncToolCallGroupSummary==='function') _syncToolCallGroupSummary(group);
|
||||
}else if(row.classList&&row.classList.contains('transparent-event-row')){
|
||||
_renderThinkingInto(row, clean);
|
||||
const eventAt=row.getAttribute&&row.getAttribute('data-event-at');
|
||||
const nextTs=typeof _firstValidTimestampSeconds==='function'
|
||||
? _firstValidTimestampSeconds(opts&&opts.ts, opts&&opts.timestamp, opts&&opts.created_at, eventAt)
|
||||
: null;
|
||||
if(typeof _decorateTransparentEventRow==='function'){
|
||||
_decorateTransparentEventRow(row,{
|
||||
type:'thinking',
|
||||
text:clean,
|
||||
preview:clean,
|
||||
ts:nextTs||undefined,
|
||||
live:true,
|
||||
segmentSeq:opts&&opts.segmentSeq,
|
||||
burstId:opts&&opts.burstId,
|
||||
});
|
||||
}
|
||||
}else{
|
||||
_renderThinkingInto(row, clean);
|
||||
}
|
||||
if(turn&&typeof _syncTransparentEventControls==='function') _syncTransparentEventControls(turn);
|
||||
if(typeof scrollIfPinned==='function') scrollIfPinned();
|
||||
return true;
|
||||
}
|
||||
function renderLiveAnchorActivityScene(streamId, scene, opts){
|
||||
opts=opts||{};
|
||||
const requestedMode=opts.mode;
|
||||
|
|
@ -12360,6 +12437,12 @@ function renderLiveAnchorActivityScene(streamId, scene, opts){
|
|||
const knownMode=(m)=>m==='compact_worklog'||m==='transparent_stream'||m==='hide_all_activity';
|
||||
const sceneMode=knownMode(activeMode)?activeMode:(knownMode(requestedMode)?requestedMode:activeMode);
|
||||
if(sceneMode==='hide_all_activity') return false;
|
||||
const existingTurn=$('liveAssistantTurn');
|
||||
const requestedSessionId=String(opts.sessionId||'');
|
||||
const existingTurnSessionId=String(existingTurn&&existingTurn.dataset&&existingTurn.dataset.sessionId||'');
|
||||
if(existingTurn&&requestedSessionId&&existingTurnSessionId&&existingTurnSessionId!==requestedSessionId){
|
||||
if(!_resetMismatchedLiveAssistantTurnForSession(existingTurn, requestedSessionId)) return false;
|
||||
}
|
||||
if(sceneMode==='transparent_stream'){
|
||||
return _renderLiveAnchorActivitySceneTransparent(streamId,scene,opts);
|
||||
}
|
||||
|
|
@ -18435,9 +18518,19 @@ function appendThinking(text='', options){
|
|||
// without this check they would pollute the new session's DOM.
|
||||
options=options||{};
|
||||
const allowPendingPlaceholder=!!(options&&options.pending===true);
|
||||
const anchorRenderFallback=!!(options&&options.anchorRenderFallback===true);
|
||||
if(typeof isFinalAnswerOnlyMode==='function'&&isFinalAnswerOnlyMode()) return;
|
||||
if(!S.session||(!S.activeStreamId&&!allowPendingPlaceholder)) return;
|
||||
if(!allowPendingPlaceholder&&isLiveAnchorActivitySceneOwner(S.activeStreamId)){
|
||||
if(options.sessionId&&String(options.sessionId)!==String(S.session.session_id||'')) return;
|
||||
if(options.streamId&&String(options.streamId)!==String(S.activeStreamId||'')) return;
|
||||
const existingLiveTurn=$('liveAssistantTurn');
|
||||
if(anchorRenderFallback&&existingLiveTurn&&existingLiveTurn.dataset&&
|
||||
existingLiveTurn.dataset.sessionId&&
|
||||
String(existingLiveTurn.dataset.sessionId)!==String(S.session.session_id||'')){
|
||||
if(!_resetMismatchedLiveAssistantTurnForSession(existingLiveTurn, S.session.session_id)) return;
|
||||
}
|
||||
if(anchorRenderFallback&&existingLiveTurn&&_updateLiveAnchorReasoningRowForFallback(existingLiveTurn,text,options)) return;
|
||||
if(!allowPendingPlaceholder&&!anchorRenderFallback&&isLiveAnchorActivitySceneOwner(S.activeStreamId)){
|
||||
_renderLiveAnchorActivitySceneForStream(S.activeStreamId, S.session.session_id);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
672
tests/test_issue5720_reasoning_owner.py
Normal file
672
tests/test_issue5720_reasoning_owner.py
Normal file
|
|
@ -0,0 +1,672 @@
|
|||
"""Behavior regression for #5720 live reasoning presentation ownership."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
NODE = shutil.which("node")
|
||||
|
||||
|
||||
def _run_reasoning_scene(
|
||||
*,
|
||||
activity_mode: str = "transparent_stream",
|
||||
fail_first_anchor_render: bool = False,
|
||||
fail_anchor_render_on: int | None = None,
|
||||
multi_segment_fallback: bool = False,
|
||||
stale_active_stream: bool = False,
|
||||
stale_live_turn: bool = False,
|
||||
show_thinking: bool = True,
|
||||
) -> dict:
|
||||
assert NODE, "node is required for the #5720 browser-chain regression"
|
||||
env = os.environ.copy()
|
||||
env.setdefault("ISSUE5720_UI_JS", str(ROOT / "static" / "ui.js"))
|
||||
env.setdefault("ISSUE5720_MESSAGES_JS", str(ROOT / "static" / "messages.js"))
|
||||
env.setdefault(
|
||||
"ISSUE5720_ANCHORS_JS",
|
||||
str(ROOT / "static" / "assistant_turn_anchors.js"),
|
||||
)
|
||||
env["ISSUE5720_ACTIVITY_MODE"] = activity_mode
|
||||
if fail_first_anchor_render:
|
||||
env["ISSUE5720_FAIL_FIRST_ANCHOR_RENDER"] = "1"
|
||||
if fail_anchor_render_on is not None:
|
||||
env["ISSUE5720_FAIL_ANCHOR_RENDER_ON"] = str(fail_anchor_render_on)
|
||||
if multi_segment_fallback:
|
||||
env["ISSUE5720_MULTI_SEGMENT_FALLBACK"] = "1"
|
||||
if stale_active_stream:
|
||||
env["ISSUE5720_STALE_ACTIVE_STREAM"] = "1"
|
||||
if stale_live_turn:
|
||||
env["ISSUE5720_STALE_LIVE_TURN"] = "1"
|
||||
if not show_thinking:
|
||||
env["ISSUE5720_SHOW_THINKING"] = "0"
|
||||
result = subprocess.run(
|
||||
[NODE, "-e", _NODE_SCENE],
|
||||
cwd=ROOT,
|
||||
env=env,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=10,
|
||||
check=False,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
||||
def test_deepseek_reasoning_deltas_keep_one_live_row_owner():
|
||||
result = _run_reasoning_scene()
|
||||
|
||||
assert result["first_text"] == "Plan"
|
||||
assert result["second_text"] == "Plan step"
|
||||
assert result["same_node"] is True
|
||||
assert result["live_reasoning_rows"] == 1
|
||||
assert result["render_passes"] == [1, 2]
|
||||
assert result["anchor_reasoning_events"] == 1
|
||||
assert result["anchor_reasoning_text"] == "Plan step"
|
||||
assert result["inflight_reasoning_text"] == "Plan step"
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
||||
def test_reasoning_uses_visible_fallback_when_anchor_declines_first_paint():
|
||||
result = _run_reasoning_scene(fail_first_anchor_render=True)
|
||||
|
||||
assert result["first_text"] is None
|
||||
assert result["first_fallback_text"] == "Plan"
|
||||
assert result["second_text"] == "Plan step"
|
||||
assert result["live_reasoning_rows"] == 1
|
||||
assert result["fallback_rows_after_recovery"] == 0
|
||||
assert result["anchor_reasoning_events"] == 1
|
||||
assert result["anchor_reasoning_text"] == "Plan step"
|
||||
assert result["inflight_reasoning_text"] == "Plan step"
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
||||
def test_stale_stream_reasoning_cannot_claim_the_active_turn():
|
||||
result = _run_reasoning_scene(stale_active_stream=True)
|
||||
|
||||
assert result["anchor_render_attempts"] == 0
|
||||
assert result["live_reasoning_rows"] == 0
|
||||
assert result["fallback_rows_after_recovery"] == 0
|
||||
assert result["anchor_reasoning_events"] == 0
|
||||
assert result["inflight_reasoning_text"] == ""
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
||||
def test_reasoning_fallback_respects_show_thinking_false():
|
||||
result = _run_reasoning_scene(
|
||||
fail_first_anchor_render=True,
|
||||
show_thinking=False,
|
||||
)
|
||||
|
||||
assert result["anchor_render_attempts"] == 0
|
||||
assert result["live_reasoning_rows"] == 0
|
||||
assert result["fallback_rows_after_recovery"] == 0
|
||||
assert result["anchor_reasoning_events"] == 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
||||
def test_reasoning_rebuilds_when_another_sessions_live_turn_blocks_anchor():
|
||||
result = _run_reasoning_scene(stale_live_turn=True)
|
||||
|
||||
assert result["first_text"] == "Plan"
|
||||
assert result["second_text"] == "Plan step"
|
||||
assert result["live_reasoning_rows"] == 1
|
||||
assert result["fallback_rows_after_recovery"] == 0
|
||||
assert result["anchor_reasoning_events"] == 1
|
||||
assert result["anchor_reasoning_text"] == "Plan step"
|
||||
assert result["inflight_reasoning_text"] == "Plan step"
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
||||
def test_reasoning_fallback_rebuilds_from_another_sessions_live_turn():
|
||||
result = _run_reasoning_scene(
|
||||
fail_first_anchor_render=True,
|
||||
stale_live_turn=True,
|
||||
)
|
||||
|
||||
assert result["first_text"] is None
|
||||
assert result["first_fallback_text"] == "Plan"
|
||||
assert result["second_text"] == "Plan step"
|
||||
assert result["live_reasoning_rows"] == 1
|
||||
assert result["fallback_rows_after_recovery"] == 0
|
||||
assert result["anchor_reasoning_events"] == 1
|
||||
assert result["anchor_reasoning_text"] == "Plan step"
|
||||
assert result["inflight_reasoning_text"] == "Plan step"
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
||||
@pytest.mark.parametrize("activity_mode", ["transparent_stream", "compact_worklog"])
|
||||
def test_later_deferred_anchor_paint_updates_existing_reasoning_owner(activity_mode):
|
||||
result = _run_reasoning_scene(
|
||||
activity_mode=activity_mode,
|
||||
fail_anchor_render_on=2,
|
||||
)
|
||||
|
||||
assert result["first_text"] == "Plan"
|
||||
assert result["second_text"] == "Plan step"
|
||||
assert result["same_node"] is True
|
||||
assert result["live_reasoning_rows"] == 1
|
||||
assert result["fallback_rows_after_recovery"] == 0
|
||||
assert result["visible_reasoning_owners"] == 1
|
||||
assert result["anchor_render_attempts"] == 2
|
||||
assert result["anchor_reasoning_events"] == 1
|
||||
assert result["anchor_reasoning_text"] == "Plan step"
|
||||
assert result["inflight_reasoning_text"] == "Plan step"
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
||||
@pytest.mark.parametrize("activity_mode", ["transparent_stream", "compact_worklog"])
|
||||
def test_later_deferred_anchor_paint_updates_exact_reasoning_owner(activity_mode):
|
||||
result = _run_reasoning_scene(
|
||||
activity_mode=activity_mode,
|
||||
multi_segment_fallback=True,
|
||||
)
|
||||
exact = result["multi_segment_exact_fallback"]
|
||||
|
||||
assert exact["first_before"] == "Plan step"
|
||||
assert exact["first_after"] == "Plan step"
|
||||
assert exact["second_before"] == "Second draft"
|
||||
assert exact["second_after"] == "Second updated"
|
||||
assert exact["legacy_after"] == "Legacy unkeyed"
|
||||
assert exact["anchor_run_id"] == "run-5720"
|
||||
assert exact["second_row_id"] == "run-5720:2"
|
||||
assert exact["second_local_id"] == "live-reasoning:stream-1:2"
|
||||
assert exact["live_reasoning_rows"] == 2
|
||||
assert exact["fallback_rows"] == 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
||||
def test_later_deferred_anchor_paint_remains_hidden_in_final_answer_only_mode():
|
||||
result = _run_reasoning_scene(
|
||||
activity_mode="hide_all_activity",
|
||||
fail_anchor_render_on=2,
|
||||
)
|
||||
|
||||
assert result["first_text"] is None
|
||||
assert result["second_text"] is None
|
||||
assert result["live_reasoning_rows"] == 0
|
||||
assert result["fallback_rows_after_recovery"] == 0
|
||||
assert result["visible_reasoning_owners"] == 0
|
||||
assert result["anchor_render_attempts"] == 2
|
||||
assert result["anchor_reasoning_events"] == 1
|
||||
assert result["anchor_reasoning_text"] == "Plan step"
|
||||
|
||||
|
||||
_NODE_SCENE = r"""
|
||||
const fs = require('fs');
|
||||
const uiSrc = fs.readFileSync(process.env.ISSUE5720_UI_JS, 'utf8');
|
||||
const messagesSrc = fs.readFileSync(process.env.ISSUE5720_MESSAGES_JS, 'utf8');
|
||||
const anchorsSrc = fs.readFileSync(process.env.ISSUE5720_ANCHORS_JS, 'utf8');
|
||||
|
||||
function extractFunc(src, name){
|
||||
const start = src.indexOf('function ' + name);
|
||||
if(start < 0){
|
||||
if(name==='isFinalAnswerOnlyMode'||name==='isHideAllActivityMode'){
|
||||
return `function ${name}(){ return false; }`;
|
||||
}
|
||||
if(name==='_anchorSceneRowTimestampSeconds'){
|
||||
return `function ${name}(){ return null; }`;
|
||||
}
|
||||
throw new Error(name + ' not found');
|
||||
}
|
||||
const params = src.indexOf('(', start);
|
||||
let depth = 0, close = -1;
|
||||
for(let i=params; i<src.length; i++){
|
||||
if(src[i] === '(') depth++;
|
||||
else if(src[i] === ')' && --depth === 0){ close = i; break; }
|
||||
}
|
||||
const brace = src.indexOf('{', close);
|
||||
depth = 0;
|
||||
for(let i=brace; i<src.length; i++){
|
||||
if(src[i] === '{') depth++;
|
||||
else if(src[i] === '}' && --depth === 0) return src.slice(start, i + 1);
|
||||
}
|
||||
throw new Error(name + ' did not close');
|
||||
}
|
||||
|
||||
class FakeElement {
|
||||
constructor(tag='div'){
|
||||
this.tagName=String(tag).toUpperCase();
|
||||
this.children=[];
|
||||
this.parentNode=null;
|
||||
this.attributes=Object.create(null);
|
||||
this.dataset=Object.create(null);
|
||||
this.style=Object.create(null);
|
||||
this.hidden=false;
|
||||
this.id='';
|
||||
this._text='';
|
||||
this._classes=new Set();
|
||||
const self=this;
|
||||
this.classList={
|
||||
add(...names){ names.forEach(name=>self._classes.add(name)); },
|
||||
remove(...names){ names.forEach(name=>self._classes.delete(name)); },
|
||||
contains(name){ return self._classes.has(name); },
|
||||
toggle(name, force){
|
||||
if(force===true){ self._classes.add(name); return true; }
|
||||
if(force===false){ self._classes.delete(name); return false; }
|
||||
if(self._classes.has(name)){ self._classes.delete(name); return false; }
|
||||
self._classes.add(name); return true;
|
||||
},
|
||||
};
|
||||
}
|
||||
get parentElement(){ return this.parentNode; }
|
||||
get nextSibling(){
|
||||
if(!this.parentNode) return null;
|
||||
const siblings=this.parentNode.children;
|
||||
return siblings[siblings.indexOf(this)+1]||null;
|
||||
}
|
||||
get className(){ return Array.from(this._classes).join(' '); }
|
||||
set className(value){ this._classes=new Set(String(value).split(/\s+/).filter(Boolean)); }
|
||||
get textContent(){
|
||||
return this.children.length ? this.children.map(child=>child.textContent).join('') : this._text;
|
||||
}
|
||||
set textContent(value){ this._text=String(value??''); this.children=[]; }
|
||||
get innerHTML(){ return this.textContent; }
|
||||
set innerHTML(value){ this.textContent=value; }
|
||||
setAttribute(name, value){
|
||||
const key=String(name), val=String(value);
|
||||
this.attributes[key]=val;
|
||||
if(key==='id') this.id=val;
|
||||
if(key==='class') this.className=val;
|
||||
if(key.startsWith('data-')){
|
||||
const dataKey=key.slice(5).replace(/-([a-z])/g,(_,c)=>c.toUpperCase());
|
||||
this.dataset[dataKey]=val;
|
||||
}
|
||||
}
|
||||
getAttribute(name){
|
||||
return Object.prototype.hasOwnProperty.call(this.attributes,name)?this.attributes[name]:null;
|
||||
}
|
||||
getAttributeNames(){ return Object.keys(this.attributes); }
|
||||
removeAttribute(name){
|
||||
delete this.attributes[name];
|
||||
if(name==='id') this.id='';
|
||||
if(name.startsWith('data-')){
|
||||
const dataKey=name.slice(5).replace(/-([a-z])/g,(_,c)=>c.toUpperCase());
|
||||
delete this.dataset[dataKey];
|
||||
}
|
||||
}
|
||||
appendChild(child){
|
||||
if(child.tagName==='#FRAGMENT'){
|
||||
child.children.slice().forEach(node=>this.appendChild(node));
|
||||
child.children=[];
|
||||
return child;
|
||||
}
|
||||
if(child.parentNode) child.remove();
|
||||
child.parentNode=this;
|
||||
this.children.push(child);
|
||||
return child;
|
||||
}
|
||||
insertBefore(child, ref){
|
||||
if(child.parentNode) child.remove();
|
||||
child.parentNode=this;
|
||||
const idx=this.children.indexOf(ref);
|
||||
if(idx<0) this.children.push(child); else this.children.splice(idx,0,child);
|
||||
return child;
|
||||
}
|
||||
remove(){
|
||||
if(!this.parentNode) return;
|
||||
const siblings=this.parentNode.children;
|
||||
const idx=siblings.indexOf(this);
|
||||
if(idx>=0) siblings.splice(idx,1);
|
||||
this.parentNode=null;
|
||||
}
|
||||
matches(selector){ return matchesSelector(this,selector); }
|
||||
querySelector(selector){ return this.querySelectorAll(selector)[0]||null; }
|
||||
querySelectorAll(selector){
|
||||
const out=[];
|
||||
const walk=node=>node.children.forEach(child=>{
|
||||
if(matchesSelector(child,selector)) out.push(child);
|
||||
walk(child);
|
||||
});
|
||||
walk(this);
|
||||
return out;
|
||||
}
|
||||
closest(selector){
|
||||
let node=this;
|
||||
while(node){ if(matchesSelector(node,selector)) return node; node=node.parentNode; }
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function matchesSelector(el, selector){
|
||||
return String(selector).split(',').some(part=>matchesChain(el,part.trim()));
|
||||
}
|
||||
function matchesChain(el, selector){
|
||||
const parts=selector.split(/\s+/).filter(Boolean);
|
||||
if(!parts.length||!matchesSimple(el,parts[parts.length-1])) return false;
|
||||
let node=el.parentNode;
|
||||
for(let i=parts.length-2;i>=0;i--){
|
||||
while(node&&!matchesSimple(node,parts[i])) node=node.parentNode;
|
||||
if(!node) return false;
|
||||
node=node.parentNode;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function matchesSimple(el, selector){
|
||||
selector=selector.replace(/^:scope\s*>\s*/,'');
|
||||
const nots=[];
|
||||
selector=selector.replace(/:not\(([^)]+)\)/g,(_,inner)=>{nots.push(inner);return '';});
|
||||
if(nots.some(inner=>matchesSimple(el,inner))) return false;
|
||||
const tag=selector.match(/^[A-Za-z][A-Za-z0-9-]*/);
|
||||
if(tag&&el.tagName!==tag[0].toUpperCase()) return false;
|
||||
const id=selector.match(/#([A-Za-z0-9_-]+)/);
|
||||
if(id&&el.id!==id[1]) return false;
|
||||
for(const match of selector.matchAll(/\.([A-Za-z0-9_-]+)/g)){
|
||||
if(!el.classList.contains(match[1])) return false;
|
||||
}
|
||||
for(const match of selector.matchAll(/\[([^\]=]+)(?:=["']?([^\]"']*)["']?)?\]/g)){
|
||||
const value=el.getAttribute(match[1]);
|
||||
if(value===null) return false;
|
||||
if(match[2]!==undefined&&String(value)!==String(match[2])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
global.window={
|
||||
_chatActivityDisplayMode:process.env.ISSUE5720_ACTIVITY_MODE||'transparent_stream',
|
||||
_showThinking:process.env.ISSUE5720_SHOW_THINKING!=='0',
|
||||
_simplifiedToolCalling:true,
|
||||
};
|
||||
global.document={
|
||||
baseURI:'http://test.local/',
|
||||
hidden:false,
|
||||
createElement:tag=>new FakeElement(tag),
|
||||
createTextNode:text=>{const node=new FakeElement('#text');node.textContent=text;return node;},
|
||||
createDocumentFragment:()=>new FakeElement('#fragment'),
|
||||
querySelector:()=>null,
|
||||
};
|
||||
global.location={href:'http://test.local/'};
|
||||
global.CSS={escape:value=>String(value)};
|
||||
global.requestAnimationFrame=fn=>{fn();return 1;};
|
||||
global.setTimeout=()=>1;
|
||||
global.clearTimeout=()=>{};
|
||||
|
||||
const emptyState=new FakeElement('div');
|
||||
const msgInner=new FakeElement('div');
|
||||
const messages=new FakeElement('div');
|
||||
const turn=new FakeElement('div');
|
||||
turn.id='liveAssistantTurn';
|
||||
turn.className='assistant-turn';
|
||||
turn.dataset.sessionId=process.env.ISSUE5720_STALE_LIVE_TURN==='1'?'sid-old':'sid-1';
|
||||
msgInner.appendChild(turn);
|
||||
const byId={emptyState,msgInner,messages,liveAssistantTurn:turn};
|
||||
global.$=id=>byId[id]||null;
|
||||
global._createAssistantTurn=()=>turn;
|
||||
global._assistantTurnBlocks=()=>turn;
|
||||
global._captureMessageScrollSnapshot=()=>({scrollHeight:1000});
|
||||
global._prepareLiveAnchorScrollRebuildGuard=()=>({readerAwayFromBottom:false,release:null});
|
||||
global._restoreMessageScrollSnapshotSameFrame=()=>{};
|
||||
global.scrollIfPinned=()=>{};
|
||||
global._moveLiveRunStatusToTurnEnd=()=>{};
|
||||
global._messageUserUnpinned=false;
|
||||
global._syncTransparentEventControls=()=>{};
|
||||
global._syncToolCallGroupSummary=()=>{};
|
||||
global._captureWorklogDetailDisclosureState=()=>null;
|
||||
global._restoreWorklogDetailDisclosureState=()=>{};
|
||||
global._startActivityElapsedTimer=()=>{};
|
||||
global._dedupeLiveProcessedWorklogAnchors=()=>{};
|
||||
global._toolWorklogListEl=group=>group&&group.querySelector&&group.querySelector('.tool-worklog-list')||group;
|
||||
global.ensureActivityGroup=(blocks,opts)=>{
|
||||
opts=opts||{};
|
||||
const key=String(opts.activityKey||'');
|
||||
let group=key&&blocks.querySelector&&blocks.querySelector(`.tool-worklog-group[data-tool-worklog-key="${CSS.escape(key)}"]`);
|
||||
if(group) return group;
|
||||
group=new FakeElement('div');
|
||||
group.className='tool-worklog-group tool-call-group';
|
||||
group.setAttribute('data-tool-worklog-group','1');
|
||||
group.setAttribute('data-live-tool-call-group','1');
|
||||
group.setAttribute('data-tool-worklog-key',key);
|
||||
const list=new FakeElement('div');
|
||||
list.className='tool-worklog-list';
|
||||
group.appendChild(list);
|
||||
blocks.appendChild(group);
|
||||
return group;
|
||||
};
|
||||
global.ensureLiveWorklogContainer=(blocks,opts)=>ensureActivityGroup(blocks,opts);
|
||||
global._thinkingActivityNode=text=>{
|
||||
const row=new FakeElement('div');
|
||||
row.className='agent-activity-thinking transparent-thinking-event';
|
||||
const card=new FakeElement('div');
|
||||
card.className='thinking-card open';
|
||||
const header=new FakeElement('div');
|
||||
header.className='thinking-card-header';
|
||||
const preview=new FakeElement('span');
|
||||
preview.className='transparent-event-thinking-preview';
|
||||
preview.textContent=text;
|
||||
const body=new FakeElement('div');
|
||||
body.className='thinking-card-body';
|
||||
const pre=new FakeElement('pre');
|
||||
pre.textContent=text;
|
||||
header.appendChild(preview);
|
||||
body.appendChild(pre);
|
||||
card.appendChild(header);
|
||||
card.appendChild(body);
|
||||
row.appendChild(card);
|
||||
return row;
|
||||
};
|
||||
global._decorateTransparentEventRow=(row,opts)=>{
|
||||
row.classList.add('transparent-event-row');
|
||||
row.setAttribute('data-transparent-event-row','1');
|
||||
row.setAttribute('data-event-type',String(opts&&opts.type||'activity'));
|
||||
const preview=row.querySelector('.transparent-event-thinking-preview');
|
||||
if(preview) preview.textContent=String(opts&&opts.preview||opts&&opts.text||'');
|
||||
return row;
|
||||
};
|
||||
global._rehydrateTransparentLiveRow=()=>{};
|
||||
global._sanitizeThinkingDisplayText=value=>String(value||'').trim();
|
||||
global._firstValidTimestampSeconds=()=>null;
|
||||
|
||||
eval(anchorsSrc);
|
||||
for(const name of [
|
||||
'chatActivityMode','isTransparentStream','isFinalAnswerOnlyMode','isCompactWorklogMode','isSimplifiedToolCalling',
|
||||
'_anchorSceneIsSettledSuccessfulCompression','_anchorSceneRowsForRendering',
|
||||
'_anchorSceneRowTimestampSeconds','_anchorSceneTransparentNodeForRow',
|
||||
'_transparentLiveRowKey','_transparentLiveRowsCompatible',
|
||||
'_transparentLiveRowAttributePairs','_transparentLiveRowInteractiveState',
|
||||
'_refreshTransparentThinkingLiveRow','_refreshTransparentLiveRow',
|
||||
'_thinkingMarkup','_renderThinkingInto',
|
||||
'_resetMismatchedLiveAssistantTurnForSession',
|
||||
'_liveAnchorReasoningRowForFallback','_updateLiveAnchorReasoningRowForFallback',
|
||||
'_anchorSceneNodeForRow','_anchorSceneWorklogGroup','_renderAnchorSceneRowsIntoWorklog',
|
||||
'isLiveAnchorActivitySceneOwner','_projectLiveAnchorActivitySceneForStream',
|
||||
'_renderLiveAnchorActivitySceneTransparent','renderLiveAnchorActivityScene',
|
||||
'_renderLiveAnchorActivitySceneForStream','appendThinking','updateThinking',
|
||||
]) eval(extractFunc(uiSrc,name));
|
||||
|
||||
let transparentRenderPasses=0;
|
||||
const realTransparentRender=_renderLiveAnchorActivitySceneTransparent;
|
||||
_renderLiveAnchorActivitySceneTransparent=function(...args){
|
||||
transparentRenderPasses+=1;
|
||||
return realTransparentRender(...args);
|
||||
};
|
||||
const failFirstAnchorRender=process.env.ISSUE5720_FAIL_FIRST_ANCHOR_RENDER==='1';
|
||||
const failAnchorRenderOn=Number(process.env.ISSUE5720_FAIL_ANCHOR_RENDER_ON||'0');
|
||||
let anchorRenderAttempts=0;
|
||||
window._renderLiveAnchorActivitySceneForStream=function(...args){
|
||||
anchorRenderAttempts+=1;
|
||||
if(failFirstAnchorRender&&anchorRenderAttempts===1) return false;
|
||||
if(failAnchorRenderOn&&anchorRenderAttempts===failAnchorRenderOn) return false;
|
||||
return _renderLiveAnchorActivitySceneForStream(...args);
|
||||
};
|
||||
window.isLiveAnchorActivitySceneOwner=isLiveAnchorActivitySceneOwner;
|
||||
|
||||
const S=global.S={
|
||||
session:{session_id:'sid-1',pending_started_at:1},
|
||||
messages:[{role:'user',content:'question'}],
|
||||
activeStreamId:'stream-1',
|
||||
};
|
||||
const INFLIGHT=global.INFLIGHT={};
|
||||
const LIVE_STREAMS=global.LIVE_STREAMS={};
|
||||
const _STREAM_WAS_HIDDEN=global._STREAM_WAS_HIDDEN={};
|
||||
const _STREAM_NOTIFICATION_BACKGROUND=global._STREAM_NOTIFICATION_BACKGROUND={};
|
||||
const _desktopBackgroundedForNotifications=false;
|
||||
global._bindStreamHiddenTracker=()=>{};
|
||||
global.closeOtherLiveStreams=()=>{};
|
||||
global.closeLiveStream=()=>{};
|
||||
global.resetTurnWorkspaceMutations=()=>{};
|
||||
global._resetStreamScrollFollow=()=>{};
|
||||
global._suspendSessionStreamForLiveChat=()=>{};
|
||||
global.ensureLiveWorklogShell=()=>null;
|
||||
global._extractInlineThinkingFromContent=(content,reasoning)=>({content:String(content||''),reasoning:String(reasoning||'')});
|
||||
|
||||
class FakeEventSource {
|
||||
static instances=[];
|
||||
static OPEN=1;
|
||||
static CONNECTING=0;
|
||||
constructor(){ this.listeners=Object.create(null);this.readyState=1;FakeEventSource.instances.push(this); }
|
||||
addEventListener(name,fn){ (this.listeners[name]||(this.listeners[name]=[])).push(fn); }
|
||||
emit(name,data){ for(const fn of this.listeners[name]||[]) fn({data:JSON.stringify(data),lastEventId:''}); }
|
||||
close(){ this.readyState=2; }
|
||||
}
|
||||
global.EventSource=FakeEventSource;
|
||||
|
||||
const attachStart=messagesSrc.indexOf('function attachLiveStream(');
|
||||
const attachEnd=messagesSrc.indexOf('\nfunction transcript(){',attachStart);
|
||||
if(attachStart<0||attachEnd<0) throw new Error('attachLiveStream source boundary not found');
|
||||
eval(messagesSrc.slice(attachStart,attachEnd));
|
||||
attachLiveStream('sid-1','stream-1');
|
||||
const source=FakeEventSource.instances[0];
|
||||
if(!source) throw new Error('attachLiveStream did not create EventSource');
|
||||
if(process.env.ISSUE5720_STALE_ACTIVE_STREAM==='1') S.activeStreamId='stream-new';
|
||||
|
||||
function anchorReasoningRows(){
|
||||
return turn.querySelectorAll(
|
||||
'[data-anchor-scene-row="1"][data-anchor-source-event-type="reasoning"],'+
|
||||
'[data-anchor-scene-row="1"][data-anchor-row-role="thinking"]'
|
||||
);
|
||||
}
|
||||
function fallbackReasoningRows(){
|
||||
return turn.querySelectorAll('.agent-activity-thinking[data-live-thinking="1"]')
|
||||
.filter(row=>row.getAttribute('data-anchor-scene-row')!=='1');
|
||||
}
|
||||
function reasoningText(row){
|
||||
if(!row) return null;
|
||||
const pre=row.querySelector&&row.querySelector('pre');
|
||||
return pre?pre.textContent:row.textContent;
|
||||
}
|
||||
function applyProductionAnchorEvent(sourceEventType, data){
|
||||
const registry=window._liveAnchorRegistries&&window._liveAnchorRegistries.get('stream-1');
|
||||
if(!registry) throw new Error('missing live anchor registry');
|
||||
window.HermesAssistantTurnAnchors.applyAssistantTurnAnchorSourceEvent(
|
||||
registry,
|
||||
{
|
||||
source_event_type:sourceEventType,
|
||||
run_id:'run-5720',
|
||||
stream_id:'stream-1',
|
||||
...data,
|
||||
},
|
||||
{
|
||||
session_id:'sid-1',
|
||||
stream_id:'stream-1',
|
||||
run_id:'run-5720',
|
||||
}
|
||||
);
|
||||
return registry;
|
||||
}
|
||||
function appendProductionAnchorReasoningRow(localId, text, seq){
|
||||
applyProductionAnchorEvent('reasoning',{
|
||||
local_id:localId,
|
||||
seq,
|
||||
text,
|
||||
activitySegmentSeq:seq,
|
||||
activityBurstId:0,
|
||||
});
|
||||
window._renderLiveAnchorActivitySceneForStream('stream-1','sid-1',{
|
||||
mode:process.env.ISSUE5720_ACTIVITY_MODE||'transparent_stream',
|
||||
});
|
||||
const row=anchorReasoningRows().find(candidate=>
|
||||
candidate.getAttribute('data-anchor-row-id')===`run-5720:${seq}`
|
||||
);
|
||||
if(!row) throw new Error('production anchor reasoning row not rendered');
|
||||
return row;
|
||||
}
|
||||
function appendUnkeyedLegacyReasonRow(text){
|
||||
const row=new FakeElement('div');
|
||||
row.className='wl-reason';
|
||||
row.setAttribute('data-worklog-anchor-reason','1');
|
||||
row.textContent=text;
|
||||
if(isTransparentStream()){
|
||||
turn.appendChild(row);
|
||||
}else{
|
||||
const group=ensureActivityGroup(turn,{activityKey:'live:stream-1'});
|
||||
const list=_toolWorklogListEl(group);
|
||||
(list||group||turn).appendChild(row);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
source.emit('reasoning',{text:'Plan '});
|
||||
const first=anchorReasoningRows()[0]||null;
|
||||
const firstText=reasoningText(first);
|
||||
const firstFallback=fallbackReasoningRows()[0]||null;
|
||||
const firstFallbackText=reasoningText(firstFallback);
|
||||
const passAfterFirst=transparentRenderPasses;
|
||||
source.emit('reasoning',{text:'step'});
|
||||
const second=anchorReasoningRows()[0]||null;
|
||||
const secondText=reasoningText(second);
|
||||
const anchorRowsAfterRecovery=anchorReasoningRows();
|
||||
const fallbackRowsAfterRecovery=fallbackReasoningRows();
|
||||
const registry=window._liveAnchorRegistries&&window._liveAnchorRegistries.get('stream-1');
|
||||
const anchorReasoningEvents=registry&®istry.anchor&&Array.isArray(registry.anchor.activity_events)
|
||||
? registry.anchor.activity_events.filter(event=>event&&event.source_event_type==='reasoning')
|
||||
: [];
|
||||
let multiSegmentExactFallback=null;
|
||||
if(process.env.ISSUE5720_MULTI_SEGMENT_FALLBACK==='1'){
|
||||
const firstReasoningRow=anchorReasoningRows()[0]||null;
|
||||
const firstBefore=reasoningText(firstReasoningRow);
|
||||
const secondId='live-reasoning:stream-1:2';
|
||||
const registry=applyProductionAnchorEvent('interim_assistant',{
|
||||
seq:1,
|
||||
text:'',
|
||||
});
|
||||
const secondReasoningRow=appendProductionAnchorReasoningRow(secondId,'Second draft',2);
|
||||
const secondBefore=reasoningText(secondReasoningRow);
|
||||
const legacyRow=appendUnkeyedLegacyReasonRow('Legacy unkeyed');
|
||||
appendThinking('Second updated',{
|
||||
anchorRenderFallback:true,
|
||||
sessionId:'sid-1',
|
||||
streamId:'stream-1',
|
||||
anchorReasoningLocalId:secondId,
|
||||
segmentSeq:2,
|
||||
burstId:0,
|
||||
});
|
||||
multiSegmentExactFallback={
|
||||
anchor_run_id:registry&®istry.anchor&®istry.anchor.identity&®istry.anchor.identity.run_id,
|
||||
first_row_id:firstReasoningRow&&firstReasoningRow.getAttribute('data-anchor-row-id'),
|
||||
first_before:firstBefore,
|
||||
first_after:reasoningText(firstReasoningRow),
|
||||
second_row_id:secondReasoningRow.getAttribute('data-anchor-row-id'),
|
||||
second_local_id:secondReasoningRow.getAttribute('data-anchor-local-id'),
|
||||
second_before:secondBefore,
|
||||
second_after:reasoningText(secondReasoningRow),
|
||||
legacy_after:reasoningText(legacyRow),
|
||||
live_reasoning_rows:anchorReasoningRows().length,
|
||||
fallback_rows:fallbackReasoningRows().length,
|
||||
};
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify({
|
||||
first_text:firstText,
|
||||
first_fallback_text:firstFallbackText,
|
||||
second_text:secondText,
|
||||
same_node:first===second,
|
||||
live_reasoning_rows:anchorRowsAfterRecovery.length,
|
||||
fallback_rows_after_recovery:fallbackRowsAfterRecovery.length,
|
||||
visible_reasoning_owners:anchorRowsAfterRecovery.length+fallbackRowsAfterRecovery.length,
|
||||
render_passes:[passAfterFirst,transparentRenderPasses],
|
||||
anchor_render_attempts:anchorRenderAttempts,
|
||||
anchor_reasoning_events:anchorReasoningEvents.length,
|
||||
anchor_reasoning_text:anchorReasoningEvents.length
|
||||
? String(anchorReasoningEvents[anchorReasoningEvents.length-1].payload&&anchorReasoningEvents[anchorReasoningEvents.length-1].payload.text||'')
|
||||
: null,
|
||||
inflight_reasoning_text:String(INFLIGHT['sid-1']&&INFLIGHT['sid-1'].lastReasoningText||''),
|
||||
multi_segment_exact_fallback:multiSegmentExactFallback,
|
||||
}));
|
||||
"""
|
||||
|
|
@ -938,9 +938,13 @@ def test_messages_js_supports_live_reasoning_and_tool_completion(cleanup_test_se
|
|||
"live reasoning SSE events must update the active Worklog Thinking Card text"
|
||||
assert "const liveThinkingText=_liveThinkingText();" in src, \
|
||||
"live reasoning SSE events must compute the current segment's Worklog Thinking Card text once"
|
||||
assert "if(!_upsertAnchorReasoning(liveThinkingText))" in src, \
|
||||
assert "const anchorReasoningFallback={};" in src, \
|
||||
"live reasoning SSE events must capture the active anchor id for fallback"
|
||||
assert "if(!_upsertAnchorReasoning(liveThinkingText, anchorReasoningFallback))" in src, \
|
||||
"live reasoning SSE events must prefer the anchor renderer before falling back"
|
||||
assert "_updateLiveThinkingCard(liveThinkingText)" in src, \
|
||||
assert "_updateLiveThinkingCard(liveThinkingText,{" in src and "...anchorReasoningFallback" in src, \
|
||||
"live reasoning SSE events must carry anchor identity into the fallback renderer"
|
||||
assert "anchorRenderFallback:true" in src and "sessionId:activeSid" in src and "streamId" in src, \
|
||||
"live reasoning SSE events must keep the current segment's Worklog Thinking Card as fallback"
|
||||
assert "source.addEventListener('tool_complete'" in src or 'source.addEventListener("tool_complete"' in src, \
|
||||
"messages.js must listen for live tool completion SSE events"
|
||||
|
|
|
|||
|
|
@ -1307,8 +1307,13 @@ def test_slice6_live_shadow_feed_wires_anchor_scene_for_visible_order_handoff():
|
|||
reasoning_body = _event_listener_body(src, "reasoning")
|
||||
assert "_applyToAnchor" not in reasoning_body
|
||||
assert "const liveThinkingText=_liveThinkingText();" in reasoning_body
|
||||
assert "if(!_upsertAnchorReasoning(liveThinkingText))" in reasoning_body
|
||||
assert "_updateLiveThinkingCard(liveThinkingText)" in reasoning_body
|
||||
assert "const anchorReasoningFallback={};" in reasoning_body
|
||||
assert "if(!_upsertAnchorReasoning(liveThinkingText, anchorReasoningFallback))" in reasoning_body
|
||||
assert "_updateLiveThinkingCard(liveThinkingText,{" in reasoning_body
|
||||
assert "...anchorReasoningFallback" in reasoning_body
|
||||
assert "anchorRenderFallback:true" in reasoning_body
|
||||
assert "sessionId:activeSid" in reasoning_body
|
||||
assert "streamId" in reasoning_body
|
||||
assert "function _flushReasoningToAnchor()" in src
|
||||
assert "_upsertAnchorReasoning(reasoningText" in src
|
||||
assert "`live-reasoning:${streamId}:final`" in src
|
||||
|
|
|
|||
|
|
@ -686,20 +686,23 @@ class TestToolCallGroupingStatic:
|
|||
assert "const liveThinkingText=_liveThinkingText();" in reasoning_fn, (
|
||||
"Reasoning SSE updates should cache the live thinking text before routing."
|
||||
)
|
||||
assert "_upsertAnchorReasoning(liveThinkingText)" in reasoning_fn, (
|
||||
primary_call = "_upsertAnchorReasoning(liveThinkingText, anchorReasoningFallback)"
|
||||
assert primary_call in reasoning_fn, (
|
||||
"Anchor reasoning must remain the primary renderer path."
|
||||
)
|
||||
assert reasoning_fn.index("_upsertAnchorReasoning(liveThinkingText)") < reasoning_fn.index(
|
||||
"_updateLiveThinkingCard(liveThinkingText)"
|
||||
), (
|
||||
fallback_call = "_updateLiveThinkingCard(liveThinkingText,{"
|
||||
assert reasoning_fn.index(primary_call) < reasoning_fn.index(fallback_call), (
|
||||
"The legacy thinking card should only run after anchor upsert fails."
|
||||
)
|
||||
assert "if(!_upsertAnchorReasoning(liveThinkingText)){" in reasoning_fn, (
|
||||
assert "if(!_upsertAnchorReasoning(liveThinkingText, anchorReasoningFallback)){" in reasoning_fn, (
|
||||
"The legacy thinking card should be a falsy-anchor fallback."
|
||||
)
|
||||
assert reasoning_fn.count("_updateLiveThinkingCard(liveThinkingText)") == 1, (
|
||||
assert reasoning_fn.count(fallback_call) == 1, (
|
||||
"Reasoning SSE updates should call the live thinking card only in fallback."
|
||||
)
|
||||
assert "...anchorReasoningFallback" in reasoning_fn, (
|
||||
"The fallback renderer must receive the exact Anchor reasoning identity."
|
||||
)
|
||||
assert "_updateLiveThinkingCard(" in render_live_thinking_fn, (
|
||||
"Inline parsed thinking still needs the live thinking card renderer."
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue