fix(assets): refresh file facts when the off→on drain confirms an unchanged hash

drain_transition_queue branched on null-hash and changed-hash with no else, so the
success case — a recomputed digest equal to the stored one, which is proof the bytes are
unchanged — fell through and left size_bytes/mtime_ns untouched. A row whose stored stat
was stale then failed lookup._stat_consistent and stayed unservable by hash until the next
full scan, even though the drain had just verified it.

Adopt the observed stat on the equal-hash arm, matching what drain_pending_verifications
already does on its matching-hash path.
This commit is contained in:
Simon Pinfold
2026-08-27 14:56:40 -07:00
parent ae65f1ac08
commit 4a7d6c012a
2 changed files with 42 additions and 0 deletions

View File

@@ -1,5 +1,6 @@
from pathlib import Path
import folder_paths
import pytest
from blake3 import blake3
from sqlalchemy import select
@@ -16,6 +17,7 @@ from app.assets.services.hash_mode_state import (
record_transition_intent,
write_stored_mode,
)
from app.assets.services.lookup import lookup_for_view
from app.assets.services.path_utils import get_name_and_tags_from_asset_path
from app.assets.services.snapshot_hash import snapshot_hash
@@ -81,6 +83,43 @@ def test_transition_drain_splits_changed_content(session, temp_dir, monkeypatch)
assert any(record.content_id == live_content.id for record in records)
def test_transition_drain_serves_unchanged_content_whose_stored_stat_went_stale(
session, temp_dir, monkeypatch
):
path = temp_dir / "unchanged.bin"
path.write_bytes(b"bytes that outlive the mode flip")
stat = path.stat()
stored_hash = _stored_hash(path)
size_before_the_restore = stat.st_size - 1
mtime_before_the_restore = stat.st_mtime_ns - 1_000_000_000
content = create_content(
session, str(path), stored_hash, size_before_the_restore, mtime_before_the_restore
)
content_id = content.id
create_record(session, content_id, "unchanged.bin")
monkeypatch.setattr(folder_paths, "get_temp_directory", lambda: str(temp_dir / "never"))
write_stored_mode(session, "off")
monkeypatch.setattr(hash_mode_state._mode, "hashing_enabled", lambda: True)
assert lookup_for_view(session, stored_hash) is None, (
"precondition: the stale stat makes the row unservable by hash"
)
transition = record_transition_intent(session)
enqueue_transition_work(session, transition)
drain_transition_queue(session)
session.commit()
refreshed = session.get(AssetContent, content_id)
assert (refreshed.size_bytes, refreshed.mtime_ns) == (stat.st_size, stat.st_mtime_ns)
served = lookup_for_view(session, stored_hash)
assert served is not None and served.id == content_id, (
"the drain verified these bytes but left the row unservable until the next full scan"
)
assert [row.id for row in session.scalars(select(AssetContent))] == [content_id]
assert [row.content_id for row in session.scalars(select(Asset))] == [content_id]
assert read_stored_mode(session) == "on"
def test_transition_drain_requeues_permission_errors_and_processes_other_paths(
session, temp_dir, monkeypatch
):