Cover store engine and repository paths

Add vector-index creation tests including the warned failure, chunk
repository get_by_id, list_all pagination, blank-query and precomputed-vector
search, the unknown-score-column guard, settings row recreation, and
replace_for_document with no items.
This commit is contained in:
Yiorgis Gozadinos 2026-07-26 19:36:37 +03:00
parent dc5ad8f699
commit 2afe1bd28c
No known key found for this signature in database
6 changed files with 242 additions and 0 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -986,3 +986,20 @@ class TestPictureDataPreservedThroughRoundTrip:
after = await rag.document_item_repository.get_all_picture_data(created.id)
assert after.get("#/pictures/0") == original.get("#/pictures/0")
@pytest.mark.asyncio
async def test_replace_for_document_with_no_items_deletes_existing(temp_db_path):
"""Replacing with an empty list clears the document's items."""
from haiku.rag.store.engine import Store
from haiku.rag.store.repositories.document_item import DocumentItemRepository
async with Store(temp_db_path, create=True) as store:
repo = DocumentItemRepository(store)
docling_doc = _make_docling_doc()
await repo.replace_for_document("doc-1", extract_items("doc-1", docling_doc))
assert await repo.get_all_items("doc-1")
await repo.replace_for_document("doc-1", [])
assert await repo.get_all_items("doc-1") == []

View file

@ -454,3 +454,76 @@ async def test_ensure_fts_index_warns_on_failure(temp_db_path):
assert [r for r in records if r.levelno == logging.WARNING]
assert any("index build failed" in r.getMessage() for r in records)
@pytest.mark.vcr()
async def test_chunk_repository_get_by_id_and_list_all_pagination(temp_db_path):
"""get_by_id resolves a stored chunk; list_all honours limit and offset."""
async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client:
doc = await client.create_document(
content="First paragraph.\n\nSecond paragraph.\n\nThird paragraph."
)
assert doc.id is not None
stored = await client.chunk_repository.get_by_document_id(doc.id)
assert stored
fetched = await client.get_chunk_by_id(stored[0].id)
assert fetched is not None
assert fetched.id == stored[0].id
assert fetched.content == stored[0].content
assert await client.get_chunk_by_id("no-such-chunk") is None
everything = await client.chunk_repository.list_all()
assert len(everything) == len(stored)
first = await client.chunk_repository.list_all(limit=1)
assert len(first) == 1
assert first[0].id == everything[0].id
if len(everything) > 1:
second = await client.chunk_repository.list_all(limit=1, offset=1)
assert len(second) == 1
assert second[0].id == everything[1].id
async def test_chunk_search_returns_empty_for_blank_query(temp_db_path):
"""A blank query with no precomputed vector has nothing to search for."""
async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client:
assert await client.chunk_repository.search(" ") == []
@pytest.mark.vcr()
async def test_chunk_search_with_precomputed_vector_skips_text_query(temp_db_path):
"""The image-as-query path searches vector-only using a stored embedding."""
async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client:
doc = await client.create_document(content="Vector-only search target.")
assert doc.id is not None
rows = (await client.store.chunks_table.query().limit(1).to_arrow()).to_pylist()
stored_vector = list(rows[0]["vector"])
results = await client.chunk_repository.search("", query_vector=stored_vector)
assert results
assert any(c.document_id == doc.id for c, _ in results)
async def test_get_chunk_ids_by_self_ref_grouped_without_documents(temp_db_path):
async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client:
assert await client.chunk_repository.get_chunk_ids_by_self_ref_grouped([]) == {}
async def test_process_search_results_rejects_unknown_score_column(temp_db_path):
"""A result frame with no recognised score column is a programming error."""
import pandas as pd
async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client:
class _Frame:
async def to_pandas(self):
return pd.DataFrame([{"id": "c1", "content": "x", "metadata": "{}"}])
with pytest.raises(ValueError, match="Unknown search result format"):
await client.chunk_repository._process_search_results(_Frame()) # ty: ignore[invalid-argument-type]

View file

@ -310,3 +310,48 @@ class TestInitFailureCleanup:
pass
assert close_calls, "Store.close() was not called when _initialize raised"
class TestVectorIndexCreation:
"""_ensure_vector_index needs 256 rows of training data before it builds."""
@staticmethod
async def _seed_chunks(store, count: int) -> None:
import random
records = [
store.ChunkRecord(
document_id="doc-1",
content=f"row {i}",
content_fts=f"row {i}",
metadata="{}",
order=i,
vector=[random.random() for _ in range(store.embedder.vector_dim)],
)
for i in range(count)
]
await store.chunks_table.add(records)
@pytest.mark.asyncio
async def test_builds_index_once_enough_rows_exist(self, temp_db_path):
async with Store(temp_db_path, create=True) as store:
await self._seed_chunks(store, 256)
await store._ensure_vector_index()
indexes = await store.chunks_table.list_indices()
assert any("vector" in idx.columns for idx in indexes)
@pytest.mark.asyncio
async def test_index_failure_is_warned_not_raised(self, temp_db_path):
async with Store(temp_db_path, create=True) as store:
await self._seed_chunks(store, 256)
async def boom(*_args, **_kwargs):
raise RuntimeError("index build failed")
with patch.object(store.chunks_table, "create_index", boom):
await store._ensure_vector_index()
indexes = await store.chunks_table.list_indices()
assert not any("vector" in idx.columns for idx in indexes)

View file

@ -263,3 +263,23 @@ class TestValidateConfigCompatibility:
await settings_repo.validate_config_compatibility()
assert "9999" in str(exc_info.value)
@pytest.mark.asyncio
async def test_save_current_settings_recreates_a_deleted_row(temp_db_path):
from haiku.rag.store.engine import Store
from haiku.rag.store.repositories.settings import SettingsRepository
async with Store(temp_db_path, create=True, skip_validation=True) as store:
settings_repo = SettingsRepository(store)
await store.settings_table.delete("id = 'settings'")
assert await settings_repo.get_current_settings() == {}
await settings_repo.save_current_settings()
recreated = await settings_repo.get_current_settings()
assert (
recreated["embeddings"]
== store._config.model_dump(mode="json")["embeddings"]
)