QTZPOC-20: Implement explicit joblib model flavor and enhance model import workflow with extensive E2E tests. This commit introduces a first-class joblib model flavor for direct loading and saving of Python pickle artifacts, including custom code within the artifact's code/ directory, bypassing MLflow's flavor loaders. The model import workflow's status database configuration is now streamlined to default to the POSTGRES_* server, with explicit IMPORT_STATUS_DB_* overrides. Comprehensive end-to-end tests have been added for the model import workflow, covering success paths, gate rejections, digest enforcement, provisioning failures, status record integrity, cleanup, and retry/recovery scenarios using a real-clock environment. New E2
43 KiB
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/:
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:
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 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.pyexercises 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.pyuses an in-process asyncua server and realOpcRepository(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_modelsuite is opt-in and has its own harness, containers and fidelity contract — see §4.0. Run it withpytest e2e -m import_e2e. - joblib flavor tests:
e2e/test_predictions_batch_joblib_flavor.pycovers reading a model through the explicitjoblibflavor (predict and transform), ande2e/test_retrain_joblib_flavor.pycovers writing one back on a retrain — see §5. Neitherjoblib.dumpnorjoblib.loadis mocked in either module: the artifact fixture holds a realjoblib.dump(..., compress=3)pickle whose class ships inside the artifact'scode/directory, so only the flavor's ownsys.pathhandling makes it deserialize. Run them withpytest e2e/test_predictions_batch_joblib_flavor.py e2e/test_retrain_joblib_flavor.py -m integration. - Nothing under
e2e/is covered byvalidate: that script lintslaborious/andtests/only, and its test step ispytest tests/(testpaths = ["tests"]). Lint these files by hand (ruff format --check e2e/ && ruff check e2e/), and remember thataddoptscarries--strict-markers: a test without@pytest.mark.integrationis 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:
- Loads data using a custom SQL query
- Prepares prediction configuration
- Delegates to
prediction_processchild 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_predictionchild workflow
- The
format_and_export_predictionworkflow:- 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
queryreturning non-empty DataFrame - Valid
schema,table_name,transform_table_name - Optional
datetime_columnsfor 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_queryreturns DataFrame with data- Workflow prepares prediction input with all configurations
prediction_processchild 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_processworkflow 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_queryraises 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
queryormodel_idorschemaortable_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_querymay 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_filtersconfigured withPOLICY: 'CONTINUE'path_priorityincludes CONTINUE
Expected Behavior:
input_gatereturnspath_flag='CONTINUE'path_flag_handlercalls export workflow with input data directly- MLFlow transform and predict skipped
- Data exported as-is
Assertions:
input_gatecalled- 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_filtersconfigured withPOLICY: 'STOP'
Expected Behavior:
input_gatereturnspath_flag='STOP'path_flag_handlerdetects STOP- Workflow returns early without calling MLFlow
- No prediction exported
Assertions:
input_gatecalledpath_flag_handlerreturns 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_filtersconfigured withPOLICY: 'REPEAT'path_priorityincludes REPEAT
Expected Behavior:
input_gatereturnspath_flag='REPEAT'path_flag_handlercallsrepeat_last_predictionactivity- MLFlow transform and predict skipped
- Last prediction repeated and exported
Assertions:
input_gatecalled- MLFlow operations NOT called
repeat_last_predictionactivity 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_filtersconfigured withPOLICY: 'CONTINUE'path_priorityincludes CONTINUE
Expected Behavior:
request_transformsucceedsmlflow_response_gatefor transform returnspath_flag='CONTINUE'path_flag_handlercalls export workflow with transform data- MLFlow predict skipped
- Transform data exported as-is
Assertions:
- Transform completed
mlflow_response_gatecalled 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_filtersconfigured withPOLICY: 'STOP'
Expected Behavior:
request_transformsucceeds but response invalidmlflow_response_gatefor transform returnspath_flag='STOP'- Workflow exits without calling predict or export
Assertions:
- Transform completed but validation failed
mlflow_response_gatecalled 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_filtersconfigured withPOLICY: 'REPEAT'path_priorityincludes REPEAT
Expected Behavior:
request_transformsucceeds but response has issuesmlflow_response_gatefor transform returnspath_flag='REPEAT'path_flag_handlercallsrepeat_last_predictionactivity- MLFlow predict skipped
- Last prediction repeated and exported
Assertions:
- Transform completed but validation triggered REPEAT
mlflow_response_gatecalled for transform- MLFlow predict NOT called
repeat_last_predictionactivity 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_filtersconfigured withPOLICY: 'CONTINUE'path_priorityincludes CONTINUE
Expected Behavior:
request_predictsucceedsmlflow_response_gatefor predict returnspath_flag='CONTINUE'path_flag_handlercalls export workflow with predict data- Prediction exported despite quality issues
Assertions:
- Transform and predict completed
mlflow_response_gatecalled 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_filtersconfigured withPOLICY: 'STOP'
Expected Behavior:
request_predictsucceeds but response invalidmlflow_response_gatefor predict returnspath_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_filtersconfigured withPOLICY: 'REPEAT'path_priorityincludes REPEAT
Expected Behavior:
request_predictsucceeds but response has issuesmlflow_response_gatefor predict returnspath_flag='REPEAT'path_flag_handlercallsrepeat_last_predictionactivity- Last prediction repeated and exported
Assertions:
- Transform and predict completed but validation triggered REPEAT
mlflow_response_gatecalled for predictrepeat_last_predictionactivity 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)commentprovided with error details
Expected Behavior:
format_default_predictioncalled instead offormat_prediction- Default prediction created with error metadata
- Exported to PostgreSQL only
- Transformed data NOT processed
- Metrics written
Assertions:
format_default_predictioncalledformat_predictionNOT calledformat_transformed_dataNOT 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: Noneopc_output_configconfigured with valid OPC settingspi_web_api_output_config: Noneor{}
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_metricspopulated
Scenario 3.1.3: Export with PI Web API only
Description: Export to PostgreSQL and PI Web API only (no OPC)
Input:
path_flag: Nonepi_web_api_output_configconfigured with valid PI Web API settingsopc_output_config: Noneor{}
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: Noneopc_output_config: Noneor{}pi_web_api_output_config: Noneor{}
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: Nonetransformed_data: Noneorsave_transform: Falseopc_output_config: Noneor{}pi_web_api_output_config: Noneor{}
Expected Behavior:
- Only prediction formatted and exported
- Transform export skipped
- Single PostgreSQL write
Assertions:
format_transformed_dataNOT 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_datasurfaces 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_confidence13 and expectedcomments
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_datareports 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_confidence12 and expectedcomments
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
UaStatusCodeErroron write (no write retry in the same activity)
Expected Behavior:
write_opc_datafails forward for affected tags; background reconnect may be scheduled ifOPC_RECONNECTION_INTERVALallows- Workflow completes
- PostgreSQL row uses
prediction_confidence14 and comment prefixOPC UA session/channel error:(including OPC status name) opc_write_attempts_totalrecordsresult=BadSessionIdInvalid(or matching status); no second write attempt in the same activity
Assertions:
- Workflow completes
prediction_confidence = 14commentsmatchesOPC UA session/channel error:%- Generic OPC error confidence 12 is not used for this case
Reference: 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_readyis cleared — without callingwrite_value - No wait/sleep on the write path; no duplicate
connectfrom parallel writers (connection lock) prediction_confidence = 14,comments = OPC UA reconnect in progress(distinguish from Tier-1Bad*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 intest_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_dataprocesses responseprocess_pi_web_api_responsedetects 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 carryslow.testpathsstays["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 anmlflow serversubprocess started from this repository's venv. Dev requirements installtestcontainers[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. OnlyCoreNotificationHandler'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 frome2e/conftest.py. Every fixture isimport_-prefixed. - Prerequisite: the one-time reaper setup at the top of this document (Running automated E2E tests). 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.sientiauploaded underimported_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_modelanddata_modelat 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, statusFINISHED prediction_modelcontainsMLmodelandmodel.pkl- Bundle parameters logged, origin logged under
source.* - Destination version is
1, independent of the bundle'smodel_versionof3 - Model document
idis the seeded maximum plus one;activeisfalse - Row:
IMPORT_SUCCESS,error_messagenull, 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_configholds exactlytransform_flavor,predict_flavor,retention_minutesandtarget import.workflow_idin 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_idreturns 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_versions
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_nameis still the inserted placeholder, so no write reached theVARCHAR(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_nameis still null andexperiment_namestill 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
IMPORTrow - 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…V12leave (noALTER TABLEby 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_projectormodel_registryreference (with quoted literals stripped, so the bundle's ownmodel_projectvalue 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_envelopeis 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_writeandverdict_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_constraintconfirms the fixture'schk_experiment_run_statusreally does not allowIMPORT_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_artifactsserving the realprediction_modelartifact
Expected Behavior:
- The artifact is downloaded once and deserialized with
joblib.load - No
models:/<name>/productionURI 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_artifactscalled once, with'prediction_model'- No
mlflow.{sklearn,pyfunc,pytorch}.load_modelcall 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_artifactsserving the realdata_modelartifact
Expected Behavior:
data_modelis downloaded and deserialized; the prediction model stays on its MLflow flavor- No artifact URI is resolved for the transform, so
mlflow.get_runis never called
Assertions:
- The prediction lands with the mocked prediction model's value
download_artifactscalled once, with'data_model'mlflow.get_runnot called; no flavor loader was given a/data_modelURI
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_artifactsNOT 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_TZtimestamps, 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_modelwritesprediction_model/model.pkl(ajoblib.dump(..., compress=3)pickle) and re-logsprediction_model/code/, without anMLmodelmanifest- 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_modelreturnssuccess: Truedownload_artifactswas asked for bothdata_modelandprediction_modelsklearn/pyfuncloaders have zero calls- The written run has
prediction_model/model.pkl,prediction_model/code/<module>.pyanddata_model/model.pkl, and noprediction_model/MLmodel - The reloaded model's prediction equals the original's, and the written
code/is onsys.path