""" Phase B verification: ensure all 146 routes survived the monolith split. Run inside Docker: docker run --rm -v ./bot/memory:/app/memory miku-discord-miku-bot \ python -m pytest tests/test_route_split.py -v """ import pytest import sys, os # ── make /app importable ── sys.path.insert(0, "/app") os.chdir("/app") os.environ.setdefault("DISCORD_BOT_TOKEN", "test_token") # ── now import the FastAPI app ── from api import app # noqa: E402 # Collect all routes from the app def _collect_routes(): """Return set of (method, path) tuples registered on the FastAPI app.""" routes = set() for route in app.routes: # Skip Mount routes (static files) and other non-API routes if not hasattr(route, "methods"): continue for method in route.methods: # Normalize: uppercase method, path as-is routes.add((method.upper(), route.path)) return routes REGISTERED = _collect_routes() # ── Expected routes: every route from the original monolith ── EXPECTED_ROUTES = [ # core.py (7) ("GET", "/"), ("GET", "/logs"), ("GET", "/prompt"), ("GET", "/prompt/cat"), ("GET", "/status"), ("GET", "/autonomous/stats"), ("GET", "/conversation/{user_id}"), # mood.py (10) ("GET", "/mood"), ("POST", "/mood"), ("POST", "/mood/reset"), ("POST", "/mood/calm"), ("GET", "/servers/{guild_id}/mood"), ("POST", "/servers/{guild_id}/mood"), ("POST", "/servers/{guild_id}/mood/reset"), ("GET", "/servers/{guild_id}/mood/state"), ("GET", "/moods/available"), ("POST", "/test/mood/{guild_id}"), # language.py (3) ("GET", "/language"), ("POST", "/language/toggle"), ("POST", "/language/set"), # evil_mode.py (6) ("GET", "/evil-mode"), ("POST", "/evil-mode/enable"), ("POST", "/evil-mode/disable"), ("POST", "/evil-mode/toggle"), ("GET", "/evil-mode/mood"), ("POST", "/evil-mode/mood"), # bipolar_mode.py (9) ("GET", "/bipolar-mode"), ("POST", "/bipolar-mode/enable"), ("POST", "/bipolar-mode/disable"), ("POST", "/bipolar-mode/toggle"), ("POST", "/bipolar-mode/trigger-argument"), ("POST", "/bipolar-mode/trigger-dialogue"), ("GET", "/bipolar-mode/scoreboard"), ("POST", "/bipolar-mode/cleanup-webhooks"), ("GET", "/bipolar-mode/arguments"), # gpu.py (2) ("GET", "/gpu-status"), ("POST", "/gpu-select"), # bot_actions.py (4) ("POST", "/conversation/reset"), ("POST", "/sleep"), ("POST", "/wake"), ("POST", "/bedtime"), # autonomous.py (13) ("POST", "/autonomous/general"), ("POST", "/autonomous/engage"), ("POST", "/autonomous/tweet"), ("POST", "/autonomous/custom"), ("POST", "/autonomous/reaction"), ("POST", "/autonomous/join-conversation"), ("POST", "/servers/{guild_id}/autonomous/general"), ("POST", "/servers/{guild_id}/autonomous/engage"), ("POST", "/servers/{guild_id}/autonomous/custom"), ("POST", "/servers/{guild_id}/autonomous/tweet"), ("GET", "/autonomous/v2/stats/{guild_id}"), ("GET", "/autonomous/v2/check/{guild_id}"), ("GET", "/autonomous/v2/status"), # profile_picture.py (26) ("POST", "/profile-picture/change"), ("GET", "/profile-picture/metadata"), ("POST", "/profile-picture/restore-fallback"), ("POST", "/role-color/custom"), ("POST", "/role-color/reset-fallback"), ("GET", "/profile-picture/image/original"), ("GET", "/profile-picture/image/current"), ("POST", "/profile-picture/change-no-crop"), ("POST", "/profile-picture/manual-crop"), ("POST", "/profile-picture/auto-crop"), ("POST", "/profile-picture/description"), ("POST", "/profile-picture/regenerate-description"), ("GET", "/profile-picture/description"), ("GET", "/profile-picture/album"), ("GET", "/profile-picture/album/disk-usage"), ("GET", "/profile-picture/album/{entry_id}"), ("GET", "/profile-picture/album/{entry_id}/image/{image_type}"), ("POST", "/profile-picture/album/add"), ("POST", "/profile-picture/album/add-batch"), ("POST", "/profile-picture/album/{entry_id}/set-current"), ("POST", "/profile-picture/album/{entry_id}/manual-crop"), ("POST", "/profile-picture/album/{entry_id}/auto-crop"), ("POST", "/profile-picture/album/{entry_id}/description"), ("DELETE", "/profile-picture/album/{entry_id}"), ("POST", "/profile-picture/album/delete-bulk"), ("POST", "/profile-picture/album/add-current"), # manual_send.py (3) ("POST", "/manual/send"), ("POST", "/manual/send-webhook"), ("POST", "/messages/react"), # servers.py (6) ("GET", "/servers"), ("POST", "/servers"), ("DELETE", "/servers/{guild_id}"), ("PUT", "/servers/{guild_id}"), ("POST", "/servers/{guild_id}/bedtime-range"), ("POST", "/servers/repair"), # figurines.py (5) ("GET", "/figurines/subscribers"), ("POST", "/figurines/subscribers"), ("DELETE", "/figurines/subscribers/{user_id}"), ("POST", "/figurines/send_now"), ("POST", "/figurines/send_to_user"), # dms.py (18) ("POST", "/dm/{user_id}/custom"), ("POST", "/dm/{user_id}/manual"), ("GET", "/dms/users"), ("GET", "/dms/users/{user_id}"), ("GET", "/dms/users/{user_id}/conversations"), ("GET", "/dms/users/{user_id}/search"), ("GET", "/dms/users/{user_id}/export"), ("DELETE", "/dms/users/{user_id}"), ("GET", "/dms/blocked-users"), ("POST", "/dms/users/{user_id}/block"), ("POST", "/dms/users/{user_id}/unblock"), ("POST", "/dms/users/{user_id}/conversations/{conversation_id}/delete"), ("POST", "/dms/users/{user_id}/conversations/delete-all"), ("POST", "/dms/users/{user_id}/delete-completely"), ("POST", "/dms/analysis/run"), ("POST", "/dms/users/{user_id}/analyze"), ("GET", "/dms/analysis/reports"), ("GET", "/dms/analysis/reports/{user_id}"), # image_generation.py (4) ("POST", "/image/generate"), ("GET", "/image/status"), ("POST", "/image/test-detection"), ("GET", "/image/view/{filename}"), # chat.py (1) ("POST", "/chat/stream"), # config.py (7) ("GET", "/config"), ("GET", "/config/static"), ("GET", "/config/runtime"), ("POST", "/config/set"), ("POST", "/config/reset"), ("POST", "/config/validate"), ("GET", "/config/state"), # logging_config.py (9) ("GET", "/api/log/config"), ("POST", "/api/log/config"), ("GET", "/api/log/components"), ("POST", "/api/log/reload"), ("POST", "/api/log/filters"), ("POST", "/api/log/reset"), ("POST", "/api/log/global-level"), ("POST", "/api/log/timestamp-format"), ("GET", "/api/log/files/{component}"), # voice.py (3) ("POST", "/voice/call"), ("GET", "/voice/debug-mode"), ("POST", "/voice/debug-mode"), # memory.py (10) ("GET", "/memory/status"), ("POST", "/memory/toggle"), ("GET", "/memory/stats"), ("GET", "/memory/facts"), ("GET", "/memory/episodic"), ("POST", "/memory/consolidate"), ("POST", "/memory/delete"), ("DELETE", "/memory/point/{collection}/{point_id}"), ("PUT", "/memory/point/{collection}/{point_id}"), ("POST", "/memory/create"), ] class TestRoutePresence: """Verify each expected route is registered on the FastAPI app.""" @pytest.mark.parametrize("method,path", EXPECTED_ROUTES, ids=[f"{m} {p}" for m, p in EXPECTED_ROUTES]) def test_route_exists(self, method, path): assert (method, path) in REGISTERED, ( f"Route {method} {path} missing from app.routes! " f"Registered routes with similar path: " f"{[r for r in REGISTERED if path.split('/')[1] in r[1]]}" ) def test_total_route_count(self): """Sanity check: we expect exactly 146 API routes.""" assert len(EXPECTED_ROUTES) == 146, f"Expected list has {len(EXPECTED_ROUTES)} routes, want 146" def test_no_unexpected_route_loss(self): """Every expected route must be registered.""" missing = [(m, p) for m, p in EXPECTED_ROUTES if (m, p) not in REGISTERED] assert not missing, f"Missing {len(missing)} routes:\n" + "\n".join(f" {m} {p}" for m, p in missing) def test_registered_count_at_least_expected(self): """Registered API routes should be >= expected (HEAD routes are auto-added).""" # Filter out HEAD duplicates that FastAPI adds automatically for GET routes non_head = {r for r in REGISTERED if r[0] != "HEAD"} assert len(non_head) >= 146, f"Only {len(non_head)} non-HEAD routes registered, expected >= 146"