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.
73 lines
2.3 KiB
Python
73 lines
2.3 KiB
Python
"""LanceDB manager singletons.
|
|
|
|
Verifies ``get_connection`` / ``get_table`` / ``dispose_connection``
|
|
are idempotent and rebuild after dispose.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from lancedb.pydantic import Vector
|
|
|
|
from everos.core.persistence import BaseLanceTable
|
|
from everos.infra.persistence.lancedb import lancedb_manager
|
|
|
|
|
|
class _DemoVec(BaseLanceTable):
|
|
"""Demo schema — only used by this test module."""
|
|
|
|
text: str
|
|
vector: Vector(3) # type: ignore[valid-type]
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
async def _reset(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
|
"""Point the singleton at an isolated memory-root and reset module state."""
|
|
monkeypatch.setenv("EVEROS_MEMORY__ROOT", str(tmp_path))
|
|
lancedb_manager._conn = None
|
|
lancedb_manager._tables.clear()
|
|
yield
|
|
await lancedb_manager.dispose_connection()
|
|
|
|
|
|
async def test_get_connection_is_singleton() -> None:
|
|
c1 = await lancedb_manager.get_connection()
|
|
c2 = await lancedb_manager.get_connection()
|
|
assert c1 is c2
|
|
|
|
|
|
async def test_get_table_creates_then_caches() -> None:
|
|
t1 = await lancedb_manager.get_table("demo", _DemoVec)
|
|
t2 = await lancedb_manager.get_table("demo", _DemoVec)
|
|
assert t1 is t2
|
|
assert "demo" in lancedb_manager._tables
|
|
|
|
|
|
async def test_get_table_reopens_existing() -> None:
|
|
"""A second connection cycle must reopen (not recreate) the table."""
|
|
await lancedb_manager.get_table("demo", _DemoVec)
|
|
await lancedb_manager.dispose_connection()
|
|
|
|
t = await lancedb_manager.get_table("demo", _DemoVec)
|
|
assert t is not None
|
|
# Round-trip a row to prove the schema survived.
|
|
await t.add([_DemoVec(text="hello", vector=[0.1, 0.2, 0.3])])
|
|
assert await t.count_rows() == 1
|
|
|
|
|
|
async def test_dispose_resets_state() -> None:
|
|
await lancedb_manager.get_connection()
|
|
await lancedb_manager.get_table("demo", _DemoVec)
|
|
await lancedb_manager.dispose_connection()
|
|
assert lancedb_manager._conn is None
|
|
assert lancedb_manager._tables == {}
|
|
|
|
|
|
async def test_dispose_is_idempotent() -> None:
|
|
await lancedb_manager.dispose_connection() # nothing built yet
|
|
await lancedb_manager.get_connection()
|
|
await lancedb_manager.dispose_connection()
|
|
await lancedb_manager.dispose_connection() # second call must not raise
|