1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
|
"""
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 import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from unittest.mock import MagicMock
from meshbay_common.crypto import generate_gek
from meshbay_node.config import Config, HubConfig, NodeConfig, GroupConfig, KeystoreConfig
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()
|