fix(session): survive missing FTS5 runtimes

This commit is contained in:
helix4u
2026-05-30 18:59:08 -07:00
committed by Teknium
parent ec67def5bf
commit 355af2c20f
2 changed files with 322 additions and 91 deletions
+121 -1
View File
@@ -4,7 +4,7 @@ import sqlite3
import time
import pytest
from hermes_state import SessionDB
from hermes_state import SCHEMA_SQL, SessionDB
class _NoFtsCursor(sqlite3.Cursor):
@@ -12,6 +12,8 @@ class _NoFtsCursor(sqlite3.Cursor):
def execute(self, sql, parameters=()):
probe = sql.strip()
if "USING fts5" in probe:
raise sqlite3.OperationalError("no such module: fts5")
if probe in (
"SELECT * FROM messages_fts LIMIT 0",
"SELECT * FROM messages_fts_trigram LIMIT 0",
@@ -30,6 +32,24 @@ class _NoFtsConnection(sqlite3.Connection):
return super().cursor(factory or _NoFtsCursor)
class _NoFtsExistingTableCursor(_NoFtsCursor):
"""Simulate existing FTS virtual tables under a runtime without FTS5."""
def execute(self, sql, parameters=()):
probe = sql.strip()
if probe in (
"SELECT * FROM messages_fts LIMIT 0",
"SELECT * FROM messages_fts_trigram LIMIT 0",
):
raise sqlite3.OperationalError("no such module: fts5")
return super().execute(sql, parameters)
class _NoFtsExistingTableConnection(sqlite3.Connection):
def cursor(self, factory=None):
return super().cursor(factory or _NoFtsExistingTableCursor)
@pytest.fixture()
def db(tmp_path):
"""Create a SessionDB with a temp database file."""
@@ -210,6 +230,106 @@ class TestSessionLifecycle:
finally:
db.close()
def test_existing_fts_tables_do_not_break_without_fts5(
self, tmp_path, monkeypatch
):
db_path = tmp_path / "state.db"
seeded = SessionDB(db_path=db_path)
try:
seeded.create_session(session_id="s1", source="cli")
seeded.append_message("s1", role="user", content="before runtime change")
finally:
seeded.close()
real_connect = sqlite3.connect
def connect_without_fts(*args, **kwargs):
kwargs["factory"] = _NoFtsExistingTableConnection
return real_connect(*args, **kwargs)
monkeypatch.setattr("hermes_state.sqlite3.connect", connect_without_fts)
db = SessionDB(db_path=db_path)
try:
assert db._fts_enabled is False
assert db.get_session("s1") is not None
assert len(db.get_messages("s1")) == 1
# Existing FTS triggers must be disabled too; otherwise this write
# would try to insert into an unusable FTS virtual table.
db.append_message("s1", role="assistant", content="after runtime change")
messages = db.get_messages("s1")
assert len(messages) == 2
assert messages[1]["content"] == "after runtime change"
finally:
db.close()
def test_old_schema_without_fts5_does_not_crash(self, tmp_path, monkeypatch):
db_path = tmp_path / "legacy.db"
conn = sqlite3.connect(str(db_path))
conn.executescript(SCHEMA_SQL)
conn.execute("DELETE FROM schema_version")
conn.execute("INSERT INTO schema_version (version) VALUES (?)", (9,))
conn.commit()
conn.close()
real_connect = sqlite3.connect
def connect_without_fts(*args, **kwargs):
kwargs["factory"] = _NoFtsConnection
return real_connect(*args, **kwargs)
monkeypatch.setattr("hermes_state.sqlite3.connect", connect_without_fts)
db = SessionDB(db_path=db_path)
try:
assert db._fts_enabled is False
db.create_session(session_id="s1", source="cli")
db.append_message("s1", role="user", content="legacy no fts")
assert db.get_messages("s1")[0]["content"] == "legacy no fts"
assert db.search_messages("legacy") == []
# Leave the FTS migration version in place so a future FTS-capable
# runtime can still rebuild and backfill the indexes.
row = db._conn.execute("SELECT version FROM schema_version").fetchone()
assert row["version"] == 9
finally:
db.close()
def test_fts_runtime_restores_triggers_after_no_fts_open(
self, tmp_path, monkeypatch
):
db_path = tmp_path / "state.db"
seeded = SessionDB(db_path=db_path)
try:
seeded.create_session(session_id="s1", source="cli")
seeded.append_message("s1", role="user", content="first searchable")
finally:
seeded.close()
real_connect = sqlite3.connect
def connect_without_fts(*args, **kwargs):
kwargs["factory"] = _NoFtsExistingTableConnection
return real_connect(*args, **kwargs)
monkeypatch.setattr("hermes_state.sqlite3.connect", connect_without_fts)
no_fts = SessionDB(db_path=db_path)
try:
no_fts.append_message("s1", role="assistant", content="not indexed yet")
finally:
no_fts.close()
monkeypatch.setattr("hermes_state.sqlite3.connect", real_connect)
restored = SessionDB(db_path=db_path)
try:
assert restored._fts_enabled is True
restored.append_message("s1", role="assistant", content="indexed again")
assert len(restored.search_messages("not indexed yet")) == 1
assert len(restored.search_messages("indexed")) == 2
finally:
restored.close()
# =========================================================================
# Message storage