* stage v0.51.347: render/stream cluster (#3892 #3898 #3885 #3882 #3868) + 2 Opus SHOULD-FIX * stage v0.51.347: trim #3885 error-guard comment to fit diagnostic-test window * Stamp v0.51.347 — Release LK (streaming & render reliability cluster) * Remove stray uv.lock accidentally staged (not part of any cluster PR) --------- Co-authored-by: nesquena-hermes <[email protected]>
131 lines
5.8 KiB
Python
131 lines
5.8 KiB
Python
"""Regression tests for #3875 — blank transcript from accumulated empty recovery anchors.
|
|
|
|
A session that was interrupted-and-recovered many times accumulated thousands of
|
|
empty-content assistant rows tagged ``_recovered_from_run_journal``. Each was an
|
|
"anchor" created by ``_append_journaled_partial_output`` / ``ensure_assistant_anchor``
|
|
to host recovered tool cards for a tool-first stream (one that emitted tools before
|
|
any visible text). Because a tool-first stream has no text to dedup on, the read-side
|
|
lazy-retry path re-created a fresh empty anchor on every retry, and every distinct
|
|
interrupted stream added its own — so the session filled with empty rows. Combined
|
|
with the render path (which dropped empty-content reasoning-only rows), the transcript
|
|
painted blank (only date separators).
|
|
|
|
This file covers the DATA side: the anchor-dedup guard so recovery reuses a single
|
|
empty anchor per stream instead of appending an unbounded run of them. The render-side
|
|
fix (surface the message's `reasoning` field so an empty-content turn never paints
|
|
blank) is covered by tests/test_issue3875_blank_transcript_failsafe.py.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
import api.profiles as profiles
|
|
from api.models import Session, _append_journaled_partial_output
|
|
from api.run_journal import append_run_event
|
|
|
|
|
|
@pytest.fixture
|
|
def hermes_home(tmp_path, monkeypatch):
|
|
# Mirror tests/test_session_lost_response_regression.py — isolate HERMES_HOME
|
|
# so Session.save() + run-journal writes land in a throwaway sandbox.
|
|
home = tmp_path / "hermes_home"
|
|
home.mkdir()
|
|
(home / "sessions").mkdir()
|
|
monkeypatch.setenv("HERMES_HOME", str(home))
|
|
monkeypatch.setattr(profiles, "_DEFAULT_HERMES_HOME", home)
|
|
return home
|
|
|
|
|
|
def _tool_first_journal(session_id: str, stream_id: str) -> None:
|
|
"""Write a run journal for a stream that emitted a tool BEFORE any text —
|
|
the shape that forces an empty assistant anchor on recovery."""
|
|
append_run_event(session_id, stream_id, "tool", {"name": "search_files", "preview": "q=x"})
|
|
append_run_event(session_id, stream_id, "tool_complete", {"name": "search_files", "preview": "done"})
|
|
|
|
|
|
def test_tool_first_recovery_creates_single_empty_anchor(hermes_home):
|
|
sid = "issue3875_anchor"
|
|
stream_id = "stream-A"
|
|
_tool_first_journal(sid, stream_id)
|
|
s = Session(session_id=sid, title="repro", messages=[{"role": "user", "content": "go"}])
|
|
|
|
# First recovery: one empty anchor is created to host the recovered tool card.
|
|
assert _append_journaled_partial_output(s, stream_id, dedupe_existing=True) is True
|
|
anchors = [
|
|
m for m in s.messages
|
|
if isinstance(m, dict)
|
|
and m.get("_recovered_from_run_journal")
|
|
and m.get("role") == "assistant"
|
|
and not str(m.get("content") or "").strip()
|
|
]
|
|
assert len(anchors) == 1, "first recovery should create exactly one empty anchor"
|
|
|
|
|
|
def test_repeated_recovery_does_not_accumulate_empty_anchors(hermes_home):
|
|
"""The #3875 root cause: re-running recovery for the SAME stream must reuse the
|
|
existing empty anchor, not pile up a fresh one each time."""
|
|
sid = "issue3875_repeat"
|
|
stream_id = "stream-B"
|
|
_tool_first_journal(sid, stream_id)
|
|
s = Session(session_id=sid, title="repro", messages=[{"role": "user", "content": "go"}])
|
|
|
|
for _ in range(20):
|
|
_append_journaled_partial_output(s, stream_id, dedupe_existing=True)
|
|
|
|
anchors = [
|
|
m for m in s.messages
|
|
if isinstance(m, dict)
|
|
and m.get("_recovered_from_run_journal")
|
|
and m.get("role") == "assistant"
|
|
and not str(m.get("content") or "").strip()
|
|
]
|
|
assert len(anchors) == 1, (
|
|
f"repeated recovery for one stream must reuse a single empty anchor, "
|
|
f"got {len(anchors)} (the unbounded-accumulation bug)"
|
|
)
|
|
|
|
|
|
def test_distinct_streams_get_distinct_anchors(hermes_home):
|
|
"""Dedup is scoped per stream — two genuinely different interrupted streams
|
|
each keep their own anchor (we are not collapsing unrelated turns)."""
|
|
sid = "issue3875_multi"
|
|
s = Session(session_id=sid, title="repro", messages=[{"role": "user", "content": "go"}])
|
|
for stream_id in ("stream-X", "stream-Y", "stream-Z"):
|
|
_tool_first_journal(sid, stream_id)
|
|
# run each twice to prove per-stream reuse on top of per-stream distinctness
|
|
_append_journaled_partial_output(s, stream_id, dedupe_existing=True)
|
|
_append_journaled_partial_output(s, stream_id, dedupe_existing=True)
|
|
|
|
anchors = [
|
|
m for m in s.messages
|
|
if isinstance(m, dict)
|
|
and m.get("_recovered_from_run_journal")
|
|
and m.get("role") == "assistant"
|
|
and not str(m.get("content") or "").strip()
|
|
]
|
|
stream_ids = {m.get("_recovered_stream_id") for m in anchors}
|
|
assert len(anchors) == 3, f"expected one anchor per distinct stream, got {len(anchors)}"
|
|
assert stream_ids == {"stream-X", "stream-Y", "stream-Z"}
|
|
|
|
|
|
def test_text_bearing_recovery_still_appends_real_content(hermes_home):
|
|
"""The dedup guard must not suppress recovery of genuine visible text — a
|
|
stream that emitted tokens still produces a content-bearing recovered row."""
|
|
sid = "issue3875_text"
|
|
stream_id = "stream-T"
|
|
append_run_event(sid, stream_id, "token", {"text": "Hello "})
|
|
append_run_event(sid, stream_id, "token", {"text": "world"})
|
|
append_run_event(sid, stream_id, "done", {})
|
|
s = Session(session_id=sid, title="repro", messages=[{"role": "user", "content": "go"}])
|
|
|
|
assert _append_journaled_partial_output(s, stream_id, dedupe_existing=True) is True
|
|
recovered_text = [
|
|
m for m in s.messages
|
|
if isinstance(m, dict)
|
|
and m.get("_recovered_from_run_journal")
|
|
and str(m.get("content") or "").strip()
|
|
]
|
|
assert any("Hello world" in m["content"] for m in recovered_text), (
|
|
"token-bearing recovery must still append the visible assistant text"
|
|
)
|