Files
the-nexus/tests/test_mempalace_fleet_api.py
Claude (Opus 4.6) 07a4be3bb9
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
[claude] Weekly privacy audit cron + fleet HTTP API (#1075) (#1109)
2026-04-07 14:54:41 +00:00

240 lines
8.0 KiB
Python

"""
Tests for mempalace/fleet_api.py — Alpha-side HTTP fleet memory API.
Refs: #1078, #1075
"""
from __future__ import annotations
import io
import json
import threading
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
# Import handler directly so we can test without running a server process.
from mempalace.fleet_api import FleetAPIHandler, _handle_health, _handle_search, _handle_wings, make_server
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
class _FakeSocket:
"""Minimal socket stub for BaseHTTPRequestHandler."""
def makefile(self, mode: str, *args, **kwargs): # noqa: ANN001
return io.BytesIO(b"")
def _make_handler(path: str = "/health") -> tuple[FleetAPIHandler, io.BytesIO]:
"""Construct a handler pointed at *path*, capture wfile output."""
buf = io.BytesIO()
request = _FakeSocket()
client_address = ("127.0.0.1", 0)
handler = FleetAPIHandler.__new__(FleetAPIHandler)
handler.path = path
handler.request = request
handler.client_address = client_address
handler.server = MagicMock()
handler.wfile = buf
handler.rfile = io.BytesIO(b"")
handler.command = "GET"
handler._headers_buffer = []
# Stub send_response / send_header / end_headers to write minimal HTTP
handler._response_code = None
def _send_response(code, message=None): # noqa: ANN001
handler._response_code = code
def _send_header(k, v): # noqa: ANN001
pass
def _end_headers(): # noqa: ANN001
pass
handler.send_response = _send_response
handler.send_header = _send_header
handler.end_headers = _end_headers
return handler, buf
def _parse_response(buf: io.BytesIO) -> dict:
buf.seek(0)
return json.loads(buf.read())
# ---------------------------------------------------------------------------
# /health
# ---------------------------------------------------------------------------
def test_health_returns_ok(tmp_path, monkeypatch):
monkeypatch.setenv("FLEET_PALACE_PATH", str(tmp_path))
handler, buf = _make_handler("/health")
_handle_health(handler)
data = _parse_response(buf)
assert data["status"] == "ok"
assert data["palace_exists"] is True
def test_health_missing_palace(tmp_path, monkeypatch):
missing = tmp_path / "nonexistent"
monkeypatch.setenv("FLEET_PALACE_PATH", str(missing))
handler, buf = _make_handler("/health")
_handle_health(handler)
data = _parse_response(buf)
assert data["status"] == "ok"
assert data["palace_exists"] is False
# ---------------------------------------------------------------------------
# /search
# ---------------------------------------------------------------------------
def _mock_search_fleet(results):
"""Return a patch target that returns *results*."""
mock = MagicMock(return_value=results)
return mock
def _make_result(text="hello", room="forge", wing="bezalel", score=0.9):
from nexus.mempalace.searcher import MemPalaceResult
return MemPalaceResult(text=text, room=room, wing=wing, score=score)
def test_search_missing_q_param():
handler, buf = _make_handler("/search")
_handle_search(handler, {})
data = _parse_response(buf)
assert "error" in data
assert handler._response_code == 400
def test_search_returns_results(tmp_path, monkeypatch):
monkeypatch.setenv("FLEET_PALACE_PATH", str(tmp_path))
(tmp_path / "chroma.sqlite3").touch()
result = _make_result(text="CI green", room="forge", wing="bezalel", score=0.95)
with patch("mempalace.fleet_api.FleetAPIHandler") as _:
handler, buf = _make_handler("/search?q=CI")
import nexus.mempalace.searcher as s_module
with patch.object(s_module, "search_fleet", return_value=[result]):
import importlib
import mempalace.fleet_api as api_module
# Patch search_fleet inside the handler's import context
with patch("nexus.mempalace.searcher.search_fleet", return_value=[result]):
_handle_search(handler, {"q": ["CI"]})
data = _parse_response(buf)
assert data["count"] == 1
assert data["results"][0]["text"] == "CI green"
assert data["results"][0]["room"] == "forge"
assert data["results"][0]["wing"] == "bezalel"
assert data["results"][0]["score"] == 0.95
assert handler._response_code == 200
def test_search_with_room_filter(tmp_path, monkeypatch):
monkeypatch.setenv("FLEET_PALACE_PATH", str(tmp_path))
result = _make_result()
import nexus.mempalace.searcher as s_module
with patch.object(s_module, "search_fleet", return_value=[result]) as mock_sf:
_handle_search(MagicMock(), {"q": ["test"], "room": ["hermes"]})
# Verify room was passed through
mock_sf.assert_called_once_with("test", room="hermes", n_results=10)
def test_search_invalid_n_param():
handler, buf = _make_handler("/search?q=test&n=bad")
_handle_search(handler, {"q": ["test"], "n": ["bad"]})
data = _parse_response(buf)
assert "error" in data
assert handler._response_code == 400
def test_search_palace_unavailable(monkeypatch):
from nexus.mempalace.searcher import MemPalaceUnavailable
handler, buf = _make_handler("/search?q=test")
import nexus.mempalace.searcher as s_module
with patch.object(s_module, "search_fleet", side_effect=MemPalaceUnavailable("no palace")):
_handle_search(handler, {"q": ["test"]})
data = _parse_response(buf)
assert "error" in data
assert handler._response_code == 503
def test_search_n_clamped_to_max():
"""n > MAX_RESULTS is silently clamped."""
import nexus.mempalace.searcher as s_module
with patch.object(s_module, "search_fleet", return_value=[]) as mock_sf:
handler = MagicMock()
_handle_search(handler, {"q": ["test"], "n": ["9999"]})
mock_sf.assert_called_once_with("test", room=None, n_results=50)
# ---------------------------------------------------------------------------
# /wings
# ---------------------------------------------------------------------------
def test_wings_returns_list(tmp_path, monkeypatch):
monkeypatch.setenv("FLEET_PALACE_PATH", str(tmp_path))
(tmp_path / "bezalel").mkdir()
(tmp_path / "timmy").mkdir()
# A file should not appear in wings
(tmp_path / "README.txt").touch()
handler, buf = _make_handler("/wings")
_handle_wings(handler)
data = _parse_response(buf)
assert set(data["wings"]) == {"bezalel", "timmy"}
assert handler._response_code == 200
def test_wings_missing_palace(tmp_path, monkeypatch):
missing = tmp_path / "nonexistent"
monkeypatch.setenv("FLEET_PALACE_PATH", str(missing))
handler, buf = _make_handler("/wings")
_handle_wings(handler)
data = _parse_response(buf)
assert "error" in data
assert handler._response_code == 503
# ---------------------------------------------------------------------------
# 404 unknown endpoint
# ---------------------------------------------------------------------------
def test_unknown_endpoint():
handler, buf = _make_handler("/foobar")
handler.do_GET()
data = _parse_response(buf)
assert "error" in data
assert handler._response_code == 404
assert "/search" in data["endpoints"]
# ---------------------------------------------------------------------------
# audit fixture smoke test
# ---------------------------------------------------------------------------
def test_audit_fixture_is_clean():
"""Ensure tests/fixtures/fleet_palace/ passes privacy audit (no violations)."""
from mempalace.audit_privacy import audit_palace
fixture_dir = Path(__file__).parent / "fixtures" / "fleet_palace"
assert fixture_dir.exists(), f"Fixture directory missing: {fixture_dir}"
result = audit_palace(fixture_dir)
assert result.clean, (
f"Privacy violations found in CI fixture:\n" +
"\n".join(f" [{v.rule}] {v.path}: {v.detail}" for v in result.violations)
)