Files
hermes-agent/tests/tui_gateway/test_bot_mode_silence_delivery.py
T
teknium1 04e3e89d49 fix(tui): hold back Bot Chat stream deltas that could still be a silence marker
The completion-time emptying only takes effect when nothing streamed: the
Desktop keeps already-streamed text when message.complete carries an empty
text (#95514), so a bare NO_REPLY that arrived via message.delta still
rendered in the Bot Chat bubble. Mirror gateway.stream_consumer: for Bot
Chat sessions withhold deltas while the accumulated buffer satisfies
is_partial_silence_marker; flush the held text verbatim once the buffer
diverges from every marker, so a marker never streams and prose that merely
starts like one is delivered intact.

Review finding: streamed NO_REPLY still visible in Desktop Bot Chat because message.delta carried the marker verbatim.
2026-09-15 06:31:31 -07:00

72 lines
3.7 KiB
Python

"""Bot Mode never renders or relays a bare intentional-silence marker (#110782).
Silence is a delivery decision shared with the gateway (``gateway/response_filters``):
the assistant row stays persisted, only the outbound text is emptied; failed turns and
prose that merely mentions a marker are delivered unchanged.
"""
import contextlib
from types import SimpleNamespace
import tui_gateway.server as srv
def _turn(result):
return SimpleNamespace(
result=result, agent=SimpleNamespace(_session_title_hint="Bot Chat"), terminal_callback=None,
receipt_committed=True, receipt_attempted=False, marker_key="", error_retained=False,
error_detail="", prompt_text="ping",
)
def test_live_bot_chat_completion_empties_marker_only_for_successful_turns(monkeypatch):
monkeypatch.setattr(srv, "_get_usage", lambda _agent: {})
monkeypatch.setattr(srv, "render_message", lambda _text, _cols: None)
monkeypatch.setattr(srv, "_clear_inflight_turn", lambda _session: None)
session = {"pending_title": None, "session_key": "k", "history_lock": contextlib.nullcontext(),
"agent": SimpleNamespace(_session_title_hint="Bot Chat")}
payload, _, status = srv._complete_turn_payload(session, _turn({"final_response": " *NO_REPLY* "}), None, 80)
assert (status, payload["text"]) == ("complete", "")
prose = "[SILENT] is mentioned here, but this is a real answer."
payload, _, _ = srv._complete_turn_payload(session, _turn({"final_response": prose}), None, 80)
assert payload["text"] == prose
failed = {"final_response": "NO_REPLY", "error": "provider failed", "failed": True}
payload, _, status = srv._complete_turn_payload(session, _turn(failed), None, 80)
assert (status, payload["text"]) == ("error", "NO_REPLY")
# A plain (non-Bot-Chat) desktop session keeps the marker: the gate is the canonical title.
session["agent"] = SimpleNamespace(_session_title_hint="Scratch")
monkeypatch.setattr(srv, "_session_live_title", lambda _s, _k: "Scratch")
payload, _, _ = srv._complete_turn_payload(session, _turn({"final_response": "NO_REPLY"}), None, 80)
assert payload["text"] == "NO_REPLY"
def test_live_bot_chat_stream_holds_back_partial_silence_marker(monkeypatch):
"""Mirror of stream_consumer's hold-back: a marker never reaches message.delta, prose that
diverges from every marker is flushed intact once it diverges."""
events = []
monkeypatch.setattr(srv, "_emit", lambda event, _sid, payload=None: events.append((event, payload)))
monkeypatch.setattr(srv, "_load_interim_assistant_messages", lambda: False)
monkeypatch.setattr(srv, "_start_usage_ticker", lambda _sid, _agent: (SimpleNamespace(set=lambda: None), SimpleNamespace(join=lambda: None)))
def _run(final, chunks):
events.clear()
def run_conversation(_message, **kwargs):
for chunk in chunks:
kwargs["stream_callback"](chunk)
return {"final_response": final}
agent = SimpleNamespace(_session_title_hint="Bot Chat", run_conversation=run_conversation)
session = {"pending_title": None, "session_key": "k", "history_lock": contextlib.nullcontext(), "agent": agent}
st = srv._TurnRun(agent=agent, one_turn_restore=None, terminal_callback=None, receipt_committed=True)
srv._invoke_agent("sid", session, st, "ping", "ping", None, [], None, None)
return [p["text"] for e, p in events if e == "message.delta"], (session.get("inflight_turn") or {}).get("assistant", "")
assert _run("NO_REPLY", ["NO_", "REPLY"]) == ([], "")
assert _run("NO way, here is the answer.", ["NO", " way,", " here is the answer."]) == (
["NO way,", " here is the answer."], "NO way, here is the answer.")