""" Tests for mempalace/tunnel_sync.py — remote wizard wing sync client. Refs: #1078, #1075 """ from __future__ import annotations import json from pathlib import Path from unittest.mock import MagicMock, patch import pytest from mempalace.tunnel_sync import ( SyncResult, _peer_url, _write_closet, get_remote_wings, search_remote_room, sync_peer, ) # --------------------------------------------------------------------------- # _peer_url # --------------------------------------------------------------------------- def test_peer_url_strips_trailing_slash(): assert _peer_url("http://host:7771/", "/wings") == "http://host:7771/wings" def test_peer_url_with_path(): assert _peer_url("http://host:7771", "/search") == "http://host:7771/search" # --------------------------------------------------------------------------- # get_remote_wings # --------------------------------------------------------------------------- def test_get_remote_wings_returns_list(): with patch("mempalace.tunnel_sync._get", return_value={"wings": ["bezalel", "timmy"]}): wings = get_remote_wings("http://peer:7771") assert wings == ["bezalel", "timmy"] def test_get_remote_wings_empty(): with patch("mempalace.tunnel_sync._get", return_value={"wings": []}): wings = get_remote_wings("http://peer:7771") assert wings == [] # --------------------------------------------------------------------------- # search_remote_room # --------------------------------------------------------------------------- def _make_entry(text: str, room: str = "forge", wing: str = "bezalel", score: float = 0.9) -> dict: return {"text": text, "room": room, "wing": wing, "score": score} def test_search_remote_room_deduplicates(): entry = _make_entry("CI passed") # Same entry returned from multiple queries — should only appear once with patch("mempalace.tunnel_sync._get", return_value={"results": [entry]}): results = search_remote_room("http://peer:7771", "forge", n=50) assert len(results) == 1 assert results[0]["text"] == "CI passed" def test_search_remote_room_respects_n_limit(): entries = [_make_entry(f"item {i}") for i in range(100)] with patch("mempalace.tunnel_sync._get", return_value={"results": entries}): results = search_remote_room("http://peer:7771", "forge", n=5) assert len(results) <= 5 def test_search_remote_room_handles_request_error(): import urllib.error with patch("mempalace.tunnel_sync._get", side_effect=urllib.error.URLError("refused")): results = search_remote_room("http://peer:7771", "forge") assert results == [] # --------------------------------------------------------------------------- # _write_closet # --------------------------------------------------------------------------- def test_write_closet_creates_file(tmp_path): entries = [_make_entry("a memory")] ok = _write_closet(tmp_path, "bezalel", "forge", entries, dry_run=False) assert ok is True closet = tmp_path / "bezalel" / "forge.closet.json" assert closet.exists() data = json.loads(closet.read_text()) assert data["wing"] == "bezalel" assert data["room"] == "forge" assert len(data["drawers"]) == 1 assert data["drawers"][0]["closet"] is True assert data["drawers"][0]["text"] == "a memory" def test_write_closet_dry_run_does_not_create(tmp_path): entries = [_make_entry("a memory")] ok = _write_closet(tmp_path, "bezalel", "forge", entries, dry_run=True) assert ok is True closet = tmp_path / "bezalel" / "forge.closet.json" assert not closet.exists() def test_write_closet_creates_wing_subdirectory(tmp_path): entries = [_make_entry("memory")] _write_closet(tmp_path, "timmy", "hermes", entries, dry_run=False) assert (tmp_path / "timmy").is_dir() def test_write_closet_source_file_is_tunnel_tagged(tmp_path): entries = [_make_entry("memory")] _write_closet(tmp_path, "bezalel", "hermes", entries, dry_run=False) closet = tmp_path / "bezalel" / "hermes.closet.json" data = json.loads(closet.read_text()) assert data["drawers"][0]["source_file"].startswith("tunnel:") # --------------------------------------------------------------------------- # sync_peer # --------------------------------------------------------------------------- def _mock_get_responses(peer_url: str) -> dict: """Minimal mock _get returning health, wings, and search results.""" def _get(url: str) -> dict: if url.endswith("/health"): return {"status": "ok", "palace": "/var/lib/mempalace/fleet"} if url.endswith("/wings"): return {"wings": ["bezalel"]} if "/search" in url: return {"results": [_make_entry("test memory")]} return {} return _get def test_sync_peer_writes_closets(tmp_path): (tmp_path / ".gitkeep").touch() # ensure palace dir exists with patch("mempalace.tunnel_sync._get", side_effect=_mock_get_responses("http://peer:7771")): result = sync_peer("http://peer:7771", tmp_path, n_results=10) assert result.ok assert "bezalel" in result.wings_found assert result.closets_written > 0 def test_sync_peer_dry_run_no_files(tmp_path): (tmp_path / ".gitkeep").touch() with patch("mempalace.tunnel_sync._get", side_effect=_mock_get_responses("http://peer:7771")): result = sync_peer("http://peer:7771", tmp_path, n_results=10, dry_run=True) assert result.ok # No closet files should be written closets = list(tmp_path.rglob("*.closet.json")) assert closets == [] def test_sync_peer_unreachable_returns_error(tmp_path): import urllib.error with patch("mempalace.tunnel_sync._get", side_effect=urllib.error.URLError("refused")): result = sync_peer("http://unreachable:7771", tmp_path) assert not result.ok assert any("unreachable" in e or "refused" in e for e in result.errors) def test_sync_peer_unhealthy_returns_error(tmp_path): with patch("mempalace.tunnel_sync._get", return_value={"status": "degraded"}): result = sync_peer("http://peer:7771", tmp_path) assert not result.ok assert any("unhealthy" in e for e in result.errors) def test_sync_peer_no_wings_is_ok(tmp_path): def _get(url: str) -> dict: if "/health" in url: return {"status": "ok"} return {"wings": []} with patch("mempalace.tunnel_sync._get", side_effect=_get): result = sync_peer("http://peer:7771", tmp_path) assert result.ok assert result.closets_written == 0 # --------------------------------------------------------------------------- # SyncResult.ok # --------------------------------------------------------------------------- def test_sync_result_ok_no_errors(): r = SyncResult(wings_found=["bezalel"], rooms_pulled=5, closets_written=5) assert r.ok is True def test_sync_result_not_ok_with_errors(): r = SyncResult(errors=["connection refused"]) assert r.ok is False