# tests/test_image_indexing.py — Images in the tree & index (roadmap #108-A) import asyncio import base64 from pathlib import Path from backend.indexer import _index_single_file_sync, _scan_vault from backend.services.vaults import browse_directory from backend.watcher import VaultEventHandler PNG_1x1 = base64.b64decode( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAen63NgAAAAASUVORK5CYII=" ) def _make_vault(tmp_path: Path) -> Path: vault = tmp_path / "Vault" vault.mkdir() (vault / "note.md").write_text("# Note\n", encoding="utf-8") (vault / "chatScreenshot.png").write_bytes(PNG_1x1) assets = vault / "assets" assets.mkdir() (assets / "photo.jpg").write_bytes(PNG_1x1) return vault class TestScanVaultImages: def test_image_indexed_without_content(self, tmp_path): vault = _make_vault(tmp_path) result = _scan_vault("V", str(vault)) by_path = {f["path"]: f for f in result["files"]} assert "chatScreenshot.png" in by_path image = by_path["chatScreenshot.png"] assert image["content"] == "" # never read the bytes (#108-A2) assert image["content_preview"] == "" assert image["extension"] == ".png" assert image["size"] > 0 assert "assets/photo.jpg" in by_path def test_images_present_in_paths(self, tmp_path): vault = _make_vault(tmp_path) result = _scan_vault("V", str(vault)) files = {p["path"] for p in result["paths"] if p["type"] == "file"} assert "chatScreenshot.png" in files assert "assets/photo.jpg" in files def test_no_binary_content_leaks(self, tmp_path): vault = _make_vault(tmp_path) result = _scan_vault("V", str(vault)) for f in result["files"]: if f["extension"] in (".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".bmp", ".ico"): assert f["content"] == "" class TestSingleFileImage: def test_metadata_only(self, tmp_path): vault = _make_vault(tmp_path) info = _index_single_file_sync( "V", str(vault), str(vault / "chatScreenshot.png") ) assert info is not None assert info["content"] == "" assert info["extension"] == ".png" assert info["path"] == "chatScreenshot.png" class TestWatcherRelevance: def test_image_extensions_relevant(self, tmp_path): handler = VaultEventHandler("V", asyncio.Queue(), asyncio.new_event_loop()) assert handler._is_relevant(str(tmp_path / "a.png")) is True assert handler._is_relevant(str(tmp_path / "a.jpg")) is True assert handler._is_relevant(str(tmp_path / "a.svg")) is True assert handler._is_relevant(str(tmp_path / "a.exe")) is False class TestBrowseAndDashboard: def test_browse_lists_image(self, client): resp = client.get("/api/browse/TestVault") assert resp.status_code == 200 items = resp.json()["items"] names = {i["name"]: i for i in items} assert "chatScreenshot.png" in names assert names["chatScreenshot.png"]["type"] == "file" assert names["chatScreenshot.png"]["extension"] == ".png" def test_dashboard_counts_images(self, client): resp = client.get("/api/dashboard") assert resp.status_code == 200 data = resp.json() assert data["total_images"] >= 2 vault = next(v for v in data["vaults"] if v["name"] == "TestVault") assert vault["image_count"] >= 2 def test_ext_filter_returns_images(self, client): resp = client.get("/api/search/advanced", params={"q": "ext:png"}) assert resp.status_code == 200 paths = [r["path"] for r in resp.json()["results"]] assert any(p.endswith(".png") for p in paths) class TestBrowseService: def test_browse_directory_service(self, app_with_vault, test_vault_dir): # The shared index is populated by the app_with_vault fixture. data = browse_directory("TestVault", "") names = [i["name"] for i in data["items"]] assert "chatScreenshot.png" in names