kronector / tests /test_api_endpoints.py
Prathamesh Bhamare
Initial commit: KRONECTOR MLOps & Multi-Agent AI system
2532605
Raw
History Blame Contribute Delete
6.92 kB
"""
Integration tests for api.main — FastAPI endpoints.
Run:
python -m pytest tests/test_api_endpoints.py -v -s
"""
import json
from pathlib import Path
import pandas as pd
import pytest
from fastapi.testclient import TestClient
from api.main import app
from agents.data_agent import QueryIntent
@pytest.fixture
def client():
"""FastAPI test client."""
return TestClient(app)
@pytest.fixture
def mock_startup(monkeypatch):
"""Mock model and data loading."""
import pandas as pd
# Create minimal mock data
mock_data = pd.DataFrame(
{
"season": [2023, 2023],
"round": [1, 1],
"driver_id": ["VER", "HAM"],
"driver_name": ["Max Verstappen", "Lewis Hamilton"],
"team": ["Red Bull Racing", "Mercedes"],
"grid_position": [1.0, 2.0],
"finish_position": [1.0, 2.0],
"circuit_id": ["Bahrain Grand Prix", "Bahrain Grand Prix"],
"sector_1_time": [28.7, 28.9],
"sector_2_time": [38.5, 38.7],
"sector_3_time": [22.4, 22.6],
"avg_lap_time_practice": [95.1, 95.4],
"tire_compound": [0.0, 1.0],
"tire_age_laps": [14.0, 12.0],
"fresh_tire": [0.0, 1.0],
"pit_stop_count": [2.0, 2.0],
"team_pit_speed": [2.5, 2.8],
"weather_temp_track": [31.0, 31.0],
"weather_rainfall": [0.0, 0.0],
"telemetry_available": [True, True],
}
)
# Mock data loading
import api.main
api.main._races_data = mock_data
api.main._drivers_set = {"VER", "HAM"}
# Mock model
class MockModel:
def predict_proba(self, X):
import numpy as np
return np.array([[0.3, 0.7], [0.6, 0.4]])
api.main._model = MockModel()
api.main._encoders = {}
yield
# Cleanup
api.main._races_data = None
api.main._drivers_set = set()
api.main._model = None
api.main._encoders = None
def test_health_endpoint(client):
"""GET /health returns status."""
response = client.get("/health")
assert response.status_code == 200
data = response.json()
assert "status" in data
assert "model_loaded" in data
assert "data_available" in data
def test_root_endpoint(client):
"""GET / returns API info."""
response = client.get("/")
assert response.status_code == 200
data = response.json()
assert data["name"] == "KRONECTOR F1 Intelligence API"
assert "docs" in data
def test_predict_requires_model(client, mock_startup):
"""POST /predict/f1 returns 503 if model not loaded."""
import api.main
api.main._model = None
response = client.post(
"/predict/f1", json={"query": "Verstappen 2023 Bahrain"}
)
assert response.status_code == 503
assert "Model not loaded" in response.json()["detail"]
def test_predict_requires_query_length(client, mock_startup):
"""POST /predict/f1 validates query length."""
response = client.post("/predict/f1", json={"query": "ab"}) # Too short
assert response.status_code == 422 # Validation error
def test_predict_invalid_query(client, mock_startup):
"""POST /predict/f1 handles invalid queries gracefully."""
def mock_parser(q: str) -> QueryIntent:
# Return impossible race
return {"season": 2099, "round": 999}
import api.main
api.main.data_agent = lambda q: (__import__("agents.data_agent", fromlist=["data_agent"]).data_agent(q, parser=mock_parser))
response = client.post(
"/predict/f1", json={"query": "Impossible race"}
)
# May be 400 (validation) or 500 (internal error)
assert response.status_code in [400, 500]
def test_list_drivers(client, mock_startup):
"""GET /drivers returns driver list."""
response = client.get("/drivers")
assert response.status_code == 200
drivers = response.json()
assert len(drivers) == 2
assert any(d["driver_id"] == "VER" for d in drivers)
assert any(d["driver_id"] == "HAM" for d in drivers)
def test_list_drivers_by_season(client, mock_startup):
"""GET /drivers?season=2023 filters drivers."""
response = client.get("/drivers?season=2023")
assert response.status_code == 200
drivers = response.json()
assert len(drivers) == 2
def test_list_drivers_nonexistent_season(client, mock_startup):
"""GET /drivers?season=2099 returns empty list."""
response = client.get("/drivers?season=2099")
assert response.status_code == 200
drivers = response.json()
assert len(drivers) == 0
def test_list_races(client, mock_startup):
"""GET /races/{season} returns race list."""
response = client.get("/races/2023")
assert response.status_code == 200
races = response.json()
assert len(races) == 1
assert races[0]["round"] == 1
assert races[0]["season"] == 2023
def test_list_races_nonexistent_season(client, mock_startup):
"""GET /races/{season} for nonexistent season returns 404."""
response = client.get("/races/2099")
assert response.status_code == 404
assert "No races found" in response.json()["detail"]
def test_predict_with_mock_encoders(client, mock_startup):
"""POST /predict/f1 handles predictions when encoders are available."""
import api.main
from unittest.mock import patch
import numpy as np
# Create minimal encoders dict
from sklearn.preprocessing import LabelEncoder
encoders = {}
for col in ["team", "track_type", "regulation_era"]:
enc = LabelEncoder()
enc.fit([f"{col}_1", f"{col}_2", "unknown"])
encoders[col] = enc
api.main._encoders = encoders
# Create a mock that tracks calls
with patch("api.main.predict_dataframe") as mock_predict:
mock_predict.return_value = pd.DataFrame(
{
"season": [2023],
"round": [1],
"driver_id": ["VER"],
"driver_name": ["Max Verstappen"],
"team": ["Red Bull Racing"],
"grid_position": [1.0],
"finish_position": [1.0],
"circuit_id": ["Bahrain"],
"win_probability": [0.75],
"shap_values": [{"grid_position": 0.3, "sector_1_time": 0.2}],
}
)
with patch("api.main.data_agent") as mock_da:
mock_da.return_value = {
"query": "Verstappen 2023",
"intent": {"season": 2023, "round": 1, "driver_id": "VER"},
"rows": [],
"dataframe": pd.DataFrame({"season": [2023]}),
}
response = client.post(
"/predict/f1", json={"query": "Verstappen 2023 Bahrain"}
)
# Verify predict_dataframe was called
assert mock_predict.called
if __name__ == "__main__":
pytest.main([__file__, "-v", "-s"])