From 1e65d36766161f7aaf32d226727c613f9a7848cc Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Tue, 16 Jun 2026 15:30:31 +0300 Subject: [PATCH] Pass fetched FetchResult to ingester metadata providers --- CHANGELOG.md | 1 + docs/ingester.md | 26 ++- haiku_rag_slim/haiku/rag/client/__init__.py | 3 + haiku_rag_slim/haiku/rag/client/documents.py | 45 ++++- haiku_rag_slim/haiku/rag/ingester/metadata.py | 11 +- .../haiku/rag/ingester/workers/pipeline.py | 17 +- ...threads_configured_source_to_provider.yaml | 42 +++++ ...data_provider_applies_to_fresh_create.yaml | 42 +++++ ...ged_revision_keeps_head_short_circuit.yaml | 42 +++++ ...result_cannot_corrupt_source_metadata.yaml | 42 +++++ tests/ingester/test_metadata.py | 13 +- tests/ingester/test_pipeline.py | 52 +++--- tests/ingester/test_revision_round_trip.py | 159 +++++++++++++++++- tests/ingester/test_workers.py | 2 +- 14 files changed, 436 insertions(+), 61 deletions(-) create mode 100644 tests/cassettes/test_revision_round_trip/test_directory_ingest_threads_configured_source_to_provider.yaml create mode 100644 tests/cassettes/test_revision_round_trip/test_metadata_provider_applies_to_fresh_create.yaml create mode 100644 tests/cassettes/test_revision_round_trip/test_provider_backed_unchanged_revision_keeps_head_short_circuit.yaml create mode 100644 tests/cassettes/test_revision_round_trip/test_provider_mutating_fetch_result_cannot_corrupt_source_metadata.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e159d05..be6a5cd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Bump `docling>=2.102.2,<3.0.0` and `docling-core>=2.82.0,<3.0.0`; the `<3.0.0` cap holds the DoclingDocument schema at 1.10.0. - Relax `opencv-python-headless` to `>=4.6.0.66,<5.0.0.0` (was `>=4.13.0.92`) to match `docling-ibm-models`' declared range. +- `haiku.rag.metadata_providers` callables take a third argument, the fetched `FetchResult`: `__call__(source_id, uri, result)`. The provider runs after fetch instead of before; on revision-unchanged sweeps it is skipped and existing provider metadata is preserved. ### Fixed diff --git a/docs/ingester.md b/docs/ingester.md index f48b883e..bd60684b 100644 --- a/docs/ingester.md +++ b/docs/ingester.md @@ -188,8 +188,9 @@ apply. A source can attach custom metadata to every document it ingests by naming a `metadata_provider`. The provider is a callable that an external package registers under the `haiku.rag.metadata_providers` entry-point -group; the ingester calls it per document with `(source_id, uri)` and -merges the returned dict into the document's metadata. +group; when the document is fetched for ingestion, the ingester calls it +with `(source_id, uri, result)`, where `result` is the source's +`FetchResult`, and merges the returned dict into the document's metadata. ```yaml - type: webdav @@ -205,13 +206,18 @@ so a class is its own factory: # example_pkg/__init__.py from urllib.parse import urlparse +from haiku.rag.ingester.sources import FetchResult + class Provider: - async def __call__(self, source_id: str, uri: str) -> dict: + async def __call__( + self, source_id: str, uri: str, result: FetchResult + ) -> dict: path = urlparse(uri).path return { "collection": source_id, "folder": path.rsplit("/", 1)[0] or "/", + "bytes": str(len(result.body)), } ``` @@ -222,11 +228,15 @@ example-provider = "example_pkg:Provider" ``` The provider is built once at startup, so it can hold a client or cache -across calls. The source-derived keys (`md5`, `source_revision`, -`content_type`) are stripped from provider output, so a provider cannot -override them. A `metadata_provider` name with no installed entry point -fails at startup. A provider exception is classified like any other -ingestion error (network and timeout errors retry; others go to the DLQ). +across calls. When a document's source revision is unchanged, the +ingester keeps the existing cheap HEAD short-circuit and preserves the +stored provider metadata; the provider runs again when the document is +fetched for a new or changed revision. The source-derived keys (`md5`, +`source_revision`, `content_type`) are stripped from provider output, so +a provider cannot override them. A `metadata_provider` name with no +installed entry point fails at startup. A provider exception is +classified like any other ingestion error (network and timeout errors +retry; others go to the DLQ). ### Custom sources diff --git a/haiku_rag_slim/haiku/rag/client/__init__.py b/haiku_rag_slim/haiku/rag/client/__init__.py index 3c86667a..4546da7d 100644 --- a/haiku_rag_slim/haiku/rag/client/__init__.py +++ b/haiku_rag_slim/haiku/rag/client/__init__.py @@ -34,6 +34,7 @@ if TYPE_CHECKING: from PIL import Image as PILImage from haiku.rag.embeddings import EmbedderWrapper + from haiku.rag.ingester.metadata import MetadataProvider from haiku.rag.ingester.sources.base import Source from haiku.rag.reranking.base import RerankerBase from haiku.rag.sandbox import AnalysisResult @@ -279,6 +280,7 @@ class HaikuRAG: storage_options: dict[str, str] | None = None, sources: "list[Source] | None" = None, source_id: str | None = None, + metadata_provider: "MetadataProvider | None" = None, ) -> Document | list[Document]: from haiku.rag.client.documents import create_document_from_source @@ -291,6 +293,7 @@ class HaikuRAG: storage_options=storage_options, sources=sources, source_id=source_id, + metadata_provider=metadata_provider, ) async def update_document( diff --git a/haiku_rag_slim/haiku/rag/client/documents.py b/haiku_rag_slim/haiku/rag/client/documents.py index fd868546..c2c17cde 100644 --- a/haiku_rag_slim/haiku/rag/client/documents.py +++ b/haiku_rag_slim/haiku/rag/client/documents.py @@ -24,6 +24,7 @@ if TYPE_CHECKING: from docling_core.types.doc.document import DoclingDocument from haiku.rag.client import HaikuRAG + from haiku.rag.ingester.metadata import MetadataProvider from haiku.rag.ingester.sources.base import FetchResult, Source logger = logging.getLogger(__name__) @@ -51,6 +52,12 @@ class DocumentImport: # and is skipped. MAX_ATTACHMENT_DEPTH = 3 +# Keys the source pipeline owns (content_type/md5/source_revision, which drive +# sync_state). A provider must not set them, or the metadata-only refresh path +# would let provider values overwrite the real source-derived ones. Stripped +# before provider metadata is merged into the document. +_RESERVED_METADATA_KEYS = frozenset({"content_type", "md5", "source_revision"}) + def parent_uri_filter(parent_uri: str) -> str: """SQL `WHERE` clause matching documents whose ``metadata.parent_uri`` @@ -332,6 +339,26 @@ async def _refresh_doc_metadata( return doc +async def _provider_metadata( + provider: "MetadataProvider | None", + source_id: str, + uri: str, + result: "FetchResult", +) -> dict: + if provider is None: + return {} + # Hand the provider an isolated copy: mutating the live FetchResult + # (e.g. result.content_hash or result.extra_metadata) would feed the + # MD5 short-circuit and source_meta, bypassing the reserved-key filter + # that only guards the returned dict. + provider_result = result.model_copy(deep=True) + return { + k: v + for k, v in (await provider(source_id, uri, provider_result)).items() + if k not in _RESERVED_METADATA_KEYS + } + + async def _ingest_fetch_result( client: "HaikuRAG", result: "FetchResult", @@ -575,6 +602,7 @@ async def create_document_from_source( storage_options: dict[str, str] | None = None, sources: "list[Source] | None" = None, source_id: str | None = None, + metadata_provider: "MetadataProvider | None" = None, ) -> Document | list[Document]: """Create or update document(s) from a file path, directory, or URL. @@ -621,7 +649,13 @@ async def create_document_from_source( for child in local_path.rglob("*"): if child.is_file() and filter.include_file(str(child)): doc = await create_document_from_source( - client, child, title=None, metadata=metadata + client, + child, + title=None, + metadata=metadata, + sources=sources, + source_id=source_id, + metadata_provider=metadata_provider, ) assert isinstance(doc, Document) documents.append(doc) @@ -692,6 +726,11 @@ async def create_document_from_source( fetch_span.set_attribute("bytes", len(result.body)) fetch_span.set_attribute("content_hash", result.content_hash) + provider_metadata = await _provider_metadata( + metadata_provider, source_id or fetcher.source_id, source_str, result + ) + user_metadata = {**metadata, **provider_metadata} + # MD5 short-circuit: the bytes are unchanged even if the revision wasn't. # Refresh the source-derived metadata (revision may have rolled) but skip # convert/embed/store entirely. @@ -707,7 +746,7 @@ async def create_document_from_source( client, existing_doc, title=title, - user_metadata=metadata, + user_metadata=user_metadata, source_metadata=source_meta, ) @@ -715,7 +754,7 @@ async def create_document_from_source( client, result, title=title, - user_metadata=metadata, + user_metadata=user_metadata, stored_uri=stored_uri, existing_doc=existing_doc, ) diff --git a/haiku_rag_slim/haiku/rag/ingester/metadata.py b/haiku_rag_slim/haiku/rag/ingester/metadata.py index 1a8a2d54..39a98d3b 100644 --- a/haiku_rag_slim/haiku/rag/ingester/metadata.py +++ b/haiku_rag_slim/haiku/rag/ingester/metadata.py @@ -1,6 +1,9 @@ from collections.abc import Callable, Iterable, Mapping from importlib.metadata import entry_points -from typing import Protocol, runtime_checkable +from typing import TYPE_CHECKING, Protocol, runtime_checkable + +if TYPE_CHECKING: + from haiku.rag.ingester.sources.base import FetchResult ENTRY_POINT_GROUP = "haiku.rag.metadata_providers" @@ -10,9 +13,11 @@ class MetadataProvider(Protocol): """Computes per-document metadata for the ingester. A package registers a zero-arg factory under the ``haiku.rag.metadata_providers`` entry-point group; the factory returns an instance whose ``__call__`` the ingester - invokes per job with the document's source id and uri.""" + invokes per job with the document's source id, uri, and fetched content.""" - async def __call__(self, source_id: str, uri: str) -> dict: ... + async def __call__( + self, source_id: str, uri: str, result: "FetchResult" + ) -> dict: ... MetadataProviderFactory = Callable[[], MetadataProvider] diff --git a/haiku_rag_slim/haiku/rag/ingester/workers/pipeline.py b/haiku_rag_slim/haiku/rag/ingester/workers/pipeline.py index d8df7efd..f6c4bc90 100644 --- a/haiku_rag_slim/haiku/rag/ingester/workers/pipeline.py +++ b/haiku_rag_slim/haiku/rag/ingester/workers/pipeline.py @@ -20,13 +20,6 @@ if TYPE_CHECKING: from haiku.rag.ingester.sources.base import Source -# Keys the source pipeline owns (content_type/md5/source_revision and the -# source_revision/md5 that drive sync_state). A provider must not set them, or -# the metadata-only refresh path would let provider values overwrite the real -# source-derived ones. Stripped before provider metadata reaches the client. -_RESERVED_METADATA_KEYS = frozenset({"content_type", "md5", "source_revision"}) - - class JobResult(BaseModel): """What the worker needs after a successful job: enough metadata to update sync_state. document_id is None for DELETE ops.""" @@ -134,19 +127,11 @@ async def run_job( await client.delete_document(doc.id) return JobResult(deleted=True) - provider = (metadata_providers or {}).get(job.source_id) - extra_metadata: dict | None = None - if provider is not None: - extra_metadata = { - k: v - for k, v in (await provider(job.source_id, job.uri)).items() - if k not in _RESERVED_METADATA_KEYS - } result = await client.create_document_from_source( job.uri, sources=sources, source_id=job.source_id, - metadata=extra_metadata, + metadata_provider=(metadata_providers or {}).get(job.source_id), ) # Directory ingestion returns list[Document] — workers ingest single # resources, so a list here is a programming error in the caller. diff --git a/tests/cassettes/test_revision_round_trip/test_directory_ingest_threads_configured_source_to_provider.yaml b/tests/cassettes/test_revision_round_trip/test_directory_ingest_threads_configured_source_to_provider.yaml new file mode 100644 index 00000000..030d3227 --- /dev/null +++ b/tests/cassettes/test_revision_round_trip/test_directory_ingest_threads_configured_source_to_provider.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '75' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - hello + 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: 2 + total_tokens: 2 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_revision_round_trip/test_metadata_provider_applies_to_fresh_create.yaml b/tests/cassettes/test_revision_round_trip/test_metadata_provider_applies_to_fresh_create.yaml new file mode 100644 index 00000000..030d3227 --- /dev/null +++ b/tests/cassettes/test_revision_round_trip/test_metadata_provider_applies_to_fresh_create.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '75' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - hello + 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: 2 + total_tokens: 2 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_revision_round_trip/test_provider_backed_unchanged_revision_keeps_head_short_circuit.yaml b/tests/cassettes/test_revision_round_trip/test_provider_backed_unchanged_revision_keeps_head_short_circuit.yaml new file mode 100644 index 00000000..030d3227 --- /dev/null +++ b/tests/cassettes/test_revision_round_trip/test_provider_backed_unchanged_revision_keeps_head_short_circuit.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '75' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - hello + 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: 2 + total_tokens: 2 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_revision_round_trip/test_provider_mutating_fetch_result_cannot_corrupt_source_metadata.yaml b/tests/cassettes/test_revision_round_trip/test_provider_mutating_fetch_result_cannot_corrupt_source_metadata.yaml new file mode 100644 index 00000000..030d3227 --- /dev/null +++ b/tests/cassettes/test_revision_round_trip/test_provider_mutating_fetch_result_cannot_corrupt_source_metadata.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '75' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - hello + 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: 2 + total_tokens: 2 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/ingester/test_metadata.py b/tests/ingester/test_metadata.py index 28f5adbb..bd0adb20 100644 --- a/tests/ingester/test_metadata.py +++ b/tests/ingester/test_metadata.py @@ -7,10 +7,11 @@ from haiku.rag.ingester.metadata import ( build_providers, load_metadata_providers, ) +from haiku.rag.ingester.sources.base import FetchResult class _Provider: - async def __call__(self, source_id: str, uri: str) -> dict: + async def __call__(self, source_id: str, uri: str, result: FetchResult) -> dict: return {"source": source_id} @@ -50,12 +51,18 @@ def test_load_is_empty_when_none_registered(monkeypatch): @pytest.mark.asyncio async def test_callable_object_satisfies_protocol(): class Provider: - async def __call__(self, source_id: str, uri: str) -> dict: + async def __call__(self, source_id: str, uri: str, result: FetchResult) -> dict: return {"classification": "secret"} provider = Provider() + result = FetchResult( + uri="u", + body=b"x", + content_type="text/plain", + content_hash="9dd4e461268c8034f5c8564e155c67a6", + ) assert isinstance(provider, MetadataProvider) - assert await provider("src", "u") == {"classification": "secret"} + assert await provider("src", "u", result) == {"classification": "secret"} def test_build_providers_instantiates_named_factories(): diff --git a/tests/ingester/test_pipeline.py b/tests/ingester/test_pipeline.py index f467e88c..34468670 100644 --- a/tests/ingester/test_pipeline.py +++ b/tests/ingester/test_pipeline.py @@ -59,7 +59,10 @@ async def test_upsert_calls_create_document_from_source_and_returns_metadata(): assert result.content_hash == "abcd" assert result.deleted is False client.create_document_from_source.assert_awaited_once_with( - "https://example.com/a.pdf", sources=None, source_id="src", metadata=None + "https://example.com/a.pdf", + sources=None, + source_id="src", + metadata_provider=None, ) @@ -80,7 +83,7 @@ async def test_upsert_threads_configured_sources_to_client(): "https://example.com/a.pdf", sources=[configured], source_id="src", - metadata=None, + metadata_provider=None, ) @@ -91,19 +94,20 @@ class _MetadataProvider: self._metadata = metadata or {} self._error = error - async def __call__(self, source_id: str, uri: str) -> dict: + async def __call__(self, source_id: str, uri: str, result: FetchResult) -> dict: if self._error is not None: raise self._error return {**self._metadata, "source": source_id} @pytest.mark.asyncio -async def test_provider_metadata_passed_to_client(): +async def test_provider_passed_to_client(): client = _mock_client() client.create_document_from_source.return_value = Document( id="d", content="x", uri="u", metadata={} ) - providers = {"src": _MetadataProvider({"classification": "secret"})} + provider = _MetadataProvider({"classification": "secret"}) + providers = {"src": provider} await run_job(client, _job(), metadata_providers=providers) @@ -111,37 +115,29 @@ async def test_provider_metadata_passed_to_client(): "https://example.com/a.pdf", sources=None, source_id="src", - metadata={"classification": "secret", "source": "src"}, + metadata_provider=provider, ) @pytest.mark.asyncio -async def test_provider_cannot_override_system_keys(): - """Reserved source-derived keys are stripped from provider output so the - metadata-only refresh path can't let a provider overwrite md5 / - source_revision / content_type (which would corrupt sync_state).""" +async def test_provider_not_called_in_pipeline(): + """Provider execution happens inside create_document_from_source after + fetch, where FetchResult exists.""" client = _mock_client() client.create_document_from_source.return_value = Document( id="d", content="x", uri="u", metadata={} ) - providers = { - "src": _MetadataProvider( - { - "md5": "spoof", - "source_revision": "spoof", - "content_type": "text/spoof", - "classification": "secret", - } - ) - } + provider = _MetadataProvider( + error=AssertionError("pipeline must not call provider") + ) - await run_job(client, _job(), metadata_providers=providers) + await run_job(client, _job(), metadata_providers={"src": provider}) client.create_document_from_source.assert_awaited_once_with( "https://example.com/a.pdf", sources=None, source_id="src", - metadata={"classification": "secret", "source": "src"}, + metadata_provider=provider, ) @@ -157,19 +153,23 @@ async def test_no_provider_for_source_passes_no_metadata(): await run_job(client, _job(), metadata_providers=providers) client.create_document_from_source.assert_awaited_once_with( - "https://example.com/a.pdf", sources=None, source_id="src", metadata=None + "https://example.com/a.pdf", + sources=None, + source_id="src", + metadata_provider=None, ) @pytest.mark.asyncio -async def test_provider_error_is_classified_and_blocks_ingest(): +async def test_provider_error_from_client_is_classified_and_blocks_ingest(): client = _mock_client() - providers = {"src": _MetadataProvider(error=httpx.ConnectError("provider down"))} + client.create_document_from_source.side_effect = httpx.ConnectError("provider down") + providers = {"src": _MetadataProvider()} with pytest.raises(TransientError): await run_job(client, _job(), metadata_providers=providers) - client.create_document_from_source.assert_not_awaited() + client.create_document_from_source.assert_awaited_once() @pytest.mark.asyncio diff --git a/tests/ingester/test_revision_round_trip.py b/tests/ingester/test_revision_round_trip.py index af3b745f..7bffbde0 100644 --- a/tests/ingester/test_revision_round_trip.py +++ b/tests/ingester/test_revision_round_trip.py @@ -4,12 +4,13 @@ as unchanged. Catches the FS-specific bug where revision was lost in the pipeline and every periodic sweep re-enqueued every file forever. """ +import hashlib from pathlib import Path import pytest from haiku.rag.client import HaikuRAG -from haiku.rag.ingester.sources.base import SourceEventKind +from haiku.rag.ingester.sources.base import FetchResult, SourceEventKind from haiku.rag.ingester.sources.fs import FSSource @@ -107,3 +108,159 @@ async def test_fs_head_short_circuit_skips_fetch_for_unchanged_revision( assert second.id == first.id assert fetch_calls == [] + + +@pytest.mark.asyncio +@pytest.mark.vcr() +async def test_provider_backed_unchanged_revision_keeps_head_short_circuit( + temp_db_path, tmp_path, monkeypatch +): + """Provider-backed sources keep the cheap HEAD path when the revision is + unchanged. Existing provider metadata persists until the content changes.""" + file_path = tmp_path / "doc.md" + file_path.write_text("hello") + content_hash = hashlib.md5(b"hello", usedforsecurity=False).hexdigest() + revision = str(file_path.stat().st_mtime_ns) + stored_uri = file_path.absolute().as_uri() + + seen: dict = {} + + class Provider: + async def __call__(self, source_id: str, uri: str, result: FetchResult) -> dict: + seen["called"] = True + raise AssertionError("provider must not run on unchanged HEAD") + + async with HaikuRAG(temp_db_path, create=True) as client: + first = await client.create_document( + "hello", + uri=stored_uri, + metadata={ + "md5": content_hash, + "source_revision": revision, + "content_type": "text/markdown", + "classification": "secret", + }, + ) + + fetch_calls: list[str] = [] + original_fetch = FSSource.fetch + + async def _track_fetch(self: FSSource, uri: str): # type: ignore[no-untyped-def] + fetch_calls.append(uri) + return await original_fetch(self, uri) + + monkeypatch.setattr(FSSource, "fetch", _track_fetch) + + second = await client.create_document_from_source( + file_path, metadata_provider=Provider() + ) + + assert second.id == first.id + assert fetch_calls == [] + assert seen == {} + assert second.metadata["classification"] == "secret" + assert second.metadata["md5"] == first.metadata["md5"] + assert second.metadata["source_revision"] == first.metadata["source_revision"] + assert second.metadata["content_type"] == first.metadata["content_type"] + + +@pytest.mark.asyncio +@pytest.mark.vcr() +async def test_metadata_provider_applies_to_fresh_create(temp_db_path, tmp_path): + """Fresh ingests pass FetchResult to the provider, merge provider metadata, + and still keep source-owned metadata authoritative.""" + file_path = tmp_path / "doc.md" + file_path.write_text("hello") + content_hash = hashlib.md5(b"hello", usedforsecurity=False).hexdigest() + revision = str(file_path.stat().st_mtime_ns) + + seen: dict = {} + + class Provider: + async def __call__(self, source_id: str, uri: str, result: FetchResult) -> dict: + seen["source_id"] = source_id + seen["uri"] = uri + seen["body"] = result.body + seen["disk_path"] = result.disk_path + seen["content_type"] = result.content_type + return { + "classification": "secret", + "md5": "spoof", + "source_revision": "spoof", + "content_type": "text/spoof", + } + + async with HaikuRAG(temp_db_path, create=True) as client: + doc = await client.create_document_from_source( + file_path, metadata_provider=Provider() + ) + + assert seen["body"] == b"hello" + assert seen["disk_path"] == file_path + assert seen["uri"] == str(file_path) + assert seen["source_id"].startswith("fs:") + assert doc.metadata["classification"] == "secret" + assert doc.metadata["md5"] == content_hash + assert doc.metadata["source_revision"] == revision + assert doc.metadata["content_type"] == seen["content_type"] + + +@pytest.mark.asyncio +@pytest.mark.vcr() +async def test_provider_mutating_fetch_result_cannot_corrupt_source_metadata( + temp_db_path, tmp_path +): + """A provider only contributes metadata via its returned dict. Mutating the + FetchResult it receives must not reach the md5 short-circuit or source + metadata, so source-owned keys stay authoritative.""" + file_path = tmp_path / "doc.md" + file_path.write_text("hello") + content_hash = hashlib.md5(b"hello", usedforsecurity=False).hexdigest() + + class Provider: + async def __call__(self, source_id: str, uri: str, result: FetchResult) -> dict: + result.content_hash = "spoof" + result.extra_metadata["md5"] = "spoof" + result.extra_metadata["injected"] = "x" + return {"classification": "secret"} + + async with HaikuRAG(temp_db_path, create=True) as client: + doc = await client.create_document_from_source( + file_path, metadata_provider=Provider() + ) + + assert doc.metadata["classification"] == "secret" + assert doc.metadata["md5"] == content_hash + assert "injected" not in doc.metadata + + +@pytest.mark.asyncio +@pytest.mark.vcr() +async def test_directory_ingest_threads_configured_source_to_provider( + temp_db_path, tmp_path +): + """Directory ingestion with a configured source passes that source's id and + fetch context to each child, so the provider sees the configured source id + rather than an ad-hoc fs: identity.""" + (tmp_path / "doc.md").write_text("hello") + + seen_source_ids: list[str] = [] + + class Provider: + async def __call__(self, source_id: str, uri: str, result: FetchResult) -> dict: + seen_source_ids.append(source_id) + return {"collection": source_id} + + source = FSSource(root=tmp_path, source_id="docs") + + async with HaikuRAG(temp_db_path, create=True) as client: + docs = await client.create_document_from_source( + tmp_path, + sources=[source], + source_id="docs", + metadata_provider=Provider(), + ) + + assert isinstance(docs, list) + assert seen_source_ids == ["docs"] + assert docs[0].metadata["collection"] == "docs" diff --git a/tests/ingester/test_workers.py b/tests/ingester/test_workers.py index cd0fd052..f13f826e 100644 --- a/tests/ingester/test_workers.py +++ b/tests/ingester/test_workers.py @@ -685,7 +685,7 @@ async def test_breaker_isolates_sources(client, jobs, sync): """An open breaker pauses only the failing source. Workers keep draining a healthy source's jobs while the failing source's jobs stay queued.""" - def _route(uri, *, sources=None, source_id=None, metadata=None): + def _route(uri, *, sources=None, source_id=None, metadata_provider=None): if source_id == "bad": raise TransientError("downstream down") return Document(