SIENTIAPDE-1646

SIENTIAPDE-1646 Add new scheduling configurations and remove outdated documentation

- Introduced new scheduling configurations for minimal retrain, drift analysis, and simple metrics in `input_sample.json`.
- Removed obsolete documentation files related to drift analysis and E2E test reports to streamline project resources.
- Updated E2E tests for minimal retrain to enhance reporting and error handling during model retraining processes.
This commit is contained in:
vitor-aignosi
2026-05-11 17:02:08 -03:00
parent 16ea436e45
commit 4989cfcb3c
5 changed files with 182 additions and 131 deletions

View File

@@ -22,6 +22,7 @@ from datetime import datetime, timedelta, timezone
from unittest.mock import MagicMock, patch
import pytest
import pandas as pd
from sqlalchemy import text
from temporalio.testing import WorkflowEnvironment
from temporalio.worker import Worker
@@ -34,6 +35,7 @@ from e2e.helpers import (
)
from laborious.activities.activities import Activities
from laborious.workflows.minimal_retrain import MinimalRetrain
from sientia_model.wrappers.sientia_model import SientiaModel
# Columns defined by the production DDL for ``sientia_data.log_retrain``.
# The legacy ``retrain_reports`` table had ``id`` and ``created_at``; the new
@@ -49,6 +51,86 @@ EXPECTED_RETRAIN_REPORT_COLUMNS = [
'version',
]
# Matches ``retrain_model`` return ``message`` when ``success`` is True (also written to ``log_retrain.status``).
RETRAIN_ACTIVITY_SUCCESS_MESSAGE = 'Model retrained successfully.'
class _FakeSientiaModelForMinimalRetrain(SientiaModel):
"""
Fake SientiaModel that uses the real SientiaModel lifecycle to surface
index-alignment issues during ``retrain()``.
It intentionally performs strict alignment inside ``_retrain_model``:
``y.loc[x.index]``.
"""
def __init__(self, *, target: str = 'sensor_1'):
super().__init__(
model_type='FakeMinimalRetrain',
model_version='0.0.0',
model=object(),
transformer=object(),
)
self.target = target
self.model_is_fitted = True
self.force_retrain_error = False
def store_model( # type: ignore[override]
self,
name: str,
signature=None,
pip_requirements=None,
code_path=None,
) -> None:
# No-op: E2E tests validate workflow persistence, not real MLflow artifacts.
return None
def _predict(self, data: pd.DataFrame):
pred = pd.DataFrame({'prediction': [0.5] * len(data)}, index=data.index)
return pred, {}
def _transform(self, data: pd.DataFrame):
out = data.drop(columns=[self.target], errors='ignore').copy()
out.index = data.index
return out, {}
def _train_transformer(self, train_data: pd.DataFrame, val_data: pd.DataFrame) -> None:
return None
def _train_model(
self,
x: pd.DataFrame,
y: pd.DataFrame,
x_val: pd.DataFrame | None = None,
y_val: pd.DataFrame | None = None,
) -> None:
return None
def _retrain_transformer(self, data: pd.DataFrame) -> None:
return None
def _retrain_model(self, x: pd.DataFrame, y: pd.DataFrame | None) -> None:
if self.force_retrain_error:
raise RuntimeError('training did not converge')
if y is None:
return
# Strict alignment on purpose to reproduce the production failure mode.
_ = y.loc[x.index]
@pytest.fixture
def mlflow_repository_stub():
"""
Override the shared E2E fixture: return a real fake ``SientiaModel`` wrapper
instead of a MagicMock wrapper.
"""
repo = MagicMock()
repo._client = MagicMock()
wrapper = _FakeSientiaModelForMinimalRetrain(target='sensor_1')
repo.get_cached_model = MagicMock(return_value=wrapper)
return repo
def _retrain_input(model_id: int, **overrides) -> dict:
"""Load and override the minimal-retrain base scenario."""
@@ -86,8 +168,6 @@ def _configure_retrain_happy_path(mlflow_repository_stub) -> None:
- ``_client.get_model_version_by_alias``: returns ``mv`` with a stable
``run_id`` (used as ``source_run_id``).
- ``get_cached_model``: returns a wrapper exposing inert ``retrain`` and
``store_model`` methods.
- ``start_run``: returns a context manager yielding a ``run_info`` with
run/experiment ids.
- ``log_params``: inert.
@@ -104,11 +184,6 @@ def _configure_retrain_happy_path(mlflow_repository_stub) -> None:
mlflow_repository_stub._client.get_model_version_by_alias.return_value = mv_src
cached_wrapper = MagicMock()
cached_wrapper.retrain = MagicMock(return_value=None)
cached_wrapper.store_model = MagicMock(return_value=None)
mlflow_repository_stub.get_cached_model.return_value = cached_wrapper
@contextmanager
def fake_start_run(**kwargs):
run_info = MagicMock()
@@ -155,8 +230,6 @@ async def test_minimal_retrain_happy_path_writes_success_report(
make_workflow_id('test-retrain-happy'),
)
assert log_artifact_mock.called, 'retrain_model should log the input CSV artifact'
with postgres_engine.connect() as conn:
rows = (
conn.execute(
@@ -171,14 +244,27 @@ async def test_minimal_retrain_happy_path_writes_success_report(
)
assert len(rows) == 1
for column in EXPECTED_RETRAIN_REPORT_COLUMNS:
assert column in rows[0], f'Missing log_retrain column: {column}'
row = rows[0]
for column in EXPECTED_RETRAIN_REPORT_COLUMNS:
assert column in row, f'Missing log_retrain column: {column}'
assert row['status'] == RETRAIN_ACTIVITY_SUCCESS_MESSAGE, (
"Expected retrain_model to return success (experiment_response['success'] is True). "
'Persisted log_retrain.status is the activity message; when success is False the run '
'never reaches mlflow.log_artifact — diagnose the retrain failure from status below, '
'not from a skipped artifact upload. '
f"Got status={row['status']!r}, version={row.get('version')!r}, "
f"mlflow_run_id={row.get('mlflow_run_id')!r}."
)
assert log_artifact_mock.called, (
'After a successful retrain, retrain_model must call mlflow.log_artifact for the '
'input CSV inside start_run.'
)
# ``model_id`` is now ``text``; compare against the stringified id.
assert row['model_id'] == str(model_id)
assert row['model_name'] == 'test_model'
assert row['status'] == 'Model retrained successfully.'
assert row['version'] == '7'
assert row['mlflow_run_id'] == 'retrain-run-id'
# ``mlflow_experiment_id`` is now ``int8``; assert the integer value
@@ -213,9 +299,7 @@ async def test_minimal_retrain_failure_writes_report_without_version_columns(
_seed_retrain_training_rows(postgres_engine, model_id)
_configure_retrain_happy_path(mlflow_repository_stub)
mlflow_repository_stub.get_cached_model.return_value.retrain.side_effect = RuntimeError(
'training did not converge'
)
mlflow_repository_stub.get_cached_model.return_value.force_retrain_error = True
input_data = _retrain_input(model_id)