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:
parent
dc5ad8f699
commit
2afe1bd28c
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
|
|
@ -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") == []
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue