Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions tests/tui_gateway/test_session_steer_text_coerce.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""session.steer / session.redirect must tolerate non-string ``text``.

Both RPCs run inline (not in ``_LONG_HANDLERS``). A bare ``.strip()`` on a
list/int ``text`` raises AttributeError and can tear down the stdin/WS
reader loop.
"""

from __future__ import annotations

import threading

from tui_gateway import server


def _install_session(sid: str, *, support_steer: bool = True):
class _Agent:
def steer(self, text):
self.last = text
return True

def redirect(self, text):
self.last = text
return True

agent = _Agent() if support_steer else object()
ready = threading.Event()
ready.set()
server._sessions[sid] = {
"agent": agent,
"agent_ready": ready,
"session_key": sid,
"history_lock": threading.Lock(),
"last_active": 0,
}
return agent


def test_session_steer_rejects_empty_after_coerce():
sid = "steer-empty"
_install_session(sid)
try:
resp = server.dispatch(
{
"id": "1",
"method": "session.steer",
"params": {"session_id": sid, "text": None},
}
)
assert resp["error"]["code"] == 4002
finally:
server._sessions.pop(sid, None)


def test_session_steer_coerces_int_text():
sid = "steer-int"
agent = _install_session(sid)
try:
resp = server.dispatch(
{
"id": "2",
"method": "session.steer",
"params": {"session_id": sid, "text": 42},
}
)
assert "error" not in resp, resp
assert resp["result"]["status"] == "queued"
assert resp["result"]["text"] == "42"
assert agent.last == "42"
finally:
server._sessions.pop(sid, None)


def test_session_redirect_coerces_list_text_without_crash():
"""List text must not AttributeError on the inline reader path."""
sid = "redir-list"
_install_session(sid)
try:
resp = server.dispatch(
{
"id": "3",
"method": "session.redirect",
"params": {"session_id": sid, "text": ["go", "left"]},
}
)
# Coerced to "['go', 'left']" which is non-empty — either queued/rejected
# or a later agent error, but never an uncaught AttributeError.
assert isinstance(resp, dict)
assert "error" in resp or "result" in resp
finally:
server._sessions.pop(sid, None)
12 changes: 7 additions & 5 deletions tui_gateway/methods_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -1082,11 +1082,11 @@ def _(rid, params: dict) -> dict:
configured auxiliary ``task`` backend. Never mutates session history, so
prompt caching is untouched.
"""
template = (params.get("template") or "").strip() or None
template = str(params.get("template") or "").strip() or None
instructions = params.get("instructions") or ""
user_input = params.get("input") or ""
variables = params.get("variables") if isinstance(params.get("variables"), dict) else {}
task = (params.get("task") or "title_generation").strip() or "title_generation"
task = str(params.get("task") or "title_generation").strip() or "title_generation"

try:
max_tokens = int(params.get("max_tokens") or 1024)
Expand Down Expand Up @@ -1151,7 +1151,7 @@ def _(rid, params: dict) -> dict:
"session busy — wait for the current turn to finish, then retry the handoff",
)

platform_name = (params.get("platform", "") or "").strip().lower()
platform_name = str(params.get("platform") or "").strip().lower()
if not platform_name:
return _err(rid, 4023, "platform required")

Expand Down Expand Up @@ -3061,7 +3061,9 @@ def _(rid, params: dict) -> dict:
it on its next iteration. No interrupt, no new user turn, no role
alternation violation.
"""
text = (params.get("text") or "").strip()
# Inline RPC: non-string text (JSON null already falsy; list/int is truthy)
# must not AttributeError on .strip() and tear down the reader thread.
text = str(params.get("text") or "").strip()
if not text:
return _err(rid, 4002, "text is required")
session, err = _sess_nowait(params, rid)
Expand All @@ -3088,7 +3090,7 @@ def _(rid, params: dict) -> dict:
@method("session.redirect")
def _(rid, params: dict) -> dict:
"""Redirect the active model turn while preserving valid work/context."""
text = (params.get("text") or "").strip()
text = str(params.get("text") or "").strip()
if not text:
return _err(rid, 4002, "text is required")
session, err = _sess_nowait(params, rid)
Expand Down
Loading