fix(evidence): validate active corpus artifacts
This commit is contained in:
@@ -5,9 +5,9 @@ import pytest
|
||||
from tht.corpus.chunk import ChunkPolicy
|
||||
from tht.corpus.pipeline import CorpusPipeline, PipelineError, PipelineResult
|
||||
from tht.corpus.store import CorpusStore
|
||||
from tht.corpus.models import CorpusManifest
|
||||
from tht.corpus.models import CanonicalChunk, CorpusManifest
|
||||
from tht.ports.evidence import AcquiredDocument, SourceObject
|
||||
from tht.ports.vector import VectorCapabilities
|
||||
from tht.ports.vector import VectorCapabilities, VectorHealth
|
||||
|
||||
|
||||
class Source:
|
||||
@@ -23,7 +23,9 @@ class Source:
|
||||
payload = next(payload for source, payload in self.documents if source.source_id == item.source_id)
|
||||
if isinstance(payload, Exception):
|
||||
raise payload
|
||||
return AcquiredDocument(source=item, content=payload.encode())
|
||||
return AcquiredDocument(
|
||||
source=item, content=payload.encode(), media_type=item.metadata.get("media_type")
|
||||
)
|
||||
|
||||
|
||||
class Embedder:
|
||||
@@ -45,6 +47,7 @@ class Vectors:
|
||||
def __init__(self, fail=False):
|
||||
self.fail = fail
|
||||
self.records = []
|
||||
self.dimension = 3
|
||||
|
||||
def upsert(self, collection, records):
|
||||
self.records.extend(records[:1] if self.fail else records)
|
||||
@@ -57,6 +60,12 @@ class Vectors:
|
||||
value.record.id: value.content_hash for value in self.records
|
||||
}
|
||||
|
||||
def health(self):
|
||||
return VectorHealth(
|
||||
ok=True, expected_dimension=3, observed_dimensions=(self.dimension,),
|
||||
dimension_compatible=self.dimension == 3,
|
||||
)
|
||||
|
||||
def delete_generation(self, collection, generation, workspace_id):
|
||||
self.records = [
|
||||
value for value in self.records
|
||||
@@ -538,6 +547,65 @@ def test_job_incomplete_active_contract_never_noops(tmp_path, damage):
|
||||
assert source.acquire_calls == ["fs:one"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"damage", ["modified_at", "source_metadata", "media_type", "missing_chunk",
|
||||
"altered_chunk", "extra_chunk", "vector_dimension"]
|
||||
)
|
||||
def test_job_corrupt_canonical_document_or_chunk_never_noops(tmp_path, damage):
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
vectors = Vectors()
|
||||
source_object = SourceObject(
|
||||
source_id="fs:one", uri="file:///safe/one.md", fingerprint="sha256:a",
|
||||
modified_at=datetime(2026, 1, 1, tzinfo=UTC),
|
||||
metadata={"media_type": "text/markdown", "size": 11, "owner": "docs"},
|
||||
)
|
||||
args = dict(workspace_id="demo", workspace_root=tmp_path,
|
||||
config_fingerprint="sha256:" + "1" * 64,
|
||||
input_fingerprint="sha256:" + "2" * 64)
|
||||
candidate = pipeline(
|
||||
tmp_path, Source([(source_object, "hello world")]), vectors=vectors,
|
||||
policy=ChunkPolicy(version="chunk-v1", max_chars=6),
|
||||
)
|
||||
first = candidate.run_as_job(**args)
|
||||
manifest_path = candidate.store.generation_path(first.generation) / "manifest.json"
|
||||
payload = json.loads(manifest_path.read_text())
|
||||
document = payload["documents"][0]
|
||||
chunks = payload["chunks"]
|
||||
if damage == "modified_at":
|
||||
document["modified_at"] = "2026-01-01T00:00:01Z"
|
||||
elif damage == "source_metadata":
|
||||
document["metadata"]["source"]["owner"] = "attacker"
|
||||
elif damage == "media_type":
|
||||
document["media_type"] = "text/plain"
|
||||
elif damage == "missing_chunk":
|
||||
payload["chunks"] = chunks[:-1]
|
||||
elif damage == "altered_chunk":
|
||||
chunks[0]["content"] = "HELLO "
|
||||
chunks[0]["content_hash"] = "sha256:" + hashlib.sha256(b"HELLO ").hexdigest()
|
||||
chunks[0]["chunk_id"] = "chunk:" + "a" * 64
|
||||
elif damage == "extra_chunk":
|
||||
extra = CanonicalChunk(
|
||||
chunk_id="chunk:" + "b" * 64, document_id=document["document_id"],
|
||||
ordinal=len(chunks), content="", content_hash="sha256:" + hashlib.sha256(b"").hexdigest(),
|
||||
source_uri=document["source_uri"], pipeline_version=document["pipeline_version"],
|
||||
)
|
||||
chunks.append(extra.model_dump(mode="json"))
|
||||
else:
|
||||
vectors.dimension = 4
|
||||
manifest_path.write_text(json.dumps(payload))
|
||||
|
||||
source = Source([(source_object, "hello world")])
|
||||
result = pipeline(
|
||||
tmp_path, source, vectors=vectors,
|
||||
policy=ChunkPolicy(version="chunk-v1", max_chars=6),
|
||||
).run_as_job(**args)
|
||||
assert result.published is True
|
||||
assert result.generation != first.generation
|
||||
assert source.acquire_calls == ["fs:one"]
|
||||
|
||||
|
||||
def test_removed_documents_are_marked_and_absent_from_new_manifest(tmp_path):
|
||||
one, two = item("one", "a"), item("two", "b")
|
||||
pipeline(tmp_path, Source([(one, "one"), (two, "two")])).run()
|
||||
|
||||
Reference in New Issue
Block a user