SIENTIAPDE-1646

Update README, requirements, and E2E tests for improved configuration and functionality

- Enhanced the README with updated model configuration examples, including the addition of an alias for production.
- Removed the `requirements-light.txt` file and updated `requirements-local.txt` and `requirements.txt` to replace `asyncua` with `opcua`.
- Refactored E2E test scenarios to utilize scenario input files for better maintainability and clarity.
- Improved test coverage for MinIO offload functionality and added new helper functions for loading scenario inputs.
- Updated `values.yaml` to reflect new global configurations and environment variables for the laborious worker.
This commit is contained in:
2026-05-07 17:02:25 -03:00
parent aaf647efdf
commit e6018af23f
51 changed files with 4408 additions and 2660 deletions

View File

@@ -1,8 +1,9 @@
from unittest.mock import ANY, AsyncMock, MagicMock, call, patch
from unittest.mock import ANY, MagicMock, call, patch
from pandas import DataFrame
from pytest import fixture, mark
from pytest import fixture
from sientia_do.notifications.models import NotificationLevel
from sientia_do.observability.sientia_monitoring import SientiaMonitoring
from laborious.activities.gates import Gates
@@ -17,17 +18,17 @@ def _passthrough_from_dict():
def _minio_payload(retrieve_return, status=None):
"""
Build a MinioDataFramePayload-like test double with async retrieve.
Build a MinioDataFramePayload-like test double with retrieve.
Args:
retrieve_return: Value returned from await retrieve(minio_repo, metadata).
retrieve_return: Value returned from retrieve(minio_repo, metadata).
status: Optional status dict for MLflow response gate (payload.status).
Return:
MagicMock: Object with async retrieve and optional status.
"""
p = MagicMock()
p.retrieve = AsyncMock(return_value=retrieve_return)
p.retrieve = MagicMock(return_value=retrieve_return)
p.status = status
return p
@@ -37,7 +38,7 @@ def gates_activity():
gates = Gates(
logger=MagicMock(),
notification_handler=MagicMock(),
metrics_controller=AsyncMock(),
metrics_controller=MagicMock(),
)
gates.error = MagicMock()
gates.debug = MagicMock()
@@ -45,8 +46,7 @@ def gates_activity():
gates.warning = MagicMock()
gates.critical = MagicMock()
gates.send_notification = MagicMock()
gates.send_notification_async = AsyncMock()
gates.emit_metric = AsyncMock()
gates.emit_metric_sync = MagicMock()
return gates
@@ -60,8 +60,7 @@ metadata = {
}
@mark.asyncio
async def test_input_gate_invalid_filter(gates_activity):
def test_input_gate_invalid_filter(gates_activity):
# Arrange
input_data = {
**metadata,
@@ -71,7 +70,7 @@ async def test_input_gate_invalid_filter(gates_activity):
}
# Act
result = await gates_activity.input_gate(input_data)
result = gates_activity.input_gate(input_data)
# Assert
assert result == (None, 0, '')
@@ -80,9 +79,8 @@ async def test_input_gate_invalid_filter(gates_activity):
)
@mark.asyncio
@patch('laborious.activities.gates.input_filter_functions')
async def test_input_gate_filter_exception(mock_input_filter_functions, gates_activity):
def test_input_gate_filter_exception(mock_input_filter_functions, gates_activity):
# Arrange
mock_input_filter_functions.__contains__.return_value = True
mock_input_filter_functions.__getitem__.return_value = MagicMock(
@@ -96,11 +94,11 @@ async def test_input_gate_filter_exception(mock_input_filter_functions, gates_ac
}
# Act
result = await gates_activity.input_gate(input_data)
result = gates_activity.input_gate(input_data)
# Assert
assert result == (None, 0, '')
gates_activity.send_notification_async.assert_called_once_with(
gates_activity.send_notification.assert_called_once_with(
metadata=metadata['metadata'],
notification_id='INTPUT_GATE_ERROR__EMPTY_DATA',
message="Error in filter EMPTY_DATA:{'POLICY': 'STOP', 'CONFIG': {}}: \n Test error",
@@ -110,8 +108,7 @@ async def test_input_gate_filter_exception(mock_input_filter_functions, gates_ac
)
@mark.asyncio
async def test_input_gate_no_filters(gates_activity):
def test_input_gate_no_filters(gates_activity):
# Arrange
input_data = {
**metadata,
@@ -121,15 +118,14 @@ async def test_input_gate_no_filters(gates_activity):
}
# Act
result = await gates_activity.input_gate(input_data)
result = gates_activity.input_gate(input_data)
# Assert
assert result == (None, 0, '')
gates_activity.debug.assert_called()
@mark.asyncio
async def test_input_gate_with_filter(gates_activity):
def test_input_gate_with_filter(gates_activity):
# Arrange
input_data = {
**metadata,
@@ -139,15 +135,14 @@ async def test_input_gate_with_filter(gates_activity):
}
# Act
result = await gates_activity.input_gate(input_data)
result = gates_activity.input_gate(input_data)
# Assert
assert result == ('STOP', -1, 'Input data with bad quality')
gates_activity.debug.assert_called()
@mark.asyncio
async def test_input_gate_with_filter_lowercase_keys(gates_activity):
def test_input_gate_with_filter_lowercase_keys(gates_activity):
# Arrange
input_data = {
**metadata,
@@ -157,14 +152,13 @@ async def test_input_gate_with_filter_lowercase_keys(gates_activity):
}
# Act
result = await gates_activity.input_gate(input_data)
result = gates_activity.input_gate(input_data)
# Assert
assert result == ('STOP', -1, 'Input data with bad quality')
@mark.asyncio
async def test_input_gate_with_filter_capitalized_keys(gates_activity):
def test_input_gate_with_filter_capitalized_keys(gates_activity):
# Arrange
input_data = {
**metadata,
@@ -174,14 +168,13 @@ async def test_input_gate_with_filter_capitalized_keys(gates_activity):
}
# Act
result = await gates_activity.input_gate(input_data)
result = gates_activity.input_gate(input_data)
# Assert
assert result == ('STOP', -1, 'Input data with bad quality')
@mark.asyncio
async def test_input_gate_with_filter_not_caught(gates_activity):
def test_input_gate_with_filter_not_caught(gates_activity):
# Arrange
input_data = {
**metadata,
@@ -191,15 +184,14 @@ async def test_input_gate_with_filter_not_caught(gates_activity):
}
# Act
result = await gates_activity.input_gate(input_data)
result = gates_activity.input_gate(input_data)
# Assert
assert result == (None, 0, '')
gates_activity.debug.assert_called()
@mark.asyncio
async def test_mlflow_response_gate_invalid_filter(gates_activity):
def test_mlflow_response_gate_invalid_filter(gates_activity):
# Arrange
input_data = {
**metadata,
@@ -213,15 +205,14 @@ async def test_mlflow_response_gate_invalid_filter(gates_activity):
}
# Act
result = await gates_activity.mlflow_response_gate(input_data)
result = gates_activity.mlflow_response_gate(input_data)
# Assert
assert result == (None, 0, '')
@mark.asyncio
@patch('laborious.activities.gates.mlflow_response_filter_functions')
async def test_mlflow_response_gate_filter_exception(
def test_mlflow_response_gate_filter_exception(
mock_mlflow_response_filter_functions, gates_activity
):
# Arrange
@@ -241,11 +232,11 @@ async def test_mlflow_response_gate_filter_exception(
}
# Act
result = await gates_activity.mlflow_response_gate(input_data)
result = gates_activity.mlflow_response_gate(input_data)
# Assert
assert result == (None, 0, '')
gates_activity.send_notification_async.assert_called_once_with(
gates_activity.send_notification.assert_called_once_with(
metadata=metadata['metadata'],
notification_id='MLFLOW_GATE_RESPONSE_FILTER__INVALID_FILTER',
message="Error in filter INVALID_FILTER:{'POLICY': 'STOP'}: \n Test error",
@@ -255,8 +246,7 @@ async def test_mlflow_response_gate_filter_exception(
)
@mark.asyncio
async def test_mlflow_response_gate_no_filters(gates_activity):
def test_mlflow_response_gate_no_filters(gates_activity):
# Arrange
input_data = {
**metadata,
@@ -270,15 +260,14 @@ async def test_mlflow_response_gate_no_filters(gates_activity):
}
# Act
result = await gates_activity.mlflow_response_gate(input_data)
result = gates_activity.mlflow_response_gate(input_data)
# Assert
assert result == (None, 0, '')
gates_activity.debug.assert_called()
@mark.asyncio
async def test_mlflow_response_gate_with_filter(gates_activity):
def test_mlflow_response_gate_with_filter(gates_activity):
# Arrange
input_data = {
**metadata,
@@ -292,16 +281,15 @@ async def test_mlflow_response_gate_with_filter(gates_activity):
}
# Act
result = await gates_activity.mlflow_response_gate(input_data)
result = gates_activity.mlflow_response_gate(input_data)
# Assert
assert result == ('STOP', -1, 'API error occurred')
gates_activity.debug.assert_called()
gates_activity.send_notification_async.assert_called()
gates_activity.send_notification.assert_called()
@mark.asyncio
async def test_mlflow_response_gate_with_filter_capitalized_keys(gates_activity):
def test_mlflow_response_gate_with_filter_capitalized_keys(gates_activity):
# Arrange
input_data = {
**metadata,
@@ -315,14 +303,13 @@ async def test_mlflow_response_gate_with_filter_capitalized_keys(gates_activity)
}
# Act
result = await gates_activity.mlflow_response_gate(input_data)
result = gates_activity.mlflow_response_gate(input_data)
# Assert
assert result == ('STOP', -1, 'API error occurred')
@mark.asyncio
async def test_mlflow_response_gate_with_filter_not_caught(gates_activity):
def test_mlflow_response_gate_with_filter_not_caught(gates_activity):
# Arrange
input_data = {
**metadata,
@@ -336,15 +323,14 @@ async def test_mlflow_response_gate_with_filter_not_caught(gates_activity):
}
# Act
result = await gates_activity.mlflow_response_gate(input_data)
result = gates_activity.mlflow_response_gate(input_data)
# Assert
assert result == (None, 0, '')
gates_activity.debug.assert_called()
@mark.asyncio
async def test_mlflow_content_gate_invalid_filter(gates_activity):
def test_mlflow_content_gate_invalid_filter(gates_activity):
# Arrange
input_data = {
**metadata,
@@ -355,17 +341,14 @@ async def test_mlflow_content_gate_invalid_filter(gates_activity):
}
# Act
result = await gates_activity.mlflow_content_gate(input_data)
result = gates_activity.mlflow_content_gate(input_data)
# Assert
assert result == (None, 0, '')
@mark.asyncio
@patch('laborious.activities.gates.mlflow_content_filter_functions')
async def test_mlflow_content_gate_filter_exception(
mock_mlflow_content_filter_functions, gates_activity
):
def test_mlflow_content_gate_filter_exception(mock_mlflow_content_filter_functions, gates_activity):
# Arrange
mock_mlflow_content_filter_functions.__contains__.return_value = True
mock_mlflow_content_filter_functions.__getitem__.return_value = MagicMock(
@@ -380,12 +363,12 @@ async def test_mlflow_content_gate_filter_exception(
}
# Act
result = await gates_activity.mlflow_content_gate(input_data)
result = gates_activity.mlflow_content_gate(input_data)
# Assert
assert result == (None, 0, '')
gates_activity.debug.assert_called()
gates_activity.send_notification_async.assert_called_once_with(
gates_activity.send_notification.assert_called_once_with(
metadata=metadata['metadata'],
notification_id='MLFLOW_GATE_CONTENT_FILTER__API_ERROR',
message="Error in filter API_ERROR:{'POLICY': 'STOP'}: \n Test error",
@@ -395,8 +378,7 @@ async def test_mlflow_content_gate_filter_exception(
)
@mark.asyncio
async def test_mlflow_content_gate_no_filters(gates_activity):
def test_mlflow_content_gate_no_filters(gates_activity):
# Arrange
input_data = {
**metadata,
@@ -407,15 +389,14 @@ async def test_mlflow_content_gate_no_filters(gates_activity):
}
# Act
result = await gates_activity.mlflow_content_gate(input_data)
result = gates_activity.mlflow_content_gate(input_data)
# Assert
assert result == (None, 0, '')
gates_activity.debug.assert_called()
@mark.asyncio
async def test_mlflow_content_gate_with_filter(gates_activity):
def test_mlflow_content_gate_with_filter(gates_activity):
# Arrange
input_data = {
**metadata,
@@ -426,16 +407,15 @@ async def test_mlflow_content_gate_with_filter(gates_activity):
}
# Act
result = await gates_activity.mlflow_content_gate(input_data)
result = gates_activity.mlflow_content_gate(input_data)
# Assert
assert result == ('STOP', -1, 'Transformed data not passed the content filter')
gates_activity.debug.assert_called()
gates_activity.send_notification_async.assert_called()
gates_activity.send_notification.assert_called()
@mark.asyncio
async def test_mlflow_content_gate_with_filter_not_caught(gates_activity):
def test_mlflow_content_gate_with_filter_not_caught(gates_activity):
# Arrange
input_data = {
**metadata,
@@ -446,15 +426,14 @@ async def test_mlflow_content_gate_with_filter_not_caught(gates_activity):
}
# Act
result = await gates_activity.mlflow_content_gate(input_data)
result = gates_activity.mlflow_content_gate(input_data)
# Assert
assert result == (None, 0, '')
gates_activity.debug.assert_called()
@mark.asyncio
async def test_mlflow_content_gate_filter_returns_false(gates_activity):
def test_mlflow_content_gate_filter_returns_false(gates_activity):
input_data = {
**metadata,
'filters': {'NAN_VALUES': {'POLICY': 'STOP', 'CONFIG': {}}},
@@ -463,7 +442,7 @@ async def test_mlflow_content_gate_filter_returns_false(gates_activity):
'path_priority': ['STOP', 'CONTINUE', 'REPEAT'],
}
result = await gates_activity.mlflow_content_gate(input_data)
result = gates_activity.mlflow_content_gate(input_data)
assert result == (None, 0, '')
gates_activity.debug.assert_called()
@@ -525,8 +504,7 @@ def test_get_prediction_store_policy_valid_policy(gates_activity):
assert policy_value == 1
@mark.asyncio
async def test_format_prediction_no_timestamp(gates_activity):
def test_format_prediction_no_timestamp(gates_activity):
# Arrange
input_data = {
**metadata,
@@ -545,7 +523,7 @@ async def test_format_prediction_no_timestamp(gates_activity):
}
# Act
result = await gates_activity.format_prediction(input_data)
result = gates_activity.format_prediction(input_data)
# Assert
assert result['prediction'] == {0: 1}
@@ -557,8 +535,7 @@ async def test_format_prediction_no_timestamp(gates_activity):
assert result['comments'] == {0: ''}
@mark.asyncio
async def test_format_prediction_with_timestamp_erl(gates_activity):
def test_format_prediction_with_timestamp_erl(gates_activity):
# Arrange
input_data = {
**metadata,
@@ -585,7 +562,7 @@ async def test_format_prediction_with_timestamp_erl(gates_activity):
}
# Act
result = await gates_activity.format_prediction(input_data)
result = gates_activity.format_prediction(input_data)
# Assert
assert result['prediction'] == {0: 2, 1: 1}
@@ -597,8 +574,7 @@ async def test_format_prediction_with_timestamp_erl(gates_activity):
assert result['comments'] == {0: '', 1: ''}
@mark.asyncio
async def test_format_prediction_with_timestamp_lts(gates_activity):
def test_format_prediction_with_timestamp_lts(gates_activity):
# Arrange
input_data = {
**metadata,
@@ -625,7 +601,7 @@ async def test_format_prediction_with_timestamp_lts(gates_activity):
}
# Act
result = await gates_activity.format_prediction(input_data)
result = gates_activity.format_prediction(input_data)
# Assert
assert result['prediction'] == {0: 3, 1: 2}
@@ -637,8 +613,7 @@ async def test_format_prediction_with_timestamp_lts(gates_activity):
assert result['comments'] == {0: '', 1: ''}
@mark.asyncio
async def test_format_prediction_with_timestamp_invalid_policy(gates_activity):
def test_format_prediction_with_timestamp_invalid_policy(gates_activity):
# Arrange
input_data = {
**metadata,
@@ -663,16 +638,15 @@ async def test_format_prediction_with_timestamp_invalid_policy(gates_activity):
gates_activity.get_prediction_store_policy = MagicMock(return_value=('invalid', 1))
try:
await gates_activity.format_prediction(input_data)
gates_activity.format_prediction(input_data)
except ValueError as e:
assert str(e) == 'Invalid policy type: invalid'
else:
raise AssertionError('Expected ValueError')
@mark.asyncio
@patch('laborious.activities.gates.MinioDataFramePayload.from_dataframe', new_callable=AsyncMock)
async def test_format_transformed_data_single_row(mock_from_dataframe, gates_activity):
@patch('laborious.activities.gates.MinioDataFramePayload.from_dataframe', new_callable=MagicMock)
def test_format_transformed_data_single_row(mock_from_dataframe, gates_activity):
# Arrange
payload_result = MagicMock()
mock_from_dataframe.return_value = payload_result
@@ -691,7 +665,7 @@ async def test_format_transformed_data_single_row(mock_from_dataframe, gates_act
}
# Act
result = await gates_activity.format_transformed_data(input_data)
result = gates_activity.format_transformed_data(input_data)
# Assert
assert result is payload_result
@@ -705,9 +679,8 @@ async def test_format_transformed_data_single_row(mock_from_dataframe, gates_act
gates_activity.info.assert_called()
@mark.asyncio
@patch('laborious.activities.gates.MinioDataFramePayload.from_dataframe', new_callable=AsyncMock)
async def test_format_transformed_data_multiple_rows(mock_from_dataframe, gates_activity):
@patch('laborious.activities.gates.MinioDataFramePayload.from_dataframe', new_callable=MagicMock)
def test_format_transformed_data_multiple_rows(mock_from_dataframe, gates_activity):
# Arrange
payload_result = MagicMock()
mock_from_dataframe.return_value = payload_result
@@ -732,7 +705,7 @@ async def test_format_transformed_data_multiple_rows(mock_from_dataframe, gates_
}
# Act
result = await gates_activity.format_transformed_data(input_data)
result = gates_activity.format_transformed_data(input_data)
# Assert
assert result is payload_result
@@ -746,9 +719,8 @@ async def test_format_transformed_data_multiple_rows(mock_from_dataframe, gates_
gates_activity.info.assert_called()
@mark.asyncio
@patch('laborious.activities.gates.MinioDataFramePayload.from_dataframe', new_callable=AsyncMock)
async def test_format_transformed_data_empty_data(mock_from_dataframe, gates_activity):
@patch('laborious.activities.gates.MinioDataFramePayload.from_dataframe', new_callable=MagicMock)
def test_format_transformed_data_empty_data(mock_from_dataframe, gates_activity):
# Arrange
payload_result = MagicMock()
mock_from_dataframe.return_value = payload_result
@@ -760,7 +732,7 @@ async def test_format_transformed_data_empty_data(mock_from_dataframe, gates_act
}
# Act
result = await gates_activity.format_transformed_data(input_data)
result = gates_activity.format_transformed_data(input_data)
# Assert
assert result is payload_result
@@ -774,8 +746,7 @@ async def test_format_transformed_data_empty_data(mock_from_dataframe, gates_act
gates_activity.info.assert_called()
@mark.asyncio
async def test_format_default_prediction(gates_activity):
def test_format_default_prediction(gates_activity):
# Arrange
input_data = {
**metadata,
@@ -786,7 +757,7 @@ async def test_format_default_prediction(gates_activity):
}
# Act
result = await gates_activity.format_default_prediction(input_data)
result = gates_activity.format_default_prediction(input_data)
# Assert
assert result['prediction'] == {0: 0}
@@ -799,8 +770,7 @@ async def test_format_default_prediction(gates_activity):
gates_activity.debug.assert_called()
@mark.asyncio
async def test_format_retrain_report(gates_activity):
def test_format_retrain_report(gates_activity):
# Arrange
input_data = {
**metadata,
@@ -819,7 +789,7 @@ async def test_format_retrain_report(gates_activity):
}
# Act
result = await gates_activity.format_retrain_report(input_data)
result = gates_activity.format_retrain_report(input_data)
# Assert
assert result['model_id'] == {0: 'test_model'}
@@ -831,8 +801,7 @@ async def test_format_retrain_report(gates_activity):
assert result['mlflow_experiment_id'] == {0: 'test_mlflow_experiment_id'}
@mark.asyncio
async def test_format_retrain_report_failure(gates_activity):
def test_format_retrain_report_failure(gates_activity):
# Arrange
input_data = {
**metadata,
@@ -851,7 +820,7 @@ async def test_format_retrain_report_failure(gates_activity):
}
# Act
result = await gates_activity.format_retrain_report(input_data)
result = gates_activity.format_retrain_report(input_data)
# Assert
assert result['model_id'] == {0: 'test_model'}
@@ -865,9 +834,8 @@ async def test_format_retrain_report_failure(gates_activity):
gates_activity.debug.assert_called()
@mark.asyncio
@patch('laborious.activities.gates.metrics')
async def test_write_metrics(mock_metrics, gates_activity):
def test_write_metrics(mock_metrics, gates_activity):
"""Test write_metrics method."""
input_data = {
**metadata,
@@ -878,7 +846,7 @@ async def test_write_metrics(mock_metrics, gates_activity):
},
'opc_metrics': {'server1': {'tag1': 0.1, 'tag2': 0.2}},
}
await gates_activity.write_metrics(input_data)
gates_activity.write_metrics(input_data)
core_tags = {
'pod_id': gates_activity.pod_id,
'runtime': gates_activity.runtime,
@@ -886,7 +854,7 @@ async def test_write_metrics(mock_metrics, gates_activity):
'model_name': metadata['metadata']['model_name'],
'workflow_name': metadata['metadata']['workflow_name'],
}
gates_activity.emit_metric.assert_has_calls(
gates_activity.emit_metric_sync.assert_has_calls(
[
call(
metric_object=mock_metrics.PREDICTIONS_WRITTEN_COUNT,
@@ -894,7 +862,7 @@ async def test_write_metrics(mock_metrics, gates_activity):
),
]
)
gates_activity.emit_metric.assert_has_calls(
gates_activity.emit_metric_sync.assert_has_calls(
[
call(
metric_object=mock_metrics.PREDICTION_CONFIDENCE_MONITOR,
@@ -904,7 +872,7 @@ async def test_write_metrics(mock_metrics, gates_activity):
),
]
)
gates_activity.emit_metric.assert_has_calls(
gates_activity.emit_metric_sync.assert_has_calls(
[
call(
metric_object=mock_metrics.PREDICTION_RESPONSE_TIME_MONITOR,
@@ -914,7 +882,7 @@ async def test_write_metrics(mock_metrics, gates_activity):
),
]
)
gates_activity.emit_metric.assert_has_calls(
gates_activity.emit_metric_sync.assert_has_calls(
[
call(
metric_object=mock_metrics.PREDICTION_OPC_WRITING_COUNT,
@@ -926,7 +894,7 @@ async def test_write_metrics(mock_metrics, gates_activity):
),
]
)
gates_activity.emit_metric.assert_has_calls(
gates_activity.emit_metric_sync.assert_has_calls(
[
call(
metric_object=mock_metrics.PREDICTION_OPC_WRITING_RESPONSE_TIME_MONITOR,
@@ -940,7 +908,7 @@ async def test_write_metrics(mock_metrics, gates_activity):
),
]
)
gates_activity.emit_metric.assert_has_calls(
gates_activity.emit_metric_sync.assert_has_calls(
[
call(
metric_object=mock_metrics.PREDICTION_OPC_WRITING_COUNT,
@@ -952,7 +920,7 @@ async def test_write_metrics(mock_metrics, gates_activity):
),
]
)
gates_activity.emit_metric.assert_has_calls(
gates_activity.emit_metric_sync.assert_has_calls(
[
call(
metric_object=mock_metrics.PREDICTION_OPC_WRITING_RESPONSE_TIME_MONITOR,
@@ -968,9 +936,8 @@ async def test_write_metrics(mock_metrics, gates_activity):
)
@mark.asyncio
@patch('laborious.activities.gates.metrics')
async def test_write_metrics_with_none_opc_response_time(mock_metrics, gates_activity):
def test_write_metrics_with_none_opc_response_time(mock_metrics, gates_activity):
"""Test write_metrics method with None response_time in opc_metrics."""
input_data = {
**metadata,
@@ -981,10 +948,10 @@ async def test_write_metrics_with_none_opc_response_time(mock_metrics, gates_act
},
'opc_metrics': {'server1': {'tag1': 0.1, 'tag2': None}},
}
await gates_activity.write_metrics(input_data)
gates_activity.write_metrics(input_data)
# Verify that metrics for tag1 are emitted
gates_activity.emit_metric.assert_any_call(
gates_activity.emit_metric_sync.assert_any_call(
metric_object=mock_metrics.PREDICTION_OPC_WRITING_RESPONSE_TIME_MONITOR,
method='observe',
tags={
@@ -1002,7 +969,38 @@ async def test_write_metrics_with_none_opc_response_time(mock_metrics, gates_act
# Verify that metrics for tag2 (with None response_time) are NOT emitted
calls = [
c
for c in gates_activity.emit_metric.call_args_list
for c in gates_activity.emit_metric_sync.call_args_list
if len(c[1].get('tags', {})) > 0 and c[1]['tags'].get('tag') == 'tag2'
]
assert len(calls) == 0, 'Metrics should not be emitted for None response_time'
@patch.object(SientiaMonitoring, 'shutdown')
def test_close_disposes_minio_repository(mock_shutdown):
"""
``Gates.close`` should close the optional MinIO client and clear the repository reference.
"""
minio = MagicMock()
gates = Gates(
minio_repository=minio,
logger=MagicMock(),
notification_handler=MagicMock(),
metrics_controller=MagicMock(),
)
gates.close()
minio.close.assert_called_once()
assert gates.minio_repository is None
mock_shutdown.assert_called_once_with(gates)
@patch.object(SientiaMonitoring, 'shutdown')
def test_close_without_minio_repository(mock_shutdown):
"""When no MinIO repository is configured, ``close`` only shuts down monitoring."""
gates = Gates(
minio_repository=None,
logger=MagicMock(),
notification_handler=MagicMock(),
metrics_controller=MagicMock(),
)
gates.close()
mock_shutdown.assert_called_once_with(gates)