From e5a2c80c0e795ce345bf248e08216c799d8a4d71 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sat, 15 Aug 2026 01:02:48 -0700 Subject: [PATCH] fix: harden /save for test doubles, async session-store boundary, snapshot shape pin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three CI-red follow-ups on the /save rework: - cli.py save_conversation: getattr-guard _session_db/session_id so SimpleNamespace/object.__new__ test doubles (and any embedder passing a minimal stub) don't AttributeError (pitfall #17 pattern). - gateway/slash_commands.py: route through the awaited async_session_store.get_or_create_session boundary — the architecture test forbids raw session_store calls in async gateway code. - tests/cli/test_save_conversation_location.py: /save now emits the canonical export_session payload shape; the session id key is "id" (was "session_id" in the legacy snapshot format) — update the pin. The slice-8 lost-and-found failure was pre-existing on main and is fixed there by f2a30fa400 (test: derive lost-and-found synthetic width from the live schema); picked up via rebase. --- cli.py | 8 ++++++-- gateway/slash_commands.py | 2 +- tests/cli/test_save_conversation_location.py | 4 +++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/cli.py b/cli.py index 4dd7864808..c174fd8fcb 100644 --- a/cli.py +++ b/cli.py @@ -9374,10 +9374,14 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin): # Prefer the durable DB row (has metadata + tool calls); fall back to # the in-memory history for sessions that never touched the DB. + # getattr: test doubles (SimpleNamespace / object.__new__) may not + # carry _session_db or session_id. session_data = None - if self._session_db and self.session_id: + _db = getattr(self, "_session_db", None) + _sid = getattr(self, "session_id", None) + if _db and _sid: try: - session_data = self._session_db.export_session(self.session_id) + session_data = _db.export_session(_sid) except Exception: session_data = None if not session_data: diff --git a/gateway/slash_commands.py b/gateway/slash_commands.py index 338e7f1a2f..0d39589277 100644 --- a/gateway/slash_commands.py +++ b/gateway/slash_commands.py @@ -4543,7 +4543,7 @@ class GatewaySlashCommandsMixin: ) source = event.source - session_entry = self.session_store.get_or_create_session(source) + session_entry = await self.async_session_store.get_or_create_session(source) session_id = session_entry.session_id if not self._session_db: diff --git a/tests/cli/test_save_conversation_location.py b/tests/cli/test_save_conversation_location.py index 5e460fe81b..714ddabddb 100644 --- a/tests/cli/test_save_conversation_location.py +++ b/tests/cli/test_save_conversation_location.py @@ -75,7 +75,9 @@ def test_save_conversation_writes_under_hermes_home(hermes_home, tmp_path, monke payload = json.loads(files[0].read_text()) assert payload["model"] == "test-model" - assert payload["session_id"] == "20260101_120000_abc123" + # /save now emits the canonical export_session shape: the session id + # lives under "id" (was "session_id" in the legacy snapshot format). + assert payload["id"] == "20260101_120000_abc123" assert payload["messages"] == [ {"role": "user", "content": "hi"}, {"role": "assistant", "content": "hello"},