Files
hermes-webui/tests/test_issue3397_transparent_stream_prose_segments.py
T
nesquena-hermes c4b9edc84e fix(#4932): wire ordered transparent path to #4927 durable snippet fallback + gate scans to transparent mode
Codex+Opus gate (both reproduced): the new mixed-content ordered render path
resolved tool snippets only from the S.messages join (_collectToolResultSnippetsByTid)
and never consulted persistedSnippetByTid (S.session.tool_calls) — so on a
cold/paginated load where the tool_result join misses (#4927 scenario) the inline
card rendered an EMPTY body, AND its tid was added to transparentOrderedToolIds,
suppressing the post-loop derived card that WOULD have recovered via #4927. Build
a transparentPersistedSnippetByTid map from S.session.tool_calls and thread it as a
5th arg to _transparentOrderedToolCall, falling back in both the live and non-live
branches. Also gate the transparent-only scans (S.toolCalls + _collectToolResultSnippetsByTid)
behind isTransparentStream() so non-transparent renders skip the O(messages×parts)
work (Opus perf finding). Redaction (#4926/#4928) was already safe (shared buildToolCard).
+ cold-load persisted-fallback regression test.
2026-06-25 21:21:28 +00:00

163 lines
6.3 KiB
Python

"""Regression tests for issue #3397 Transparent Stream settled prose segments."""
from __future__ import annotations
import json
import shutil
import subprocess
import tempfile
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
UI_JS_PATH = ROOT / "static" / "ui.js"
UI_JS = UI_JS_PATH.read_text(encoding="utf-8")
NODE = shutil.which("node")
_DRIVER_SRC = r"""
const fs = require('fs');
const src = fs.readFileSync(process.argv[2], 'utf8');
const transparent = process.argv[3] === '1';
const message = JSON.parse(process.argv[4]);
function extractFunc(name) {
const re = new RegExp('function\\s+' + name + '\\s*\\(');
const start = src.search(re);
if (start < 0) throw new Error(name + ' not found');
let i = src.indexOf('{', start);
let depth = 1; i++;
while (depth > 0 && i < src.length) {
if (src[i] === '{') depth++;
else if (src[i] === '}') depth--;
i++;
}
return src.slice(start, i);
}
function isTransparentStream() { return transparent; }
eval(extractFunc('_transparentStreamOrderedParts'));
process.stdout.write(JSON.stringify(_transparentStreamOrderedParts(message)));
"""
def _run_helper(message: dict, transparent: bool) -> object:
if NODE is None:
pytest.skip("node not on PATH")
with tempfile.NamedTemporaryFile("w", suffix=".js", encoding="utf-8", delete=False) as handle:
handle.write(_DRIVER_SRC)
script_path = Path(handle.name)
try:
result = subprocess.run(
[NODE, str(script_path), str(UI_JS_PATH), "1" if transparent else "0", json.dumps(message)],
capture_output=True,
text=True,
timeout=30,
check=False,
)
finally:
script_path.unlink(missing_ok=True)
if result.returncode != 0:
raise RuntimeError(f"node driver failed: {result.stderr}")
return json.loads(result.stdout)
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_transparent_stream_ordered_parts_preserve_text_tool_text_sequence():
message = {
"role": "assistant",
"content": [
{"type": "text", "text": "Let me search first."},
{"type": "tool_use", "id": "toolu_1", "name": "grep", "input": {"pattern": "TODO"}},
{"type": "text", "text": "Found it, here's the fix."},
],
}
ordered = _run_helper(message, transparent=True)
assert [part["kind"] for part in ordered] == ["text", "tool", "text"]
assert ordered[0]["text"] == "Let me search first."
assert ordered[1]["toolUseId"] == "toolu_1"
assert ordered[2]["text"] == "Found it, here's the fix."
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_compact_mode_keeps_ordered_parts_helper_off():
message = {
"role": "assistant",
"content": [
{"type": "text", "text": "Before"},
{"type": "tool_use", "id": "toolu_1", "name": "grep", "input": {}},
{"type": "text", "text": "After"},
],
}
assert _run_helper(message, transparent=False) is None
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_anchor_scene_messages_keep_dedicated_transparent_renderer():
message = {
"role": "assistant",
"_anchor_activity_scene": {"version": "activity_scene_v1"},
"content": [
{"type": "text", "text": "Before"},
{"type": "tool_use", "id": "toolu_1", "name": "grep", "input": {}},
{"type": "text", "text": "After"},
],
}
assert _run_helper(message, transparent=True) is None
def test_render_messages_wires_ordered_parts_into_transparent_stream_and_skips_duplicate_tool_rows():
assert "let orderedTransparentParts=_transparentStreamOrderedParts(m);" in UI_JS
assert "if(message._anchor_activity_scene) return null;" in UI_JS
assert "const partDisplayText=_transparentOrderedDisplayText(part.text);" in UI_JS
assert "const partBodyHtml=_getCachedRender(partDisplayText,false);" in UI_JS
assert "const transparentOrderedToolIds=new Set();" in UI_JS
assert "const toolCall=_transparentOrderedToolCall(part, rawIdx, transparentOrderedToolCallsByTid, transparentToolResultsByTid, transparentPersistedSnippetByTid);" in UI_JS
assert "if(part.toolUseId) transparentOrderedToolIds.add(part.toolUseId);" in UI_JS
assert "if(tid&&transparentOrderedToolIds.has(tid)) continue;" in UI_JS
def test_ordered_tool_cards_refresh_from_settled_results_even_after_live_preview():
assert "const liveSnip=(resultsByTid&&resultsByTid[tid])||(persistedByTid&&persistedByTid[tid])||'';" in UI_JS
assert "(next.snippet===undefined||next.snippet===null||next.snippet==='')" not in UI_JS
def test_ordered_tool_card_falls_back_to_persisted_snippet_on_cold_load():
# #4932 gate (#4927 parity): on a cold/paginated load the S.messages
# tool_result join misses; the ordered inline card must still recover the
# body from the persisted session.tool_calls snippet (persistedByTid),
# otherwise its card renders empty AND suppresses the post-loop derived card
# that would have recovered it.
if NODE is None:
import pytest
pytest.skip("node not on PATH")
driver = r"""
const fs=require('fs');
const src=fs.readFileSync(process.argv[2],'utf8');
function grab(n){const re=new RegExp('function '+n+'\\([^]*?\\n}','m');const m=src.match(re);if(!m)throw new Error('not found '+n);return m[0];}
global._cliPatchSnippetFromArgs=()=> '';
global._cliToolCardSnippet=(a,b)=> a||b||'';
global._cliToolCardHasDiffSnippet=()=> false;
global._toolArgsSnapshot=(a)=> a||{};
eval(grab('_transparentOrderedToolCall'));
const part={kind:'tool', toolUseId:'call_x', name:'terminal', input:{command:'ls'}};
// resultsByTid MISSES (cold load), persistedByTid HAS the snippet.
const out=_transparentOrderedToolCall(part, 3, new Map(), {}, {call_x:'PERSISTED_OUTPUT_42'});
process.stdout.write(JSON.stringify({snippet: out.snippet}));
"""
import tempfile, os
with tempfile.NamedTemporaryFile("w", suffix=".js", delete=False) as fh:
fh.write(driver)
path = fh.name
try:
result = subprocess.run([NODE, path, str(UI_JS_PATH)], capture_output=True, text=True, timeout=30)
finally:
os.unlink(path)
assert result.returncode == 0, result.stderr
import json as _json
assert _json.loads(result.stdout)["snippet"] == "PERSISTED_OUTPUT_42", result.stdout