mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-28 14:07:55 +00:00
* fix(channels): wire channel→agent handler and fix Telegram send pipeline * format code * add supported tests
190 lines
7.2 KiB
Python
190 lines
7.2 KiB
Python
"""Parametrized storage tests across all memory backends."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from openjarvis.core.registry import MemoryRegistry
|
|
from openjarvis.tools.storage.sqlite import SQLiteMemory
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Backend factory helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_sqlite(tmp_path):
|
|
if not MemoryRegistry.contains("sqlite"):
|
|
MemoryRegistry.register_value("sqlite", SQLiteMemory)
|
|
return SQLiteMemory(db_path=tmp_path / "test.db")
|
|
|
|
|
|
def _make_bm25():
|
|
bm25_mod = pytest.importorskip(
|
|
"openjarvis.tools.storage.bm25",
|
|
exc_type=ImportError,
|
|
)
|
|
BM25Memory = bm25_mod.BM25Memory
|
|
if not MemoryRegistry.contains("bm25"):
|
|
MemoryRegistry.register_value("bm25", BM25Memory)
|
|
return BM25Memory()
|
|
|
|
|
|
def _make_backend(key, tmp_path):
|
|
"""Create a backend instance by key, skipping if dependencies are missing."""
|
|
if key == "sqlite":
|
|
return _make_sqlite(tmp_path)
|
|
elif key == "bm25":
|
|
return _make_bm25()
|
|
elif key == "faiss":
|
|
mod = pytest.importorskip(
|
|
"openjarvis.tools.storage.faiss_backend",
|
|
exc_type=ImportError,
|
|
)
|
|
return mod.FAISSMemory(db_path=str(tmp_path / "faiss"))
|
|
elif key == "colbert":
|
|
mod = pytest.importorskip(
|
|
"openjarvis.tools.storage.colbert_backend",
|
|
exc_type=ImportError,
|
|
)
|
|
return mod.ColBERTMemory(db_path=str(tmp_path / "colbert"))
|
|
elif key == "hybrid":
|
|
mod = pytest.importorskip(
|
|
"openjarvis.tools.storage.hybrid",
|
|
exc_type=ImportError,
|
|
)
|
|
sqlite = _make_sqlite(tmp_path)
|
|
bm25 = _make_bm25()
|
|
return mod.HybridMemory(sparse=sqlite, dense=bm25)
|
|
else:
|
|
pytest.skip(f"Unknown backend key: {key}")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Core backends (always available)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize("backend_key", ["sqlite", "bm25"])
|
|
class TestStorageSuiteCore:
|
|
"""Storage operations that must pass for guaranteed-available backends."""
|
|
|
|
def test_store_and_retrieve(self, backend_key, tmp_path):
|
|
backend = _make_backend(backend_key, tmp_path)
|
|
backend.store("Python is a programming language", source="wiki.md")
|
|
results = backend.retrieve("Python programming")
|
|
assert len(results) >= 1
|
|
assert "Python" in results[0].content
|
|
|
|
def test_store_multiple_documents(self, backend_key, tmp_path):
|
|
backend = _make_backend(backend_key, tmp_path)
|
|
doc_ids = []
|
|
for i in range(10):
|
|
doc_id = backend.store(
|
|
f"Document number {i} about testing software",
|
|
source=f"doc{i}.md",
|
|
)
|
|
doc_ids.append(doc_id)
|
|
assert len(doc_ids) == 10
|
|
assert len(set(doc_ids)) == 10 # all unique
|
|
|
|
def test_retrieve_respects_top_k(self, backend_key, tmp_path):
|
|
backend = _make_backend(backend_key, tmp_path)
|
|
for i in range(10):
|
|
backend.store(f"document {i} about testing software quality")
|
|
results = backend.retrieve("testing software", top_k=3)
|
|
assert len(results) <= 3
|
|
|
|
def test_delete_document(self, backend_key, tmp_path):
|
|
if backend_key == "bm25":
|
|
pytest.skip("Rust BM25Memory PyO3 bindings do not expose delete()")
|
|
backend = _make_backend(backend_key, tmp_path)
|
|
doc_id = backend.store("content to delete")
|
|
assert backend.delete(doc_id) is True
|
|
assert backend.delete(doc_id) is False # already deleted
|
|
|
|
def test_clear_all(self, backend_key, tmp_path):
|
|
if backend_key == "bm25":
|
|
pytest.skip("Rust BM25Memory PyO3 bindings do not expose clear()")
|
|
backend = _make_backend(backend_key, tmp_path)
|
|
backend.store("first document")
|
|
backend.store("second document")
|
|
backend.clear()
|
|
results = backend.retrieve("first")
|
|
assert len(results) == 0
|
|
|
|
def test_metadata_roundtrip(self, backend_key, tmp_path):
|
|
backend = _make_backend(backend_key, tmp_path)
|
|
meta = {"author": "test_user", "version": 2}
|
|
backend.store(
|
|
"content with metadata fields",
|
|
source="paper.pdf",
|
|
metadata=meta,
|
|
)
|
|
results = backend.retrieve("content metadata")
|
|
assert len(results) >= 1
|
|
assert results[0].source == "paper.pdf"
|
|
assert results[0].metadata["author"] == "test_user"
|
|
assert results[0].metadata["version"] == 2
|
|
|
|
def test_empty_retrieve(self, backend_key, tmp_path):
|
|
backend = _make_backend(backend_key, tmp_path)
|
|
results = backend.retrieve("anything at all")
|
|
assert results == []
|
|
|
|
def test_store_returns_doc_id(self, backend_key, tmp_path):
|
|
backend = _make_backend(backend_key, tmp_path)
|
|
doc_id = backend.store("some content")
|
|
assert isinstance(doc_id, str)
|
|
assert len(doc_id) > 0
|
|
|
|
def test_duplicate_content(self, backend_key, tmp_path):
|
|
backend = _make_backend(backend_key, tmp_path)
|
|
id1 = backend.store("identical content here")
|
|
id2 = backend.store("identical content here")
|
|
assert id1 != id2 # different IDs even for same content
|
|
|
|
def test_large_document(self, backend_key, tmp_path):
|
|
backend = _make_backend(backend_key, tmp_path)
|
|
base = "This is a large test document about software engineering. "
|
|
large_content = base * 200
|
|
assert len(large_content) > 10000
|
|
doc_id = backend.store(large_content, source="large.txt")
|
|
assert isinstance(doc_id, str)
|
|
results = backend.retrieve("software engineering")
|
|
assert len(results) >= 1
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Optional backends (may need extra dependencies)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize("backend_key", ["faiss", "colbert", "hybrid"])
|
|
class TestStorageSuiteOptional:
|
|
"""Same core operations for backends that require optional dependencies."""
|
|
|
|
def test_store_and_retrieve(self, backend_key, tmp_path):
|
|
backend = _make_backend(backend_key, tmp_path)
|
|
backend.store("Python is a programming language", source="wiki.md")
|
|
results = backend.retrieve("Python programming")
|
|
assert len(results) >= 1
|
|
assert "Python" in results[0].content
|
|
|
|
def test_delete_document(self, backend_key, tmp_path):
|
|
if backend_key == "hybrid":
|
|
pytest.skip("HybridMemory sub-backend BM25 lacks delete()")
|
|
backend = _make_backend(backend_key, tmp_path)
|
|
doc_id = backend.store("content to delete")
|
|
assert backend.delete(doc_id) is True
|
|
assert backend.delete(doc_id) is False
|
|
|
|
def test_clear_all(self, backend_key, tmp_path):
|
|
if backend_key == "hybrid":
|
|
pytest.skip("HybridMemory sub-backend BM25 lacks clear()")
|
|
backend = _make_backend(backend_key, tmp_path)
|
|
backend.store("first document")
|
|
backend.store("second document")
|
|
backend.clear()
|
|
results = backend.retrieve("first")
|
|
assert len(results) == 0
|