diff --git a/tests/gateway/test_session_list_all_sources.py b/tests/gateway/test_session_list_all_sources.py deleted file mode 100644 index f354c6029..000000000 --- a/tests/gateway/test_session_list_all_sources.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Regression test for the TUI gateway's ``session.list`` handler. - -Reported during the TUI v2 blitz retest: the ``/resume`` modal inside a -TUI session only surfaced ``tui``/``cli`` rows — telegram/discord/whatsapp -sessions stayed hidden even though the user could still paste the id -directly into ``hermes --tui --resume `` and get a working session. - -The fix removes the adapter-kind filter so every session the DB surfaces -appears in the picker, sorted by ``started_at`` like before. -""" - -from __future__ import annotations - -import types - -from tui_gateway import server - - -class _StubDB: - def __init__(self, rows): - self.rows = rows - self.calls: list[dict] = [] - - def list_sessions_rich(self, **kwargs): - self.calls.append(kwargs) - return list(self.rows) - - -def _call(limit: int = 20): - return server.handle_request({ - "id": "1", - "method": "session.list", - "params": {"limit": limit}, - }) - - -def test_session_list_does_not_filter_by_source(monkeypatch): - rows = [ - {"id": "tui-1", "source": "tui", "title": "a", "preview": "", "started_at": 3, "message_count": 1}, - {"id": "tg-1", "source": "telegram", "title": "b", "preview": "", "started_at": 2, "message_count": 1}, - {"id": "cli-1", "source": "cli", "title": "c", "preview": "", "started_at": 1, "message_count": 1}, - ] - db = _StubDB(rows) - monkeypatch.setattr(server, "_get_db", lambda: db) - - resp = _call(limit=10) - - assert "result" in resp, resp - assert len(db.calls) == 1 - assert db.calls[0].get("source") is None, db.calls[0] - assert db.calls[0].get("limit") == 10 - - kinds = [s["source"] for s in resp["result"]["sessions"]] - assert "telegram" in kinds and "tui" in kinds and "cli" in kinds, kinds - - -def test_session_list_preserves_ordering(monkeypatch): - rows = [ - {"id": "newest", "source": "telegram", "title": "", "preview": "", "started_at": 5, "message_count": 1}, - {"id": "middle", "source": "tui", "title": "", "preview": "", "started_at": 3, "message_count": 1}, - {"id": "oldest", "source": "discord", "title": "", "preview": "", "started_at": 1, "message_count": 1}, - ] - monkeypatch.setattr(server, "_get_db", lambda: _StubDB(rows)) - - resp = _call() - ids = [s["id"] for s in resp["result"]["sessions"]] - - assert ids == ["newest", "middle", "oldest"] - - -def test_session_list_surfaces_missing_fields_as_empty(monkeypatch): - rows = [{"id": "bare", "source": "whatsapp"}] - monkeypatch.setattr(server, "_get_db", lambda: _StubDB(rows)) - - sess = _call()["result"]["sessions"][0] - - assert sess == { - "id": "bare", - "title": "", - "preview": "", - "started_at": 0, - "message_count": 0, - "source": "whatsapp", - } diff --git a/tests/gateway/test_session_list_allowed_sources.py b/tests/gateway/test_session_list_allowed_sources.py new file mode 100644 index 000000000..bd6791ff4 --- /dev/null +++ b/tests/gateway/test_session_list_allowed_sources.py @@ -0,0 +1,76 @@ +"""Regression tests for the TUI gateway's ``session.list`` handler. + +Reported during TUI v2 blitz retest: the ``/resume`` modal inside a TUI +session only surfaced ``tui``/``cli`` rows, hiding telegram sessions users +could still resume directly via ``hermes --tui --resume ``. + +The fix widens the picker to a curated allowlist of user-facing sources +(tui/cli + chat adapters) while still filtering internal/system sources. +""" + +from __future__ import annotations + +from tui_gateway import server + + +class _StubDB: + def __init__(self, rows): + self.rows = rows + self.calls: list[dict] = [] + + def list_sessions_rich(self, **kwargs): + self.calls.append(kwargs) + return list(self.rows) + + +def _call(limit: int = 20): + return server.handle_request({ + "id": "1", + "method": "session.list", + "params": {"limit": limit}, + }) + + +def test_session_list_includes_telegram_but_filters_internal_sources(monkeypatch): + 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}, + ] + db = _StubDB(rows) + monkeypatch.setattr(server, "_get_db", lambda: db) + + resp = _call(limit=10) + sessions = resp["result"]["sessions"] + ids = [s["id"] for s in sessions] + + assert "tg-1" in ids and "tui-1" in ids and "cli-1" in ids, ids + assert "tool-1" not in ids and "acp-1" not in ids, ids + + +def test_session_list_fetches_wider_window_before_filtering(monkeypatch): + db = _StubDB([{"id": "x", "source": "cli", "started_at": 1}]) + monkeypatch.setattr(server, "_get_db", lambda: db) + + _call(limit=10) + + assert len(db.calls) == 1 + assert db.calls[0].get("source") is None, db.calls[0] + assert db.calls[0].get("limit") == 100, db.calls[0] + + +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": "oldest", "source": "discord", "started_at": 1}, + ] + monkeypatch.setattr(server, "_get_db", lambda: _StubDB(rows)) + + resp = _call() + ids = [s["id"] for s in resp["result"]["sessions"]] + + assert ids == ["newest", "middle", "oldest"] diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 36a7bc6dd..1397e9b04 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -1231,12 +1231,34 @@ def _(rid, params: dict) -> dict: @method("session.list") def _(rid, params: dict) -> dict: try: - # Show sessions from every adapter — users resume telegram/discord/etc - # sessions by pasting the id directly, so the picker should surface them - # too. Children (subagents/compression runs) stay filtered out via the - # hermes_state default. - limit = params.get("limit", 20) - rows = _get_db().list_sessions_rich(source=None, limit=limit) + # Resume picker should include human conversation surfaces beyond + # tui/cli (notably telegram from blitz row #7), but avoid internal + # sources that clutter the modal (tool/acp/etc). + allow = frozenset( + { + "cli", + "tui", + "telegram", + "discord", + "slack", + "whatsapp", + "wecom", + "weixin", + "feishu", + "signal", + "mattermost", + "matrix", + "qq", + } + ) + + limit = int(params.get("limit", 20) or 20) + fetch_limit = max(limit * 5, 100) + rows = [ + s + for s in _get_db().list_sessions_rich(source=None, limit=fetch_limit) + if (s.get("source") or "").strip().lower() in allow + ][:limit] return _ok(rid, {"sessions": [ {"id": s["id"], "title": s.get("title") or "", "preview": s.get("preview") or "", "started_at": s.get("started_at") or 0, "message_count": s.get("message_count") or 0,