Re-enrich a reference whose asset no longer has a hash

count_active_siblings excludes soft-deleted references, so rewriting the last
active reference to a shared asset clears that asset's hash in place while a
soft-deleted sibling still points at it. Restoring the sibling then matched the
unchanged check on its own untouched file and kept enrichment_level at HASHED,
so it was never re-hashed.

Preserving state only makes sense when there is state to preserve: require a
recorded hash before taking the unchanged path.
This commit is contained in:
Simon Pinfold
2026-08-11 16:44:45 -07:00
parent b02435c49d
commit 789bcf34f7
2 changed files with 39 additions and 7 deletions

View File

@@ -199,9 +199,10 @@ def ingest_existing_file(
"""Register an existing on-disk file as an asset stub.
If a reference already exists for this path, updates mtime_ns and job_id.
When the file's mtime or size differ from what was recorded the file has
been rewritten, so its hash, size and enrichment state are discarded and
the enricher re-hashes it. An unchanged file keeps the hash it already has.
A file whose mtime and size still match what was recorded keeps its hash,
size and enrichment state. If the file was rewritten, or there is no
recorded hash left to keep, that state is discarded and the enricher
re-hashes it.
For brand-new paths, inserts a stub record (hash=NULL) for immediate
UX visibility.
@@ -219,10 +220,16 @@ def ingest_existing_file(
existing_ref = get_reference_by_file_path(session, locator)
if existing_ref is not None:
asset = existing_ref.asset
unchanged = asset is not None and verify_file_unchanged(
mtime_db=existing_ref.mtime_ns,
size_db=asset.size_bytes,
stat_result=stat_result,
# mtime + size is the subsystem-wide staleness test (see
# verify_file_unchanged); strengthening it is not a local decision.
unchanged = (
asset is not None
and asset.hash is not None
and verify_file_unchanged(
mtime_db=existing_ref.mtime_ns,
size_db=asset.size_bytes,
stat_result=stat_result,
)
)
now = get_utc_now()

View File

@@ -730,6 +730,31 @@ class TestIngestExistingFileContentState:
assert ref.is_missing is False
assert asset.hash == "blake3:seeded"
def test_restored_reference_is_re_enriched_when_its_asset_lost_its_hash(
self, mock_create_session, output_root: Path, session: Session
):
content = b"image data"
first = _write_output(output_root, "ComfyUI_00001_.png", content)
second = _write_output(output_root, "ComfyUI_00002_.png", content)
asset, first_ref = _seed_hashed_reference(session, first)
_, second_ref = _seed_hashed_reference(session, second, asset=asset)
second_ref.deleted_at = get_utc_now()
session.commit()
_rewrite_with_newer_mtime(first, b"other data")
assert ingest_existing_file(str(first)) is True
session.expire_all()
assert asset.hash is None
assert ingest_existing_file(str(second)) is True
session.expire_all()
assert second_ref.deleted_at is None
assert (
second_ref.enrichment_level == ENRICHMENT_STUB
), "a reference that lost its hash must be re-enriched, not left at HASHED"
assert second_ref.asset_id != asset.id
def test_soft_deleted_reference_with_rewritten_file_is_restored_and_reset(
self, mock_create_session, output_root: Path, session: Session
):