Files
turnstone/tests/test_storage_registry.py
T
Patrick Buckley 2b58c127b1 Add pluggable storage backend (SQLite + PostgreSQL) and deployment packaging (#20)
* Add pluggable storage backend (SQLite + PostgreSQL) and deployment packaging

Database abstraction: StorageBackend protocol with 21 methods, SQLAlchemy Core
schema, SQLite backend (FTS5), PostgreSQL backend (tsvector/ILIKE), Alembic
migrations, singleton registry. memory.py reduced to thin facade. Session.py
open_db() calls replaced with generic KV methods. [database] config section
with env var support.

Deployment: Docker Compose production profile with PostgreSQL, Dockerfile with
postgres extras and migration entrypoint, Helm chart with bitnami subcharts,
Terraform AWS ECS/Fargate module with RDS + ElastiCache + ALB.

39 new storage tests (934 total). mypy strict clean. Docs and diagrams updated.

* Address PR #20 review feedback (16 items)

- Backends only call create_all() when Alembic migrations are disabled
- Helm configmap uses correct TURNSTONE_DB_BACKEND env var; DB URL
  constructed via env expansion with secret reference instead of ConfigMap
- Migration errors fail fast for PostgreSQL (only non-fatal for SQLite)
- save_memory/delete_memory wrapped in exception handling like other facade fns
- pool_size passed through from config/env to init_storage() in cli + server
- Terraform: DB URL moved to Secrets Manager, auth enabled flag set,
  optional TLS listeners with certificate_arn, Redis transit encryption on
- Docker entrypoint no longer suppresses migration output
- Diagram fixes: removed StaticPool claim, removed non-existent migration ref
- compose.yaml/README: clarified production profile requires DB env vars
2026-03-03 22:57:34 -08:00

55 lines
1.8 KiB
Python

"""Tests for the storage backend registry."""
import pytest
from turnstone.core.storage import get_storage, init_storage, reset_storage
from turnstone.core.storage._sqlite import SQLiteBackend
@pytest.fixture(autouse=True)
def _clean_registry():
"""Reset the storage registry before and after each test."""
reset_storage()
yield
reset_storage()
class TestInitStorage:
def test_sqlite_default(self, tmp_path):
backend = init_storage("sqlite", path=str(tmp_path / "test.db"), run_migrations=False)
assert isinstance(backend, SQLiteBackend)
def test_unknown_backend_raises(self):
with pytest.raises(ValueError, match="Unknown storage backend"):
init_storage("mongodb")
def test_postgresql_requires_url(self):
with pytest.raises(ValueError, match="requires a connection URL"):
init_storage("postgresql")
class TestGetStorage:
def test_auto_init(self, tmp_path, monkeypatch):
"""get_storage() auto-initializes with SQLite if not yet initialized."""
monkeypatch.chdir(tmp_path)
storage = get_storage()
assert isinstance(storage, SQLiteBackend)
def test_returns_same_instance(self, tmp_path):
init_storage("sqlite", path=str(tmp_path / "test.db"), run_migrations=False)
s1 = get_storage()
s2 = get_storage()
assert s1 is s2
class TestResetStorage:
def test_reset_clears_singleton(self, tmp_path):
init_storage("sqlite", path=str(tmp_path / "test.db"), run_migrations=False)
s1 = get_storage()
reset_storage()
# After reset, get_storage() auto-inits a new instance
monkeypatch_not_needed = True # noqa: F841
init_storage("sqlite", path=str(tmp_path / "test2.db"), run_migrations=False)
s2 = get_storage()
assert s1 is not s2