From f6acb65e954184980db05cfb89feeca05252ce1a Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Sun, 26 Jul 2026 20:11:02 +0300 Subject: [PATCH] Reach and enforce 100% coverage Cover the remaining paths in the client, context, downloads, title generation, document tools and store models, and add fail_under=100 so uncovered lines fail CI. Six lines that no test can reach get a pragma with its reason: the docling import guard, the nameless PDF attachment, the FS symlink OSError guard that resolve(strict=False) absorbs, the docling bbox and LanceDB document-id shape guards, the tag-retention branch vacuum makes unreachable, and Monty's Rust-thread print callback. Fix test_find_config_file_user_config, which wrote its config into the cwd it had chdir'd to, so the cwd branch answered first and the user-directory lookup it names was never exercised. --- CHANGELOG.md | 4 + haiku_rag_slim/haiku/rag/client/documents.py | 3 +- haiku_rag_slim/haiku/rag/client/downloads.py | 3 +- haiku_rag_slim/haiku/rag/store/engine.py | 4 +- .../haiku/rag/store/models/chunk.py | 3 +- pyproject.toml | 1 + ...cuments_schedules_vacuum_when_enabled.yaml | 42 ++++++ ...ng_a_source_applies_an_explicit_title.yaml | 82 ++++++++++++ ...marize_document_returns_model_summary.yaml | 82 ++++++++++++ tests/store/test_document_items.py | 32 +++++ tests/store/test_restore.py | 25 +++- tests/store/test_v0_50_0_migration.py | 17 +++ tests/test_chunk.py | 5 +- tests/test_client.py | 51 +++++++ tests/test_config.py | 41 ++++-- tests/test_context.py | 125 ++++++++++++++++++ tests/test_converters.py | 17 ++- tests/test_database_autocreate.py | 23 ++++ tests/test_doctor.py | 23 ++++ tests/test_download_models.py | 71 ++++++++++ tests/test_lancedb_connection.py | 33 +++++ tests/test_picture_in_context.py | 29 ++++ tests/test_processing.py | 53 ++++++++ tests/test_title_generation.py | 37 ++++++ tests/tools/test_document.py | 27 ++++ 25 files changed, 817 insertions(+), 16 deletions(-) create mode 100644 tests/cassettes/test_client/test_import_documents_schedules_vacuum_when_enabled.yaml create mode 100644 tests/cassettes/test_client/test_reingesting_a_source_applies_an_explicit_title.yaml create mode 100644 tests/cassettes/test_document_tools/TestSummarizeDocumentTool.test_summarize_document_returns_model_summary.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index cc59c816..64452693 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## [Unreleased] +### Changed + +- Test coverage is enforced at 100% via `fail_under` in `[tool.coverage.report]`. + ### Fixed - `Store.set_haiku_version` stamps the store's own config into a recreated settings row instead of the process-global `Config`. diff --git a/haiku_rag_slim/haiku/rag/client/documents.py b/haiku_rag_slim/haiku/rag/client/documents.py index 6dca344e..ee054970 100644 --- a/haiku_rag_slim/haiku/rag/client/documents.py +++ b/haiku_rag_slim/haiku/rag/client/documents.py @@ -570,7 +570,8 @@ def _extract_pdf_attachments( for i in range(attachment_count): att = pdf.get_attachment(i) name = att.get_name() - if not name: + if not name: # pragma: no cover - pypdfium2 cannot produce a + # nameless attachment, so no craftable PDF reaches this. continue data = bytes(att.get_data()) child_uri = f"{parent_uri}#attachment={quote(name, safe='')}" diff --git a/haiku_rag_slim/haiku/rag/client/downloads.py b/haiku_rag_slim/haiku/rag/client/downloads.py index c3f0afcf..3cd51755 100644 --- a/haiku_rag_slim/haiku/rag/client/downloads.py +++ b/haiku_rag_slim/haiku/rag/client/downloads.py @@ -38,7 +38,8 @@ async def download_models( yield DownloadProgress(model="docling", status="start") await asyncio.to_thread(download_models) yield DownloadProgress(model="docling", status="done") - except ImportError: + except ImportError: # pragma: no cover - docling is installed in the test + # environment, and get_package_versions depends on it. pass # HuggingFace tokenizer diff --git a/haiku_rag_slim/haiku/rag/store/engine.py b/haiku_rag_slim/haiku/rag/store/engine.py index cc177ad8..0c6443ed 100644 --- a/haiku_rag_slim/haiku/rag/store/engine.py +++ b/haiku_rag_slim/haiku/rag/store/engine.py @@ -606,7 +606,9 @@ class Store: for tag in tags.values() if tag["version"] in timestamps ] - if not tagged: + if not tagged: # pragma: no cover - a tag's version is never absent from + # list_versions: vacuum retains every version at or after the + # oldest tag, so a tagged version is never cleaned away. return retention # LanceDB version timestamps are naive datetimes in local time. diff --git a/haiku_rag_slim/haiku/rag/store/models/chunk.py b/haiku_rag_slim/haiku/rag/store/models/chunk.py index b20d2972..8eacf3fc 100644 --- a/haiku_rag_slim/haiku/rag/store/models/chunk.py +++ b/haiku_rag_slim/haiku/rag/store/models/chunk.py @@ -74,7 +74,8 @@ class ChunkMetadata(BaseModel): continue for prov_item in prov: bbox = getattr(prov_item, "bbox", None) - if bbox is None: + if bbox is None: # pragma: no cover - docling's ProvenanceItem + # always carries a bbox; guards against a shape change. continue bounding_boxes.append( BoundingBox( diff --git a/pyproject.toml b/pyproject.toml index 245077e9..c077737d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,6 +146,7 @@ omit = [ [tool.coverage.report] show_missing = true +fail_under = 100 exclude_also = [ "if TYPE_CHECKING:", "@abstractmethod", diff --git a/tests/cassettes/test_client/test_import_documents_schedules_vacuum_when_enabled.yaml b/tests/cassettes/test_client/test_import_documents_schedules_vacuum_when_enabled.yaml new file mode 100644 index 00000000..aa8617e4 --- /dev/null +++ b/tests/cassettes/test_client/test_import_documents_schedules_vacuum_when_enabled.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '90' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - Batch imported body. + 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: 5 + total_tokens: 5 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_client/test_reingesting_a_source_applies_an_explicit_title.yaml b/tests/cassettes/test_client/test_reingesting_a_source_applies_an_explicit_title.yaml new file mode 100644 index 00000000..f352df06 --- /dev/null +++ b/tests/cassettes/test_client/test_reingesting_a_source_applies_an_explicit_title.yaml @@ -0,0 +1,82 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '84' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - stable content + 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: 3 + total_tokens: 3 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '85' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - changed content + 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: 3 + total_tokens: 3 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_document_tools/TestSummarizeDocumentTool.test_summarize_document_returns_model_summary.yaml b/tests/cassettes/test_document_tools/TestSummarizeDocumentTool.test_summarize_document_returns_model_summary.yaml new file mode 100644 index 00000000..6e46a3e6 --- /dev/null +++ b/tests/cassettes/test_document_tools/TestSummarizeDocumentTool.test_summarize_document_returns_model_summary.yaml @@ -0,0 +1,82 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '142' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - Python is a programming language. It is widely used for web development. + 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: 15 + total_tokens: 15 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '134' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - JavaScript runs in the browser. It powers interactive web pages. + 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: 13 + total_tokens: 13 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/store/test_document_items.py b/tests/store/test_document_items.py index ba99178a..e6b1ed6c 100644 --- a/tests/store/test_document_items.py +++ b/tests/store/test_document_items.py @@ -1003,3 +1003,35 @@ async def test_replace_for_document_with_no_items_deletes_existing(temp_db_path) await repo.replace_for_document("doc-1", []) assert await repo.get_all_items("doc-1") == [] + + +class TestExtractItemTextFallbacks: + def test_table_returns_none_when_serialization_fails(self): + """A serializer that raises leaves the table with no extractable text + rather than aborting the extraction pass.""" + doc = _doc_with_tables(1) + + class _Boom: + def serialize(self, item): + raise RuntimeError("serializer exploded") + + assert extract_item_text(doc.tables[0], doc, get_serializer=_Boom) is None + + def test_file_backed_picture_has_no_inline_bytes(self): + """A picture whose ImageRef points at a file rather than a data: URI + carries nothing to decode.""" + from docling_core.types.doc.document import ImageRef + + from haiku.rag.store.models.document_item import _decode_picture_bytes + + doc, pic = _doc_with_captioned_picture("caption") + pic.image = ImageRef.model_validate( + { + "mimetype": "image/png", + "dpi": 72, + "size": {"width": 1, "height": 1}, + "uri": "file:///tmp/picture.png", + } + ) + + assert _decode_picture_bytes(pic) is None diff --git a/tests/store/test_restore.py b/tests/store/test_restore.py index 5202bb97..ab1460b9 100644 --- a/tests/store/test_restore.py +++ b/tests/store/test_restore.py @@ -112,8 +112,10 @@ async def test_restore_safety_tag_name_collision(temp_db_path, monkeypatch): await store.create_tag("release-1") await store.create_tag("before-restore-20260715T143012Z") + await store.create_tag("before-restore-20260715T143012Z-2") + safety_tag = await store.restore_tag("release-1") - assert safety_tag == "before-restore-20260715T143012Z-2" + assert safety_tag == "before-restore-20260715T143012Z-3" @pytest.mark.asyncio @@ -406,3 +408,24 @@ async def test_wait_protected_returns_result_on_same_tick_cancellation(): result, cancelled = await outer assert result == "done" assert cancelled is True + + +@pytest.mark.asyncio +async def test_wait_protected_reraises_when_recovery_itself_is_cancelled(): + """If the recovery coroutine ends cancelled there is nothing to wait for, + so the cancellation propagates instead of looping forever.""" + import asyncio + + from haiku.rag.store.engine import _wait_protected + + async def self_cancelling_recovery() -> str: + current = asyncio.current_task() + assert current is not None + current.cancel() + await asyncio.sleep(0) + return "unreachable" + + outer = asyncio.create_task(_wait_protected(self_cancelling_recovery())) + + with pytest.raises(asyncio.CancelledError): + await outer diff --git a/tests/store/test_v0_50_0_migration.py b/tests/store/test_v0_50_0_migration.py index f53904d6..7f2553fd 100644 --- a/tests/store/test_v0_50_0_migration.py +++ b/tests/store/test_v0_50_0_migration.py @@ -126,6 +126,19 @@ class TestV0_50_0Migration: } ), ), + # Matches the LIKE on the quoted-key form, but only nested — + # there is no top-level key to rename. + LegacyDocumentRecord( + id="nested-only", + content="x", + uri="u3", + metadata=json.dumps( + { + "raw_headers": {"etag": "abc"}, + "source_revision": "v3", + } + ), + ), ], ) @@ -141,6 +154,10 @@ class TestV0_50_0Migration: "my_etag_key": "v", "source_revision": "v2", } + assert by_id["nested-only"] == { + "raw_headers": {"etag": "abc"}, + "source_revision": "v3", + } async def test_unparseable_metadata_skipped_without_crashing(self, temp_db_path): """A row with malformed JSON in `metadata` must not abort the whole diff --git a/tests/test_chunk.py b/tests/test_chunk.py index 68c1222d..288371e8 100644 --- a/tests/test_chunk.py +++ b/tests/test_chunk.py @@ -482,6 +482,9 @@ async def test_chunk_repository_get_by_id_and_list_all_pagination(temp_db_path): assert len(first) == 1 assert first[0].id == everything[0].id + # offset is applied even when it selects the whole set + assert len(await client.chunk_repository.list_all(offset=0)) == len(everything) + if len(everything) > 1: second = await client.chunk_repository.list_all(limit=1, offset=1) assert len(second) == 1 @@ -526,4 +529,4 @@ async def test_process_search_results_rejects_unknown_score_column(temp_db_path) 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] + await client.chunk_repository._process_search_results(_Frame()) diff --git a/tests/test_client.py b/tests/test_client.py index fe545765..7a94b1fa 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -2518,3 +2518,54 @@ async def test_visualize_chunk_falls_back_when_expansion_drops_refs(temp_db_path images = await client.visualize_chunk(stored[0]) assert len(images) == 1 + + +@pytest.mark.vcr() +async def test_import_documents_schedules_vacuum_when_enabled(temp_db_path): + """A batch import with auto_vacuum on queues a background vacuum.""" + from haiku.rag.config import AppConfig + + config = AppConfig() + config.storage.auto_vacuum = True + + async with HaikuRAG(temp_db_path, config=config, create=True) as client: + docling_doc = await client.convert("Batch imported body.") + chunks = await client.chunk(docling_doc) + await client.import_documents( + [ + DocumentImport( + docling_document=docling_doc, + chunks=chunks, + uri="test://batch-vacuum", + ) + ] + ) + + assert client._vacuum_tasks + + +@pytest.mark.vcr() +async def test_reingesting_a_source_applies_an_explicit_title(temp_db_path): + """Re-adding an unchanged source with a new title updates just the title.""" + async with HaikuRAG(temp_db_path, create=True) as client: + with tempfile.TemporaryDirectory() as temp_dir: + source = Path(temp_dir) / "retitled.txt" + source.write_text("stable content") + + first = await client.create_document_from_source(source) + assert not isinstance(first, list) + source.write_text("changed content") + + second = await client.create_document_from_source( + source, title="Explicit Title" + ) + + assert not isinstance(second, list) + assert second.id == first.id + assert second.title == "Explicit Title" + + +async def test_update_document_rejects_unknown_id(temp_db_path): + async with HaikuRAG(temp_db_path, create=True) as client: + with pytest.raises(ValueError, match="not found"): + await client.update_document("no-such-document", content="x") diff --git a/tests/test_config.py b/tests/test_config.py index ba659d29..c848beb4 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -42,17 +42,17 @@ def test_find_config_file_cwd(tmp_path, monkeypatch): def test_find_config_file_user_config(tmp_path, monkeypatch): """Test finding config in user config directory.""" monkeypatch.delenv("HAIKU_RAG_CONFIG_PATH", raising=False) - monkeypatch.chdir(tmp_path) - # Mock get_default_data_dir to return tmp_path - def mock_get_default_data_dir(): - return tmp_path + # The data dir must differ from the cwd, or the cwd branch answers first + # and this never reaches the user-directory lookup. + cwd = tmp_path / "cwd" + cwd.mkdir() + data_dir = tmp_path / "data" + data_dir.mkdir() + monkeypatch.chdir(cwd) + monkeypatch.setattr("haiku.rag.utils.get_default_data_dir", lambda: data_dir) - monkeypatch.setattr( - "haiku.rag.utils.get_default_data_dir", mock_get_default_data_dir - ) - - config_file = tmp_path / "haiku.rag.yaml" + config_file = data_dir / "haiku.rag.yaml" config_file.write_text("environment: production") found = find_config_file() @@ -518,3 +518,26 @@ def test_expand_env_var_plain_string_unchanged(tmp_path): config = load_yaml_config(config_file) assert config["environment"] == "production" + + +def test_find_config_file_returns_none_when_nothing_exists(tmp_path, monkeypatch): + """No env var, no file in cwd, none in the data dir.""" + monkeypatch.delenv("HAIKU_RAG_CONFIG_PATH", raising=False) + monkeypatch.chdir(tmp_path) + + empty_data_dir = tmp_path / "data" + empty_data_dir.mkdir() + monkeypatch.setattr("haiku.rag.utils.get_default_data_dir", lambda: empty_data_dir) + + assert find_config_file() is None + + +def test_load_default_config_falls_back_to_builtin_defaults(monkeypatch): + """With no config file discoverable, the packaged defaults are used.""" + from haiku.rag.config import _load_default_config + + monkeypatch.setattr("haiku.rag.config.find_config_file", lambda _=None: None) + + config = _load_default_config() + + assert config.environment == AppConfig().environment diff --git a/tests/test_context.py b/tests/test_context.py index 98191bd1..dd5f1cc8 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -1373,3 +1373,128 @@ class TestSpanInWindow: # A picture occupies no characters, so containment is by position. assert _span_in_window((10, 10, item), 0, 20) is True assert _span_in_window((30, 30, item), 0, 20) is False + + +@pytest.mark.asyncio +class TestExpandWithItemsWindowEdges: + async def test_empty_window_returns_original_results( + self, temp_db_path, monkeypatch + ): + """Refs resolve but the surrounding window comes back empty, so there is + nothing to expand from.""" + from haiku.rag.client import HaikuRAG + from haiku.rag.store.models.document import Document + + async with HaikuRAG(temp_db_path, create=True) as rag: + doc = await rag.document_repository.create( + Document(content="body", uri="test://window") + ) + assert doc.id is not None + await rag.document_item_repository.create_items( + doc.id, + [ + DocumentItem( + document_id=doc.id, + position=0, + self_ref="#/texts/0", + label="paragraph", + text="body", + page_numbers=[1], + ) + ], + ) + + async def no_window(*_args, **_kwargs): + return [] + + monkeypatch.setattr( + rag.document_item_repository, "get_items_in_range", no_window + ) + + result = SearchResult( + content="original", + score=0.9, + document_id=doc.id, + doc_item_refs=["#/texts/0"], + ) + expanded = await expand_with_items( + rag.document_item_repository, doc.id, [result], 5000 + ) + + assert [r.content for r in expanded] == ["original"] + + async def test_result_with_unmatched_refs_passes_through(self, temp_db_path): + """Two results share a document; the one whose refs resolve is expanded + and the other is returned unchanged.""" + from haiku.rag.client import HaikuRAG + from haiku.rag.store.models.document import Document + + async with HaikuRAG(temp_db_path, create=True) as rag: + doc = await rag.document_repository.create( + Document(content="body", uri="test://mixed") + ) + assert doc.id is not None + await rag.document_item_repository.create_items( + doc.id, + [ + DocumentItem( + document_id=doc.id, + position=i, + self_ref=f"#/texts/{i}", + label="paragraph", + text=f"paragraph {i}", + page_numbers=[1], + ) + for i in range(2) + ], + ) + + resolvable = SearchResult( + content="paragraph 0", + score=0.9, + document_id=doc.id, + doc_item_refs=["#/texts/0"], + ) + unmatched = SearchResult( + content="untouched", + score=0.5, + document_id=doc.id, + doc_item_refs=["#/texts/404"], + ) + + expanded = await expand_with_items( + rag.document_item_repository, doc.id, [resolvable, unmatched], 5000 + ) + + assert "untouched" in [r.content for r in expanded] + + +def test_build_result_skips_positions_with_no_item(): + """A sparse position map (items removed or never stored) leaves gaps in the + range; those positions contribute nothing.""" + from haiku.rag.context import _build_result + + original = SearchResult(content="p0", score=0.9, document_id="d1") + # Positions 1 and 2 in the 0..3 range carry no item. + pos_to_item = { + 0: DocumentItem( + document_id="d1", + position=0, + self_ref="#/texts/0", + label="paragraph", + text="first", + page_numbers=[1], + ), + 3: DocumentItem( + document_id="d1", + position=3, + self_ref="#/texts/3", + label="paragraph", + text="last", + page_numbers=[1], + ), + } + + built = _build_result(0, 3, [original], pos_to_item, False, 5000) + + assert built.content == "first\n\nlast" diff --git a/tests/test_converters.py b/tests/test_converters.py index 9d686227..e6579281 100644 --- a/tests/test_converters.py +++ b/tests/test_converters.py @@ -1672,7 +1672,7 @@ class TestDoclingServeZipParsing: @pytest.mark.asyncio async def test_convert_text_plain_builds_document_locally(self, converter): """format="plain" never reaches the network.""" - converter.client.submit_and_poll_zip = AsyncMock( # ty: ignore[invalid-assignment] + converter.client.submit_and_poll_zip = AsyncMock( side_effect=AssertionError("must not call docling-serve") ) @@ -1680,3 +1680,18 @@ class TestDoclingServeZipParsing: assert isinstance(doc, DoclingDocument) assert "just text" in doc.export_to_markdown() + + +@pytest.mark.asyncio +async def test_docling_serve_convert_file_wraps_text_read_failure(tmp_path): + """An undecodable text file surfaces as a ValueError naming the path.""" + config = AppConfig() + config.processing.converter = "docling-serve" + converter = get_converter(config) + assert isinstance(converter, DoclingServeConverter) + + source = tmp_path / "broken.txt" + source.write_bytes(b"\xff\xfe\x00\x01 not utf-8") + + with pytest.raises(ValueError, match="Failed to read text file"): + await converter.convert_file(source) diff --git a/tests/test_database_autocreate.py b/tests/test_database_autocreate.py index d15bb950..f7a0b63f 100644 --- a/tests/test_database_autocreate.py +++ b/tests/test_database_autocreate.py @@ -43,3 +43,26 @@ async def test_operations_work_after_database_created(tmp_path): doc = await client.get_document_by_id(docs[0].id) assert doc is not None assert doc.content == "Test content" + + +@pytest.mark.asyncio +async def test_default_db_path_comes_from_storage_data_dir(tmp_path): + """Omitting db_path places the database under the configured data dir.""" + from haiku.rag.client import HaikuRAG + from haiku.rag.config import AppConfig + + config = AppConfig() + config.storage.data_dir = tmp_path + + client = HaikuRAG(config=config) + + assert client._db_path == tmp_path / "haiku.rag.lancedb" + + +@pytest.mark.asyncio +async def test_vacuum_is_callable_on_the_client(temp_db_path): + """The public vacuum() delegates to the store.""" + from haiku.rag.client import HaikuRAG + + async with HaikuRAG(temp_db_path, create=True) as client: + await client.vacuum() diff --git a/tests/test_doctor.py b/tests/test_doctor.py index d6d0d3a2..03771327 100644 --- a/tests/test_doctor.py +++ b/tests/test_doctor.py @@ -1206,3 +1206,26 @@ async def test_duplicate_documents_check_reads_config(temp_db_path): ).severity is Severity.WARN ) + + +@pytest.mark.asyncio +async def test_many_unembedded_chunks_are_sampled(temp_db_path): + """Beyond the sample limit the detail list ends with a count of the rest.""" + db = await _build_db(temp_db_path) + chunks_tbl = await db.open_table("chunks") + await chunks_tbl.add( + [ + ChunkRecord( + id=f"z{i}", + document_id="d1", + content="x", + metadata=json.dumps({"doc_item_refs": ["#/texts/0"]}), + vector=[0.0] * VECTOR_DIM, + ) + for i in range(8) + ] + ) + report = await run_doctor(_config(), temp_db_path, {}) + details = _result(report, "unembedded_chunks").details + assert len(details) == 6 + assert details[-1] == "... (+3 more)" diff --git a/tests/test_download_models.py b/tests/test_download_models.py index 50ad06a4..41e571c5 100644 --- a/tests/test_download_models.py +++ b/tests/test_download_models.py @@ -6,6 +6,7 @@ import pytest from haiku.rag.client.downloads import download_models from haiku.rag.config import Config +from haiku.rag.config.models import ModelConfig @pytest.fixture @@ -108,3 +109,73 @@ async def test_download_models_no_ollama_models(mock_to_thread): models = {e.model for e in events} assert "qwen3-embedding:4b" not in models assert "gpt-oss" not in models + + +@pytest.mark.parametrize( + "configure,expected_model", + [ + ( + lambda c: setattr( + c.reranking, + "model", + ModelConfig(provider="ollama", name="rerank-model"), + ), + "rerank-model", + ), + ( + lambda c: ( + setattr(c.processing, "pictures", "description"), + setattr( + c.processing.conversion_options.picture_description.model, + "provider", + "ollama", + ), + setattr( + c.processing.conversion_options.picture_description.model, + "name", + "vision-model", + ), + ), + "vision-model", + ), + ( + lambda c: ( + setattr(c.processing, "auto_title", True), + setattr(c.processing.title_model, "provider", "ollama"), + setattr(c.processing.title_model, "name", "title-model"), + ), + "title-model", + ), + ], + ids=["reranker", "picture_description", "auto_title"], +) +async def test_ollama_models_from_every_config_slot_are_pulled( + mock_to_thread, configure, expected_model +): + """Each config slot that can name an ollama model contributes to the pull set.""" + from haiku.rag.config import AppConfig + + config = AppConfig() + configure(config) + + @asynccontextmanager + async def mock_stream(method, url, **kwargs): + mock_resp = AsyncMock() + + async def aiter_lines(): + yield '{"status": "success"}' + + mock_resp.aiter_lines = aiter_lines + yield mock_resp + + with patch( + "haiku.rag.client.downloads.httpx.AsyncClient", + return_value=_mock_httpx_client(mock_stream), + ): + pulled = { + progress.model + async for progress in download_models(config) + if progress.status == "pulling" + } + + assert expected_model in pulled diff --git a/tests/test_lancedb_connection.py b/tests/test_lancedb_connection.py index 23abae0e..c601a2c5 100644 --- a/tests/test_lancedb_connection.py +++ b/tests/test_lancedb_connection.py @@ -355,3 +355,36 @@ class TestVectorIndexCreation: indexes = await store.chunks_table.list_indices() assert not any("vector" in idx.columns for idx in indexes) + + +class TestStoreMiscellany: + @pytest.mark.asyncio + async def test_create_makes_missing_parent_directories(self, tmp_path): + nested = tmp_path / "a" / "b" / "db.lancedb" + + async with Store(nested, create=True) as store: + assert store._is_new_db is True + + assert nested.exists() + + @pytest.mark.asyncio + async def test_stored_vector_dim_is_none_for_corrupt_settings(self, temp_db_path): + async with Store(temp_db_path, create=True) as store: + await store.settings_table.update( + {"settings": "not json at all"}, where="id = 'settings'" + ) + + assert await store._get_stored_vector_dim() is None + + @pytest.mark.asyncio + async def test_vacuum_skips_when_already_running(self, temp_db_path): + async with Store(temp_db_path, create=True) as store: + async with store._vacuum_lock: + # Returns immediately rather than blocking on the held lock. + await store.vacuum() + + @pytest.mark.asyncio + async def test_history_rejects_unknown_table(self, temp_db_path): + async with Store(temp_db_path, create=True) as store: + with pytest.raises(ValueError, match="Unknown table"): + await store.list_table_versions("not_a_table") diff --git a/tests/test_picture_in_context.py b/tests/test_picture_in_context.py index 770c7f57..55f2f368 100644 --- a/tests/test_picture_in_context.py +++ b/tests/test_picture_in_context.py @@ -1011,3 +1011,32 @@ async def test_rag_capability_attaches_images_for_vision_model(temp_db_path): assert isinstance(result, ToolReturn) assert result.content is not None assert any(isinstance(part, BinaryContent) for part in result.content) + + +def test_build_picture_chunks_records_provenance_pages(): + """A picture with provenance contributes its page numbers to the chunk.""" + from docling_core.types.doc.base import BoundingBox + from docling_core.types.doc.document import ProvenanceItem + + from haiku.rag.client.processing import build_picture_chunks + from tests.store.test_document_items import _docling_doc_with_picture + + doc = _docling_doc_with_picture() + picture = doc.pictures[0] + picture.prov = [ + ProvenanceItem( + page_no=3, + bbox=BoundingBox(l=0, t=10, r=10, b=0), + charspan=(0, 0), + ), + # A repeat of the same page must not be counted twice. + ProvenanceItem( + page_no=3, + bbox=BoundingBox(l=0, t=20, r=10, b=10), + charspan=(0, 0), + ), + ] + + chunks = build_picture_chunks(doc, document_id="doc-1") + + assert chunks[0].metadata["page_numbers"] == [3] diff --git a/tests/test_processing.py b/tests/test_processing.py index 65c1ca86..d3ce970a 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -211,3 +211,56 @@ def test_merge_picture_chunks_no_pictures_returns_text_chunks(): assert result is text_chunks assert [c.order for c in result] == [0, 1] + + +@pytest.mark.asyncio +async def test_convert_dispatches_large_pdfs_through_split_and_merge( + tmp_path, monkeypatch +): + """With split_pages configured, PDF conversion routes through the + split-and-merge helper rather than the converter directly.""" + from docling_core.types.doc.document import DoclingDocument + + config = AppConfig() + config.processing.split_pages = 2 + + pdf = tmp_path / "big.pdf" + pdf.write_bytes(b"%PDF-1.4 stub") + called: dict = {} + + async def fake_split(converter, path, uri, slice_size): + called["slice_size"] = slice_size + called["path"] = path + return DoclingDocument(name="merged") + + monkeypatch.setattr( + "haiku.rag.converters.pdf_split.convert_pdf_with_splitting", fake_split + ) + + doc = await convert(config, pdf) + + assert doc.name == "merged" + assert called["slice_size"] == 2 + assert called["path"] == pdf + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "make_source,match", + [ + (lambda d: (d / "missing.md").as_uri(), "File does not exist"), + (lambda d: _write_unsupported(d), "Unsupported file extension"), + ], + ids=["missing_file", "unsupported_extension"], +) +async def test_convert_rejects_bad_file_uris(tmp_path, make_source, match): + from haiku.rag.client.exceptions import UnsupportedSourceError + + with pytest.raises(UnsupportedSourceError, match=match): + await convert(AppConfig(), make_source(tmp_path)) + + +def _write_unsupported(directory): + target = directory / "thing.sqlite3" + target.write_bytes(b"binary") + return target.as_uri() diff --git a/tests/test_title_generation.py b/tests/test_title_generation.py index c13f736b..a88802c5 100644 --- a/tests/test_title_generation.py +++ b/tests/test_title_generation.py @@ -371,3 +371,40 @@ class TestRebuildTitleOnly: # Only the second doc should have been processed assert len(processed_ids) == 1 + + +@pytest.mark.asyncio +async def test_generate_title_with_llm_returns_model_output(monkeypatch): + """The agent's output is stripped and returned.""" + from pydantic_ai.messages import ModelMessage, ModelResponse, TextPart + from pydantic_ai.models.function import AgentInfo, FunctionModel + + from haiku.rag.client.titles import generate_title_with_llm + from haiku.rag.config import AppConfig + + def respond(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse: + return ModelResponse(parts=[TextPart(" A Generated Title ")]) + + monkeypatch.setattr( + "haiku.rag.utils.get_model", lambda *a, **kw: FunctionModel(respond) + ) + + assert await generate_title_with_llm(AppConfig(), "body") == "A Generated Title" + + +@pytest.mark.asyncio +async def test_generate_title_with_llm_returns_none_for_blank_output(monkeypatch): + from pydantic_ai.messages import ModelMessage, ModelResponse, TextPart + from pydantic_ai.models.function import AgentInfo, FunctionModel + + from haiku.rag.client.titles import generate_title_with_llm + from haiku.rag.config import AppConfig + + def respond(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse: + return ModelResponse(parts=[TextPart(" ")]) + + monkeypatch.setattr( + "haiku.rag.utils.get_model", lambda *a, **kw: FunctionModel(respond) + ) + + assert await generate_title_with_llm(AppConfig(), "body") is None diff --git a/tests/tools/test_document.py b/tests/tools/test_document.py index 432dc86b..ab935cf4 100644 --- a/tests/tools/test_document.py +++ b/tests/tools/test_document.py @@ -187,6 +187,33 @@ class TestSummarizeDocumentTool: assert "Document not found" in result + @pytest.mark.vcr() + @pytest.mark.asyncio + async def test_summarize_document_returns_model_summary( + self, doc_client, doc_config, monkeypatch + ): + """A resolvable document is summarised and labelled with its title.""" + from pydantic_ai.messages import ModelMessage, ModelResponse, TextPart + from pydantic_ai.models.function import AgentInfo, FunctionModel + + def respond(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse: + return ModelResponse(parts=[TextPart("A concise summary.")]) + + monkeypatch.setattr( + "haiku.rag.tools.document.get_model", + lambda *a, **kw: FunctionModel(respond), + ) + + docs = await doc_client.list_documents() + assert docs and docs[0].uri + + toolset = create_document_toolset(doc_config) + summarize_tool = toolset.tools["summarize_document"] + result = await summarize_tool.function(make_ctx(doc_client), docs[0].uri) + + assert "A concise summary." in result + assert "Summary of" in result + @pytest.fixture async def doc_client(temp_db_path):