""" Tests for the ArcisVLM FastAPI backend. Uses TestClient (synchronous) from FastAPI for fast, no-server-needed testing. Model is NOT loaded in tests — we test API contract, routing, and validation. """ import pytest import sys import os # Ensure project root is on path sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) # Set env to skip model loading for fast tests os.environ["ARCISVLM_CONFIG"] = "configs/scale_1.3b.yaml" # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture(scope="module") def client(): """Create a TestClient for the FastAPI app.""" from fastapi.testclient import TestClient from api.main import app with TestClient(app) as c: yield c # --------------------------------------------------------------------------- # Health endpoint # --------------------------------------------------------------------------- class TestHealth: def test_health_returns_200(self, client): resp = client.get("/health") assert resp.status_code == 200 def test_health_has_status(self, client): resp = client.get("/health") data = resp.json() assert "status" in data assert data["status"] in ("ok", "starting") def test_health_has_model_info(self, client): resp = client.get("/health") data = resp.json() assert "model" in data assert data["model"] == "arcisvlm-1.6b" assert "version" in data # --------------------------------------------------------------------------- # Inference endpoints # --------------------------------------------------------------------------- class TestInference: def test_query_requires_question(self, client): """POST /api/v1/query without 'question' should return 422.""" resp = client.post("/api/v1/query", json={}) assert resp.status_code == 422 def test_query_requires_image_source(self, client): """POST /api/v1/query with question but no image should return 400.""" resp = client.post("/api/v1/query", json={"question": "What is this?"}) assert resp.status_code == 400 def test_query_validates_temperature(self, client): """Temperature out of range should return 422.""" resp = client.post("/api/v1/query", json={ "question": "test", "image_path": "/tmp/test.jpg", "temperature": 5.0, }) assert resp.status_code == 422 def test_query_validates_max_tokens(self, client): """max_tokens < 1 should return 422.""" resp = client.post("/api/v1/query", json={ "question": "test", "image_path": "/tmp/test.jpg", "max_tokens": 0, }) assert resp.status_code == 422 def test_embed_requires_image(self, client): """POST /api/v1/embed without image should return 400 or 503.""" resp = client.post("/api/v1/embed", json={}) assert resp.status_code in (400, 503) # --------------------------------------------------------------------------- # Agent endpoints # --------------------------------------------------------------------------- class TestAgents: def test_agents_status_returns_200(self, client): resp = client.get("/api/v1/agents/status") assert resp.status_code == 200 def test_agents_status_has_agents_key(self, client): resp = client.get("/api/v1/agents/status") data = resp.json() assert "agents" in data assert "total_agents" in data def test_agent_types_returns_list(self, client): resp = client.get("/api/v1/agents/types") assert resp.status_code == 200 data = resp.json() assert "expert_types" in data assert "vqa" in data["expert_types"] assert "detect" in data["expert_types"] # --------------------------------------------------------------------------- # Alert endpoints # --------------------------------------------------------------------------- class TestAlerts: def test_list_rules_returns_200(self, client): resp = client.get("/api/v1/alerts/rules") assert resp.status_code == 200 assert isinstance(resp.json(), list) def test_create_rule(self, client): """Create an alert rule and verify it returns correctly.""" rule = { "rule_id": "test-person-alert", "condition_type": "presence", "target_object": "person", "action": "log", } resp = client.post("/api/v1/alerts/rules", json=rule) # May return 200 if orchestrator is up, or 503 if not assert resp.status_code in (200, 503) if resp.status_code == 200: data = resp.json() assert data["rule_id"] == "test-person-alert" assert data["condition_type"] == "presence" def test_alert_history_returns_200(self, client): resp = client.get("/api/v1/alerts/history") assert resp.status_code == 200 assert isinstance(resp.json(), list) def test_alert_history_limit(self, client): resp = client.get("/api/v1/alerts/history?limit=10") assert resp.status_code == 200 # --------------------------------------------------------------------------- # Stream endpoints # --------------------------------------------------------------------------- class TestStreams: def test_streams_status_returns_200(self, client): resp = client.get("/api/v1/streams/status") assert resp.status_code == 200 assert isinstance(resp.json(), list) def test_start_stream_validates_fields(self, client): """Missing required fields should return 422.""" resp = client.post("/api/v1/streams/start", json={}) assert resp.status_code == 422 def test_start_stream_validates_fps(self, client): """FPS out of range should return 422.""" resp = client.post("/api/v1/streams/start", json={ "camera_id": "test", "rtsp_url": "rtsp://test", "target_fps": 100.0, }) assert resp.status_code == 422 # --------------------------------------------------------------------------- # OpenAPI docs # --------------------------------------------------------------------------- class TestDocs: def test_openapi_schema(self, client): resp = client.get("/openapi.json") assert resp.status_code == 200 schema = resp.json() assert schema["info"]["title"] == "ArcisVLM API" assert "/health" in schema["paths"] assert "/api/v1/query" in schema["paths"]