fix(evidence): harden canonical corpus contracts

This commit is contained in:
2026-07-12 03:10:30 +02:00
parent d702aad93d
commit 4424fd3d90
5 changed files with 481 additions and 71 deletions
+79 -11
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime
from datetime import UTC, datetime, timedelta, timezone
import pytest
from pydantic import ValidationError
@@ -12,11 +12,11 @@ def document(source_uri: str = "https://host/a.md") -> CanonicalDocument:
source_id="source:a",
source_uri=source_uri,
source_fingerprint="etag:abc",
content_hash="sha256:def",
content_hash=f"sha256:{'d' * 64}",
title="A",
content="# A",
media_type="text/markdown",
pipeline_version="normalize-v1",
pipeline_version="evidence-v1",
)
@@ -26,9 +26,9 @@ def chunk() -> CanonicalChunk:
document_id="doc:abc",
ordinal=0,
content="# A",
content_hash="sha256:def",
content_hash=f"sha256:{'e' * 64}",
source_uri="https://host/a.md",
pipeline_version="chunk-v1",
pipeline_version="evidence-v1",
)
@@ -65,17 +65,19 @@ def test_canonical_records_are_frozen(model):
model.pipeline_version = "changed" # type: ignore[misc]
def test_manifest_collections_have_independent_defaults():
def test_manifest_collections_are_immutable_tuples_with_json_arrays():
first = CorpusManifest(
manifest_id="one", created_at=datetime.now(UTC), pipeline_version="v1"
manifest_id="manifest:one", created_at=datetime.now(UTC), pipeline_version="v1"
)
second = CorpusManifest(
manifest_id="two", created_at=datetime.now(UTC), pipeline_version="v1"
manifest_id="manifest:two", created_at=datetime.now(UTC), pipeline_version="v1"
)
first.documents.append(document())
assert second.documents == []
with pytest.raises(AttributeError):
first.documents.append(document())
assert first.documents == ()
assert second.documents == ()
assert '"documents":[]' in first.model_dump_json()
def test_manifest_validates_embedding_compatibility_fields():
@@ -104,3 +106,69 @@ def test_canonical_metadata_rejects_secrets_and_non_json_values():
pipeline_version="v1",
metadata={"bad": object()},
)
def test_manifest_rejects_duplicate_ids_and_source_ids():
first = document()
duplicate_source = first.model_copy(
update={"document_id": "doc:other", "source_uri": "https://host/b.md"}
)
with pytest.raises(ValidationError, match="source_id"):
CorpusManifest(pipeline_version="evidence-v1", documents=[first, duplicate_source])
with pytest.raises(ValidationError, match="chunk_id"):
CorpusManifest(
pipeline_version="evidence-v1", documents=[first], chunks=[chunk(), chunk()]
)
def test_manifest_rejects_orphan_noncontiguous_and_inconsistent_chunks():
with pytest.raises(ValidationError, match="unknown document"):
CorpusManifest(pipeline_version="evidence-v1", chunks=[chunk()])
second = chunk().model_copy(update={"chunk_id": "chunk:abc:2", "ordinal": 2})
with pytest.raises(ValidationError, match="contiguous"):
CorpusManifest(
pipeline_version="evidence-v1", documents=[document()], chunks=[chunk(), second]
)
wrong_uri = chunk().model_copy(update={"source_uri": "https://host/wrong.md"})
with pytest.raises(ValidationError, match="source_uri"):
CorpusManifest(
pipeline_version="evidence-v1", documents=[document()], chunks=[wrong_uri]
)
def test_manifest_rejects_inconsistent_pipeline_versions():
wrong = document().model_copy(update={"pipeline_version": "other-v1"})
with pytest.raises(ValidationError, match="pipeline_version"):
CorpusManifest(pipeline_version="evidence-v1", documents=[wrong])
def test_vector_generation_requires_embedding_compatibility():
with pytest.raises(ValidationError, match="vector_generation"):
CorpusManifest(pipeline_version="evidence-v1", vector_generation="generation:one")
@pytest.mark.parametrize(
("field", "value"),
[
("document_id", "not-namespaced"),
("content_hash", "sha256:not-hex"),
("source_uri", "https://user:pass@host/a"),
("source_uri", "https://host/a?refresh_token=secret"),
],
)
def test_canonical_document_rejects_malformed_or_sensitive_provenance(field, value):
with pytest.raises(ValidationError):
CanonicalDocument.model_validate({**document().model_dump(), field: value})
def test_manifest_datetimes_are_aware_and_normalized_to_utc():
with pytest.raises(ValidationError, match="timezone-aware"):
CorpusManifest(created_at=datetime(2026, 7, 12), pipeline_version="evidence-v1")
plus_two = datetime(2026, 7, 12, 12, tzinfo=timezone(timedelta(hours=2)))
manifest = CorpusManifest(created_at=plus_two, pipeline_version="evidence-v1")
assert manifest.created_at.tzinfo is UTC
assert manifest.created_at.hour == 10