Newer
Older
navi-1 / tests / unit / tools / test_peer.py
"""peer tool: hive book with stale cache, ask payload shape, self-ask guard."""

import json
from types import SimpleNamespace

import httpx
import pytest

from navi.identity import ensure_identity
from navi.tools import peer as peer_tool
from navi.tools.peer import PeerTool

KEY = "secret-key-123"

ME = {"name": "quiet-otter", "instance_id": "11111111-2222-3333-4444-555555555555"}
FALCON = {
    "name": "amber-falcon",
    "instance_id": "99999999-2222-3333-4444-555555555555",
    "host": "192.168.1.50",
    "port": 8099,
    "address": "192.168.1.50:8099",
    "online": True,
    "meta": {"os": "Linux 6.1", "cpu_cores": 16, "ram_mb": 65536},
    "version": "0.1.0",
}


@pytest.fixture()
def tool(tmp_path, monkeypatch):
    """Tool wired to a tmp identity/key and injectable http transport."""
    ensure_identity(tmp_path / "instance.json")
    (tmp_path / ".swarm-key").write_text(KEY)
    fake = SimpleNamespace(
        hive_url="http://hive.test",
        swarm_key_file=str(tmp_path / ".swarm-key"),
        instance_file=str(tmp_path / "instance.json"),
        peer_ask_timeout_sec=30,
    )
    monkeypatch.setattr(peer_tool, "settings", fake)

    real_client = httpx.AsyncClient

    def make_client(transport):
        # The tool builds its own httpx.AsyncClient; swap in the mock transport.
        monkeypatch.setattr(
            httpx, "AsyncClient",
            lambda timeout=None: real_client(transport=transport, timeout=timeout),
        )

    yield make_client
    peer_tool._peers_cache = None


def _hive_transport(state: dict):
    """Transport serving the hive /peers endpoint (and peer endpoints)."""

    def handler(request: httpx.Request) -> httpx.Response:
        if request.url.path == "/peers":
            if state.get("hive_down"):
                raise httpx.ConnectError("registry unreachable")
            return httpx.Response(200, json={"peers": state.get("peers", []), "ttl_sec": 300})
        if request.url.path == "/peer/status" and state.get("status_ok", True):
            return httpx.Response(200, json={
                "name": FALCON["name"], "instance_id": FALCON["instance_id"],
                "version": "0.1.0", "port": 8099, "uptime_sec": 1234,
                "machine": {"os": "Linux 6.1", "cpu_cores": 16},
                "hive": {"reachable": True},
            })
        if request.url.path == "/peer/ask" and state.get("ask_ok", True):
            body = json.loads(request.content)
            state["last_ask"] = {"body": body, "key": request.headers.get("X-Swarm-Key")}
            return httpx.Response(200, json={"answer": "load is 0.42", "completed": True, "request_id": "abc"})
        raise httpx.ConnectError("no route")

    return httpx.MockTransport(handler)


@pytest.mark.asyncio
async def test_list_success_excludes_self(tool):
    tool(_hive_transport({"peers": [FALCON]}))
    result = await PeerTool().execute({"action": "list"})
    assert result.success
    assert "amber-falcon" in result.output
    assert "192.168.1.50:8099" in result.output
    assert "online" in result.output


@pytest.mark.asyncio
async def test_list_falls_back_to_stale_cache(tool):
    state = {"peers": [FALCON]}
    tool(_hive_transport(state))
    assert (await PeerTool().execute({"action": "list"})).success  # warm the cache

    # hive goes down -> cached copy still answers, marked stale
    state["hive_down"] = True
    peer_tool._peers_cache["peers"][0]["online"] = False  # mutate what we cached
    result = await PeerTool().execute({"action": "list"})
    assert result.success
    assert "STALE" in result.output
    assert "amber-falcon" in result.output
    assert "offline" in result.output  # last-known state


@pytest.mark.asyncio
async def test_list_fails_when_no_cache_and_hive_down(tool):
    tool(_hive_transport({"peers": [FALCON], "hive_down": True}))
    result = await PeerTool().execute({"action": "list"})
    assert not result.success
    assert "Cannot reach the hive" in result.output


@pytest.mark.asyncio
async def test_ask_sends_identity_and_key(tool):
    from navi.identity import load_identity

    me = load_identity(peer_tool.settings.instance_file)

    state = {"peers": [FALCON]}
    tool(_hive_transport(state))
    result = await PeerTool().execute({"action": "ask", "peer": "amber-falcon", "question": "what is your load?"})
    assert result.success
    assert "load is 0.42" in result.output
    sent = state["last_ask"]
    assert sent["key"] == KEY
    assert sent["body"]["question"] == "what is your load?"
    assert sent["body"]["from_name"] == me.name
    assert sent["body"]["from_instance_id"] == me.instance_id  # our real generated identity
    assert result.output.startswith("amber-falcon answered:")


@pytest.mark.asyncio
async def test_ask_unknown_peer_lists_known(tool):
    tool(_hive_transport({"peers": [FALCON]}))
    result = await PeerTool().execute({"action": "ask", "peer": "ghost", "question": "hi"})
    assert not result.success
    assert "Unknown peer 'ghost'" in result.output
    assert "amber-falcon" in result.output  # helpful: names the known peers


@pytest.mark.asyncio
async def test_ask_refuses_self(tool):
    from navi.identity import load_identity

    me = load_identity(peer_tool.settings.instance_file)
    tool(_hive_transport({"peers": [FALCON]}))
    result = await PeerTool().execute({"action": "ask", "peer": me.name, "question": "hi"})
    assert not result.success
    assert "this very navi" in result.output


@pytest.mark.asyncio
async def test_ask_reports_peer_refusal(tool):
    def handler(request: httpx.Request) -> httpx.Response:
        if request.url.path == "/peers":
            return httpx.Response(200, json={"peers": [FALCON], "ttl_sec": 300})
        return httpx.Response(409, json={"detail": "ask loop detected - this question originated here"})

    tool(httpx.MockTransport(handler))
    result = await PeerTool().execute({"action": "ask", "peer": "amber-falcon", "question": "hi"})
    assert not result.success
    assert "refused" in result.output
    assert "ask loop detected" in result.output


@pytest.mark.asyncio
async def test_status_formats_peer_state(tool):
    tool(_hive_transport({"peers": [FALCON]}))
    result = await PeerTool().execute({"action": "status", "peer": "amber-falcon"})
    assert result.success
    assert "uptime: 1234s" in result.output
    assert "Linux 6.1" in result.output


@pytest.mark.asyncio
async def test_incomplete_answer_is_flagged(tool):
    def handler(request: httpx.Request) -> httpx.Response:
        if request.url.path == "/peers":
            return httpx.Response(200, json={"peers": [FALCON], "ttl_sec": 300})
        return httpx.Response(200, json={"answer": "partial", "completed": False, "request_id": "x"})

    tool(httpx.MockTransport(handler))
    result = await PeerTool().execute({"action": "ask", "peer": "amber-falcon", "question": "hi"})
    assert result.success
    assert "turn limit" in result.output