Merge branch 'release/SIENTIAPDE-1646' into feature/SIENTIAPDE-1986-simple-metrics-rewrite

This commit is contained in:
2026-10-09 14:33:16 -03:00
committed by GitHub
9 changed files with 302 additions and 77 deletions

View File

@@ -6,7 +6,13 @@ from sientia_do.notifications.models import NotificationLevel
from sientia_do.observability.metrics_controller import MetricsController
from sientia_do.observability.sientia_monitoring import SientiaMonitoring
from laborious.activities.gates import Gates, input_filter_counters, input_filter_functions
from laborious.activities.gates import (
Gates,
input_filter_counters,
input_filter_functions,
mlflow_content_filter_counters,
mlflow_content_filter_functions,
)
@fixture(autouse=True)
@@ -314,7 +320,7 @@ def test_mlflow_response_gate_invalid_filter(gates_activity):
result = gates_activity.mlflow_response_gate(input_data)
# Assert
assert result == (None, 0, '')
assert result == (None, 0, '', None)
@patch('laborious.activities.gates.mlflow_response_filter_functions')
@@ -341,7 +347,7 @@ def test_mlflow_response_gate_filter_exception(
result = gates_activity.mlflow_response_gate(input_data)
# Assert
assert result == (None, 0, '')
assert result == (None, 0, '', None)
gates_activity.send_notification.assert_called_once_with(
metadata=metadata['metadata'],
notification_id='MLFLOW_GATE_RESPONSE_FILTER__INVALID_FILTER',
@@ -369,7 +375,7 @@ def test_mlflow_response_gate_no_filters(gates_activity):
result = gates_activity.mlflow_response_gate(input_data)
# Assert
assert result == (None, 0, '')
assert result == (None, 0, '', None)
gates_activity.debug.assert_called()
@@ -380,19 +386,30 @@ def test_mlflow_response_gate_with_filter(gates_activity):
'filters': {'API_ERROR': {'POLICY': 'STOP'}},
'data': _minio_payload(
{'content': {'message': 'API error occurred', 'traceback': 'error trace'}},
status={'success': False, 'message': 'API error occurred'},
status={
'success': False,
'content': {'message': 'API error occurred', 'traceback': 'error trace'},
},
),
'type': 'test',
'path_priority': ['STOP', 'CONTINUE', 'REPEAT'],
}
# Act
result = gates_activity.mlflow_response_gate(input_data)
path_flag, confidence, comment, document = gates_activity.mlflow_response_gate(input_data)
# Assert
assert result == ('STOP', -1, 'API error occurred')
assert (path_flag, confidence, comment) == ('STOP', -1, 'API error occurred')
assert document == {'filters': [{'name': 'API_ERROR', 'policy': 'STOP', 'counter': 1}]}
gates_activity.debug.assert_called()
gates_activity.send_notification.assert_called()
gates_activity.send_notification.assert_called_once_with(
metadata=metadata['metadata'],
notification_id='TEST_GATE_RESPONSE_FILTER__API_ERROR',
message='API error occurred',
block='mlflow_gate',
level=NotificationLevel.ERROR,
attachment_content='error trace',
)
def test_mlflow_response_gate_with_filter_capitalized_keys(gates_activity):
@@ -402,7 +419,10 @@ def test_mlflow_response_gate_with_filter_capitalized_keys(gates_activity):
'filters': {'API_ERROR': {'Policy': 'STOP'}},
'data': _minio_payload(
{'content': {'message': 'API error occurred', 'traceback': 'error trace'}},
status={'success': False, 'message': 'API error occurred'},
status={
'success': False,
'content': {'message': 'API error occurred', 'traceback': 'error trace'},
},
),
'type': 'test',
'path_priority': ['STOP', 'CONTINUE', 'REPEAT'],
@@ -412,7 +432,12 @@ def test_mlflow_response_gate_with_filter_capitalized_keys(gates_activity):
result = gates_activity.mlflow_response_gate(input_data)
# Assert
assert result == ('STOP', -1, 'API error occurred')
assert result == (
'STOP',
-1,
'API error occurred',
{'filters': [{'name': 'API_ERROR', 'policy': 'STOP', 'counter': 1}]},
)
def test_mlflow_response_gate_with_filter_not_caught(gates_activity):
@@ -432,7 +457,7 @@ def test_mlflow_response_gate_with_filter_not_caught(gates_activity):
result = gates_activity.mlflow_response_gate(input_data)
# Assert
assert result == (None, 0, '')
assert result == (None, 0, '', None)
gates_activity.debug.assert_called()
@@ -450,7 +475,7 @@ def test_mlflow_content_gate_invalid_filter(gates_activity):
result = gates_activity.mlflow_content_gate(input_data)
# Assert
assert result == (None, 0, '')
assert result == (None, 0, '', None)
@patch('laborious.activities.gates.mlflow_content_filter_functions')
@@ -472,7 +497,7 @@ def test_mlflow_content_gate_filter_exception(mock_mlflow_content_filter_functio
result = gates_activity.mlflow_content_gate(input_data)
# Assert
assert result == (None, 0, '')
assert result == (None, 0, '', None)
gates_activity.debug.assert_called()
gates_activity.send_notification.assert_called_once_with(
metadata=metadata['metadata'],
@@ -498,12 +523,12 @@ def test_mlflow_content_gate_no_filters(gates_activity):
result = gates_activity.mlflow_content_gate(input_data)
# Assert
assert result == (None, 0, '')
assert result == (None, 0, '', None)
gates_activity.debug.assert_called()
def test_mlflow_content_gate_with_filter(gates_activity):
# Arrange
# Arrange: 3 rows, all-NaN after the filter's own normalization — triggers NAN_VALUES.
input_data = {
**metadata,
'filters': {'NAN_VALUES': {'POLICY': 'STOP', 'CONFIG': {}}},
@@ -513,10 +538,15 @@ def test_mlflow_content_gate_with_filter(gates_activity):
}
# Act
result = gates_activity.mlflow_content_gate(input_data)
path_flag, confidence, comment, document = gates_activity.mlflow_content_gate(input_data)
# Assert
assert result == ('STOP', -1, 'Transformed data not passed the content filter')
assert (path_flag, confidence, comment) == (
'STOP',
-1,
'Transformed data not passed the content filter',
)
assert document == {'filters': [{'name': 'NAN_VALUES', 'policy': 'STOP', 'counter': 3}]}
gates_activity.debug.assert_called()
gates_activity.send_notification.assert_called()
@@ -535,7 +565,7 @@ def test_mlflow_content_gate_with_filter_not_caught(gates_activity):
result = gates_activity.mlflow_content_gate(input_data)
# Assert
assert result == (None, 0, '')
assert result == (None, 0, '', None)
gates_activity.debug.assert_called()
@@ -550,10 +580,18 @@ def test_mlflow_content_gate_filter_returns_false(gates_activity):
result = gates_activity.mlflow_content_gate(input_data)
assert result == (None, 0, '')
assert result == (None, 0, '', None)
gates_activity.debug.assert_called()
def test_mlflow_content_filter_counters_covers_every_mlflow_content_filter_function():
# Same parity guard as `test_input_filter_counters_covers_every_input_filter_function`
# (review finding, 2026-09-30): a filter added to `mlflow_content_filter_functions` without
# a matching counter here degrades to counter=0 instead of crashing, but this test still
# catches the gap before it ships.
assert set(mlflow_content_filter_counters) == set(mlflow_content_filter_functions)
def test_get_prediction_store_policy_invalid_policy(gates_activity):
# Arrange
prediction_store_policy = 'INVALID_POLICY'

View File

@@ -59,11 +59,11 @@ async def test_run(workflow_mock, prediction_process):
workflow_mock.execute_local_activity_method.side_effect = [
('continue', 0.95, 'Input data with bad quality', None), # input_gate
# mlflow_response_gate (transform)
('continue', 0.95, 'Error'),
('continue', 0.95, 'Error', None),
# mlflow_content_gate (transform)
('continue', 0.95, 'Transformed data not passed the content filter'),
('continue', 0.95, 'Transformed data not passed the content filter', None),
# mlflow_response_gate (predict)
('continue', 0.95, 'Error'),
('continue', 0.95, 'Error', None),
]
# Act
@@ -276,7 +276,7 @@ async def test_run_stop_at_first_mlflow_response_gate(workflow_mock, prediction_
]
workflow_mock.execute_local_activity_method.side_effect = [
('repeat', 0.95, 'Input data with bad quality', None), # input_gate
('continue', 0.95, 'Error'), # mlflow_response_gate (transform)
('continue', 0.95, 'Error', None), # mlflow_response_gate (transform)
]
# Act
@@ -367,9 +367,9 @@ async def test_run_stop_at_mlflow_content_gate(workflow_mock, prediction_process
workflow_mock.execute_local_activity_method.side_effect = [
('continue', 0.95, 'Input data with bad quality', None), # input_gate
# mlflow_response_gate (transform)
('continue', 0.95, 'Error'),
('continue', 0.95, 'Error', None),
# mlflow_content_gate (transform)
('continue', 0.95, 'Transformed data not passed the content filter'),
('continue', 0.95, 'Transformed data not passed the content filter', None),
]
# Act
@@ -478,10 +478,10 @@ async def test_run_stop_at_mlflow_last_response_gate(workflow_mock, prediction_p
workflow_mock.execute_local_activity_method.side_effect = [
('continue', 0.95, 'Input data with bad quality', None), # input_gate
# mlflow_response_gate (transform)
('continue', 0.95, 'Error'),
('continue', 0.95, 'Error', None),
# mlflow_content_gate (transform)
('continue', 0.95, 'Transformed data not passed the content filter'),
('continue', 0.95, 'Error'), # mlflow_response_gate (predict)
('continue', 0.95, 'Transformed data not passed the content filter', None),
('continue', 0.95, 'Error', None), # mlflow_response_gate (predict)
]
# Act
@@ -695,10 +695,10 @@ async def test_path_flag_handler_repeat(workflow_mock, prediction_process):
async def test_path_flag_handler_repeat_no_document_skips_write_activity_call(
workflow_mock, prediction_process
):
# Arrange: no document (e.g. REPEAT triggered by a gate that doesn't build one yet — today,
# `mlflow_response_gate`/`mlflow_content_gate`) must not even dispatch the remote
# write_prediction_metadata activity — that would be a wasted Temporal round-trip on every
# REPEAT caused by those two gates, not a rare edge case (review finding, 2026-10-01).
# Arrange: no document must not even dispatch the remote write_prediction_metadata
# activity — that would be a wasted Temporal round-trip (review finding, 2026-10-01).
# Defensive case: all 3 gates now always build a document alongside a non-`None`
# `path_flag`, so this guards a caller that doesn't, not today's common case.
data = {'test': 'data'}
# Act
@@ -1075,9 +1075,9 @@ async def test_run_with_cleanup_prefixes(workflow_mock, prediction_process):
]
workflow_mock.execute_local_activity_method.side_effect = [
('continue', 0.95, 'ok', None), # input_gate
('continue', 0.95, ''),
('continue', 0.95, ''),
('continue', 0.95, ''),
('continue', 0.95, '', None), # mlflow_response_gate (transform)
('continue', 0.95, '', None), # mlflow_content_gate (transform)
('continue', 0.95, '', None), # mlflow_response_gate (predict)
]
await prediction_process.run(input_data)