from __future__ import annotations from pathlib import Path from fastapi.testclient import TestClient from app.main import app def test_ml_routes_are_exposed_by_production_app() -> None: with TestClient(app) as client: health = client.get("/ml/health") models = client.get("/ml/models") assert health.status_code == 200 assert models.status_code == 200 assert isinstance(models.json()["models"], list) def test_unknown_model_returns_404() -> None: with TestClient(app) as client: response = client.post( "/ml/predict", json={ "modelId": "missing", "sensor_id": "sensor.kitchen", "values": {"temperature": 21.0}, }, ) assert response.status_code == 404 def test_unsupported_sensor_returns_422(tmp_path: Path) -> None: from app.ml.registry.model_registry import ModelRegistry from app.ml.training import TrainedArtifact registry = ModelRegistry(tmp_path) registry.register(TrainedArtifact("default", ("sensor.kitchen",))) with TestClient(app) as client: app.state.registry = registry response = client.post( "/ml/predict", json={ "modelId": "default", "sensor_id": "sensor.unknown", "values": {"temperature": 21.0}, }, ) assert response.status_code == 422 def test_retrain_creates_and_replaces_persisted_model(tmp_path: Path) -> None: from app.ml.registry.model_registry import ModelRegistry registry = ModelRegistry(tmp_path) with TestClient(app) as client: app.state.registry = registry created = client.post( "/ml/retrain", json={ "modelId": "home-model", "samples": [ { "sensor_id": "sensor.kitchen", "values": {"temperature": 21.0}, } ], }, ) replaced = client.post( "/ml/retrain", json={ "modelId": "home-model", "samples": [ { "sensor_id": "sensor.bedroom", "values": {"temperature": 18.0}, } ], }, ) assert created.status_code == 200 assert created.json() == { "model_id": "home-model", "supported_sensors": ["sensor.kitchen"], "trained_features": 1, "model_type": "statistical_baseline", "replaced": False, } assert replaced.status_code == 200 assert replaced.json() == { "model_id": "home-model", "supported_sensors": ["sensor.bedroom"], "trained_features": 1, "model_type": "statistical_baseline", "replaced": True, } restarted = ModelRegistry(tmp_path) assert restarted.load_artifact("home-model").supported_sensors == ("sensor.bedroom",) def test_retrain_rejects_empty_samples() -> None: with TestClient(app) as client: response = client.post( "/ml/retrain", json={"modelId": "home-model", "samples": []}, ) assert response.status_code == 422 def test_predict_returns_numeric_forecast_and_confidence(tmp_path: Path) -> None: from app.ml.feature_store import FeatureStore, FeatureVector from app.ml.registry.model_registry import ModelRegistry from app.ml.training import TrainingPipeline store = FeatureStore() store.add_batch( [ FeatureVector("sensor.kitchen", {"temperature": 19.0}), FeatureVector("sensor.kitchen", {"temperature": 20.0}), ] ) registry = ModelRegistry(tmp_path) registry.register(TrainingPipeline(store).run("home-model")) with TestClient(app) as client: app.state.registry = registry response = client.post( "/ml/predict", json={ "modelId": "home-model", "sensor_id": "sensor.kitchen", "values": {"temperature": 21.0}, }, ) assert response.status_code == 200 assert response.json()["predictions"] == {"temperature": 22.0} assert 0.0 < response.json()["confidence"] <= 1.0 assert response.json()["model_type"] == "statistical_baseline" def test_evaluate_returns_real_error_metrics(tmp_path: Path) -> None: from app.ml.feature_store import FeatureStore, FeatureVector from app.ml.registry.model_registry import ModelRegistry from app.ml.training import TrainingPipeline store = FeatureStore() store.add_batch( [ FeatureVector("sensor.kitchen", {"temperature": 19.0}), FeatureVector("sensor.kitchen", {"temperature": 20.0}), ] ) registry = ModelRegistry(tmp_path) registry.register(TrainingPipeline(store).run("home-model")) with TestClient(app) as client: app.state.registry = registry response = client.post( "/ml/evaluate", json={ "modelId": "home-model", "samples": [ { "sensor_id": "sensor.kitchen", "values": {"temperature": 21.0}, } ], }, ) assert response.status_code == 200 metrics = {metric["name"]: metric["value"] for metric in response.json()["metrics"]} assert metrics == {"mae": 1.0, "rmse": 1.0, "coverage": 1.0}