fix(evidence): harden unchanged job snapshot
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user