From 3aa86717b60c82065fc1cb8623dc2f7a3762d48c Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 27 Apr 2026 11:27:51 -0500 Subject: [PATCH] fix(tui-gateway): harden pending-title retry and user errors Retry persisting queued titles on session.title reads and map title validation failures to a user-facing 4022 code instead of generic 5007. --- tests/test_tui_gateway_server.py | 56 ++++++++++++++++++++++++++++++++ tui_gateway/server.py | 18 +++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index ff6a022b52..76e0bb4f57 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -375,6 +375,34 @@ def test_session_title_get_falls_back_to_pending_when_db_read_throws(monkeypatch server._sessions.pop("sid", None) +def test_session_title_get_retries_persist_for_pending_title(monkeypatch): + class _FakeDB: + def __init__(self): + self.title = "" + + def get_session_title(self, _key): + return self.title + + def set_session_title(self, _key, title): + self.title = title + return True + + def get_session(self, _key): + return {"id": _key, "title": self.title} + + db = _FakeDB() + server._sessions["sid"] = _session(pending_title="queued title") + monkeypatch.setattr(server, "_get_db", lambda: db) + try: + resp = server.handle_request( + {"id": "1", "method": "session.title", "params": {"session_id": "sid"}} + ) + assert resp["result"]["title"] == "queued title" + assert server._sessions["sid"]["pending_title"] is None + finally: + server._sessions.pop("sid", None) + + def test_session_title_rejects_empty_title_with_specific_error_code(monkeypatch): class _FakeDB: def get_session_title(self, _key): @@ -396,6 +424,34 @@ def test_session_title_rejects_empty_title_with_specific_error_code(monkeypatch) server._sessions.pop("sid", None) +def test_session_title_set_maps_valueerror_to_user_error(monkeypatch): + class _FakeDB: + def get_session_title(self, _key): + return "" + + def get_session(self, _key): + return {"id": _key} + + def set_session_title(self, _key, _title): + raise ValueError("Title already in use") + + server._sessions["sid"] = _session() + monkeypatch.setattr(server, "_get_db", lambda: _FakeDB()) + try: + resp = server.handle_request( + { + "id": "1", + "method": "session.title", + "params": {"session_id": "sid", "title": "dup"}, + } + ) + assert "error" in resp + assert resp["error"]["code"] == 4022 + assert "already in use" in resp["error"]["message"] + finally: + server._sessions.pop("sid", None) + + def test_session_title_set_errors_when_row_lookup_fails_after_noop(monkeypatch): class _FakeDB: def get_session_title(self, _key): diff --git a/tui_gateway/server.py b/tui_gateway/server.py index f47b21f0f0..acc91c3baa 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -1765,7 +1765,21 @@ def _(rid, params: dict) -> dict: if "title" not in params: fallback = session.get("pending_title") or "" try: - resolved_title = db.get_session_title(key) or fallback + resolved_title = db.get_session_title(key) or "" + if not resolved_title and fallback: + if db.set_session_title(key, fallback): + session["pending_title"] = None + resolved_title = fallback + else: + existing_row = db.get_session(key) + existing_title = ((existing_row or {}).get("title") or "").strip() + if existing_title == fallback: + session["pending_title"] = None + resolved_title = fallback + else: + resolved_title = fallback + elif resolved_title: + session["pending_title"] = None except Exception: resolved_title = fallback return _ok( @@ -1796,6 +1810,8 @@ def _(rid, params: dict) -> dict: ) session["pending_title"] = title return _ok(rid, {"pending": True, "title": title}) + except ValueError as e: + return _err(rid, 4022, str(e)) except Exception as e: return _err(rid, 5007, str(e))