Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/tsk-7tqplf-orb-lan-route.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Added

- Add admin-gated GET /api/orb/lan for concurrent mDNS Orb browse and Agent Buddy UDP probe (tinyagentos/routes/orb.py).
99 changes: 98 additions & 1 deletion tests/test_routes_orb.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,101 @@ async def test_status_admin_transport_unavailable(client, app, monkeypatch):
resp = await client.get("/api/orb/status")
assert resp.status_code == 503
body = resp.json()
assert body == {"error": "orb_transport_unavailable"}
assert body == {"error": "orb_transport_unavailable"}

# ---------------------------------------------------------------------------
# GET /api/orb/lan (LAN discovery: mDNS + Agent Buddy)
# ---------------------------------------------------------------------------

import asyncio
import threading

from tinyagentos.orb.frames import TransportUnavailable


@pytest.mark.asyncio
async def test_lan_unauthed_requires_admin(unauthed_client):
resp = await unauthed_client.get("/api/orb/lan")
assert resp.status_code in (401, 403)


@pytest.mark.asyncio
async def test_lan_non_admin_session_gets_403(client, app, monkeypatch):
def raise_assert(*args, **kwargs):
raise AssertionError("lookup must not be called for non-admin")

monkeypatch.setattr(app.state.auth, "session_user", lambda token, user_agent=None: {"is_admin": False})
monkeypatch.setattr("tinyagentos.routes.orb.browse_lan_orbs", raise_assert)
monkeypatch.setattr("tinyagentos.routes.orb.probe_agent_buddy", raise_assert)
resp = await client.get("/api/orb/lan")
assert resp.status_code == 403


@pytest.mark.asyncio
async def test_lan_admin_both_succeed(client, app, monkeypatch):
fake_lan = [{"name": "orb-1", "addresses": ["192.168.1.10"], "port": 5353}]
fake_buddy = [{"device_id": "abcd1234abcd1234", "hostname": "buddy1", "address": "192.168.1.20", "port": 4666, "boot": 1}]
monkeypatch.setattr("tinyagentos.routes.orb.browse_lan_orbs", lambda: fake_lan)
monkeypatch.setattr("tinyagentos.routes.orb.probe_agent_buddy", lambda: fake_buddy)
resp = await client.get("/api/orb/lan")
assert resp.status_code == 200
body = resp.json()
assert body["lan_orbs"] == fake_lan
assert body["agent_buddy"] == fake_buddy
assert body["errors"] == []


@pytest.mark.asyncio
async def test_lan_admin_probe_agent_buddy_transport_unavailable(client, app, monkeypatch):
fake_lan = [{"name": "orb-1", "addresses": ["192.168.1.10"], "port": 5353}]
monkeypatch.setattr("tinyagentos.routes.orb.browse_lan_orbs", lambda: fake_lan)
monkeypatch.setattr(
"tinyagentos.routes.orb.probe_agent_buddy",
lambda: (_ for _ in ()).throw(TransportUnavailable("x")),
)
resp = await client.get("/api/orb/lan")
assert resp.status_code == 200
body = resp.json()
assert body["lan_orbs"] == fake_lan
assert body["agent_buddy"] == []
assert body["errors"] == [{"source": "agent_buddy", "error": "unavailable"}]


@pytest.mark.asyncio
async def test_lan_admin_browse_lan_orbs_oserror(client, app, monkeypatch):
fake_buddy = [{"device_id": "abcd1234abcd1234", "hostname": "buddy1", "address": "192.168.1.20", "port": 4666, "boot": 1}]
monkeypatch.setattr("tinyagentos.routes.orb.browse_lan_orbs", lambda: (_ for _ in ()).throw(OSError("x")))
monkeypatch.setattr("tinyagentos.routes.orb.probe_agent_buddy", lambda: fake_buddy)
resp = await client.get("/api/orb/lan")
assert resp.status_code == 200
body = resp.json()
assert body["lan_orbs"] == []
assert body["agent_buddy"] == fake_buddy
assert body["errors"] == [{"source": "mdns", "error": "unavailable"}]


@pytest.mark.asyncio
async def test_lan_admin_browse_lan_orbs_runtime_error_raised(client, app, monkeypatch):
fake_buddy = []
monkeypatch.setattr("tinyagentos.routes.orb.browse_lan_orbs", lambda: (_ for _ in ()).throw(RuntimeError("bad")))
monkeypatch.setattr("tinyagentos.routes.orb.probe_agent_buddy", lambda: fake_buddy)
with pytest.raises(RuntimeError):
await client.get("/api/orb/lan")


@pytest.mark.asyncio
async def test_lan_concurrent_execution_no_sleep(client, app, monkeypatch):
barrier = threading.Barrier(2, timeout=2)

def fake_browse():
barrier.wait()
return []

def fake_probe():
barrier.wait()
return []

monkeypatch.setattr("tinyagentos.routes.orb.browse_lan_orbs", fake_browse)
monkeypatch.setattr("tinyagentos.routes.orb.probe_agent_buddy", fake_probe)
resp = await client.get("/api/orb/lan")
assert resp.status_code == 200
37 changes: 35 additions & 2 deletions tinyagentos/routes/orb.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse

from tinyagentos.orb.discovery import list_usb_orbs
from tinyagentos.orb.discovery import browse_lan_orbs, list_usb_orbs, probe_agent_buddy
from tinyagentos.orb.frames import TransportUnavailable
from tinyagentos.routes.auth import _require_admin

Expand All @@ -32,4 +32,37 @@ async def orb_status(request: Request):
orbs = await asyncio.to_thread(list_usb_orbs)
except TransportUnavailable:
return JSONResponse({"error": "orb_transport_unavailable"}, status_code=503)
return {"orbs": orbs}
return {"orbs": orbs}


@router.get("/api/orb/lan")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
async def orb_lan(request: Request):
ok, err = _require_admin(request)
if not ok:
return err
results = await asyncio.gather(
asyncio.to_thread(browse_lan_orbs),
asyncio.to_thread(probe_agent_buddy),
return_exceptions=True,
)
lan_res, buddy_res = results[0], results[1]
lan_orbs = []
agent_buddy = []
errors = []
if isinstance(lan_res, Exception):
if isinstance(lan_res, (TransportUnavailable, OSError)):
lan_orbs = []
errors.append({"source": "mdns", "error": "unavailable"})
else:
raise lan_res
else:
lan_orbs = lan_res
if isinstance(buddy_res, Exception):
if isinstance(buddy_res, (TransportUnavailable, OSError)):
agent_buddy = []
errors.append({"source": "agent_buddy", "error": "unavailable"})
else:
raise buddy_res
else:
agent_buddy = buddy_res
return {"lan_orbs": lan_orbs, "agent_buddy": agent_buddy, "errors": errors}
Loading