"""Tests for the (path, size, mtime) -> hash cache (indexer/cache.py).""" import pytest from meshbay_node.indexer.cache import IndexCache @pytest.fixture async def cache(tmp_path): c = IndexCache(db_path=tmp_path / "index_cache.db") await c.open() yield c await c.close() @pytest.mark.asyncio async def test_put_then_lookup_hits(cache): await cache.put("/lib/a.mkv", size=1000, mtime=111.0, hash="abc123", type="video", added_at=42) hit = await cache.lookup("/lib/a.mkv", size=1000, mtime=111.0) assert hit is not None assert hit.hash == "abc123" assert hit.type == "video" assert hit.added_at == 42 @pytest.mark.asyncio async def test_lookup_misses_on_unknown_path(cache): assert await cache.lookup("/lib/never-seen.mkv", size=1, mtime=1.0) is None @pytest.mark.asyncio async def test_lookup_misses_on_different_mtime(cache): await cache.put("/lib/a.mkv", size=1000, mtime=111.0, hash="abc123", type="video", added_at=42) assert await cache.lookup("/lib/a.mkv", size=1000, mtime=222.0) is None @pytest.mark.asyncio async def test_lookup_misses_on_different_size(cache): await cache.put("/lib/a.mkv", size=1000, mtime=111.0, hash="abc123", type="video", added_at=42) assert await cache.lookup("/lib/a.mkv", size=2000, mtime=111.0) is None @pytest.mark.asyncio async def test_put_overwrites_previous_row_for_same_path(cache): await cache.put("/lib/a.mkv", size=1000, mtime=111.0, hash="old", type="video", added_at=1) await cache.put("/lib/a.mkv", size=2000, mtime=222.0, hash="new", type="video", added_at=2) assert await cache.lookup("/lib/a.mkv", size=1000, mtime=111.0) is None hit = await cache.lookup("/lib/a.mkv", size=2000, mtime=222.0) assert hit.hash == "new" @pytest.mark.asyncio async def test_count_reflects_number_of_rows(cache): assert await cache.count() == 0 await cache.put("/lib/a.mkv", size=1000, mtime=111.0, hash="a", type="video", added_at=1) await cache.put("/lib/b.mkv", size=2000, mtime=222.0, hash="b", type="video", added_at=2) assert await cache.count() == 2 @pytest.mark.asyncio async def test_all_paths_returns_every_row(cache): await cache.put("/lib/a.mkv", size=1000, mtime=111.0, hash="a", type="video", added_at=1) await cache.put("/lib/b.mkv", size=2000, mtime=222.0, hash="b", type="video", added_at=2) assert set(await cache.all_paths()) == {"/lib/a.mkv", "/lib/b.mkv"} @pytest.mark.asyncio async def test_remove_many_drops_only_the_given_paths(cache): await cache.put("/lib/a.mkv", size=1000, mtime=111.0, hash="a", type="video", added_at=1) await cache.put("/lib/b.mkv", size=2000, mtime=222.0, hash="b", type="video", added_at=2) await cache.remove_many(["/lib/a.mkv"]) assert await cache.lookup("/lib/a.mkv", size=1000, mtime=111.0) is None assert await cache.lookup("/lib/b.mkv", size=2000, mtime=222.0) is not None assert await cache.count() == 1 @pytest.mark.asyncio async def test_remove_many_with_empty_list_is_a_no_op(cache): await cache.put("/lib/a.mkv", size=1000, mtime=111.0, hash="a", type="video", added_at=1) await cache.remove_many([]) assert await cache.count() == 1 @pytest.mark.asyncio async def test_cache_survives_reopen(tmp_path): db_path = tmp_path / "index_cache.db" c1 = IndexCache(db_path=db_path) await c1.open() await c1.put("/lib/a.mkv", size=1000, mtime=111.0, hash="abc123", type="video", added_at=42) await c1.close() c2 = IndexCache(db_path=db_path) await c2.open() hit = await c2.lookup("/lib/a.mkv", size=1000, mtime=111.0) await c2.close() assert hit is not None assert hit.hash == "abc123" # ── hash_version support (indexing v2) ───────────────────────────────────────── @pytest.mark.asyncio async def test_put_v2_then_lookup_hits(cache): await cache.put("/lib/big.mkv", size=50_000_000, mtime=111.0, hash="partial_abc", type="video", added_at=42, hash_version=2) hit = await cache.lookup("/lib/big.mkv", size=50_000_000, mtime=111.0, hash_version=2) assert hit is not None assert hit.hash == "partial_abc" assert hit.hash_version == 2 @pytest.mark.asyncio async def test_lookup_misses_on_wrong_hash_version(cache): await cache.put("/lib/big.mkv", size=50_000_000, mtime=111.0, hash="full_hash", type="video", added_at=42, hash_version=1) assert await cache.lookup("/lib/big.mkv", size=50_000_000, mtime=111.0, hash_version=2) is None @pytest.mark.asyncio async def test_put_v2_overwrites_v1_for_same_path(cache): await cache.put("/lib/big.mkv", size=50_000_000, mtime=111.0, hash="full_hash", type="video", added_at=42, hash_version=1) await cache.put("/lib/big.mkv", size=50_000_000, mtime=111.0, hash="partial_hash", type="video", added_at=42, hash_version=2) assert await cache.lookup("/lib/big.mkv", size=50_000_000, mtime=111.0, hash_version=1) is None hit = await cache.lookup("/lib/big.mkv", size=50_000_000, mtime=111.0, hash_version=2) assert hit is not None assert hit.hash == "partial_hash" @pytest.mark.asyncio async def test_v1_schema_auto_migrates(tmp_path): """An index_cache.db created by old code (no hash_version column) gains the column on the next open(), and existing rows default to hash_version=1.""" import aiosqlite db_path = tmp_path / "old_cache.db" async with aiosqlite.connect(str(db_path)) as db: await db.executescript(""" CREATE TABLE files ( path TEXT PRIMARY KEY, mtime REAL NOT NULL, size INTEGER NOT NULL, hash TEXT NOT NULL, type TEXT NOT NULL, added_at INTEGER NOT NULL ); """) await db.execute( "INSERT INTO files (path, mtime, size, hash, type, added_at) " "VALUES (?, ?, ?, ?, ?, ?)", ("/lib/old.mkv", 111.0, 1000, "oldhash", "video", 42)) await db.commit() cache = IndexCache(db_path=db_path) await cache.open() hit = await cache.lookup("/lib/old.mkv", size=1000, mtime=111.0, hash_version=1) await cache.close() assert hit is not None assert hit.hash == "oldhash" assert hit.hash_version == 1