QTZPOC-20: Remove PyTorch from supported model flavors as it was never functional due to missing runtime dependencies.
1203 lines
43 KiB
Markdown
1203 lines
43 KiB
Markdown
# Test Scenarios for Predictions Batch Workflow
|
|
|
|
This document describes all possible test scenarios for the `predictions_batch` workflow and its child workflows `prediction_process` and `format_and_export_prediction`.
|
|
|
|
## Running automated E2E tests (`e2e/`)
|
|
|
|
### One-time setup: disable the testcontainers reaper
|
|
|
|
Do this once per machine, before running anything under `e2e/`:
|
|
|
|
```bash
|
|
echo "ryuk.disabled=true" >> ~/.testcontainers.properties
|
|
```
|
|
|
|
**Why.** Before starting any test container, testcontainers starts a reaper container ("Ryuk") whose
|
|
job is to clean up containers if the test process dies without running its teardown. Ryuk bind-mounts
|
|
the Docker socket. Under **Docker Desktop on Linux** the socket lives at
|
|
`~/.docker/desktop/docker.sock`, which Docker Desktop's File Sharing does not expose, so the mount is
|
|
denied and the reaper cannot start:
|
|
|
|
```
|
|
docker.errors.APIError: 500 Server Error ... /containers/<id>/start:
|
|
Mounts denied: The path /socket_mnt/home/<user>/.docker/desktop/docker.sock
|
|
is not shared from the host and is not known to Docker.
|
|
```
|
|
|
|
Every test then errors **at setup** — `ERROR`, not `FAILED`, and never inside an assertion. If you see
|
|
that, this is why.
|
|
|
|
**What you give up.** Nothing in a normal run: every fixture in `conftest.py` and
|
|
`import_harness.py` stops its own containers in teardown. Ryuk only covers the case where the process
|
|
is killed without teardown (`kill -9`, a crash, a hard stop from the IDE). After one of those, clean
|
|
up by hand:
|
|
|
|
```bash
|
|
docker ps -a --filter "label=org.testcontainers=true" -q | xargs -r docker rm -f
|
|
```
|
|
|
|
The setting is read by `testcontainers/core/config.py`, which accepts either `ryuk.disabled` in
|
|
`~/.testcontainers.properties` or the `TESTCONTAINERS_RYUK_DISABLED` environment variable. The
|
|
properties file is preferred here because it is per-machine and applies to every project and every
|
|
shell, so nobody has to remember a prefix on the command line.
|
|
|
|
*Alternative, if you want to keep the reaper:* point Docker at the classic socket with
|
|
`export DOCKER_HOST=unix:///var/run/docker.sock`. That only works if a native Docker Engine is
|
|
running alongside Docker Desktop; if the Desktop daemon is the only one, disabling Ryuk is the way.
|
|
|
|
### Requirements and invocation
|
|
|
|
- **Runtime**: Docker (or a Docker-compatible daemon) must be available so [testcontainers](https://testcontainers.com/) can start **PostgreSQL** and **MinIO** containers.
|
|
- **Dependencies**: install dev requirements (includes `testcontainers[postgres,minio,mongodb]`).
|
|
- **Invocation**: run only integration-marked tests, for example: `pytest e2e/ -m integration`.
|
|
- **MinIO tests**: `e2e/test_minio_offload.py` exercises real S3 uploads; other E2E modules continue to mock MinIO on the worker used by most scenarios.
|
|
- **OPC tests (real server)**: `e2e/test_opc_real_server.py` uses an in-process **asyncua** server and real `OpcRepository` (`test_activities_real_opc`). Scenarios 3.1.2, 3.2.2, 3.2.4, and 3.2.5 are covered there. Other E2E modules keep the OPC mock.
|
|
- Run only OPC real-server tests: `pytest e2e/test_opc_real_server.py -m "integration and opc"`.
|
|
- **Model import tests**: the `import_model` suite is opt-in and has its own harness, containers and
|
|
fidelity contract — see [§4.0](#40-running-the-import-e2e-tests). Run it with
|
|
`pytest e2e -m import_e2e`.
|
|
- **joblib flavor tests**: `e2e/test_predictions_batch_joblib_flavor.py` covers reading a model
|
|
through the explicit `joblib` flavor (predict and transform), and
|
|
`e2e/test_retrain_joblib_flavor.py` covers writing one back on a retrain — see
|
|
[§5](#5-joblib-flavor-scenarios). Neither `joblib.dump` nor `joblib.load` is mocked in either
|
|
module: the artifact fixture holds a real `joblib.dump(..., compress=3)` pickle whose class ships
|
|
inside the artifact's `code/` directory, so only the flavor's own `sys.path` handling makes it
|
|
deserialize. Run them with
|
|
`pytest e2e/test_predictions_batch_joblib_flavor.py e2e/test_retrain_joblib_flavor.py -m integration`.
|
|
- **Nothing under `e2e/` is covered by `validate`**: that script lints `laborious/` and `tests/`
|
|
only, and its test step is `pytest tests/` (`testpaths = ["tests"]`). Lint these files by hand
|
|
(`ruff format --check e2e/ && ruff check e2e/`), and remember that `addopts` carries
|
|
`--strict-markers`: a test without `@pytest.mark.integration` is not collected, and a run that
|
|
collects nothing still exits 0. Check the collected count, not the exit code.
|
|
|
|
## Workflow Overview
|
|
|
|
The `predictions_batch` workflow:
|
|
1. Loads data using a custom SQL query
|
|
2. Prepares prediction configuration
|
|
3. Delegates to `prediction_process` child workflow which:
|
|
- Retrieves last timestamp for incremental processing
|
|
- Applies input data quality gates
|
|
- Executes MLFlow transform operation
|
|
- Validates transform response
|
|
- Executes MLFlow predict operation
|
|
- Validates predict response
|
|
- Delegates to `format_and_export_prediction` child workflow
|
|
4. The `format_and_export_prediction` workflow:
|
|
- Formats prediction data (normal or default)
|
|
- Exports to PI Web API (optional)
|
|
- Exports to OPC server (optional)
|
|
- Exports to PostgreSQL
|
|
- Writes metrics
|
|
|
|
---
|
|
|
|
## 1. Predictions Batch - Main Workflow Scenarios
|
|
|
|
### 1.1 Success Scenarios
|
|
|
|
#### Scenario 1.1.1: Happy Path - Complete Success
|
|
**Description**: Workflow completes successfully with valid SQL query and all activities succeed
|
|
|
|
**Input**:
|
|
- Valid `schedule_name`, `model_name`, `model_id`
|
|
- Valid `query` returning non-empty DataFrame
|
|
- Valid `schema`, `table_name`, `transform_table_name`
|
|
- Optional `datetime_columns` for timestamp parsing
|
|
- Optional `input_filters`, `mlflow_transform_filters`, `mlflow_predict_filters`
|
|
- Optional `path_priority`, `opc_output_config`, `pi_web_api_output_config`
|
|
|
|
**Expected Behavior**:
|
|
- `load_custom_query` returns DataFrame with data
|
|
- Workflow prepares prediction input with all configurations
|
|
- `prediction_process` child workflow executes successfully
|
|
- All gates pass with no issues
|
|
- Transform and predict operations succeed
|
|
- Data exported to PostgreSQL
|
|
- Metrics written
|
|
|
|
**Assertions**:
|
|
- SQL query executed once
|
|
- `prediction_process` workflow called with correct parameters
|
|
- Data exists in PostgreSQL (predictions table)
|
|
- Metrics recorded
|
|
- No errors raised
|
|
|
|
---
|
|
|
|
### 1.2 Error Scenarios
|
|
|
|
#### Scenario 1.2.1: SQL Query Execution Error
|
|
**Description**: SQL query fails due to syntax error or connection issue
|
|
|
|
**Input**:
|
|
- Invalid SQL query (syntax error)
|
|
- Or database connection unavailable
|
|
|
|
**Expected Behavior**:
|
|
- `load_custom_query` raises exception (caught by Temporal retry policy)
|
|
- Notification sent with SQL error details
|
|
- After retries, activity may return empty data or workflow may fail
|
|
- If empty data returned, workflow completes with early exit via input gate
|
|
|
|
**Assertions**:
|
|
- Error notification sent
|
|
- Workflow completes (either fails or exits early)
|
|
- No data in predictions table
|
|
|
|
---
|
|
|
|
#### Scenario 1.2.2: Missing Required Parameters
|
|
**Description**: Essential parameters missing from input
|
|
|
|
**Input**:
|
|
- Missing `query` or `model_id` or `schema` or `table_name`
|
|
|
|
**Expected Behavior**:
|
|
- Workflow or activity raises KeyError or validation error
|
|
- Workflow fails immediately
|
|
|
|
**Assertions**:
|
|
- Workflow fails with parameter error
|
|
- Error notification sent
|
|
- No child workflow called
|
|
|
|
---
|
|
|
|
#### Scenario 1.2.3: Invalid Datetime Column Specification
|
|
**Description**: Datetime column specified doesn't exist in query results
|
|
|
|
**Input**:
|
|
- `datetime_columns: ['nonexistent_column']`
|
|
- Query results don't have this column
|
|
|
|
**Expected Behavior**:
|
|
- `load_custom_query` may raise KeyError or warning
|
|
- Depending on implementation, workflow may fail or continue
|
|
- Error notification sent
|
|
|
|
**Assertions**:
|
|
- Error raised or warning logged
|
|
- Workflow behavior depends on error handling policy
|
|
|
|
---
|
|
|
|
## 2. Prediction Process - Child Workflow Scenarios
|
|
|
|
### 2.1 Input gate Early Exit Scenarios
|
|
|
|
#### Scenario 2.1.1: Input Gate Triggers CONTINUE
|
|
**Description**: Input gate determines data should use previous prediction
|
|
|
|
**Input**:
|
|
- Data that should continue with input data as prediction
|
|
- `input_filters` configured with `POLICY: 'CONTINUE'`
|
|
- `path_priority` includes CONTINUE
|
|
|
|
**Expected Behavior**:
|
|
- `input_gate` returns `path_flag='CONTINUE'`
|
|
- `path_flag_handler` calls export workflow with input data directly
|
|
- MLFlow transform and predict skipped
|
|
- Data exported as-is
|
|
|
|
**Assertions**:
|
|
- `input_gate` called
|
|
- MLFlow operations NOT called
|
|
- Export workflow called with original data
|
|
- Workflow completes
|
|
|
|
|
|
#### Scenario 2.1.2: Input Gate Triggers STOP
|
|
**Description**: Input data quality gate fails with STOP policy
|
|
|
|
**Input**:
|
|
- Data with EMPTY_DATA or other critical issues
|
|
- `input_filters` configured with `POLICY: 'STOP'`
|
|
|
|
**Expected Behavior**:
|
|
- `input_gate` returns `path_flag='STOP'`
|
|
- `path_flag_handler` detects STOP
|
|
- Workflow returns early without calling MLFlow
|
|
- No prediction exported
|
|
|
|
**Assertions**:
|
|
- `input_gate` called
|
|
- `path_flag_handler` returns True (early exit)
|
|
- MLFlow transform NOT called
|
|
- Export workflow NOT called
|
|
- Workflow completes without error
|
|
|
|
|
|
#### Scenario 2.1.3: Input Gate Triggers REPEAT
|
|
**Description**: Input gate determines data should repeat last prediction
|
|
|
|
**Input**:
|
|
- Data with quality issues that require using previous prediction
|
|
- `input_filters` configured with `POLICY: 'REPEAT'`
|
|
- `path_priority` includes REPEAT
|
|
|
|
**Expected Behavior**:
|
|
- `input_gate` returns `path_flag='REPEAT'`
|
|
- `path_flag_handler` calls `repeat_last_prediction` activity
|
|
- MLFlow transform and predict skipped
|
|
- Last prediction repeated and exported
|
|
|
|
**Assertions**:
|
|
- `input_gate` called
|
|
- MLFlow operations NOT called
|
|
- `repeat_last_prediction` activity called
|
|
- Workflow completes
|
|
|
|
---
|
|
|
|
### 2.2 Transform gate Early Exit Scenarios
|
|
|
|
#### Scenario 2.2.1: Transform Gate Triggers CONTINUE
|
|
**Description**: Transform response gate determines data should continue despite issues
|
|
|
|
**Input**:
|
|
- Valid input data
|
|
- Transform response has quality issues but policy is CONTINUE
|
|
- `mlflow_transform_filters` configured with `POLICY: 'CONTINUE'`
|
|
- `path_priority` includes CONTINUE
|
|
|
|
**Expected Behavior**:
|
|
- `request_transform` succeeds
|
|
- `mlflow_response_gate` for transform returns `path_flag='CONTINUE'`
|
|
- `path_flag_handler` calls export workflow with transform data
|
|
- MLFlow predict skipped
|
|
- Transform data exported as-is
|
|
|
|
**Assertions**:
|
|
- Transform completed
|
|
- `mlflow_response_gate` called for transform
|
|
- MLFlow predict NOT called
|
|
- Export workflow called with transform data
|
|
- Workflow completes
|
|
|
|
---
|
|
|
|
#### Scenario 2.2.2: Transform Gate Triggers STOP
|
|
**Description**: Transform response validation fails with STOP policy
|
|
|
|
**Input**:
|
|
- Valid input data
|
|
- Transform response has critical errors
|
|
- `mlflow_transform_filters` configured with `POLICY: 'STOP'`
|
|
|
|
**Expected Behavior**:
|
|
- `request_transform` succeeds but response invalid
|
|
- `mlflow_response_gate` for transform returns `path_flag='STOP'`
|
|
- Workflow exits without calling predict or export
|
|
|
|
**Assertions**:
|
|
- Transform completed but validation failed
|
|
- `mlflow_response_gate` called for transform
|
|
- MLFlow predict NOT called
|
|
- Export workflow NOT called
|
|
- Workflow completes without error
|
|
|
|
---
|
|
|
|
#### Scenario 2.2.3: Transform Gate Triggers REPEAT
|
|
**Description**: Transform response gate determines data should repeat last prediction
|
|
|
|
**Input**:
|
|
- Valid input data
|
|
- Transform response has quality issues that require using previous prediction
|
|
- `mlflow_transform_filters` configured with `POLICY: 'REPEAT'`
|
|
- `path_priority` includes REPEAT
|
|
|
|
**Expected Behavior**:
|
|
- `request_transform` succeeds but response has issues
|
|
- `mlflow_response_gate` for transform returns `path_flag='REPEAT'`
|
|
- `path_flag_handler` calls `repeat_last_prediction` activity
|
|
- MLFlow predict skipped
|
|
- Last prediction repeated and exported
|
|
|
|
**Assertions**:
|
|
- Transform completed but validation triggered REPEAT
|
|
- `mlflow_response_gate` called for transform
|
|
- MLFlow predict NOT called
|
|
- `repeat_last_prediction` activity called
|
|
- Workflow completes
|
|
|
|
---
|
|
|
|
### 2.3 Predict gate Early Exit Scenarios
|
|
|
|
#### Scenario 2.3.1: Predict Gate Triggers CONTINUE
|
|
**Description**: Predict response gate determines data should continue despite issues
|
|
|
|
**Input**:
|
|
- Valid input and transform data
|
|
- Predict response has quality issues but policy is CONTINUE
|
|
- `mlflow_predict_filters` configured with `POLICY: 'CONTINUE'`
|
|
- `path_priority` includes CONTINUE
|
|
|
|
**Expected Behavior**:
|
|
- `request_predict` succeeds
|
|
- `mlflow_response_gate` for predict returns `path_flag='CONTINUE'`
|
|
- `path_flag_handler` calls export workflow with predict data
|
|
- Prediction exported despite quality issues
|
|
|
|
**Assertions**:
|
|
- Transform and predict completed
|
|
- `mlflow_response_gate` called for predict
|
|
- Export workflow called with predict data
|
|
- Workflow completes
|
|
|
|
---
|
|
|
|
#### Scenario 2.3.2: Predict Gate Triggers STOP
|
|
**Description**: Prediction validation fails with STOP policy
|
|
|
|
**Input**:
|
|
- Valid input and transform
|
|
- Predict response has critical errors
|
|
- `mlflow_predict_filters` configured with `POLICY: 'STOP'`
|
|
|
|
**Expected Behavior**:
|
|
- `request_predict` succeeds but response invalid
|
|
- `mlflow_response_gate` for predict returns `path_flag='STOP'`
|
|
- Workflow exits without export
|
|
|
|
**Assertions**:
|
|
- Transform completed
|
|
- Predict completed but validation failed
|
|
- Export workflow NOT called
|
|
- Workflow completes without error
|
|
|
|
---
|
|
|
|
#### Scenario 2.3.3: Predict Gate Triggers REPEAT
|
|
**Description**: Predict response gate determines data should repeat last prediction
|
|
|
|
**Input**:
|
|
- Valid input and transform data
|
|
- Predict response has quality issues that require using previous prediction
|
|
- `mlflow_predict_filters` configured with `POLICY: 'REPEAT'`
|
|
- `path_priority` includes REPEAT
|
|
|
|
**Expected Behavior**:
|
|
- `request_predict` succeeds but response has issues
|
|
- `mlflow_response_gate` for predict returns `path_flag='REPEAT'`
|
|
- `path_flag_handler` calls `repeat_last_prediction` activity
|
|
- Last prediction repeated and exported
|
|
|
|
**Assertions**:
|
|
- Transform and predict completed but validation triggered REPEAT
|
|
- `mlflow_response_gate` called for predict
|
|
- `repeat_last_prediction` activity called
|
|
- Export workflow NOT called with current prediction
|
|
- Workflow completes
|
|
|
|
---
|
|
|
|
## 3. Format and Export Prediction - Child Workflow Scenarios
|
|
|
|
### 3.1 Success Scenarios
|
|
|
|
#### Scenario 3.1.1: Default Prediction Export
|
|
**Description**: Error prediction path creates default prediction
|
|
|
|
**Input**:
|
|
- `path_flag: 'ERROR'` or other non-None value (not STOP/CONTINUE/REPEAT)
|
|
- `comment` provided with error details
|
|
|
|
**Expected Behavior**:
|
|
- `format_default_prediction` called instead of `format_prediction`
|
|
- Default prediction created with error metadata
|
|
- Exported to PostgreSQL only
|
|
- Transformed data NOT processed
|
|
- Metrics written
|
|
|
|
**Assertions**:
|
|
- `format_default_prediction` called
|
|
- `format_prediction` NOT called
|
|
- `format_transformed_data` NOT called
|
|
- One PostgreSQL export only
|
|
- Default values in prediction data
|
|
- Comment included
|
|
|
|
---
|
|
|
|
#### Scenario 3.1.2: Export with OPC only
|
|
**Description**: Export to PostgreSQL and OPC server only (no PI Web API)
|
|
|
|
**Input**:
|
|
- `path_flag: None`
|
|
- `opc_output_config` configured with valid OPC settings
|
|
- `pi_web_api_output_config: None` or `{}`
|
|
|
|
**Expected Behavior**:
|
|
- Normal formatting
|
|
- PostgreSQL export executed
|
|
- OPC export executed
|
|
- PI Web API activity skipped
|
|
- Metrics written with OPC metrics
|
|
|
|
**Assertions**:
|
|
- PI Web API activity NOT called
|
|
- OPC activity called
|
|
- PostgreSQL export called
|
|
- Metrics written with `opc_metrics` populated
|
|
|
|
---
|
|
|
|
#### Scenario 3.1.3: Export with PI Web API only
|
|
**Description**: Export to PostgreSQL and PI Web API only (no OPC)
|
|
|
|
**Input**:
|
|
- `path_flag: None`
|
|
- `pi_web_api_output_config` configured with valid PI Web API settings
|
|
- `opc_output_config: None` or `{}`
|
|
|
|
**Expected Behavior**:
|
|
- Normal formatting
|
|
- PostgreSQL export executed
|
|
- PI Web API export executed
|
|
- OPC activity skipped
|
|
- Metrics written without OPC metrics
|
|
|
|
**Assertions**:
|
|
- OPC activity NOT called
|
|
- PI Web API activity called
|
|
- PostgreSQL export called
|
|
- Metrics written with empty `opc_metrics`
|
|
|
|
---
|
|
|
|
#### Scenario 3.1.4: Export Without Optional Outputs
|
|
**Description**: Export only to PostgreSQL (no OPC or PI Web API)
|
|
|
|
**Input**:
|
|
- `path_flag: None`
|
|
- `opc_output_config: None` or `{}`
|
|
- `pi_web_api_output_config: None` or `{}`
|
|
|
|
**Expected Behavior**:
|
|
- Normal formatting
|
|
- Only PostgreSQL export executed
|
|
- OPC and PI Web API activities skipped
|
|
- Metrics written without OPC metrics
|
|
|
|
**Assertions**:
|
|
- PI Web API activity NOT called
|
|
- OPC activity NOT called
|
|
- PostgreSQL export called
|
|
- Metrics written with empty `opc_metrics`
|
|
|
|
---
|
|
|
|
#### Scenario 3.1.5: Export Without Transformed Data
|
|
**Description**: Only prediction exported, no transform table
|
|
|
|
**Input**:
|
|
- `path_flag: None`
|
|
- `transformed_data: None` or `save_transform: False`
|
|
- `opc_output_config: None` or `{}`
|
|
- `pi_web_api_output_config: None` or `{}`
|
|
|
|
**Expected Behavior**:
|
|
- Only prediction formatted and exported
|
|
- Transform export skipped
|
|
- Single PostgreSQL write
|
|
|
|
**Assertions**:
|
|
- `format_transformed_data` NOT called
|
|
- One PostgreSQL export
|
|
- Transform table remains empty
|
|
|
|
---
|
|
|
|
### 3.2 Error Scenarios
|
|
|
|
These paths do **not** rely on Temporal activity retries for export failures: the write activities run once, errors are handled inside the activity, and the **workflow completes successfully** with degraded metadata on the persisted prediction (`prediction_confidence` and `comments`).
|
|
|
|
#### Scenario 3.2.1: PI Web API Write Error
|
|
**Description**: PI Web API export fails
|
|
|
|
**Input**:
|
|
- Valid prediction
|
|
- PI Web API service unavailable or invalid config
|
|
|
|
**Expected Behavior**:
|
|
- `write_pi_web_api_data` surfaces the failure (exception handled in the activity layer)
|
|
- Notification may be sent
|
|
- Workflow **completes** (does not fail)
|
|
- Prediction row is still written to PostgreSQL with error confidence **13** and a comment describing the PI error
|
|
- Subsequent steps (e.g. OPC, Postgres) still run per workflow order with the updated prediction payload
|
|
|
|
**Assertions**:
|
|
- PI Web API error notification sent (when applicable)
|
|
- Workflow completes
|
|
- PostgreSQL contains the prediction with `prediction_confidence` 13 and expected `comments`
|
|
|
|
---
|
|
|
|
#### Scenario 3.2.2: OPC Write Error
|
|
**Description**: OPC server write fails
|
|
|
|
**Input**:
|
|
- Valid prediction
|
|
- OPC server unavailable or invalid configuration
|
|
|
|
**Expected Behavior**:
|
|
- `write_opc_data` reports failure without aborting the workflow
|
|
- Notification may be sent
|
|
- Workflow **completes** (does not fail)
|
|
- Prediction row is written to PostgreSQL with OPC error confidence **12** and a comment indicating OPC write issues
|
|
|
|
**Assertions**:
|
|
- OPC error notification sent (when applicable)
|
|
- Workflow completes
|
|
- PostgreSQL contains the prediction with `prediction_confidence` 12 and expected `comments`
|
|
|
|
---
|
|
|
|
#### Scenario 3.2.4: OPC Session / Channel Bad* (Tier-1)
|
|
**Description**: OPC write fails with a Tier-1 session or channel status (e.g. `BadSessionIdInvalid`) while transport may still appear open on the client
|
|
|
|
**Input**:
|
|
- Valid prediction and OPC output config
|
|
- Mock or server returning Tier-1 `UaStatusCodeError` on write (no write retry in the same activity)
|
|
|
|
**Expected Behavior**:
|
|
- `write_opc_data` fails forward for affected tags; background reconnect may be scheduled if `OPC_RECONNECTION_INTERVAL` allows
|
|
- Workflow **completes**
|
|
- PostgreSQL row uses **`prediction_confidence` 14** and comment prefix `OPC UA session/channel error:` (including OPC status name)
|
|
- `opc_write_attempts_total` records `result=BadSessionIdInvalid` (or matching status); no second write attempt in the same activity
|
|
|
|
**Assertions**:
|
|
- Workflow completes
|
|
- `prediction_confidence = 14`
|
|
- `comments` matches `OPC UA session/channel error:%`
|
|
- Generic OPC error confidence **12** is not used for this case
|
|
|
|
**Reference**: [docs/opc-communication.md](../docs/opc-communication.md), plan `.cursor/plans/opc_bad_reconnect_ac4c6045.plan.md`
|
|
|
|
---
|
|
|
|
#### Scenario 3.2.5: OPC Write Blocked During Reconnect
|
|
**Description**: A write is attempted while the repository is reconnecting (session not ready)
|
|
|
|
**Input**:
|
|
- Valid prediction
|
|
- Simulated slow reconnect (e.g. delayed `connect`) or concurrent writes where the first triggers reconnect
|
|
|
|
**Expected Behavior**:
|
|
- Second write (or parallel write) is rejected **immediately** when reconnect is in progress or `_session_ready` is cleared — **without** calling `write_value`
|
|
- No wait/sleep on the write path; no duplicate `connect` from parallel writers (connection lock)
|
|
- `prediction_confidence = 14`, `comments = OPC UA reconnect in progress` (distinguish from Tier-1 `Bad*` via comment prefix in SQL)
|
|
|
|
**Assertions**:
|
|
- At most one reconnect sequence (`disconnect` + `connect`) for the overlapping window
|
|
- No write retry after failure
|
|
- Tests in `test_opc_repository` (unit) and optional e2e in `test_predictions_batch_format_export.py`
|
|
|
|
---
|
|
|
|
#### Scenario 3.2.3: PI Web API Partial Write Error
|
|
**Description**: Two prediction tags attempt to be written to PI Web API, but only one succeeds
|
|
|
|
**Input**:
|
|
- Valid prediction
|
|
- Two prediction tags configured
|
|
- PI Web API returns partial success (one tag succeeds, one fails)
|
|
|
|
**Expected Behavior**:
|
|
- `write_pi_web_api_data` processes response
|
|
- `process_pi_web_api_response` detects partial failure
|
|
- Error confidence set (13)
|
|
- Notification sent for failed tag
|
|
- Workflow completes with error confidence (single activity attempt; no retry loop)
|
|
|
|
**Assertions**:
|
|
- One tag written successfully
|
|
- One tag failed
|
|
- Error confidence set in prediction
|
|
- Error notification sent
|
|
- Workflow completes
|
|
|
|
---
|
|
## 4. Model Import Workflow Scenarios (`import_model`)
|
|
|
|
The `import_model` workflow turns an encrypted `.sientia` bundle already in MinIO into a registered
|
|
MLflow model plus one MongoDB `models` document — or into a durable error on the row the BFF
|
|
inserted. It shares no code path with the predictions workflows above and has its own harness.
|
|
|
|
### 4.0 Running the import E2E tests
|
|
|
|
- **Marker**: every test below carries `import_e2e`; the real-clock retry tests also carry `slow`.
|
|
`testpaths` stays `["tests"]`, so none of this is collected by the unit gate.
|
|
- **Invocation**: `pytest e2e -m import_e2e`.
|
|
- **Runtime**: Docker for three containers (the shared MinIO, a dedicated Postgres for
|
|
`sientia-core-mlops-bff`, MongoDB) **plus** an `mlflow server` subprocess started from this
|
|
repository's venv. Dev requirements install `testcontainers[postgres,minio,mongodb]`.
|
|
- **Fidelity**: on the import path nothing is mocked — real MinIO, real Postgres, real MongoDB with a
|
|
unique index, a real MLflow tracking server with a working registry, real Argon2id and real
|
|
`secretstream`. Only `CoreNotificationHandler`'s Mongo client stays mocked, because notifications
|
|
are a side channel this suite counts rather than a dependency it tests.
|
|
- **Fixtures**: `e2e/import_harness.py`, registered as a pytest plugin from `e2e/conftest.py`. Every
|
|
fixture is `import_`-prefixed.
|
|
- **Prerequisite**: the one-time reaper setup at the top of this document
|
|
([Running automated E2E tests](#one-time-setup-disable-the-testcontainers-reaper)). Without it every
|
|
test here errors at setup with a Docker mount error and never reaches an assertion.
|
|
|
|
### 4.1 Success Scenarios
|
|
|
|
---
|
|
|
|
#### Scenario 4.1.1: Producer-Emitted Bundle Imports End to End
|
|
**Description**: The committed golden `.sientia` file — bytes the producer's own code wrote — is
|
|
imported at the producer's real Argon2id cost (256 MiB, 3 passes)
|
|
|
|
**Input**:
|
|
- `tests/fixtures/bundle/golden_model_v3.sientia` uploaded under `imported_models/`
|
|
- One `PENDING` / `run_type='IMPORT'` row carrying the object key and the uploader's digest
|
|
- The workflow input: `import_run_id`, `bucket`, `object_key`, `expected_digest`, `password_envelope`
|
|
|
|
**Expected Behavior**:
|
|
- The row is claimed, the seven gates pass, the experiment and a new run are created
|
|
- The artifact tree is logged with `prediction_model` and `data_model` at the run's root
|
|
- A registry version is created and left unstaged; the model document is written inactive
|
|
- One terminal write sets `IMPORT_SUCCESS`
|
|
|
|
**Assertions**:
|
|
- Exactly one run named `import-fixture_model-v3`, status `FINISHED`
|
|
- `prediction_model` contains `MLmodel` and `model.pkl`
|
|
- Bundle parameters logged, origin logged under `source.*`
|
|
- Destination version is `1`, independent of the bundle's `model_version` of `3`
|
|
- Model document `id` is the seeded maximum plus one; `active` is `false`
|
|
- Row: `IMPORT_SUCCESS`, `error_message` null, both names filled
|
|
|
|
---
|
|
|
|
#### Scenario 4.1.2: Factory-Built Bundle Imports End to End
|
|
**Description**: The same assertions over a synthesised tree with names unique to the test
|
|
|
|
**Input**: A bundle from `tests/helpers/bundle_factory.py` at the cheap KDF cost
|
|
|
|
**Expected Behavior**: As 4.1.1
|
|
|
|
**Assertions**:
|
|
- As 4.1.1, plus `model_config` holds exactly `transform_flavor`, `predict_flavor`,
|
|
`retention_minutes` and `target`
|
|
- `import.workflow_id` in the row's detail JSON matches the run
|
|
|
|
---
|
|
|
|
#### Scenario 4.1.3: The Registry Source Resolves Through the Legacy Loader
|
|
**Description**: The `source` the importer writes must be one the *existing* loader can parse
|
|
|
|
**Input**: A successful import's registered version
|
|
|
|
**Expected Behavior**: `MLFlowRepository.get_model_run_id` extracts the run id from the source
|
|
|
|
**Assertions**:
|
|
- The source ends in `/prediction_model`
|
|
- `get_model_run_id` returns the run the import created
|
|
- Both artifact paths are present under that run
|
|
- Called with the stage the version is actually in, because an import promotes nothing
|
|
|
|
---
|
|
|
|
#### Scenario 4.1.4: Second Import of the Same Model
|
|
**Description**: The same model imported twice, with different declared `model_version`s
|
|
|
|
**Input**: Two bundles declaring `model_version` `2` then `7`, same model and experiment names
|
|
|
|
**Expected Behavior**: Additive — a new run and a new registry version, nothing promoted, the
|
|
existing model document untouched
|
|
|
|
**Assertions**:
|
|
- Two versions (`1`, `2`) of one registered model; destination numbering ignores the bundle's
|
|
- Two runs in the same experiment, each logging its own `source.model_version`
|
|
- No version in `Production`
|
|
- The model document is byte-identical to what the first import wrote
|
|
|
|
---
|
|
|
|
#### Scenario 4.1.5: A Model Name Already in the Collection
|
|
**Description**: An operator-owned document already carries the bundle's model name
|
|
|
|
**Input**: A document with `active: True`, `predict_flavor: pyfunc` and a hand-set target
|
|
|
|
**Expected Behavior**: The import notices the name and leaves the document alone; the import still
|
|
succeeds
|
|
|
|
**Assertions**:
|
|
- The collection is unchanged, exactly one document carries that name, no duplicate
|
|
- Row: `IMPORT_SUCCESS` — a document that is already there is not a failure
|
|
|
|
---
|
|
|
|
### 4.2 Gate Rejection Scenarios
|
|
|
|
Every case asserts the same five things: terminal status `ORCHESTRATOR_VALIDATION_ERROR`, the
|
|
recorded step, the recorded code, `error_message` **equal** to the catalog sentence for that step,
|
|
and that no experiment, run, artifact, registry version, model document or extracted file remains.
|
|
|
|
---
|
|
|
|
#### Scenario 4.2.1: Object Above the Size Ceiling (gate 1)
|
|
**Description**: An object larger than `IMPORT_MAX_OBJECT_BYTES`, rejected from `stat_object` before
|
|
its body is fetched
|
|
|
|
**Input**: A factory bundle against a ceiling narrowed to 1 KiB for this test class
|
|
|
|
**Expected Behavior**: Rejected at `download`
|
|
|
|
**Assertions**: The five shared assertions
|
|
|
|
---
|
|
|
|
#### Scenario 4.2.2: Malformed Header (gate 2)
|
|
**Description**: Five variants — wrong magic, unsupported format version, KDF memory above the
|
|
ceiling, KDF passes above the ceiling, a header digest that does not describe the chunk stream
|
|
|
|
**Expected Behavior**: Each rejected at `download`, sharing gate 1's step
|
|
|
|
**Assertions**:
|
|
- The five shared assertions
|
|
- The recorded sentence does **not** mention the password: gates 1 and 2 must not implicate a
|
|
password that is fine
|
|
|
|
---
|
|
|
|
#### Scenario 4.2.3: Decryption Failures (gate 3)
|
|
**Description**: Three variants — a wrong bundle password inside a valid envelope, a tampered chunk
|
|
with the header digest repaired, and a stream whose every chunk is framed `MESSAGE`
|
|
|
|
**Expected Behavior**: Each rejected at `decryption`
|
|
|
|
**Assertions**:
|
|
- The five shared assertions
|
|
- The tampered case proves the file is self-consistent and only the AEAD can tell
|
|
- The framing case proves the digest cannot see an incomplete stream
|
|
|
|
---
|
|
|
|
#### Scenario 4.2.4: Hostile Archive (gate 4)
|
|
**Description**: Four variants — an entry escaping the tree, an absolute entry path, a symbolic
|
|
link, and a declared expansion over the ceiling
|
|
|
|
**Expected Behavior**: Each rejected at `archive_inspection`, from the central directory alone
|
|
|
|
**Assertions**:
|
|
- The five shared assertions
|
|
- **Nothing extracted**: the worker work directory is empty, not merely free of provisioning
|
|
|
|
---
|
|
|
|
#### Scenario 4.2.5: A Member Extraction Cannot Place (gate 5)
|
|
**Description**: An archive declaring the same member name twice — passes gate 4, which checks names
|
|
and modes but not duplicates
|
|
|
|
**Expected Behavior**: Rejected at `extraction` on the second member
|
|
|
|
**Assertions**: The five shared assertions
|
|
|
|
---
|
|
|
|
#### Scenario 4.2.6: Incomplete Structure (gate 6)
|
|
**Description**: Five variants — no `metadata.json`, metadata missing `run_id`, parameters missing
|
|
`target_variable`, no `prediction_model` directory, and a 60-character experiment name
|
|
|
|
**Expected Behavior**: Each rejected at `structure_validation`
|
|
|
|
**Assertions**:
|
|
- The five shared assertions
|
|
- For the over-long name: the row's `experiment_name` is still the inserted placeholder, so no write
|
|
reached the `VARCHAR(50)` column, and that column is confirmed still 50 wide
|
|
|
|
---
|
|
|
|
#### Scenario 4.2.7: Forbidden File Type (gate 7)
|
|
**Description**: An `artifacts/payload.exe` member with a suffix outside the allow-list
|
|
|
|
**Expected Behavior**: Rejected at `content_policy`
|
|
|
|
**Assertions**: The five shared assertions
|
|
|
|
---
|
|
|
|
### 4.3 Digest Enforcement Scenarios
|
|
|
|
---
|
|
|
|
#### Scenario 4.3.1: A Digest Computed Over Other Bytes
|
|
**Description**: A perfectly valid bundle whose `expected_digest` belongs to a different file
|
|
|
|
**Expected Behavior**: Fails at `download`, before any header is parsed
|
|
|
|
**Assertions**:
|
|
- Terminal status, step, code and catalog sentence
|
|
- The failure names the digest, not the magic or the version
|
|
- `run_name` is still null and `experiment_name` still the placeholder — the bundle was never opened
|
|
|
|
---
|
|
|
|
#### Scenario 4.3.2: The Object Is Replaced After Upload
|
|
**Description**: The same key, different bytes — also a valid bundle, so nothing but the digest can
|
|
tell
|
|
|
|
**Expected Behavior**: Fails at `download`
|
|
|
|
**Assertions**: As 4.3.1; only a real object store can show this at all
|
|
|
|
---
|
|
|
|
#### Scenario 4.3.3: No Digest at All
|
|
**Description**: `expected_digest` absent from the input
|
|
|
|
**Expected Behavior**: Fails at `received`, before anything is downloaded
|
|
|
|
**Assertions**: The request-invalid code, and the work directory was never created
|
|
|
|
---
|
|
|
|
### 4.4 Provisioning Failure Scenarios
|
|
|
|
Each asserts the platform terminal status `IMPORT_ERROR` — not the validation status, because a
|
|
provisioning failure is not the user's to fix — its own step and code, the workflow identifier in
|
|
the detail JSON, and `error_message` **equal** to the catalog sentence.
|
|
|
|
---
|
|
|
|
#### Scenario 4.4.1: The Tracking Server Is Stopped
|
|
**Description**: The `mlflow server` process is genuinely terminated for the duration
|
|
|
|
**Expected Behavior**: Fails at `experiment_creation`
|
|
|
|
**Assertions**:
|
|
- The shared assertions
|
|
- The names are already on the row: the bundle was opened before MLflow was reached
|
|
- Nothing in Mongo; nothing provisioned
|
|
|
|
---
|
|
|
|
#### Scenario 4.4.2: Artifact Upload Fails
|
|
**Description**: `log_artifacts` is patched — the one step where neither hostile input nor a broken
|
|
dependency can isolate the failure
|
|
|
|
**Expected Behavior**: Fails at `artifact_upload`
|
|
|
|
**Assertions**:
|
|
- The shared assertions
|
|
- The experiment exists (the previous step succeeded); no version, no document
|
|
|
|
---
|
|
|
|
#### Scenario 4.4.3: Registration Fails
|
|
**Description**: `create_model_version` is patched. MLflow rejects only an empty registered-model
|
|
name, and one process serves both tracking and the registry, so nothing else isolates this step
|
|
|
|
**Expected Behavior**: Fails at `registration`
|
|
|
|
**Assertions**:
|
|
- The shared assertions
|
|
- The run and both artifact paths survive — "the files were stored, the model was not registered"
|
|
|
|
---
|
|
|
|
#### Scenario 4.4.4: The Model Document Is Refused
|
|
**Description**: A real unique index on `model_config.target` refuses the activity's own insert.
|
|
(A duplicate `id` cannot be constructed: the activity derives `max(int(id)) + 1` from the same
|
|
collection, so the id it computes can never already exist)
|
|
|
|
**Expected Behavior**: Fails at `model_document`
|
|
|
|
**Assertions**:
|
|
- The shared assertions
|
|
- One registry version exists — registered, and unusable
|
|
|
|
---
|
|
|
|
### 4.5 Status Record Scenarios
|
|
|
|
---
|
|
|
|
#### Scenario 4.5.1: The Claim Precedes Every Gate
|
|
**Description**: The row is observed from inside `stat_object`, the first call gate 1 makes
|
|
|
|
**Assertions**: Running status and `import.workflow_id` already present at that moment
|
|
|
|
---
|
|
|
|
#### Scenario 4.5.2: The Names Are Visible Mid-Flight
|
|
**Description**: The row is observed from inside the first call experiment creation makes, during an
|
|
import that then succeeds
|
|
|
|
**Assertions**: `experiment_name` and `run_name` already populated before anything exists in MLflow
|
|
|
|
---
|
|
|
|
#### Scenario 4.5.3: The Inserting Side's Columns Are Never Rewritten
|
|
**Description**: After a success and after a failure
|
|
|
|
**Assertions**: `username`, `request_data`, `bucket_name`, `file_name`, `run_type`, `created_at`
|
|
unchanged; `status` and `updated_at` did change
|
|
|
|
---
|
|
|
|
#### Scenario 4.5.4: Unclaimable Rows
|
|
**Description**: Three variants — an unknown id, a `run_type='TRAIN'` row, and a row already held by
|
|
another workflow id
|
|
|
|
**Expected Behavior**: The Temporal run ends failed with the not-claimable error type, and **nothing
|
|
is written to any row**
|
|
|
|
**Assertions**: The error type from the history, the whole table byte-identical, nothing provisioned
|
|
|
|
---
|
|
|
|
#### Scenario 4.5.5: The Row Names a Different Object
|
|
**Description**: `file_name` disagrees with the input's `object_key`
|
|
|
|
**Expected Behavior**: Fails at `received` **with the error recorded on the row** — unlike 4.5.4,
|
|
this row was successfully claimed
|
|
|
|
**Assertions**: The received code and sentence on the row; nothing provisioned
|
|
|
|
---
|
|
|
|
#### Scenario 4.5.6: Constraint Conformance
|
|
**Description**: The real constraints, exercised directly
|
|
|
|
**Assertions**:
|
|
- Every status a normal import writes is accepted on an `IMPORT` row
|
|
- All four terminal values are refused across run types, naming
|
|
`chk_experiment_run_terminal_status_run_type`; the shared stage values are accepted on both
|
|
- A training row still walks its own vocabulary
|
|
- The table has exactly the 13 columns `V1`…`V12` leave (no `ALTER TABLE` by this feature)
|
|
|
|
---
|
|
|
|
### 4.6 Absence Scenarios
|
|
|
|
---
|
|
|
|
#### Scenario 4.6.1: One Table, One Database
|
|
**Description**: A full import bracketed by marker statements on both Postgres servers, read from
|
|
`log_statement=all`
|
|
|
|
**Assertions**:
|
|
- Every statement touching a user relation names `experiment_run`
|
|
- No `project`, `models`, `import_run`, `model_project` or `model_registry` reference (with quoted
|
|
literals stripped, so the bundle's own `model_project` *value* is not mistaken for a table)
|
|
- The predictions database logs **no** statement at all, though the import holds working credentials
|
|
for it
|
|
- A rejected import — which writes more status rows than a successful one — behaves the same
|
|
|
|
---
|
|
|
|
### 4.7 Cleanup, Retention and History Scenarios
|
|
|
|
---
|
|
|
|
#### Scenario 4.7.1: The Worker Filesystem Is Clean
|
|
**Description**: After a success and after rejections at two different steps (decryption and
|
|
structure validation, which unwind on different code paths)
|
|
|
|
**Assertions**: No extracted tree, no downloaded object, no temporary archive
|
|
|
|
---
|
|
|
|
#### Scenario 4.7.2: The Retention Rule Is Present
|
|
**Assertions**: `sientia-import-bundle-expiry` on the import bucket, enabled, filtered on
|
|
`imported_models/`, expiring in 7 days — and the uploaded object still there, because the import
|
|
never deletes it
|
|
|
|
---
|
|
|
|
#### Scenario 4.7.3: The Record Outlives the Object
|
|
**Description**: The uploaded object is deleted, as retention will eventually do
|
|
|
|
**Assertions**: The row still reads `IMPORT_SUCCESS` with its names
|
|
|
|
---
|
|
|
|
#### Scenario 4.7.4: No Plaintext Secret in the Event History
|
|
**Assertions**:
|
|
- The bundle password, the base64 envelope key and the key's hex form appear nowhere in the history
|
|
- `password_envelope` **is** in the history, so the negative assertions above cannot pass vacuously
|
|
|
|
---
|
|
|
|
### 4.8 Retry Envelope and Missing-`V12` Scenarios
|
|
|
|
These run on a **real clock** (`WorkflowEnvironment.start_local()`) with the status retry intervals
|
|
shortened; the production values stay asserted by the unit suite. All are marked `slow`.
|
|
|
|
---
|
|
|
|
#### Scenario 4.8.1: A Database That Comes Back
|
|
**Description**: The status database is stopped for two seconds after provisioning and restarted
|
|
inside the retry envelope
|
|
|
|
**Assertions**: The terminal write lands `IMPORT_SUCCESS` with the correct workflow id
|
|
|
|
---
|
|
|
|
#### Scenario 4.8.2: A Broken Progress Update
|
|
**Description**: A brief early outage that only the progress hints see
|
|
|
|
**Assertions**: The import completes with the correct verdict and does not spend the terminal
|
|
write's envelope on a hint nobody reads
|
|
|
|
---
|
|
|
|
#### Scenario 4.8.3: A Database That Stays Down After a Successful Import
|
|
**Description**: The database is taken away as soon as the row is claimed and never comes back
|
|
|
|
**Expected Behavior**: The model is fully provisioned and the workflow **completes** — reporting
|
|
failure would tell somebody to re-import a model that is already on the platform
|
|
|
|
**Assertions**:
|
|
- The run returns its result; the model, version and document all exist
|
|
- The failure channel carries `IMPORT_STATUS_NOT_RECORDED`, `step=status_write` and
|
|
`verdict_not_recorded=`
|
|
- The row is left in the running status
|
|
|
|
---
|
|
|
|
#### Scenario 4.8.4: A Failed Import Whose Failure Cannot Be Written Either
|
|
**Description**: The import fails at `artifact_upload` and the terminal write is then refused
|
|
|
|
**Assertions**:
|
|
- The reported cause is the artifact-upload failure, not the status write
|
|
- The status write reports itself only through the channel
|
|
- The row is left running
|
|
|
|
---
|
|
|
|
#### Scenario 4.8.5: A Table Without `V12`
|
|
**Description**: The evidence for why the migration gates enabling the import worker
|
|
|
|
**Expected Behavior**: The claim succeeds (`V11` gave the table `run_type`; `PENDING` and the running
|
|
status are pre-`V12` values), the model is fully provisioned, and only the terminal value is new — so
|
|
the last write of the import is the first thing that fails, non-retryably
|
|
|
|
**Assertions**:
|
|
- The run completes; the model, version and document all exist
|
|
- The row is left in the running status, with both names filled
|
|
- The channel carries the not-recorded code and the verdict
|
|
- Elapsed time shows the refusal was **not** retried
|
|
- `pg_constraint` confirms the fixture's `chk_experiment_run_status` really does not allow
|
|
`IMPORT_SUCCESS`, so the test cannot pass vacuously
|
|
|
|
---
|
|
|
|
## 5. joblib Flavor Scenarios
|
|
|
|
`joblib` is an explicit model flavor, declared in `model_config.predict_flavor` /
|
|
`model_config.transform_flavor` like any other. It reads the artifact directly — download the
|
|
`.pkl`, put the artifact's `code/` on `sys.path`, `joblib.load` — and **never** calls an MLflow
|
|
flavor loader. There is no fallback in either direction: a failing loader propagates.
|
|
|
|
Covered by `e2e/test_predictions_batch_joblib_flavor.py` (read) and
|
|
`e2e/test_retrain_joblib_flavor.py` (write). Both use the `joblib_flavor_artifacts` fixture, which
|
|
builds one artifact directory per model type holding a real compressed `model.pkl` plus a `code/`
|
|
directory carrying the module where the model's class lives. The fixture takes that directory off
|
|
`sys.path` again before yielding, so the pickle is **unloadable** until the flavor puts it back —
|
|
that is what makes these tests prove module resolution rather than assume it. The fixture also
|
|
restores `sys.path` in teardown, because the flavor deliberately never removes what it inserted.
|
|
|
|
### 5.1 Read Scenarios
|
|
|
|
---
|
|
|
|
#### Scenario 5.1.1: Predict Through the joblib Flavor
|
|
**Description**: `predict_flavor: 'joblib'` on a normal `predictions_batch` run
|
|
|
|
**Input**:
|
|
- `model_config.predict_flavor = 'joblib'`, `transform_flavor = 'sklearn'`
|
|
- `client.download_artifacts` serving the real `prediction_model` artifact
|
|
|
|
**Expected Behavior**:
|
|
- The artifact is downloaded once and deserialized with `joblib.load`
|
|
- No `models:/<name>/production` URI is built, so no flavor loader ever sees this model
|
|
- The prediction the artifact's own object produces is what lands in Postgres
|
|
|
|
**Assertions**:
|
|
- Exactly one prediction row, `prediction_status='Good'`, value matching the artifact's model
|
|
- `download_artifacts` called once, with `'prediction_model'`
|
|
- No `mlflow.{sklearn,pyfunc}.load_model` call names this model
|
|
|
|
---
|
|
|
|
#### Scenario 5.1.2: Transform Through the joblib Flavor
|
|
**Description**: `transform_flavor: 'joblib'`, which had no E2E coverage before
|
|
|
|
**Input**:
|
|
- `model_config.transform_flavor = 'joblib'`, `predict_flavor = 'sklearn'`
|
|
- `client.download_artifacts` serving the real `data_model` artifact
|
|
|
|
**Expected Behavior**:
|
|
- `data_model` is downloaded and deserialized; the prediction model stays on its MLflow flavor
|
|
- No artifact URI is resolved for the transform, so `mlflow.get_run` is never called
|
|
|
|
**Assertions**:
|
|
- The prediction lands with the mocked prediction model's value
|
|
- `download_artifacts` called once, with `'data_model'`
|
|
- `mlflow.get_run` not called; no flavor loader was given a `/data_model` URI
|
|
|
|
---
|
|
|
|
#### Scenario 5.1.3: An MLflow Flavor Is Unaffected
|
|
**Description**: A model declaring `sklearn` behaves exactly as before
|
|
|
|
**Assertions**:
|
|
- The prediction lands with the mocked model's value
|
|
- `download_artifacts` NOT called — the joblib path is never entered as a safety net
|
|
|
|
---
|
|
|
|
### 5.2 Error Scenarios
|
|
|
|
---
|
|
|
|
#### Scenario 5.2.1: joblib Flavor Failure Has a Single Cause
|
|
**Description**: The downloaded artifact has no top-level `.pkl`
|
|
|
|
**Expected Behavior**: The workflow stops. The reported cause is the missing pickle and nothing
|
|
else — with no fallback there is no second exception chained to a first
|
|
|
|
**Assertions**:
|
|
- The run stops (`assert_stop`)
|
|
- The log carries `No .pkl file found in prediction_model artifact`
|
|
- The log does **not** carry `Original error`, the marker of a chained cause
|
|
|
|
---
|
|
|
|
### 5.3 Write Scenarios
|
|
|
|
---
|
|
|
|
#### Scenario 5.3.1: Retraining a joblib Model Round-Trips
|
|
**Description**: `retrain_model` on a model whose `predict_flavor` and `transform_flavor` are both
|
|
`joblib` — write and read in one test
|
|
|
|
**Input**:
|
|
- Training frame indexed by `DATETIME_FORMAT_WITH_TZ` timestamps, carrying the target column
|
|
- MLflow's artifact I/O replaced by a fake store that copies into an inspectable run directory
|
|
|
|
**Expected Behavior**:
|
|
- Both models load through the joblib flavor; no flavor loader runs
|
|
- `log_model` writes `prediction_model/model.pkl` (a `joblib.dump(..., compress=3)` pickle) and
|
|
re-logs `prediction_model/code/`, without an `MLmodel` manifest
|
|
- Loading the **written** artifact through the joblib flavor deserializes it and predicts, after
|
|
the module the first load imported has been dropped from `sys.modules`
|
|
|
|
**Assertions**:
|
|
- `retrain_model` returns `success: True`
|
|
- `download_artifacts` was asked for both `data_model` and `prediction_model`
|
|
- `sklearn`/`pyfunc` loaders have zero calls
|
|
- The written run has `prediction_model/model.pkl`, `prediction_model/code/<module>.py` and
|
|
`data_model/model.pkl`, and **no** `prediction_model/MLmodel`
|
|
- The reloaded model's prediction equals the original's, and the written `code/` is on `sys.path`
|
|
|
|
---
|