""" Regression: indexer.initial_scan() (run once at startup, daemon.py's _bg_scan) never itself calls on_change — that predates the Videos app, and every existing caller only cared about the scan finishing, not about notifying anyone. Enrichment (duration/thumb_hash/display_title/...) hangs entirely off on_change (daemon._broadcast_index_change). Without an explicit call to _on_index_change right after the startup scan, a file already on disk at boot — the common case, an existing library — would never get enriched at all: only a file added later, while the node is already running (seen by the watchdog), would trigger it. Found live against a real library after the first restart with this feature enabled. Enrichment only runs once a group has a video_root configured (a group with none set gets no TMDB/thumbnail work at all, docs/MESHBAY_DESIGN.md §6.5) — this test's fake roster reports the shared root itself as the configured video_root, so the enrichment-scheduling behaviour under test is exercised the same way a real operator's group would be. """ import asyncio import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from meshbay_common.crypto import generate_gek from meshbay_node.config import Config, GroupConfig, HubConfig, KeystoreConfig, NodeConfig from meshbay_node.daemon import NodeDaemon from meshbay_node.indexer import DirectoryIndexer from meshbay_node.indexer.enrich import Enricher from meshbay_node.media_cache import MediaCache from conftest import one_root pytestmark = pytest.mark.asyncio def _free_port() -> int: import socket with socket.socket() as s: s.bind(("127.0.0.1", 0)) return s.getsockname()[1] async def test_a_file_already_on_disk_at_startup_gets_enrichment_scheduled(tmp_path): shared = tmp_path / "shared" shared.mkdir() (shared / "movie.mkv").write_bytes(b"not a real video, just needs to be indexed as one") config = Config( hub=HubConfig(url="http://localhost:9999", username="testuser"), node=NodeConfig(quic_port=_free_port(), ui_port=_free_port()), groups=[GroupConfig( id="a" * 32, name="test-group", shared_dir=str(shared), visibility="private", quic_port=29012, )], keystore=KeystoreConfig(path=tmp_path / "keystore.enc"), data_dir=tmp_path / "data", ) class _StubRoster: async def app_directories(self, group_id, app_key): # The root itself, i.e. "enrich the whole thing". return ["shared"] if app_key == "video" else [] daemon = NodeDaemon(config) daemon._broadcast_coalesce_secs = 0.01 # real value would make this test wait 0.5s daemon._media_cache = MediaCache(db_path=tmp_path / "media_cache.db") await daemon._media_cache.open() daemon._enricher = Enricher(daemon._media_cache) daemon._roster = _StubRoster() sk_node = Ed25519PrivateKey.generate() indexer = DirectoryIndexer( roots=one_root(shared), group_id="a" * 32, sk_node=sk_node, gek=generate_gek()) # Mirrors _bg_scan's actual sequence in daemon.py. await indexer.initial_scan() assert not daemon._enriched_attempted, ( "nothing should be scheduled before _on_index_change is ever called") await daemon._on_index_change(indexer) await asyncio.sleep(0.05) # let the coalescing timer fire _broadcast_index_change entry = next(iter(indexer.index.entries)) assert ("a" * 32, entry.id) in daemon._enriched_attempted, ( "a file already on disk at startup must get enrichment scheduled the " "first time its group's index is broadcast, not only on a later " "watchdog-detected change to it") await daemon._media_cache.close()