summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-25 00:29:24 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-25 00:29:24 +0200
commitd427118bd91d67f1a041e5daf267aebcd34ca9d7 (patch)
treeff9e0e71d4b115a871cf0902739942e6214b27d0 /packages/meshbay-node/tests
parent06c101154d544290d27f18d6fc08fb5f58a4e5d5 (diff)
downloadmeshbay-d427118bd91d67f1a041e5daf267aebcd34ca9d7.tar.gz
fix(node): scope _enriched_attempted by group, not just content hash0.7
Major finding: entry.id is a content hash, so the exact same physical file — the same MP3, byte-for-byte — indexed into two different groups (a shared library reused across several demo/test groups, or genuinely the same folder shared into two groups) produces the *same id* in both. _enriched_attempted was a single flat set of bare ids shared across every group this node hosts. The moment one group's copy got enriched, every other group's otherwise-identical copy read as "already attempted" and was skipped forever — nothing else ever revisits an id once it's in this set. That group's Music tab (or Videos tab, same bug, same set) showed every affected file at duration 0 with no artist/album/thumbnail, permanently, no matter how long you waited or how many times you reloaded — group A having been enriched first was enough to silently starve every later group of the same content. Now keyed by (group_id, entry.id) throughout — the enrichment gate, the sweep, and the rename re-enrichment path, for both video and audio (they already shared the one set, and the collision risk is identical for both). New regression test constructs two groups with byte-identical audio content and confirms both enrich independently.
Diffstat (limited to 'packages/meshbay-node/tests')
-rw-r--r--packages/meshbay-node/tests/test_audio_root_gates_enrichment.py61
-rw-r--r--packages/meshbay-node/tests/test_startup_scan_enrichment.py2
-rw-r--r--packages/meshbay-node/tests/test_video_root_gates_enrichment.py6
3 files changed, 62 insertions, 7 deletions
diff --git a/packages/meshbay-node/tests/test_audio_root_gates_enrichment.py b/packages/meshbay-node/tests/test_audio_root_gates_enrichment.py
index f088f5e..deab1bd 100644
--- a/packages/meshbay-node/tests/test_audio_root_gates_enrichment.py
+++ b/packages/meshbay-node/tests/test_audio_root_gates_enrichment.py
@@ -111,8 +111,8 @@ async def test_only_entries_under_the_configured_root_are_enriched(tmp_path):
await asyncio.sleep(0.05)
by_name = {e.name: e for e in indexer.index.entries}
- assert by_name["in-root.mp3"].id in daemon._enriched_attempted
- assert by_name["outside.mp3"].id not in daemon._enriched_attempted, (
+ assert (group_id, by_name["in-root.mp3"].id) in daemon._enriched_attempted
+ assert (group_id, by_name["outside.mp3"].id) not in daemon._enriched_attempted, (
"a file outside the configured audio_root must never be enriched")
finally:
await _teardown(daemon)
@@ -149,8 +149,63 @@ async def test_setting_the_audio_root_sweeps_what_it_already_contains(tmp_path):
await asyncio.sleep(0.05) # let the fire-and-forget sweep actually run
entry = next(iter(indexer.index.entries))
- assert entry.id in daemon._enriched_attempted, (
+ assert (group_id, entry.id) in daemon._enriched_attempted, (
"a file already sitting in the newly-chosen root must be picked "
"up by the sweep, not wait for some unrelated future change")
finally:
await _teardown(daemon)
+
+
+async def test_the_same_file_shared_into_two_groups_enriches_in_both(tmp_path):
+ """
+ Regression, found live: `entry.id` is a content hash, so the exact same
+ physical file (byte-for-byte, e.g. a test/demo library reused across
+ several groups) produces the *same id* wherever it's indexed.
+ `_enriched_attempted` used to be keyed by that bare id alone — global
+ across every group this node hosts — so the moment group A's copy got
+ enriched, group B's otherwise-identical copy read as "already
+ attempted" and was skipped forever, even though nothing had ever
+ populated *group B's own* index. Every file in the group manifested as
+ duration 0 with no artist/album, permanently — nothing else ever
+ revisits an id once it's in this set. Now keyed by (group_id, id).
+ """
+ shared_a = tmp_path / "shared_a"
+ shared_a.mkdir()
+ (shared_a / "Music").mkdir()
+ (shared_a / "Music" / "track.mp3").write_bytes(_AUDIO_BYTES)
+
+ shared_b = tmp_path / "shared_b"
+ shared_b.mkdir()
+ (shared_b / "Music").mkdir()
+ (shared_b / "Music" / "track.mp3").write_bytes(_AUDIO_BYTES) # identical content
+
+ group_a, group_b = "a" * 32, "b" * 32
+ daemon = await _make_daemon(tmp_path, shared_a, group_a)
+ try:
+ indexer_a = DirectoryIndexer(
+ roots=one_root(shared_a), group_id=group_a,
+ sk_node=Ed25519PrivateKey.generate(), gek=generate_gek())
+ indexer_b = DirectoryIndexer(
+ roots=one_root(shared_b), group_id=group_b,
+ sk_node=Ed25519PrivateKey.generate(), gek=generate_gek())
+ await indexer_a.initial_scan()
+ await indexer_b.initial_scan()
+
+ entry_a = next(iter(indexer_a.index.entries))
+ entry_b = next(iter(indexer_b.index.entries))
+ assert entry_a.id == entry_b.id, (
+ "the fixture itself must produce identical content hashes — "
+ "otherwise this test isn't exercising the collision at all")
+
+ await daemon._roster.set_audio_root(group_a, "shared_a/Music", set_by="op")
+ await daemon._roster.set_audio_root(group_b, "shared_b/Music", set_by="op")
+
+ await daemon._enrich_new_audio_entries(indexer_a, list(indexer_a.index.entries))
+ await daemon._enrich_new_audio_entries(indexer_b, list(indexer_b.index.entries))
+
+ assert (group_a, entry_a.id) in daemon._enriched_attempted
+ assert (group_b, entry_b.id) in daemon._enriched_attempted, (
+ "group B's copy must be enriched independently of group A's, "
+ "even though the two entries share the exact same id")
+ finally:
+ await _teardown(daemon)
diff --git a/packages/meshbay-node/tests/test_startup_scan_enrichment.py b/packages/meshbay-node/tests/test_startup_scan_enrichment.py
index cdadad9..65b9728 100644
--- a/packages/meshbay-node/tests/test_startup_scan_enrichment.py
+++ b/packages/meshbay-node/tests/test_startup_scan_enrichment.py
@@ -84,7 +84,7 @@ async def test_a_file_already_on_disk_at_startup_gets_enrichment_scheduled(tmp_p
await asyncio.sleep(0.05) # let the coalescing timer fire _broadcast_index_change
entry = next(iter(indexer.index.entries))
- assert entry.id in daemon._enriched_attempted, (
+ 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")
diff --git a/packages/meshbay-node/tests/test_video_root_gates_enrichment.py b/packages/meshbay-node/tests/test_video_root_gates_enrichment.py
index df86a79..9cfb819 100644
--- a/packages/meshbay-node/tests/test_video_root_gates_enrichment.py
+++ b/packages/meshbay-node/tests/test_video_root_gates_enrichment.py
@@ -108,8 +108,8 @@ async def test_only_entries_under_the_configured_root_are_enriched(tmp_path):
await asyncio.sleep(0.05)
by_name = {e.name: e for e in indexer.index.entries}
- assert by_name["in-root.mkv"].id in daemon._enriched_attempted
- assert by_name["outside.mkv"].id not in daemon._enriched_attempted, (
+ assert (group_id, by_name["in-root.mkv"].id) in daemon._enriched_attempted
+ assert (group_id, by_name["outside.mkv"].id) not in daemon._enriched_attempted, (
"a file outside the configured video_root must never be enriched")
finally:
await _teardown(daemon)
@@ -146,7 +146,7 @@ async def test_setting_the_video_root_sweeps_what_it_already_contains(tmp_path):
await asyncio.sleep(0.05) # let the fire-and-forget sweep actually run
entry = next(iter(indexer.index.entries))
- assert entry.id in daemon._enriched_attempted, (
+ assert (group_id, entry.id) in daemon._enriched_attempted, (
"a file already sitting in the newly-chosen root must be picked "
"up by the sweep, not wait for some unrelated future change")
finally: