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
24 changes: 12 additions & 12 deletions tests/gateway/test_session_list_allowed_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ def _call(limit: int | None = None):
def test_session_list_surfaces_all_user_facing_sources(monkeypatch):
"""acp / webhook / custom sources should all appear; only ``tool`` is hidden."""
rows = [
{"id": "tui-1", "source": "tui", "started_at": 9},
{"id": "tool-1", "source": "tool", "started_at": 8},
{"id": "tg-1", "source": "telegram", "started_at": 7},
{"id": "acp-1", "source": "acp", "started_at": 6},
{"id": "cli-1", "source": "cli", "started_at": 5},
{"id": "webhook-1", "source": "webhook", "started_at": 4},
{"id": "custom-1", "source": "my-custom-source", "started_at": 3},
{"id": "tui-1", "source": "tui", "started_at": 9, "message_count": 1},
{"id": "tool-1", "source": "tool", "started_at": 8, "message_count": 1},
{"id": "tg-1", "source": "telegram", "started_at": 7, "message_count": 1},
{"id": "acp-1", "source": "acp", "started_at": 6, "message_count": 1},
{"id": "cli-1", "source": "cli", "started_at": 5, "message_count": 1},
{"id": "webhook-1", "source": "webhook", "started_at": 4, "message_count": 1},
{"id": "custom-1", "source": "my-custom-source", "started_at": 3, "message_count": 1},
]
db = _StubDB(rows)
monkeypatch.setattr(server, "_get_db", lambda: db)
Expand Down Expand Up @@ -90,11 +90,11 @@ def test_session_list_respects_explicit_limit(monkeypatch):

def test_session_list_preserves_ordering_after_filter(monkeypatch):
rows = [
{"id": "newest", "source": "telegram", "started_at": 5},
{"id": "internal", "source": "tool", "started_at": 4},
{"id": "middle", "source": "tui", "started_at": 3},
{"id": "also-visible", "source": "webhook", "started_at": 2},
{"id": "oldest", "source": "discord", "started_at": 1},
{"id": "newest", "source": "telegram", "started_at": 5, "message_count": 1},
{"id": "internal", "source": "tool", "started_at": 4, "message_count": 1},
{"id": "middle", "source": "tui", "started_at": 3, "message_count": 1},
{"id": "also-visible", "source": "webhook", "started_at": 2, "message_count": 1},
{"id": "oldest", "source": "discord", "started_at": 1, "message_count": 1},
]
monkeypatch.setattr(server, "_get_db", lambda: _StubDB(rows))

Expand Down
102 changes: 96 additions & 6 deletions tests/test_tui_gateway_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3450,6 +3450,65 @@ def test_session_list_returns_clean_error_when_state_db_is_unavailable(monkeypat
assert "state.db unavailable: locking protocol" in resp["error"]["message"]


def test_session_list_includes_message_bearing_child_sessions(monkeypatch):
"""The historical picker should not hide child rows with real transcripts."""
calls = []

class _DB:
def list_sessions_rich(self, **kwargs):
calls.append(kwargs)
return [
{
"id": "parent-shell",
"source": "tui",
"title": "empty parent",
"started_at": 200,
"message_count": 0,
},
{
"id": "child-real",
"source": "tui",
"title": "real transcript",
"preview": "hello",
"started_at": 199,
"message_count": 4,
},
{
"id": "tool-noise",
"source": "tool",
"title": "subagent",
"started_at": 198,
"message_count": 3,
},
]

monkeypatch.setattr(server, "_get_db", lambda: _DB())

resp = server.handle_request(
{"id": "1", "method": "session.list", "params": {"limit": 10}}
)

assert calls == [
{
"limit": 200,
"include_children": True,
"project_compression_tips": False,
"order_by_last_active": True,
}
]
assert resp is not None
assert resp["result"]["sessions"] == [
{
"id": "child-real",
"title": "real transcript",
"preview": "hello",
"started_at": 199,
"message_count": 4,
"source": "tui",
}
]


# --------------------------------------------------------------------------
# session.delete β€” TUI resume picker `d` key
# --------------------------------------------------------------------------
Expand Down Expand Up @@ -4075,12 +4134,14 @@ def test_session_activate_switches_live_session_without_closing_siblings(monkeyp

def test_session_most_recent_returns_first_non_denied(monkeypatch):
"""Drops `tool` rows like session.list does, returns the first hit."""
calls = []

class _DB:
def list_sessions_rich(self, *, source=None, limit=200):
def list_sessions_rich(self, **kwargs):
calls.append(kwargs)
return [
{"id": "tool-1", "source": "tool", "title": "noise", "started_at": 100},
{"id": "tui-1", "source": "tui", "title": "real", "started_at": 99},
{"id": "tool-1", "source": "tool", "title": "noise", "started_at": 100, "message_count": 1},
{"id": "tui-1", "source": "tui", "title": "real", "started_at": 99, "message_count": 2},
]

monkeypatch.setattr(server, "_get_db", lambda: _DB())
Expand All @@ -4089,15 +4150,44 @@ def list_sessions_rich(self, *, source=None, limit=200):
{"id": "1", "method": "session.most_recent", "params": {}}
)

assert calls == [
{
"limit": 200,
"include_children": True,
"project_compression_tips": False,
"order_by_last_active": True,
}
]
assert resp["result"]["session_id"] == "tui-1"
assert resp["result"]["title"] == "real"
assert resp["result"]["source"] == "tui"


def test_session_most_recent_skips_empty_parent_for_child_session(monkeypatch):
"""Auto-resume should select a message-bearing child over an empty shell."""

class _DB:
def list_sessions_rich(self, **kwargs):
return [
{"id": "empty-parent", "source": "tui", "title": "shell", "started_at": 200, "message_count": 0},
{"id": "child-real", "source": "tui", "title": "real", "started_at": 199, "message_count": 5},
]

monkeypatch.setattr(server, "_get_db", lambda: _DB())

resp = server.handle_request(
{"id": "1", "method": "session.most_recent", "params": {}}
)

assert resp is not None
assert resp["result"]["session_id"] == "child-real"
assert resp["result"]["title"] == "real"


def test_session_most_recent_returns_null_when_only_tool_rows(monkeypatch):
class _DB:
def list_sessions_rich(self, *, source=None, limit=200):
return [{"id": "tool-1", "source": "tool", "started_at": 1}]
def list_sessions_rich(self, **kwargs):
return [{"id": "tool-1", "source": "tool", "started_at": 1, "message_count": 1}]

monkeypatch.setattr(server, "_get_db", lambda: _DB())

Expand All @@ -4114,7 +4204,7 @@ def test_session_most_recent_folds_db_exception_into_null_result(monkeypatch):
'no answer' (Copilot review on #17130)."""

class _BrokenDB:
def list_sessions_rich(self, *, source=None, limit=200):
def list_sessions_rich(self, **kwargs):
raise RuntimeError("db locked")

monkeypatch.setattr(server, "_get_db", lambda: _BrokenDB())
Expand Down
22 changes: 18 additions & 4 deletions tui_gateway/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2385,13 +2385,20 @@ def _(rid, params: dict) -> dict:

limit = int(params.get("limit", 200) or 200)
# Over-fetch modestly so per-source filtering doesn't leave us
# short; the compression-tip projection in ``list_sessions_rich``
# can also merge rows.
# short. Include child sessions because resumable transcripts can live
# under a parent coordination/compression shell, and keep this raw
# picker query ordered by the actual last-active session row.
fetch_limit = max(limit * 2, 200)
rows = [
s
for s in db.list_sessions_rich(source=None, limit=fetch_limit)
for s in db.list_sessions_rich(
limit=fetch_limit,
include_children=True,
project_compression_tips=False,
order_by_last_active=True,
)
if (s.get("source") or "").strip().lower() not in deny
and int(s.get("message_count") or 0) > 0
][:limit]
return _ok(
rid,
Expand Down Expand Up @@ -2437,11 +2444,18 @@ def _(rid, params: dict) -> dict:
# users (lots of recent ``tool`` rows) don't get a false
# "no eligible session" answer. ``session.list`` uses a
# similar over-fetch strategy.
rows = db.list_sessions_rich(source=None, limit=200)
rows = db.list_sessions_rich(
limit=200,
include_children=True,
project_compression_tips=False,
order_by_last_active=True,
)
for row in rows:
src = (row.get("source") or "").strip().lower()
if src in deny:
continue
if int(row.get("message_count") or 0) <= 0:
continue
return _ok(
rid,
{
Expand Down