Files
openswarm/backend/tests/test_api_agents_subscriptions.py
T
Arnav Naval 5cc9f6f01a [arnav] remove 6 dead HTTP endpoints
Verified each had no production caller (frontend, electron, or
internal backend) — only test references kept them looking alive
to coverage tools. Drops ~140 LOC of route handlers + helpers.
- POST /api/outputs/vibe-code: never wired up; frontend has no
  vibe-code UI. Also drops VibeCodeRequest model and
  VIBE_CODE_SYSTEM_PROMPT.
- GET /api/service/cost-breakdown: frontend Usage page reads
  /usage-summary, which already returns by_model/by_provider.
- GET /api/service/status: zero callers; was placeholder.
- GET /api/service/spool/count: debug-only, no UI surface.
- GET /api/settings/default-system-prompt: frontend defines its
  own DEFAULT_SYSTEM_PROMPT in settingsSlice.ts and never fetches
  the backend constant.
- POST /api/browser/command: sole caller (browser_mcp_server.py
  subprocess) was deleted in 8286cc1; browser_agent.py now calls
  ws_manager.send_browser_command directly in-process.
Tests covering the removed endpoints are dropped along with the
now-unused mock-anthropic helpers in test_api_outputs.py.
2026-05-06 17:11:57 -05:00

210 lines
7.4 KiB
Python

"""REST-surface tests for /api/agents/subscriptions.
These routes wrap 9Router (the bundled OAuth/proxy daemon). The
`backend.tests.conftest` already no-ops `nine_router.ensure_running` so
no node subprocess is spawned, but `nine_router.is_running()` still tries
a real `httpx.get` to localhost:20128 and we don't want any network
chatter from these tests. Two fixtures here:
- `nine_router_down` — pins is_running() to False.
- `nine_router_up` — pins is_running() to True and stubs the four
async helpers (get_providers, get_models,
start_oauth, poll_oauth, exchange_oauth) with
canned dicts.
Coverage:
- GET /subscriptions/status — both down + up shapes.
- POST /subscriptions/connect — missing provider → 400; 503 when
9Router is unavailable; happy path
round-trips the device_code flow.
- POST /subscriptions/poll — missing provider / device_code → 400.
- POST /subscriptions/exchange — missing provider / code → 400.
- POST /subscriptions/disconnect — missing provider → 400.
Happy paths for poll / exchange / disconnect involve writing real
provider connection state via 9Router and are out of scope for the REST
surface tests; they're covered by the integration suite.
"""
from __future__ import annotations
import pytest
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def nine_router_down(monkeypatch):
"""Pin nine_router.is_running() to False.
Defends against the cached True branch in the real implementation
(see _IS_RUNNING_TTL in backend/apps/nine_router.py) leaking from
other test runs.
"""
import backend.apps.nine_router as _nr
monkeypatch.setattr(_nr, "is_running", lambda: False)
# ensure_running is also stubbed in conftest, but re-stubbing here
# makes the contract of this fixture self-evident.
async def _async_noop(*args, **kwargs):
return None
monkeypatch.setattr(_nr, "ensure_running", _async_noop)
return _nr
@pytest.fixture
def nine_router_up(monkeypatch):
"""Pretend 9Router is running and serve canned data.
All async helpers used by the subscriptions routes are replaced.
Tests can override individual helpers via `monkeypatch.setattr`
after this fixture runs.
"""
import backend.apps.nine_router as _nr
monkeypatch.setattr(_nr, "is_running", lambda: True)
async def _providers():
return [
{"provider": "claude", "id": "conn-1", "isActive": True},
]
async def _models():
return [{"id": "claude-sonnet-4", "owned_by": "anthropic"}]
async def _start_oauth(provider: str):
return {
"flow": "device_code",
"user_code": "ABCD-1234",
"verification_uri": "https://example.com/activate",
"device_code": "dev-code-xyz",
"code_verifier": "",
"extra_data": {},
}
async def _poll_oauth(provider, device_code, **_):
return {"success": True, "connection": {"provider": provider, "id": "conn-1"}}
async def _exchange_oauth(provider, code, redirect_uri, code_verifier, state=""):
return {"success": True, "connection": {"provider": provider, "id": "conn-1"}}
monkeypatch.setattr(_nr, "get_providers", _providers)
monkeypatch.setattr(_nr, "get_models", _models)
monkeypatch.setattr(_nr, "start_oauth", _start_oauth)
monkeypatch.setattr(_nr, "poll_oauth", _poll_oauth)
monkeypatch.setattr(_nr, "exchange_oauth", _exchange_oauth)
return _nr
# ---------------------------------------------------------------------------
# GET /subscriptions/status
# ---------------------------------------------------------------------------
def test_status_when_9router_down(client, nine_router_down):
resp = client.get("/api/agents/subscriptions/status")
assert resp.status_code == 200
assert resp.json() == {"running": False, "providers": [], "models": []}
def test_status_when_9router_up(client, nine_router_up):
"""When up, the route wraps providers in a `connections` envelope —
the OnboardingModal and Settings UI both read `data.providers.connections`.
Pin that shape so an accidental rename surfaces as a test failure."""
resp = client.get("/api/agents/subscriptions/status")
assert resp.status_code == 200
body = resp.json()
assert body["running"] is True
assert "connections" in body["providers"]
assert isinstance(body["providers"]["connections"], list)
assert isinstance(body["models"], list)
# ---------------------------------------------------------------------------
# POST /subscriptions/connect
# ---------------------------------------------------------------------------
def test_connect_requires_provider(client, nine_router_down):
resp = client.post("/api/agents/subscriptions/connect", json={})
assert resp.status_code == 400
def test_connect_returns_503_when_9router_unavailable(client, nine_router_down):
"""Provider given but 9Router can't be started — the route returns
503 with a Node-install hint. `nine_router_down` pins both
is_running() AND ensure_running() so the route's retry path
short-circuits."""
resp = client.post(
"/api/agents/subscriptions/connect",
json={"provider": "claude"},
)
assert resp.status_code == 503
def test_connect_round_trips_device_code(client, nine_router_up):
resp = client.post(
"/api/agents/subscriptions/connect",
json={"provider": "github"},
)
assert resp.status_code == 200
body = resp.json()
assert body["flow"] == "device_code"
assert body["user_code"] == "ABCD-1234"
assert body["device_code"] == "dev-code-xyz"
# ---------------------------------------------------------------------------
# POST /subscriptions/poll
# ---------------------------------------------------------------------------
def test_poll_requires_provider_and_device_code(client):
"""Both fields must be present. We test each missing-half separately
so a regression that only validates one of them is caught."""
no_provider = client.post(
"/api/agents/subscriptions/poll",
json={"device_code": "dev"},
)
assert no_provider.status_code == 400
no_device = client.post(
"/api/agents/subscriptions/poll",
json={"provider": "claude"},
)
assert no_device.status_code == 400
# ---------------------------------------------------------------------------
# POST /subscriptions/exchange
# ---------------------------------------------------------------------------
def test_exchange_requires_provider_and_code(client):
no_provider = client.post(
"/api/agents/subscriptions/exchange",
json={"code": "abc"},
)
assert no_provider.status_code == 400
no_code = client.post(
"/api/agents/subscriptions/exchange",
json={"provider": "claude"},
)
assert no_code.status_code == 400
# ---------------------------------------------------------------------------
# POST /subscriptions/disconnect
# ---------------------------------------------------------------------------
def test_disconnect_requires_provider(client):
resp = client.post("/api/agents/subscriptions/disconnect", json={})
assert resp.status_code == 400