Newer
Older
navi-1 / tests / unit / test_swarm.py
"""Swarm announce loop: PSK reading, state transitions, context provider."""

import httpx
import pytest

from navi.identity import InstanceIdentity
from navi.swarm import HiveAnnouncer, read_swarm_key

IDENT = InstanceIdentity(name="quiet-otter", instance_id="11111111-2222-3333-4444-555555555555", created_at="")


def _announcer(transport) -> HiveAnnouncer:
    client = httpx.AsyncClient(transport=transport, base_url="http://hive.test")
    return HiveAnnouncer(
        hive_url="http://hive.test",
        key="secret",
        identity=IDENT,
        port=8099,
        interval=120,
        client=client,
    )


def test_read_swarm_key(tmp_path):
    missing = read_swarm_key(tmp_path / "absent")
    assert missing is None
    empty = tmp_path / "empty"
    empty.write_text("   \n")
    assert read_swarm_key(empty) is None
    good = tmp_path / ".swarm-key"
    good.write_text("  deadbeef  \n")
    assert read_swarm_key(good) == "deadbeef"


@pytest.mark.asyncio
async def test_announce_success_marks_reachable():
    seen = {}

    def handler(request: httpx.Request) -> httpx.Response:
        import json

        seen["url"] = str(request.url)
        seen["key"] = request.headers.get("X-Swarm-Key")
        seen["json"] = json.loads(request.content)
        return httpx.Response(200, json={"ok": True})

    announcer = _announcer(httpx.MockTransport(handler))
    ok = await announcer.announce_once()
    assert ok is True
    status = announcer.get_status()
    assert status["reachable"] is True
    assert status["down_since"] is None
    assert status["last_error"] is None
    # request shape
    assert seen["url"] == "http://hive.test/announce"
    assert seen["key"] == "secret"
    assert seen["json"]["name"] == "quiet-otter"
    assert seen["json"]["port"] == 8099
    assert seen["json"]["instance_id"] == IDENT.instance_id
    assert seen["json"]["meta"]["hostname"]


@pytest.mark.asyncio
async def test_announce_failure_sets_down_state_and_keeps_it():
    calls = {"n": 0}

    def handler(request: httpx.Request) -> httpx.Response:
        calls["n"] += 1
        raise httpx.ConnectError("connection refused")

    announcer = _announcer(httpx.MockTransport(handler))
    assert await announcer.announce_once() is False
    status = announcer.get_status()
    assert status["reachable"] is False
    assert status["down_since"] is not None
    assert "ConnectError" in status["last_error"]
    assert status["consecutive_failures"] == 1

    # second failure: down_since does not shift (no re-log of transition)
    await announcer.announce_once()
    assert announcer.get_status()["down_since"] == status["down_since"]
    assert announcer.get_status()["consecutive_failures"] == 2


@pytest.mark.asyncio
async def test_announce_recovers_after_failure():
    fail = {"on": True}

    def handler(request: httpx.Request) -> httpx.Response:
        if fail["on"]:
            raise httpx.ConnectError("down")
        return httpx.Response(200, json={"ok": True})

    announcer = _announcer(httpx.MockTransport(handler))
    await announcer.announce_once()
    assert announcer.get_status()["reachable"] is False

    fail["on"] = False
    assert await announcer.announce_once() is True
    status = announcer.get_status()
    assert status["reachable"] is True
    assert status["down_since"] is None
    assert status["consecutive_failures"] == 0


@pytest.mark.asyncio
async def test_hive_status_provider_silent_when_healthy_or_unconfigured(monkeypatch):
    from types import SimpleNamespace

    from navi.context_providers import hive_status as provider
    from navi import swarm

    # unconfigured: no context at all
    monkeypatch.setattr(provider, "settings", SimpleNamespace(hive_url=""))
    assert await provider.get_context() is None

    # configured + healthy: silent (token economy)
    monkeypatch.setattr(provider, "settings", SimpleNamespace(hive_url="http://hive.test"))

    class FakeAnnouncer:
        def get_status(self):
            return {"reachable": True, "url": "http://hive.test", "down_since": None, "last_error": None}

    monkeypatch.setattr(swarm, "get_announcer", lambda: FakeAnnouncer())
    assert await provider.get_context() is None


@pytest.mark.asyncio
async def test_hive_status_provider_reports_outage(monkeypatch):
    from types import SimpleNamespace

    from navi.context_providers import hive_status as provider
    from navi import swarm

    monkeypatch.setattr(provider, "settings", SimpleNamespace(hive_url="http://hive.test"))

    class FakeAnnouncer:
        def get_status(self):
            return {
                "reachable": False,
                "url": "http://hive.test",
                "down_since": "2026-09-10T12:00:00+00:00",
                "last_error": "ConnectError: connection refused",
            }

    monkeypatch.setattr(swarm, "get_announcer", lambda: FakeAnnouncer())
    text = await provider.get_context()
    assert text is not None
    assert "unreachable" in text
    assert "http://hive.test" in text
    assert "connection refused" in text