Harden live material polling state
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
from datetime import UTC, datetime
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from production_analytics.calculations.material_consumption import (
|
||||
MaterialIntegrationState,
|
||||
MaterialSample,
|
||||
@@ -241,3 +243,85 @@ def test_new_run_of_same_order_keeps_total_but_restarts_integration_at_run_start
|
||||
start=run.start,
|
||||
end=datetime(2026, 9, 3, 8, 0, 30, tzinfo=UTC),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def polling(tmp_path):
|
||||
from production_analytics.service.material_state_store import JsonMaterialStateStore
|
||||
|
||||
gateway = Mock()
|
||||
gateway.get_open_production_run.return_value = EnlyzeProductionRun(
|
||||
"run", "machine", None, "order", datetime(2026, 9, 3, tzinfo=UTC), None,
|
||||
)
|
||||
gateway.get_material_samples.return_value = []
|
||||
store = JsonMaterialStateStore(tmp_path)
|
||||
service = MaterialPollingService(
|
||||
gateway=gateway, state_store=store, machine_id="machine",
|
||||
rate_variable_id="rate", gate_variable_id="gate", gate_threshold=0.5,
|
||||
max_sample_gap_seconds=20,
|
||||
)
|
||||
return service, gateway, store
|
||||
|
||||
|
||||
def test_empty_response_persists_state_without_inferred_consumption(polling) -> None:
|
||||
service, gateway, store = polling
|
||||
now = datetime(2026, 9, 3, 1, tzinfo=UTC)
|
||||
assert service.poll_once(now=now).state == MaterialIntegrationState()
|
||||
saved = MaterialPollingState("run", MaterialIntegrationState(
|
||||
cumulative_consumption_kg=10, integrated_running_seconds=10,
|
||||
last_processed_timestamp=now, last_material_rate_kg_per_hour=3600,
|
||||
last_gate_value=1, integration_active=True,
|
||||
))
|
||||
store.save("machine", "order", saved)
|
||||
assert service.poll_once(now=now).state == saved.integration_state
|
||||
assert store.load("machine", "order") == saved
|
||||
|
||||
|
||||
def test_different_order_does_not_reuse_state(polling) -> None:
|
||||
service, gateway, store = polling
|
||||
store.save("machine", "other-order", MaterialPollingState(
|
||||
"old", MaterialIntegrationState(cumulative_consumption_kg=100),
|
||||
))
|
||||
result = service.poll_once(now=datetime(2026, 9, 3, 1, tzinfo=UTC))
|
||||
assert result.state == MaterialIntegrationState()
|
||||
assert gateway.get_material_samples.call_args.kwargs["start"] == result.run.start
|
||||
assert store.load("machine", "other-order").integration_state.cumulative_consumption_kg == 100
|
||||
|
||||
|
||||
def test_now_before_run_start_does_not_query_or_save(polling) -> None:
|
||||
service, gateway, store = polling
|
||||
assert service.poll_once(now=datetime(2026, 9, 2, tzinfo=UTC)) is None
|
||||
gateway.get_material_samples.assert_not_called()
|
||||
assert store.load("machine", "order") is None
|
||||
|
||||
|
||||
def test_naive_now_is_rejected_before_io(polling) -> None:
|
||||
service, gateway, store = polling
|
||||
with pytest.raises(ValueError, match="now must be timezone-aware"):
|
||||
service.poll_once(now=datetime(2026, 9, 3))
|
||||
gateway.get_open_production_run.assert_not_called()
|
||||
|
||||
|
||||
def test_new_run_empty_response_resets_baseline_and_retains_totals(polling) -> None:
|
||||
service, gateway, store = polling
|
||||
store.save("machine", "order", MaterialPollingState("previous", MaterialIntegrationState(
|
||||
cumulative_consumption_kg=100, integrated_running_seconds=50,
|
||||
last_processed_timestamp=datetime(2026, 9, 2, 23, 59, 59, tzinfo=UTC),
|
||||
last_material_rate_kg_per_hour=3600, last_gate_value=1, integration_active=True,
|
||||
)))
|
||||
result = service.poll_once(now=datetime(2026, 9, 3, 1, tzinfo=UTC))
|
||||
assert result.state == MaterialIntegrationState(
|
||||
cumulative_consumption_kg=100, integrated_running_seconds=50,
|
||||
)
|
||||
assert store.load("machine", "order") == MaterialPollingState("run", result.state)
|
||||
|
||||
|
||||
def test_now_before_saved_timestamp_leaves_state_unchanged(polling) -> None:
|
||||
service, gateway, store = polling
|
||||
saved = MaterialPollingState("run", MaterialIntegrationState(
|
||||
last_processed_timestamp=datetime(2026, 9, 3, 2, tzinfo=UTC),
|
||||
))
|
||||
store.save("machine", "order", saved)
|
||||
assert service.poll_once(now=datetime(2026, 9, 3, 1, tzinfo=UTC)) is None
|
||||
gateway.get_material_samples.assert_not_called()
|
||||
assert store.load("machine", "order") == saved
|
||||
|
||||
Reference in New Issue
Block a user