fix(evidence): harden unchanged job snapshot

This commit is contained in:
2026-07-12 06:29:31 +02:00
parent 7d41c4cefc
commit 5cc023f390
4 changed files with 220 additions and 9 deletions
+94 -1
View File
@@ -1,3 +1,5 @@
from datetime import UTC, datetime, timedelta
import pytest
from tht.corpus.chunk import ChunkPolicy
@@ -432,12 +434,27 @@ def test_unchanged_documents_skip_acquire_normalize_chunk_and_embed(tmp_path):
def test_unchanged_job_reuses_active_generation_without_new_directory(tmp_path):
source = Source([(item("one", "a"), "hello")])
source = Source([(
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": 5, "nested": {"b": 2, "a": 1}},
),
"hello",
)])
candidate = pipeline(tmp_path, source)
args = dict(workspace_id="demo", workspace_root=tmp_path,
config_fingerprint="sha256:" + "1" * 64,
input_fingerprint="sha256:" + "2" * 64)
first = candidate.run_as_job(**args)
snapshot = first.manifest.metadata["source_snapshot"]["fs:one"]
assert snapshot == {
"source_id": "fs:one", "uri": "file:///safe/one.md", "fingerprint": "sha256:a",
"modified_at": "2026-01-01T00:00:00Z",
"metadata": {"media_type": "text/markdown", "size": 5,
"nested": {"a": 1, "b": 2}},
"media_type": "text/markdown", "size": 5,
}
count = len(candidate.store.list_generations())
second = candidate.run_as_job(**args)
assert second.generation == first.generation
@@ -445,6 +462,82 @@ def test_unchanged_job_reuses_active_generation_without_new_directory(tmp_path):
assert len(candidate.store.list_generations()) == count
@pytest.mark.parametrize("field", ["uri", "modified_at", "metadata"])
def test_job_source_snapshot_change_forces_publish_with_same_fingerprint(tmp_path, field):
original = 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": 5, "label": "original"},
)
vectors = Vectors()
args = dict(workspace_id="demo", workspace_root=tmp_path,
config_fingerprint="sha256:" + "1" * 64,
input_fingerprint="sha256:" + "2" * 64)
first = pipeline(tmp_path, Source([(original, "hello")]), vectors=vectors).run_as_job(**args)
updates = {
"uri": "file:///safe/renamed.md",
"modified_at": original.modified_at + timedelta(seconds=1),
"metadata": {"media_type": "text/markdown", "size": 5, "label": "changed"},
}
changed = original.model_copy(update={field: updates[field]})
source = Source([(changed, "hello")])
result = pipeline(tmp_path, source, vectors=vectors).run_as_job(**args)
assert result.published is True
assert result.generation != first.generation
assert source.acquire_calls == ["fs:one"]
@pytest.mark.parametrize("fingerprint_name", ["config_fingerprint", "input_fingerprint"])
def test_job_binding_change_forces_publish(tmp_path, fingerprint_name):
vectors = Vectors()
source_object = item("one", "a")
args = dict(workspace_id="demo", workspace_root=tmp_path,
config_fingerprint="sha256:" + "1" * 64,
input_fingerprint="sha256:" + "2" * 64)
first = pipeline(tmp_path, Source([(source_object, "hello")]), vectors=vectors).run_as_job(**args)
args[fingerprint_name] = "sha256:" + "3" * 64
source = Source([(source_object, "hello")])
result = pipeline(tmp_path, source, vectors=vectors).run_as_job(**args)
assert result.published is True
assert result.generation != first.generation
assert source.acquire_calls == []
@pytest.mark.parametrize(
"damage", ["legacy_metadata", "corrupt_document_sources", "document", "vector"]
)
def test_job_incomplete_active_contract_never_noops(tmp_path, damage):
import json
vectors = Vectors()
source_object = item("one", "a")
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")]), vectors=vectors)
first = candidate.run_as_job(**args)
if damage == "legacy_metadata":
manifest_path = candidate.store.generation_path(first.generation) / "manifest.json"
payload = json.loads(manifest_path.read_text())
payload["metadata"].pop("source_snapshot")
manifest_path.write_text(json.dumps(payload))
elif damage == "corrupt_document_sources":
manifest_path = candidate.store.generation_path(first.generation) / "manifest.json"
payload = json.loads(manifest_path.read_text())
payload["metadata"]["document_sources"] = {}
manifest_path.write_text(json.dumps(payload))
elif damage == "document":
path = candidate.store.resolve_document(first.manifest.documents[0].document_id)
path.unlink()
else:
vectors.records.clear()
source = Source([(source_object, "hello")])
result = pipeline(tmp_path, source, vectors=vectors).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()