@@ -50,3 +50,60 @@ def test_unsupported_sensor_returns_422(tmp_path: Path) -> None:
|
||||
)
|
||||
|
||||
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"],
|
||||
"replaced": False,
|
||||
}
|
||||
assert replaced.status_code == 200
|
||||
assert replaced.json() == {
|
||||
"model_id": "home-model",
|
||||
"supported_sensors": ["sensor.bedroom"],
|
||||
"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
|
||||
|
||||
@@ -19,6 +19,17 @@ def test_registry_loads_persisted_artifacts_after_restart(tmp_path: Path) -> Non
|
||||
assert restarted.load_artifact("model-v1") == artifact
|
||||
|
||||
|
||||
def test_registry_replaces_persisted_artifact_after_restart(tmp_path: Path) -> None:
|
||||
registry = ModelRegistry(tmp_path)
|
||||
registry.register(TrainedArtifact("model-v1", ("sensor.kitchen",)))
|
||||
replacement = TrainedArtifact("model-v1", ("sensor.bedroom",))
|
||||
|
||||
registry.register(replacement)
|
||||
|
||||
assert registry.load_artifact("model-v1") == replacement
|
||||
assert ModelRegistry(tmp_path).load_artifact("model-v1") == replacement
|
||||
|
||||
|
||||
@pytest.mark.parametrize("artifact_id", ["../escape", "nested/model", "..", ""])
|
||||
def test_registry_rejects_unsafe_artifact_ids(tmp_path: Path, artifact_id: str) -> None:
|
||||
registry = ModelRegistry(tmp_path)
|
||||
|
||||
39
tests/ml/test_retraining.py
Normal file
39
tests/ml/test_retraining.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.ml.feature_store import FeatureVector
|
||||
from app.ml.registry.model_registry import ModelRegistry
|
||||
from app.ml.retraining import RetrainingService, retrain_model
|
||||
|
||||
|
||||
def _vector(sensor_id: str) -> FeatureVector:
|
||||
return FeatureVector(sensor_id=sensor_id, values={"temperature": 21.0})
|
||||
|
||||
|
||||
def test_retraining_registers_new_artifact(tmp_path: Path) -> None:
|
||||
registry = ModelRegistry(tmp_path)
|
||||
|
||||
result = retrain_model(registry, "home-model", [_vector("sensor.kitchen")])
|
||||
|
||||
assert result.replaced is False
|
||||
assert registry.load_artifact("home-model") == result.artifact
|
||||
|
||||
|
||||
def test_retraining_replaces_existing_artifact(tmp_path: Path) -> None:
|
||||
registry = ModelRegistry(tmp_path)
|
||||
service = RetrainingService(registry)
|
||||
service.retrain("home-model", [_vector("sensor.kitchen")])
|
||||
|
||||
result = service.retrain("home-model", [_vector("sensor.bedroom")])
|
||||
|
||||
assert result.replaced is True
|
||||
assert result.artifact.supported_sensors == ("sensor.bedroom",)
|
||||
assert ModelRegistry(tmp_path).load_artifact("home-model") == result.artifact
|
||||
|
||||
|
||||
def test_retraining_rejects_empty_training_data(tmp_path: Path) -> None:
|
||||
with pytest.raises(ValueError, match="keine Trainingsdaten"):
|
||||
retrain_model(ModelRegistry(tmp_path), "home-model", [])
|
||||
Reference in New Issue
Block a user