A phone Thread is a source=phone gateway session keyed by a chat_id, but
/api/sessions returns only the opaque timestamp id + source — never chat_id
or session_key. So the Android client can't map a session to its Thread to
route a reply. The relay (our plugin, co-hosted with the gateway, same user)
reads the gateway session store read-only and surfaces session_id <-> chat_id
via GET /phone/threads, filling the gap without a fork or touching the
standard path.
- plugin/relay/session_store.py: read_phone_threads() globs ~/.hermes/state.db
+ profiles/*/state.db, mode=ro (safe alongside the running gateway/WAL),
returns [{session_id, chat_id, title}] for source=phone rows, deduped;
skips blank chat_id + older schemas without the column.
- server.py: handle_phone_threads — bearer for the app, loopback for diag
(same gate as /context/injected); registered at GET /phone/threads.
- 5 unittest cases (read, skip-no-chat_id, root+profile dedup, old schema,
missing home).
Client prefers this over its in-memory learned map; redundant once upstream
adds chat_id to /api/sessions (a later PR).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
95 lines
3.7 KiB
Python
95 lines
3.7 KiB
Python
"""Tests for the phone-Thread chat_id reader (``plugin.relay.session_store``).
|
|
|
|
The relay reads the gateway's SQLite session store read-only to surface the
|
|
per-session ``chat_id`` that ``/api/sessions`` omits, so the app can route a
|
|
reply into the right Thread. These build temp session DBs and assert
|
|
``read_phone_threads`` returns ``source=phone`` rows with their ``chat_id``,
|
|
deduped across the root + profile DBs, skipping rows with no ``chat_id`` and
|
|
tolerating an older schema (no ``chat_id`` column) or a missing home.
|
|
|
|
Run with::
|
|
|
|
python -m unittest plugin.tests.test_session_store
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sqlite3
|
|
import tempfile
|
|
import unittest
|
|
|
|
from plugin.relay.session_store import read_phone_threads
|
|
|
|
|
|
def _make_db(path: str, rows, *, with_chat_id: bool = True) -> None:
|
|
con = sqlite3.connect(path)
|
|
cur = con.cursor()
|
|
if with_chat_id:
|
|
cur.execute(
|
|
"CREATE TABLE sessions (id TEXT, source TEXT, chat_id TEXT, title TEXT)"
|
|
)
|
|
cur.executemany(
|
|
"INSERT INTO sessions (id, source, chat_id, title) VALUES (?,?,?,?)", rows
|
|
)
|
|
else:
|
|
cur.execute("CREATE TABLE sessions (id TEXT, source TEXT, title TEXT)")
|
|
cur.executemany("INSERT INTO sessions (id, source, title) VALUES (?,?,?)", rows)
|
|
con.commit()
|
|
con.close()
|
|
|
|
|
|
class ReadPhoneThreadsTests(unittest.TestCase):
|
|
def test_reads_phone_rows_with_chat_id(self) -> None:
|
|
with tempfile.TemporaryDirectory() as home:
|
|
_make_db(
|
|
os.path.join(home, "state.db"),
|
|
[
|
|
("20260629_aaa", "phone", "t-proj-1", "Project One"),
|
|
("20260629_bbb", "phone", "phone", "Home"),
|
|
("20260629_ccc", "discord", "chan-9", "Discord"),
|
|
("20260629_ddd", "tui", None, None),
|
|
],
|
|
)
|
|
byid = {t["session_id"]: t for t in read_phone_threads(home)}
|
|
self.assertEqual(set(byid), {"20260629_aaa", "20260629_bbb"})
|
|
self.assertEqual(byid["20260629_aaa"]["chat_id"], "t-proj-1")
|
|
self.assertEqual(byid["20260629_aaa"]["title"], "Project One")
|
|
self.assertEqual(byid["20260629_bbb"]["chat_id"], "phone")
|
|
|
|
def test_skips_phone_rows_without_chat_id(self) -> None:
|
|
with tempfile.TemporaryDirectory() as home:
|
|
_make_db(
|
|
os.path.join(home, "state.db"),
|
|
[("s1", "phone", None, "No chat"), ("s2", "phone", "", "Blank chat")],
|
|
)
|
|
self.assertEqual(read_phone_threads(home), [])
|
|
|
|
def test_merges_root_and_profile_dbs_dedup(self) -> None:
|
|
with tempfile.TemporaryDirectory() as home:
|
|
_make_db(os.path.join(home, "state.db"), [("s1", "phone", "c1", "Root")])
|
|
prof = os.path.join(home, "profiles", "work")
|
|
os.makedirs(prof)
|
|
_make_db(
|
|
os.path.join(prof, "state.db"),
|
|
[("s2", "phone", "c2", "Work"), ("s1", "phone", "c1", "Root dup")],
|
|
)
|
|
ids = sorted(t["session_id"] for t in read_phone_threads(home))
|
|
self.assertEqual(ids, ["s1", "s2"]) # s1 deduped (root scanned first)
|
|
|
|
def test_old_schema_without_chat_id_column_ignored(self) -> None:
|
|
with tempfile.TemporaryDirectory() as home:
|
|
_make_db(
|
|
os.path.join(home, "state.db"),
|
|
[("s1", "phone", "Old")],
|
|
with_chat_id=False,
|
|
)
|
|
self.assertEqual(read_phone_threads(home), [])
|
|
|
|
def test_missing_home_returns_empty(self) -> None:
|
|
self.assertEqual(read_phone_threads("/nonexistent/hermes/home/xyz"), [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|