Files
the-nexus/tests/test_mempalace_tunnel_sync.py
Alexander Whitestone e644b00dff
Some checks failed
CI / test (pull_request) Failing after 7s
CI / validate (pull_request) Failing after 3s
Review Approval Gate / verify-review (pull_request) Failing after 4s
feat(mempalace): retention enforcement + tunnel sync client (#1083, #1078)
**retain_closets.py** — 90-day closet aging enforcement for #1083.
Removes *.closet.json files older than --days (default 90) from the
fleet palace. Supports --dry-run for safe preview. Wired into the
weekly-audit workflow as a dry-run CI step; production cron guidance
added to workflow comments.

**tunnel_sync.py** — remote wizard wing pull client for #1078.
Connects to a peer's fleet_api.py HTTP endpoint, discovers wings via
/wings, and pulls core rooms via /search into local *.closet.json
files. Zero new dependencies (stdlib urllib only). Supports --dry-run.
This is the code side of the inter-wizard tunnel; infrastructure
(second wizard VPS + fleet_api.py running) still required.

**Tests:** 29 new tests, all passing. Total suite: 294 passing.

Refs #1075, #1078, #1083
2026-04-07 11:05:00 -04:00

206 lines
6.9 KiB
Python

"""
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