md-first memory extraction framework for AI agents. Markdown is the single source of truth; SQLite holds state and LanceDB provides the rebuildable vector + BM25 + scalar index. The codebase follows a single-direction DDD layering (entrypoints -> service -> memory -> infra, with component / core / config cross-cutting) enforced by import-linter. Engineering surface: - Coding conventions in .claude/rules/ (path-scoped) and workflows in .claude/skills/ (/commit, /new-branch, /pr). - GitHub Actions CI runs make lint + test + integration; pre-commit mirrors the gates locally (ruff, hygiene hooks, gitlint commit-msg). - Commit messages follow Conventional Commits, enforced by gitlint. - make lint also enforces datetime two-zone discipline and OpenAPI drift.
145 lines
4.5 KiB
Python
145 lines
4.5 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import aiosqlite
|
|
import pytest
|
|
|
|
from everos.infra.ome._stores.storage import OMEStorage
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_storage_creates_db_and_tables(tmp_path: Path) -> None:
|
|
db = tmp_path / "ome.db"
|
|
storage = OMEStorage(db_path=db)
|
|
await storage.init()
|
|
|
|
assert db.exists()
|
|
async with aiosqlite.connect(db) as conn:
|
|
cur = await conn.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
|
|
)
|
|
names = {row[0] for row in await cur.fetchall()}
|
|
assert {"counter_store", "idle_store", "run_record"}.issubset(names)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_storage_applies_pragmas(tmp_path: Path) -> None:
|
|
db = tmp_path / "ome.db"
|
|
storage = OMEStorage(db_path=db)
|
|
await storage.init()
|
|
|
|
async with aiosqlite.connect(db) as conn:
|
|
cur = await conn.execute("PRAGMA journal_mode")
|
|
mode = (await cur.fetchone())[0]
|
|
assert mode == "wal"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_storage_init_is_idempotent(tmp_path: Path) -> None:
|
|
db = tmp_path / "ome.db"
|
|
storage = OMEStorage(db_path=db)
|
|
await storage.init()
|
|
await storage.init() # second call must not raise
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_storage_creates_parent_dir(tmp_path: Path) -> None:
|
|
db = tmp_path / "nested" / "dir" / "ome.db"
|
|
storage = OMEStorage(db_path=db)
|
|
await storage.init()
|
|
assert db.exists()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_storage_connect_applies_per_connection_pragmas(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""``synchronous`` and ``cache_size`` are per-connection PRAGMAs:
|
|
SQLite resets them to defaults on every new connection. The
|
|
``OMEStorage.connect`` wrapper must re-apply them or the module
|
|
docstring's promise is silently broken.
|
|
"""
|
|
db = tmp_path / "ome.db"
|
|
storage = OMEStorage(db_path=db)
|
|
await storage.init()
|
|
|
|
async with storage.connect() as conn:
|
|
sync_row = await (await conn.execute("PRAGMA synchronous")).fetchone()
|
|
cache_row = await (await conn.execute("PRAGMA cache_size")).fetchone()
|
|
busy_row = await (await conn.execute("PRAGMA busy_timeout")).fetchone()
|
|
|
|
# synchronous: 0=OFF, 1=NORMAL, 2=FULL, 3=EXTRA
|
|
assert sync_row[0] == 1
|
|
# cache_size: negative value is "kibibytes of memory"
|
|
assert cache_row[0] == -65536
|
|
# busy_timeout: ms before SQLITE_BUSY is raised on contended writes
|
|
assert busy_row[0] == 5000
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_storage_raw_aiosqlite_connect_does_not_carry_per_conn_pragmas(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Sanity check that documents why :meth:`OMEStorage.connect` exists:
|
|
opening the same db with raw ``aiosqlite.connect`` yields a connection
|
|
where ``synchronous`` is at SQLite's default (FULL=2), not NORMAL.
|
|
"""
|
|
db = tmp_path / "ome.db"
|
|
storage = OMEStorage(db_path=db)
|
|
await storage.init()
|
|
|
|
async with aiosqlite.connect(db) as conn:
|
|
sync_row = await (await conn.execute("PRAGMA synchronous")).fetchone()
|
|
|
|
assert sync_row[0] == 2 # default FULL — confirms scope is per-connection
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_storage_transaction_commits_on_success(tmp_path: Path) -> None:
|
|
db = tmp_path / "ome.db"
|
|
storage = OMEStorage(db_path=db)
|
|
await storage.init()
|
|
|
|
async with storage.transaction() as conn:
|
|
await conn.execute(
|
|
"INSERT INTO counter_store (strategy_name, bucket_key, counter) "
|
|
"VALUES (?, ?, ?)",
|
|
("s", "u1", 42),
|
|
)
|
|
|
|
async with storage.connect() as conn:
|
|
cur = await conn.execute(
|
|
"SELECT counter FROM counter_store WHERE strategy_name=? AND bucket_key=?",
|
|
("s", "u1"),
|
|
)
|
|
row = await cur.fetchone()
|
|
assert row is not None and row[0] == 42
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_storage_transaction_rolls_back_on_exception(tmp_path: Path) -> None:
|
|
db = tmp_path / "ome.db"
|
|
storage = OMEStorage(db_path=db)
|
|
await storage.init()
|
|
|
|
class _BoomError(Exception):
|
|
pass
|
|
|
|
with pytest.raises(_BoomError):
|
|
async with storage.transaction() as conn:
|
|
await conn.execute(
|
|
"INSERT INTO counter_store (strategy_name, bucket_key, counter) "
|
|
"VALUES (?, ?, ?)",
|
|
("s", "u1", 42),
|
|
)
|
|
raise _BoomError
|
|
|
|
async with storage.connect() as conn:
|
|
cur = await conn.execute(
|
|
"SELECT counter FROM counter_store WHERE strategy_name=? AND bucket_key=?",
|
|
("s", "u1"),
|
|
)
|
|
row = await cur.fetchone()
|
|
assert row is None
|