aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/tests')
-rw-r--r--packages/meshbay-node/tests/test_root_availability.py97
1 files changed, 97 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_root_availability.py b/packages/meshbay-node/tests/test_root_availability.py
index 82bf6bd..e92ec95 100644
--- a/packages/meshbay-node/tests/test_root_availability.py
+++ b/packages/meshbay-node/tests/test_root_availability.py
@@ -13,6 +13,7 @@ an indexer that treats a vanished root as a set of deletions, which is what the
straightforward implementation does.
"""
+import asyncio
import os
from pathlib import Path
@@ -305,3 +306,99 @@ async def test_deleting_one_copy_keeps_the_other_listed(tmp_path):
assert len(idx.index.entries) == 1, "the surviving copy was delisted"
assert idx.index.entries[0].path == survivor
+
+
+# ── A directory that leaves the root ─────────────────────────────────────────
+#
+# Moving a folder out of the root arrives from the watcher as one "directory
+# deleted" event, with nothing for the files it held. Found live: a season moved
+# out of a shared folder stayed listed, every episode "File not on disk", until
+# the reconcile backstop came round. These are the same freeze rules as above,
+# applied to that event.
+
+def _season(root: Path, name: str, *episodes: str) -> Path:
+ d = root / "Show" / name
+ d.mkdir(parents=True)
+ for ep in episodes:
+ (d / ep).write_bytes(ep.encode())
+ return d
+
+
+async def test_a_directory_moved_out_of_a_live_root_is_removed(tmp_path):
+ shows = tmp_path / "Shows"
+ low = _season(shows, "S3.LQ", "e1.mp4", "e2.mp4")
+ _season(shows, "S3", "e1.hd.mkv")
+ idx = await _indexer(_roots(shows))
+
+ low.rename(tmp_path / "S3.LQ")
+ await idx._update_entry(low, deleted=True, directory=True)
+
+ # S3 shares a prefix with S3.LQ and must survive it.
+ assert _names(idx) == {"e1.hd.mkv"}
+
+
+async def test_a_directory_event_from_a_vanished_root_freezes(tmp_path):
+ shows = tmp_path / "Shows"
+ season = _season(shows, "S1", "e1.mp4", "e2.mp4")
+ idx = await _indexer(_roots(shows))
+
+ os.rename(shows, tmp_path / "elsewhere")
+ await idx._update_entry(season, deleted=True, directory=True)
+
+ assert _names(idx) == {"e1.mp4", "e2.mp4"}
+ assert idx.roots.roots[0].available is False
+
+
+async def test_a_directory_whose_parent_is_gone_waits_for_the_parent(tmp_path):
+ """The parent going too is what a volume vanishing looks like below the
+ root. The parent's own event, when it comes, is the one that acts."""
+ shows = tmp_path / "Shows"
+ season = _season(shows, "S1", "e1.mp4")
+ _season(shows, "S2", "e2.mp4")
+ idx = await _indexer(_roots(shows))
+
+ os.rename(shows / "Show", tmp_path / "Show")
+ await idx._update_entry(season, deleted=True, directory=True)
+ assert _names(idx) == {"e1.mp4", "e2.mp4"}
+
+ await idx._update_entry(shows / "Show", deleted=True, directory=True)
+ assert _names(idx) == set()
+
+
+async def test_a_directory_that_is_back_is_left_alone(tmp_path):
+ shows = tmp_path / "Shows"
+ season = _season(shows, "S1", "e1.mp4")
+ idx = await _indexer(_roots(shows))
+
+ # Moved out and back within the debounce: the event is stale.
+ await idx._update_entry(season, deleted=True, directory=True)
+ assert _names(idx) == {"e1.mp4"}
+
+
+async def test_the_root_itself_is_never_removed_as_a_directory(tmp_path):
+ shows = tmp_path / "Shows"
+ _season(shows, "S1", "e1.mp4")
+ idx = await _indexer(_roots(shows))
+
+ await idx._update_entry(shows, deleted=True, directory=True)
+ assert _names(idx) == {"e1.mp4"}
+
+
+async def test_the_watcher_reports_a_directory_moved_out(tmp_path):
+ """End to end, with the real observer: the event is the one watchdog emits."""
+ shows = tmp_path / "Shows"
+ low = _season(shows, "S3.LQ", "e1.mp4")
+ _season(shows, "S3", "e1.hd.mkv")
+ idx = DirectoryIndexer(roots=_roots(shows), group_id="g" * 32,
+ sk_node=Ed25519PrivateKey.generate(), gek=None,
+ debounce_secs=0.05)
+ await idx.start()
+ try:
+ low.rename(tmp_path / "S3.LQ")
+ for _ in range(100):
+ if _names(idx) == {"e1.hd.mkv"}:
+ break
+ await asyncio.sleep(0.05)
+ assert _names(idx) == {"e1.hd.mkv"}
+ finally:
+ await idx.stop()