From e2b273ee2a93e22c6a3d87edd31c63b0550dca96 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Sun, 26 Jul 2026 13:30:59 +0300 Subject: [PATCH] Consolidate duplicated client-side tests Parametrize sibling tests that differed only in a literal value, and fold two strict-subset tests into the survivors that already covered their scenario. Every case that ran before still runs; the union of assertions is applied to each case, strengthening list_all, get_pages_data and resolve_doc_items. Replace four hand-rolled log-capture handlers with a shared capture_logs() contextmanager in conftest. 13 fewer test functions, 348 fewer lines. --- ...=> test_chunk_content_fts[populated].yaml} | 0 ..._chunk_content_fts[without_headings].yaml} | 0 ...reate_document_from_source_with_title.yaml | 42 -- ...client_unchanged_file_keeps_timestamp.yaml | 42 -- tests/conftest.py | 27 ++ tests/test_chunk.py | 428 +++++++----------- tests/test_client.py | 42 +- tests/test_document.py | 69 +-- tests/test_processing.py | 19 +- tests/test_rebuild.py | 34 +- tests/test_search.py | 119 +---- 11 files changed, 237 insertions(+), 585 deletions(-) rename tests/cassettes/test_chunk/{test_chunk_content_fts_populated.yaml => test_chunk_content_fts[populated].yaml} (100%) rename tests/cassettes/test_chunk/{test_chunk_content_fts_without_headings.yaml => test_chunk_content_fts[without_headings].yaml} (100%) delete mode 100644 tests/cassettes/test_client/test_client_create_document_from_source_with_title.yaml delete mode 100644 tests/cassettes/test_client/test_client_unchanged_file_keeps_timestamp.yaml diff --git a/tests/cassettes/test_chunk/test_chunk_content_fts_populated.yaml b/tests/cassettes/test_chunk/test_chunk_content_fts[populated].yaml similarity index 100% rename from tests/cassettes/test_chunk/test_chunk_content_fts_populated.yaml rename to tests/cassettes/test_chunk/test_chunk_content_fts[populated].yaml diff --git a/tests/cassettes/test_chunk/test_chunk_content_fts_without_headings.yaml b/tests/cassettes/test_chunk/test_chunk_content_fts[without_headings].yaml similarity index 100% rename from tests/cassettes/test_chunk/test_chunk_content_fts_without_headings.yaml rename to tests/cassettes/test_chunk/test_chunk_content_fts[without_headings].yaml diff --git a/tests/cassettes/test_client/test_client_create_document_from_source_with_title.yaml b/tests/cassettes/test_client/test_client_create_document_from_source_with_title.yaml deleted file mode 100644 index 3ce22209..00000000 --- a/tests/cassettes/test_client/test_client_create_document_from_source_with_title.yaml +++ /dev/null @@ -1,42 +0,0 @@ -interactions: -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '103' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - encoding_format: base64 - input: - - This is test content from a file. - model: qwen3-embedding:4b - uri: http://localhost:11434/v1/embeddings - response: - headers: - content-type: - - application/json - transfer-encoding: - - chunked - parsed_body: - data: - - embedding: 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 - index: 0 - object: embedding - model: qwen3-embedding:4b - object: list - usage: - prompt_tokens: 9 - total_tokens: 9 - status: - code: 200 - message: OK -version: 1 diff --git a/tests/cassettes/test_client/test_client_unchanged_file_keeps_timestamp.yaml b/tests/cassettes/test_client/test_client_unchanged_file_keeps_timestamp.yaml deleted file mode 100644 index cbf424cb..00000000 --- a/tests/cassettes/test_client/test_client_unchanged_file_keeps_timestamp.yaml +++ /dev/null @@ -1,42 +0,0 @@ -interactions: -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '103' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - encoding_format: base64 - input: - - Test content for timestamp check. - model: qwen3-embedding:4b - uri: http://localhost:11434/v1/embeddings - response: - headers: - content-type: - - application/json - transfer-encoding: - - chunked - parsed_body: - data: - - embedding: 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 - index: 0 - object: embedding - model: qwen3-embedding:4b - object: list - usage: - prompt_tokens: 7 - total_tokens: 7 - status: - code: 200 - message: OK -version: 1 diff --git a/tests/conftest.py b/tests/conftest.py index b5924d74..fb5229c1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,8 @@ import json import logging import os import tempfile +from collections.abc import Iterator +from contextlib import contextmanager from pathlib import Path from typing import TYPE_CHECKING, Any @@ -32,6 +34,31 @@ setattr(pydantic_ai.models, "ALLOW_MODEL_REQUESTS", False) logging.getLogger("vcr.cassette").setLevel(logging.WARNING) +@contextmanager +def capture_logs( + logger: logging.Logger, level: int +) -> Iterator[list[logging.LogRecord]]: + """Collect records emitted by ``logger`` at or above ``level``. + + Attaches directly to the given logger instead of using ``caplog``: + ``haiku.rag.logging.get_logger()`` sets ``propagate=False`` on the + ``haiku.rag`` logger, so records never reach caplog's root handler once + any test in the session has called it. + """ + records: list[logging.LogRecord] = [] + + class _ListHandler(logging.Handler): + def emit(self, record: logging.LogRecord) -> None: + records.append(record) + + handler = _ListHandler(level=level) + logger.addHandler(handler) + try: + yield records + finally: + logger.removeHandler(handler) + + @pytest.fixture(scope="session") def qa_corpus() -> list[dict[str, str]]: corpus_path = Path(__file__).parent / "data" / "qa_corpus.json" diff --git a/tests/test_chunk.py b/tests/test_chunk.py index c33637ae..f540528b 100644 --- a/tests/test_chunk.py +++ b/tests/test_chunk.py @@ -3,6 +3,7 @@ import pytest from haiku.rag.client import HaikuRAG from haiku.rag.config import Config from haiku.rag.store.models.chunk import Chunk, ChunkMetadata, SearchResult +from tests.conftest import capture_logs @pytest.mark.vcr() @@ -123,136 +124,86 @@ async def test_chunking_pipeline(qa_corpus: list[dict[str, str]], temp_db_path): assert chunk.order == i -def test_chunk_metadata_parsing(): +@pytest.mark.parametrize( + "metadata,refs,headings,labels,page_numbers", + [ + ( + { + "doc_item_refs": ["#/texts/0", "#/texts/1", "#/tables/0"], + "headings": ["Chapter 1", "Section 1.1"], + "labels": ["paragraph", "paragraph", "table"], + "page_numbers": [1, 1, 2], + }, + ["#/texts/0", "#/texts/1", "#/tables/0"], + ["Chapter 1", "Section 1.1"], + ["paragraph", "paragraph", "table"], + [1, 1, 2], + ), + ({}, [], None, [], []), + ], + ids=["populated", "defaults"], +) +def test_chunk_metadata_parsing(metadata, refs, headings, labels, page_numbers): """Test ChunkMetadata parsing from chunk metadata dict.""" - metadata_dict = { - "doc_item_refs": ["#/texts/0", "#/texts/1", "#/tables/0"], - "headings": ["Chapter 1", "Section 1.1"], - "labels": ["paragraph", "paragraph", "table"], - "page_numbers": [1, 1, 2], - } - - chunk = Chunk( - content="Test content", - metadata=metadata_dict, - ) + chunk = Chunk(content="Test content", metadata=metadata) chunk_meta = chunk.get_chunk_metadata() assert isinstance(chunk_meta, ChunkMetadata) - assert chunk_meta.doc_item_refs == ["#/texts/0", "#/texts/1", "#/tables/0"] - assert chunk_meta.headings == ["Chapter 1", "Section 1.1"] - assert chunk_meta.labels == ["paragraph", "paragraph", "table"] - assert chunk_meta.page_numbers == [1, 1, 2] + assert chunk_meta.doc_item_refs == refs + assert chunk_meta.headings == headings + assert chunk_meta.labels == labels + assert chunk_meta.page_numbers == page_numbers -def test_chunk_metadata_defaults(): - """Test ChunkMetadata with empty/default values.""" - chunk = Chunk(content="Test content", metadata={}) - chunk_meta = chunk.get_chunk_metadata() +@pytest.fixture +def two_text_docling_doc(): + """Minimal DoclingDocument with two resolvable text items.""" + from docling_core.types.doc.document import DoclingDocument - assert chunk_meta.doc_item_refs == [] - assert chunk_meta.headings is None - assert chunk_meta.labels == [] - assert chunk_meta.page_numbers == [] + return DoclingDocument.model_validate( + { + "name": "test_doc", + "texts": [ + { + "self_ref": "#/texts/0", + "text": "First text", + "orig": "First text", + "label": "paragraph", + }, + { + "self_ref": "#/texts/1", + "text": "Second text", + "orig": "Second text", + "label": "title", + }, + ], + "tables": [], + "pictures": [], + "groups": [], + "body": {"self_ref": "#/body", "children": []}, + "furniture": {"self_ref": "#/furniture", "children": []}, + } + ) -def test_chunk_metadata_resolve_doc_items(): +@pytest.mark.parametrize( + "refs,expected_texts", + [ + (["#/texts/0", "#/texts/1"], ["First text", "Second text"]), + # Out-of-range and malformed refs are skipped rather than raising. + (["#/texts/0", "#/texts/999", "#/invalid/path"], ["First text"]), + ([], []), + ], + ids=["all_valid", "graceful_degradation", "empty_refs"], +) +def test_chunk_metadata_resolve_doc_items(two_text_docling_doc, refs, expected_texts): """Test resolving doc_item_refs to actual DocItem objects.""" - from docling_core.types.doc.document import DoclingDocument + chunk_meta = ChunkMetadata(doc_item_refs=refs) - # Create a minimal DoclingDocument with some text items - doc_json = { - "name": "test_doc", - "texts": [ - { - "self_ref": "#/texts/0", - "text": "First text", - "orig": "First text", - "label": "paragraph", - }, - { - "self_ref": "#/texts/1", - "text": "Second text", - "orig": "Second text", - "label": "title", - }, - ], - "tables": [], - "pictures": [], - "groups": [], - "body": {"self_ref": "#/body", "children": []}, - "furniture": {"self_ref": "#/furniture", "children": []}, - } - docling_doc = DoclingDocument.model_validate(doc_json) + doc_items = chunk_meta.resolve_doc_items(two_text_docling_doc) - # Create chunk metadata with refs - chunk_meta = ChunkMetadata( - doc_item_refs=["#/texts/0", "#/texts/1"], - labels=["paragraph", "title"], - ) - - # Resolve refs - doc_items = chunk_meta.resolve_doc_items(docling_doc) - - assert len(doc_items) == 2 - assert getattr(doc_items[0], "text") == "First text" - assert getattr(doc_items[1], "text") == "Second text" - - -def test_chunk_metadata_resolve_doc_items_graceful_degradation(): - """Test that invalid refs are skipped gracefully.""" - from docling_core.types.doc.document import DoclingDocument - - doc_json = { - "name": "test_doc", - "texts": [ - { - "self_ref": "#/texts/0", - "text": "Only text", - "orig": "Only text", - "label": "paragraph", - }, - ], - "tables": [], - "pictures": [], - "groups": [], - "body": {"self_ref": "#/body", "children": []}, - "furniture": {"self_ref": "#/furniture", "children": []}, - } - docling_doc = DoclingDocument.model_validate(doc_json) - - # Create chunk metadata with one valid and one invalid ref - chunk_meta = ChunkMetadata( - doc_item_refs=["#/texts/0", "#/texts/999", "#/invalid/path"], - ) - - # Resolve refs - invalid ones should be skipped - doc_items = chunk_meta.resolve_doc_items(docling_doc) - - assert len(doc_items) == 1 - assert getattr(doc_items[0], "text") == "Only text" - - -def test_chunk_metadata_resolve_empty_refs(): - """Test resolving with no refs returns empty list.""" - from docling_core.types.doc.document import DoclingDocument - - doc_json = { - "name": "test_doc", - "texts": [], - "tables": [], - "pictures": [], - "groups": [], - "body": {"self_ref": "#/body", "children": []}, - "furniture": {"self_ref": "#/furniture", "children": []}, - } - docling_doc = DoclingDocument.model_validate(doc_json) - - chunk_meta = ChunkMetadata() - doc_items = chunk_meta.resolve_doc_items(docling_doc) - - assert doc_items == [] + assert [getattr(item, "text") for item in doc_items] == expected_texts def test_search_result_from_chunk_preserves_document_meta(): @@ -286,11 +237,12 @@ def test_search_result_format_for_agent_omits_document_meta(): assert "https://example.org/report/view" not in formatted -def test_search_result_format_for_agent_with_rank(): - """Test format_for_agent with rank and total parameters.""" - result = SearchResult( +@pytest.fixture +def rich_search_result(): + """SearchResult with every optional field populated.""" + return SearchResult( content="This is the chunk content about elections.", - score=0.02, # Low RRF score that would confuse agents + score=0.85, chunk_id="chunk-123", document_id="doc-456", document_uri="file:///docs/report.pdf", @@ -300,16 +252,30 @@ def test_search_result_format_for_agent_with_rank(): page_numbers=[1, 2], ) - formatted = result.format_for_agent(rank=1, total=5) +@pytest.mark.parametrize( + "kwargs,present,absent", + [ + # A rank is supplied, so the raw RRF score is withheld from the agent. + ({"rank": 1, "total": 5}, "[rank 1 of 5]", "score:"), + ({}, "(score: 0.85)", "[rank"), + ], + ids=["with_rank", "score_fallback"], +) +def test_search_result_format_for_agent_rank_vs_score( + rich_search_result, kwargs, present, absent +): + """format_for_agent shows a rank when given one, else falls back to score.""" + formatted = rich_search_result.format_for_agent(**kwargs) + + assert present in formatted + assert absent not in formatted assert "[chunk-123]" in formatted - assert "[rank 1 of 5]" in formatted - assert "score:" not in formatted # Score should NOT appear when rank is provided assert ( 'Source: "Annual Report 2024" > Chapter 1 > Section 1.1 > Elections' in formatted ) - assert "Type: table" in formatted + assert "Type: table" in formatted # table has higher priority than paragraph assert "Content:\nThis is the chunk content about elections." in formatted @@ -357,134 +323,104 @@ def test_search_result_format_for_agent_no_captions_no_line(): assert "Figure caption" not in formatted -def test_search_result_format_for_agent_rank_only(): - """Test format_for_agent with rank but no total.""" - result = SearchResult( - content="Some content.", - score=0.03, - chunk_id="chunk-abc", - ) - - formatted = result.format_for_agent(rank=2) - - assert "[chunk-abc]" in formatted - assert "[rank 2]" in formatted - assert "score:" not in formatted - - -def test_search_result_format_for_agent_fallback(): - """Test format_for_agent falls back to score when no rank provided.""" - result = SearchResult( - content="This is the chunk content about elections.", - score=0.85, - chunk_id="chunk-123", - document_id="doc-456", - document_uri="file:///docs/report.pdf", - document_title="Annual Report 2024", - headings=["Chapter 1", "Section 1.1", "Elections"], - labels=["paragraph", "table"], - page_numbers=[1, 2], - ) - - formatted = result.format_for_agent() - - assert "[chunk-123]" in formatted - assert "(score: 0.85)" in formatted - assert ( - 'Source: "Annual Report 2024" > Chapter 1 > Section 1.1 > Elections' - in formatted - ) - assert "Type: table" in formatted # table has higher priority than paragraph - assert "Content:\nThis is the chunk content about elections." in formatted - - -def test_search_result_format_for_agent_minimal(): - """Test format_for_agent with minimal metadata.""" +@pytest.mark.parametrize( + "kwargs,present,absent", + [ + ({"rank": 2}, "[rank 2]", ["score:"]), + # No structural metadata at all, so no Source:/Type: lines are emitted. + ({}, "(score: 0.72)", ["[rank", "Source:", "Type:"]), + ], + ids=["rank_only", "minimal"], +) +def test_search_result_format_for_agent_minimal(kwargs, present, absent): + """A result carrying only content/score/chunk_id formats without metadata lines.""" result = SearchResult( content="Some content here.", score=0.72, chunk_id="chunk-abc", ) - formatted = result.format_for_agent() + formatted = result.format_for_agent(**kwargs) assert "[chunk-abc]" in formatted - assert "(score: 0.72)" in formatted - assert "Source:" not in formatted # No title or headings - assert "Type:" not in formatted # No labels + assert present in formatted + for token in absent: + assert token not in formatted assert "Content:\nSome content here." in formatted -def test_search_result_format_for_agent_title_only(): - """Test format_for_agent with only document title.""" +@pytest.mark.parametrize( + "fields,expected_source", + [ + ({"document_title": "My Document"}, 'Source: "My Document"'), + ( + {"headings": ["Introduction", "Background"]}, + "Source: Introduction > Background", + ), + ], + ids=["title_only", "headings_only"], +) +def test_search_result_format_for_agent_source_line(fields, expected_source): + """The Source: line is built from the title, the headings, or both.""" result = SearchResult( content="Content text.", score=0.60, chunk_id="chunk-xyz", - document_title="My Document", + **fields, ) - formatted = result.format_for_agent() - - assert 'Source: "My Document"' in formatted + assert expected_source in result.format_for_agent() -def test_search_result_format_for_agent_headings_only(): - """Test format_for_agent with only headings (no title).""" - result = SearchResult( - content="Content text.", - score=0.60, - chunk_id="chunk-xyz", - headings=["Introduction", "Background"], - ) - - formatted = result.format_for_agent() - - assert "Source: Introduction > Background" in formatted - - -def test_search_result_get_primary_label(): +@pytest.mark.parametrize( + "labels,expected", + [ + (["paragraph", "table", "text"], "table"), + (["paragraph", "code"], "code"), + (["list_item", "code"], "code"), + (["text", "list_item"], "list_item"), + # No structural label: falls through to the first label. + (["paragraph", "text"], "paragraph"), + ([], None), + ], +) +def test_search_result_get_primary_label(labels, expected): """Test _get_primary_label prioritization.""" - # Table takes priority over text labels - result = SearchResult(content="x", score=0.5, labels=["paragraph", "table", "text"]) - assert result._get_primary_label() == "table" - - # Code takes priority over list_item - result = SearchResult(content="x", score=0.5, labels=["list_item", "code"]) - assert result._get_primary_label() == "code" - - # Text labels fall through to first - result = SearchResult(content="x", score=0.5, labels=["paragraph", "text"]) - assert result._get_primary_label() == "paragraph" - - # Empty labels - result = SearchResult(content="x", score=0.5, labels=[]) - assert result._get_primary_label() is None + result = SearchResult(content="x", score=0.5, labels=labels) + assert result._get_primary_label() == expected @pytest.mark.vcr() -async def test_chunk_content_fts_populated(temp_db_path): - """Test that content_fts column is populated with contextualized content.""" +@pytest.mark.parametrize( + "metadata,content,expected_content_fts", + [ + ( + {"headings": ["Chapter 1", "Section 1.1"]}, + "This is the raw chunk content.", + "Chapter 1\nSection 1.1\nThis is the raw chunk content.", + ), + ({}, "Plain content without headings.", "Plain content without headings."), + ], + ids=["populated", "without_headings"], +) +async def test_chunk_content_fts(temp_db_path, metadata, content, expected_content_fts): + """content_fts holds the contextualized content while content stays raw.""" from haiku.rag.embeddings import get_embedder async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client: - # Create a chunk with headings chunk = Chunk( document_id="test-doc", - content="This is the raw chunk content.", - metadata={"headings": ["Chapter 1", "Section 1.1"]}, + content=content, + metadata=metadata, order=0, ) - # Generate embedding embedder = get_embedder(Config) embedding = (await embedder.embed_documents([chunk.content]))[0] chunk.embedding = embedding - # Store the chunk await client.chunk_repository.create(chunk) - # Read the raw record from the database records = ( await client.store.chunks_table.query() .where(f"id = '{chunk.id}'") @@ -495,52 +431,8 @@ async def test_chunk_content_fts_populated(temp_db_path): assert len(records) == 1 record = records[0] - # Verify content is raw (no headings) - assert record["content"] == "This is the raw chunk content." - - # Verify content_fts is contextualized (headings + content) - assert ( - record["content_fts"] - == "Chapter 1\nSection 1.1\nThis is the raw chunk content." - ) - - -@pytest.mark.vcr() -async def test_chunk_content_fts_without_headings(temp_db_path): - """Test that content_fts equals content when no headings present.""" - from haiku.rag.embeddings import get_embedder - - async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client: - # Create a chunk without headings - chunk = Chunk( - document_id="test-doc", - content="Plain content without headings.", - metadata={}, - order=0, - ) - - # Generate embedding - embedder = get_embedder(Config) - embedding = (await embedder.embed_documents([chunk.content]))[0] - chunk.embedding = embedding - - # Store the chunk - await client.chunk_repository.create(chunk) - - # Read the raw record from the database - records = ( - await client.store.chunks_table.query() - .where(f"id = '{chunk.id}'") - .limit(1) - .to_arrow() - ).to_pylist() - - assert len(records) == 1 - record = records[0] - - # Both should be the same when no headings - assert record["content"] == "Plain content without headings." - assert record["content_fts"] == "Plain content without headings." + assert record["content"] == content + assert record["content_fts"] == expected_content_fts async def test_ensure_fts_index_warns_on_failure(temp_db_path): @@ -557,18 +449,8 @@ async def test_ensure_fts_index_warns_on_failure(temp_db_path): repo.store.chunks_table.create_index = _boom - records: list[logging.LogRecord] = [] - - class _Capture(logging.Handler): - def emit(self, record: logging.LogRecord) -> None: - records.append(record) - - handler = _Capture(level=logging.WARNING) - chunk_module.logger.addHandler(handler) - try: + with capture_logs(chunk_module.logger, logging.WARNING) as records: await repo._ensure_fts_index() - finally: - chunk_module.logger.removeHandler(handler) assert [r for r in records if r.levelno == logging.WARNING] assert any("index build failed" in r.getMessage() for r in records) diff --git a/tests/test_client.py b/tests/test_client.py index c0636e16..19b6fa6c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -298,23 +298,6 @@ async def test_client_create_document_from_source(temp_db_path): assert "md5" in doc2.metadata -@pytest.mark.vcr() -async def test_client_create_document_from_source_with_title(temp_db_path): - """Test creating a document from a file source with a title.""" - async with HaikuRAG(temp_db_path, create=True) as client: - with tempfile.TemporaryDirectory() as temp_dir: - test_content = "This is test content from a file." - temp_path = Path(temp_dir) / "test_title.txt" - temp_path.write_text(test_content) - - doc = await client.create_document_from_source( - source=temp_path, title="My Doc" - ) - assert isinstance(doc, Document) - assert doc.id is not None - assert doc.title == "My Doc" - - @pytest.mark.vcr() async def test_client_update_title_noop_behavior(temp_db_path): """When content is unchanged, updating title should update document without re-chunking.""" @@ -326,6 +309,7 @@ async def test_client_update_title_noop_behavior(temp_db_path): doc1 = await client.create_document_from_source(temp_path, title="Title A") assert isinstance(doc1, Document) assert doc1.id is not None + assert doc1.title == "Title A" # Re-add with same content but new title doc2 = await client.create_document_from_source(temp_path, title="Title B") @@ -646,12 +630,14 @@ async def test_client_create_update_no_op_behavior(temp_db_path): assert doc1.id is not None assert doc1.content == test_content original_id = doc1.id + original_updated_at = doc1.updated_at # Second call with same content - should return existing document (no-op) doc2 = await client.create_document_from_source(temp_path) assert isinstance(doc2, Document) assert doc2.id == original_id # Same document assert doc2.content == test_content + assert doc2.updated_at == original_updated_at # No-op leaves it untouched # Modify file content updated_content = "Updated content for testing." @@ -669,28 +655,6 @@ async def test_client_create_update_no_op_behavior(temp_db_path): assert retrieved_doc.content == updated_content -@pytest.mark.vcr() -async def test_client_unchanged_file_keeps_timestamp(temp_db_path): - """Test that unchanged files don't update the updated_at timestamp.""" - async with HaikuRAG(temp_db_path, create=True) as client: - # Create a temporary file - test_content = "Test content for timestamp check." - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) / "test.txt" - temp_path.write_text(test_content) - - # First call - create document - doc1 = await client.create_document_from_source(temp_path) - assert isinstance(doc1, Document) - original_updated_at = doc1.updated_at - - # Second call with same content - should not update timestamp - doc2 = await client.create_document_from_source(temp_path) - assert isinstance(doc2, Document) - assert doc2.id == doc1.id - assert doc2.updated_at == original_updated_at # Timestamp should not change - - @pytest.mark.vcr() async def test_client_url_create_update_no_op_behavior(temp_db_path): """Test create/update/no-op behavior for URLs based on MD5 changes.""" diff --git a/tests/test_document.py b/tests/test_document.py index f850915c..a1c2ccb0 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -8,49 +8,33 @@ from haiku.rag.store.repositories.document_item import DocumentItemRepository @pytest.mark.asyncio -async def test_document_list_excludes_content_by_default( - qa_corpus: list[dict[str, str]], temp_db_path +@pytest.mark.parametrize("include_content", [False, True]) +async def test_document_list_all( + qa_corpus: list[dict[str, str]], temp_db_path, include_content ): - """list_all excludes content and docling_document by default.""" + """list_all excludes content and docling_document unless include_content=True.""" async with Store(temp_db_path, create=True) as store: doc_repo = DocumentRepository(store) + content = qa_corpus[0]["document_extracted"] doc = Document( - content=qa_corpus[0]["document_extracted"], + content=content, uri="https://example.com/doc.txt", title="Test Document", metadata={"key": "value"}, ) created = await doc_repo.create(doc) - docs = await doc_repo.list_all() + docs = await doc_repo.list_all(include_content=include_content) assert len(docs) == 1 assert docs[0].id == created.id assert docs[0].title == "Test Document" assert docs[0].uri == "https://example.com/doc.txt" assert docs[0].metadata == {"key": "value"} - assert docs[0].content == "" + assert docs[0].content == (content if include_content else "") assert docs[0].docling_document is None -@pytest.mark.asyncio -async def test_document_list_includes_content_when_requested( - qa_corpus: list[dict[str, str]], temp_db_path -): - """list_all returns content when include_content=True.""" - async with Store(temp_db_path, create=True) as store: - doc_repo = DocumentRepository(store) - - content = qa_corpus[0]["document_extracted"] - doc = Document(content=content, uri="https://example.com/doc.txt") - created = await doc_repo.create(doc) - - docs = await doc_repo.list_all(include_content=True) - assert len(docs) == 1 - assert docs[0].id == created.id - assert docs[0].content == content - - @pytest.mark.asyncio async def test_document_list_with_filter(qa_corpus: list[dict[str, str]], temp_db_path): """Test listing documents with filter clause.""" @@ -391,16 +375,26 @@ async def test_get_docling_data_loads_only_docling_columns( @pytest.mark.asyncio +@pytest.mark.parametrize( + "with_pages", + # Markdown documents have no page images, so their pages blob stays None. + [True, False], + ids=["with_pages", "markdown"], +) async def test_get_pages_data_loads_only_pages_column( - qa_corpus: list[dict[str, str]], temp_db_path + qa_corpus: list[dict[str, str]], temp_db_path, with_pages ): """get_pages_data returns only page image data for a document.""" import json from haiku.rag.store.compression import compress_json - pages_blob = compress_json( - json.dumps({"1": {"size": {"width": 612, "height": 792}, "page_no": 1}}) + pages_blob = ( + compress_json( + json.dumps({"1": {"size": {"width": 612, "height": 792}, "page_no": 1}}) + ) + if with_pages + else None ) async with Store(temp_db_path, create=True) as store: @@ -424,27 +418,6 @@ async def test_get_pages_data_loads_only_pages_column( assert await doc_repo.get_pages_data("nonexistent-id") is None -@pytest.mark.asyncio -async def test_get_pages_data_none_for_markdown_document( - qa_corpus: list[dict[str, str]], temp_db_path -): - """Markdown documents have no page images — get_pages_data returns None pages.""" - async with Store(temp_db_path, create=True) as store: - doc_repo = DocumentRepository(store) - - doc = Document( - content=qa_corpus[0]["document_extracted"], - uri="https://example.com/doc.md", - ) - created = await doc_repo.create(doc) - assert created.id is not None - - result = await doc_repo.get_pages_data(created.id) - assert result is not None - assert result.id == created.id - assert result.docling_pages is None - - @pytest.mark.asyncio async def test_document_get_by_uri_with_special_characters( qa_corpus: list[dict[str, str]], temp_db_path diff --git a/tests/test_processing.py b/tests/test_processing.py index 8e7133d2..65c1ca86 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -7,6 +7,7 @@ import pytest from haiku.rag.client.processing import _warn_if_descriptions_missing, convert from haiku.rag.config import AppConfig +from tests.conftest import capture_logs def _doc_with_pictures(*, with_descriptions: bool): @@ -44,24 +45,12 @@ def _doc_without_pictures(): @pytest.fixture -def caplog_warnings(caplog): +def caplog_warnings(): """Capture WARNING-level records from the processing logger.""" - caplog.set_level(logging.WARNING, logger="haiku.rag.client.processing") - # The haiku.rag parent logger sets propagate=False after get_logger() runs, - # which can break caplog under xdist when other tests have already - # configured logging. Attach directly to the module logger. from haiku.rag.client.processing import logger as proc_logger - records: list[logging.LogRecord] = [] - - class _Capture(logging.Handler): - def emit(self, record: logging.LogRecord) -> None: - records.append(record) - - handler = _Capture(level=logging.WARNING) - proc_logger.addHandler(handler) - yield records - proc_logger.removeHandler(handler) + with capture_logs(proc_logger, logging.WARNING) as records: + yield records def test_no_warning_when_picture_description_disabled(caplog_warnings): diff --git a/tests/test_rebuild.py b/tests/test_rebuild.py index c371bae2..50ebc254 100644 --- a/tests/test_rebuild.py +++ b/tests/test_rebuild.py @@ -7,6 +7,7 @@ import pytest from haiku.rag.client import HaikuRAG, RebuildMode from haiku.rag.config import Config +from tests.conftest import capture_logs class ChunkData(TypedDict): @@ -615,25 +616,11 @@ async def test_rebuild_full_source_failure_is_logged_and_skipped( monkeypatch.setattr(client, "create_document_from_source", failing_create) - # Attach directly to the rebuild module's logger rather than - # relying on caplog — `haiku.rag.logging.get_logger()` (invoked - # by other tests) sets `propagate=False` on the `haiku.rag` - # logger, which breaks caplog under xdist ordering. - records: list[logging.LogRecord] = [] - - class _ListHandler(logging.Handler): - def emit(self, record: logging.LogRecord) -> None: - records.append(record) - - handler = _ListHandler(level=logging.ERROR) - rebuild_module.logger.addHandler(handler) - try: + with capture_logs(rebuild_module.logger, logging.ERROR) as records: processed_ids = [ doc_id async for doc_id in client.rebuild_database(mode=RebuildMode.FULL) ] - finally: - rebuild_module.logger.removeHandler(handler) assert processed_ids == [] assert any( @@ -843,7 +830,7 @@ async def test_patch_picture_descriptions_returns_zero_for_doc_without_pictures( @pytest.mark.asyncio -async def test_patch_picture_descriptions_warns_on_missing_bytes(temp_db_path, caplog): +async def test_patch_picture_descriptions_warns_on_missing_bytes(temp_db_path): """When the docling blob has pictures but document_items.picture_data is empty (e.g. legacy DB ingested before A2b), the helper logs a warning and returns 0 instead of trying to drive the VLM with no input.""" @@ -872,23 +859,10 @@ async def test_patch_picture_descriptions_warns_on_missing_bytes(temp_db_path, c where=f"document_id = '{created.id}' AND label = 'picture'", ) - # Capture warnings directly off the rebuild module logger — the - # haiku.rag parent logger is configured non-propagating elsewhere - # in the suite so caplog can miss records. from haiku.rag.client import rebuild as rebuild_module - records: list[logging.LogRecord] = [] - - class _ListHandler(logging.Handler): - def emit(self, record: logging.LogRecord) -> None: - records.append(record) - - handler = _ListHandler(level=logging.WARNING) - rebuild_module.logger.addHandler(handler) - try: + with capture_logs(rebuild_module.logger, logging.WARNING) as records: n = await _patch_picture_descriptions(rag, created) - finally: - rebuild_module.logger.removeHandler(handler) assert n == 0 assert any("no stored picture bytes" in r.getMessage() for r in records) diff --git a/tests/test_search.py b/tests/test_search.py index e1eccf66..982b44f8 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -275,72 +275,35 @@ async def test_fts_search_targets_content_fts_column(temp_db_path): ) -def test_search_result_primary_label_prioritizes_structural_types(): - """Test _get_primary_label prioritizes structural labels correctly.""" - # Table should be prioritized - result = SearchResult( - content="test", - score=0.5, - chunk_id="c1", - document_id="d1", - labels=["paragraph", "table", "text"], - ) - assert result._get_primary_label() == "table" - - # Code should be prioritized over paragraph - result = SearchResult( - content="test", - score=0.5, - chunk_id="c2", - document_id="d2", - labels=["paragraph", "code"], - ) - assert result._get_primary_label() == "code" - - # list_item should be prioritized - result = SearchResult( - content="test", - score=0.5, - chunk_id="c3", - document_id="d3", - labels=["text", "list_item"], - ) - assert result._get_primary_label() == "list_item" - - # Returns first label when no priority match - result = SearchResult( - content="test", - score=0.5, - chunk_id="c4", - document_id="d4", - labels=["paragraph", "text"], - ) - assert result._get_primary_label() == "paragraph" - - # Returns None for empty labels - result = SearchResult( - content="test", - score=0.5, - chunk_id="c5", - document_id="d5", - labels=[], - ) - assert result._get_primary_label() is None - - # Image queries (bytes / PIL.Image) +def _png_bytes_query() -> bytes: + return b"\x89PNG\r\n\x1a\n" + + +def _pil_image_query(): + from PIL import Image as PILImageModule + + return PILImageModule.new("RGB", (8, 8), "red") + + @pytest.mark.asyncio -async def test_search_with_bytes_query_uses_multimodal_embedder( - temp_db_path, monkeypatch +@pytest.mark.parametrize( + "make_query", + [_png_bytes_query, _pil_image_query], + ids=["bytes", "pil"], +) +async def test_search_with_image_query_uses_multimodal_embedder( + temp_db_path, monkeypatch, make_query ): - """``client.search(bytes)`` embeds via ``embed_image`` and dispatches + """``client.search(image)`` embeds via ``embed_image`` and dispatches to vector-only chunk search (skipping FTS and reranker).""" from haiku.rag.embeddings import EmbedderWrapper from haiku.rag.store.models.chunk import Chunk - image_calls: list[bytes] = [] + query = make_query() + image_calls: list = [] class StubMultimodal(EmbedderWrapper): supports_images = True @@ -383,12 +346,12 @@ async def test_search_with_bytes_query_uses_multimodal_embedder( async with HaikuRAG(temp_db_path, create=True) as rag: rag.chunk_repository.search = fake_chunk_search # type: ignore[method-assign] - results = await rag.search(b"\x89PNG\r\n\x1a\n", limit=3, include_images=False) + results = await rag.search(query, limit=3, include_images=False) assert len(results) == 1 assert results[0].score == 0.91 - # The bytes were sent through the image embedder once. - assert image_calls == [b"\x89PNG\r\n\x1a\n"] + # The image was passed through to the image embedder untouched. + assert image_calls == [query] # The chunk repo received a pre-computed vector and an empty text query. assert received_kwargs["query_vector"] == [0.5, 0.5, 0.5, 0.5] assert received_kwargs["query"] == "" @@ -493,42 +456,6 @@ async def test_search_attaches_picture_bytes_for_multimodal_reranker( assert reranked_picture._picture_data is None -@pytest.mark.asyncio -async def test_search_with_pil_image_works_like_bytes(temp_db_path, monkeypatch): - from PIL import Image as PILImageModule - - from haiku.rag.embeddings import EmbedderWrapper - from haiku.rag.store.models.chunk import Chunk - - seen_types: list[type] = [] - - class StubMultimodal(EmbedderWrapper): - supports_images = True - - def __init__(self): - super().__init__(embedder=None, vector_dim=4) - - async def embed_image(self, image): - seen_types.append(type(image)) - return [0.1] * 4 - - monkeypatch.setattr( - "haiku.rag.store.engine.get_embedder", - lambda *a, **kw: StubMultimodal(), - ) - - async def fake_chunk_search(**kwargs): - return [(Chunk(content="x", metadata={}), 1.0)] - - async with HaikuRAG(temp_db_path, create=True) as rag: - rag.chunk_repository.search = fake_chunk_search # type: ignore[method-assign] - img = PILImageModule.new("RGB", (8, 8), "red") - results = await rag.search(img, include_images=False) - - assert len(results) == 1 - assert seen_types == [PILImageModule.Image] - - @pytest.mark.asyncio async def test_search_with_bytes_query_raises_for_text_only_embedder( temp_db_path,