fix(bot-mode): Bot Chat identity + one-shot DM transport apply the silence rule; trim tests
Follow-up to the salvaged #110786 commit: - `_is_bot_mode_session` mirrors the system-prompt gate (`agent._session_title_hint` first, then the live DB title) instead of reading `pending_title`/`title` off the session dict: `pending_title` is cleared after turn 1 and the record never carries `title`, so the contributor's gate matched only the very first Bot Chat turn. - `tools/bot_mode_dm.py::_run_local_turn` (the `hermes -p X chat -c "Bot Chat" -Q` transport behind `message_agent` when no live owner holds the target) re-emits "" for a successful bare marker — the third delivery path of the same class. - Tests trimmed to one invariant per surface (live completion, relay RPC, one-shot transport), each proven red on origin/main sources. - Bot Mode docs gain a "Staying silent" line pointing at the shared token list.
This commit is contained in:
@@ -1,56 +1,44 @@
|
||||
"""Regression coverage for Bot Mode's shared completed-reply delivery boundary."""
|
||||
"""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 pytest
|
||||
|
||||
import tui_gateway.server as srv
|
||||
from tui_gateway.prompt_turn import _bot_mode_delivery_text, _is_bot_mode_session
|
||||
|
||||
|
||||
@pytest.mark.parametrize("response", [
|
||||
"NO_REPLY", " [silent] ", "silent", "no reply", "*NO_REPLY*",
|
||||
])
|
||||
def test_bot_mode_delivery_hides_successful_canonical_silence_markers(response):
|
||||
assert _bot_mode_delivery_text(response, successful=True) == ""
|
||||
|
||||
|
||||
@pytest.mark.parametrize("response", [
|
||||
"The NO_REPLY marker means do not answer.",
|
||||
"[SILENT] is mentioned here, but this is a real answer.",
|
||||
])
|
||||
def test_bot_mode_delivery_keeps_substantive_marker_mentions(response):
|
||||
assert _bot_mode_delivery_text(response, successful=True) == response
|
||||
|
||||
|
||||
def test_bot_mode_delivery_keeps_failed_marker_response_visible():
|
||||
assert _bot_mode_delivery_text("NO_REPLY", successful=False) == "NO_REPLY"
|
||||
|
||||
|
||||
def test_only_canonical_bot_chat_sessions_use_the_live_delivery_boundary():
|
||||
assert _is_bot_mode_session({"pending_title": "Bot Chat"})
|
||||
assert _is_bot_mode_session({"title": "Bot Chat"})
|
||||
assert not _is_bot_mode_session({"pending_title": "Scratch"})
|
||||
|
||||
|
||||
def test_live_bot_chat_completion_suppresses_markers_but_failed_turns_fail_open(monkeypatch):
|
||||
"""The prompt.submit completion path applies the shared delivery boundary."""
|
||||
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": "Bot Chat", "history_lock": contextlib.nullcontext()}
|
||||
turn = SimpleNamespace(
|
||||
result={"final_response": "NO_REPLY"}, agent=object(), terminal_callback=None,
|
||||
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",
|
||||
)
|
||||
payload, _, status = srv._complete_turn_payload(session, turn, None, 80)
|
||||
assert status == "complete"
|
||||
assert payload["text"] == ""
|
||||
|
||||
turn.result = {"final_response": "NO_REPLY", "error": "provider failed", "failed": True}
|
||||
payload, _, status = srv._complete_turn_payload(session, turn, None, 80)
|
||||
assert status == "error"
|
||||
|
||||
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"
|
||||
|
||||
@@ -108,38 +108,19 @@ def test_deliver_requires_params(home):
|
||||
assert "error" in err
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reply", [
|
||||
"NO_REPLY", " [silent] ", "silent", "no reply", "*NO_REPLY*",
|
||||
])
|
||||
def test_deliver_suppresses_successful_silence_markers(home, monkeypatch, reply):
|
||||
"""A successful subprocess relay never returns a bare silence marker to Bot Mode."""
|
||||
def test_deliver_relays_empty_reply_for_a_bare_silence_marker(home, monkeypatch):
|
||||
"""#110782: the subprocess transport applies the gateway's silence rule — a bare marker
|
||||
relays as "", prose that merely mentions one is relayed verbatim."""
|
||||
class _Proc:
|
||||
returncode = 0
|
||||
stdout = reply
|
||||
stderr = ""
|
||||
returncode, stderr = 0, ""
|
||||
stdout = " *NO_REPLY* "
|
||||
|
||||
monkeypatch.setattr("subprocess.run", lambda *_args, **_kwargs: _Proc())
|
||||
monkeypatch.setattr("subprocess.run", lambda *_a, **_k: _Proc())
|
||||
assert _result(srv._methods["bot_relay.deliver"](1, {"profile": "ops", "message": "ping"}))["reply"] == ""
|
||||
|
||||
_Proc.stdout = "The NO_REPLY marker means do not answer."
|
||||
out = _result(srv._methods["bot_relay.deliver"](1, {"profile": "ops", "message": "ping"}))
|
||||
|
||||
assert out["reply"] == ""
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reply", [
|
||||
"The NO_REPLY marker means do not answer.",
|
||||
"[SILENT] is mentioned here, but this is a real answer.",
|
||||
])
|
||||
def test_deliver_keeps_substantive_marker_mentions(home, monkeypatch, reply):
|
||||
class _Proc:
|
||||
returncode = 0
|
||||
stdout = reply
|
||||
stderr = ""
|
||||
|
||||
monkeypatch.setattr("subprocess.run", lambda *_args, **_kwargs: _Proc())
|
||||
|
||||
out = _result(srv._methods["bot_relay.deliver"](1, {"profile": "ops", "message": "ping"}))
|
||||
|
||||
assert out["reply"] == reply
|
||||
assert out["reply"] == _Proc.stdout.strip()
|
||||
|
||||
|
||||
def test_deliver_lands_in_live_bot_chat_instead_of_subprocess(home, monkeypatch):
|
||||
|
||||
Reference in New Issue
Block a user