Files
the-nexus/tests/test_gateway_health.py
Alexander Whitestone f83e103d27 feat: M6-P0 Foundation — cell spec, lazarus-pit daemon, health endpoint, issue template
Implements all P0 deliverables for the Lazarus Pit epic.

- docs/lazarus-pit/mission-cell-spec.md: Canonical `/var/missions/<uuid>/`
  directory layout, file schemas (cell.json, events.jsonl, heartbeat,
  daemon state), lifecycle state machine, and isolation guarantees.

- bin/lazarus_pit.py: Daemon skeleton with config loading, cell discovery,
  per-agent heartbeat polling, stale detection, resurrection stub (P3 placeholder),
  graceful signal handling, PID file management, and CLI subcommands
  (--status, --list-cells).

- config/lazarus-pit.toml: Documented config file with all tunable knobs:
  cells root, heartbeat thresholds, revive policy, gateway coordinates,
  Gitea notification settings.

- server.py: Added HTTP health heartbeat endpoint on NEXUS_HEALTH_PORT (default
  8766). GET /health returns JSON with status, uptime, connected_clients, ws_port,
  and timestamp. Consumed by lazarus-pit to verify gateway liveness.
  Runs in a background daemon thread — zero impact on existing WS logic.

- .gitea/ISSUE_TEMPLATE/mission-proposal.md: Gitea issue template for
  proposing new mission cells (agents, scope, isolation requirements,
  cell config, success criteria).

- tests/test_lazarus_pit.py: 25 unit tests covering config, cell discovery,
  heartbeat reading, health polling, resurrection stub, and CLI.

- tests/test_gateway_health.py: 6 unit tests covering health endpoint
  schema, 404 for unknown paths, uptime, and client count reflection.

All 31 new tests pass. Pre-existing test failures unchanged.

Fixes #879
Refs #878

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 14:16:45 -04:00

130 lines
4.1 KiB
Python

"""Tests for the gateway health heartbeat endpoint (#879 — M6-P0 Foundation).
Validates:
- /health returns 200 with correct JSON schema
- /health reports connected_clients count
- Non-/health paths return 404
- Uptime is non-negative
"""
import importlib.util
import json
import sys
import time
from http.server import HTTPServer
from io import BytesIO
from pathlib import Path
from unittest.mock import patch, MagicMock
import pytest
# ── Load server module directly ──────────────────────────────────────────────
PROJECT_ROOT = Path(__file__).parent.parent
# Patch websockets import so we don't need the package installed to test health
_ws_mock = MagicMock()
sys.modules.setdefault("websockets", _ws_mock)
_srv_spec = importlib.util.spec_from_file_location(
"nexus_server_test",
PROJECT_ROOT / "server.py",
)
_srv = importlib.util.module_from_spec(_srv_spec)
sys.modules["nexus_server_test"] = _srv
_srv_spec.loader.exec_module(_srv)
_HealthHandler = _srv._HealthHandler
# ── Fake request helper ───────────────────────────────────────────────────────
class _FakeRequest:
"""Minimal socket-like object for BaseHTTPRequestHandler testing."""
def __init__(self, raw_bytes: bytes):
self._buf = BytesIO(raw_bytes)
self.sent = BytesIO()
def makefile(self, mode, **kwargs):
if "r" in mode:
return self._buf
return self.sent
def sendall(self, data: bytes):
self.sent.write(data)
def _invoke_handler(path: str) -> tuple[int, dict]:
"""Call the health handler for a GET request and return (status_code, body_dict)."""
raw = f"GET {path} HTTP/1.1\r\nHost: localhost\r\n\r\n".encode()
request = _FakeRequest(raw)
handler = _HealthHandler.__new__(_HealthHandler)
handler.rfile = BytesIO(raw)
handler.wfile = request.sent
handler.client_address = ("127.0.0.1", 9999)
handler.server = MagicMock()
handler.request_version = "HTTP/1.1"
handler.command = "GET"
handler.path = path
handler.headers = {}
# Capture response
responses: list[tuple] = []
handler.send_response = lambda code, *a: responses.append(("status", code))
handler.send_header = lambda k, v: None
handler.end_headers = lambda: None
body_parts: list[bytes] = []
handler.wfile = MagicMock()
handler.wfile.write = lambda b: body_parts.append(b)
handler.do_GET()
status = responses[0][1] if responses else None
body = {}
if body_parts:
try:
body = json.loads(b"".join(body_parts))
except (json.JSONDecodeError, TypeError):
pass
return status, body
# ── Tests ─────────────────────────────────────────────────────────────────────
class TestHealthEndpoint:
def test_health_returns_200(self):
status, _ = _invoke_handler("/health")
assert status == 200
def test_health_body_schema(self):
_, body = _invoke_handler("/health")
assert body.get("status") == "ok"
assert body.get("service") == "nexus-gateway"
assert "uptime_seconds" in body
assert "connected_clients" in body
assert "ws_port" in body
assert "ts" in body
def test_uptime_is_non_negative(self):
_, body = _invoke_handler("/health")
assert body["uptime_seconds"] >= 0
def test_unknown_path_returns_404(self):
status, _ = _invoke_handler("/notfound")
assert status == 404
def test_root_path_returns_404(self):
status, _ = _invoke_handler("/")
assert status == 404
def test_connected_clients_reflects_module_state(self):
original = _srv.clients.copy()
try:
_srv.clients.clear()
_, body = _invoke_handler("/health")
assert body["connected_clients"] == 0
finally:
_srv.clients.update(original)