import io from datetime import UTC, datetime from pathlib import Path from unittest.mock import Mock, patch from production_analytics.calculations.channel_config import load_channel_material_calculation from production_analytics.calculations.material_consumption import MaterialIntegrationState from production_analytics.cli.__main__ import _parser, main from production_analytics.enlyze.gateway import EnlyzeProductionRun from production_analytics.service.channel_material import ChannelPollResult from production_analytics.service.channel_material_runner import ChannelMaterialPollingRunner from production_analytics.service.channel_material_runtime import build_channel_material_runner EXAMPLE = ( Path(__file__).resolve().parents[1] / "config/e1-channel-material-consumption.example.yaml" ) NOW = datetime(2026, 9, 1, tzinfo=UTC) def test_cli_channel_material_arguments(): args = _parser().parse_args( [ "run", "channel-material-poll", "--config", str(EXAMPLE), "--poll-interval-seconds", "15", "--state-directory", "state", "--secrets-file", "secrets.env", "--once", ] ) assert args.command == "channel-material-poll" assert args.config == EXAMPLE assert args.poll_interval_seconds == 15 assert args.state_directory == Path("state") assert args.secrets_file == Path("secrets.env") assert args.once def test_runtime_constructs_channel_service(tmp_path, monkeypatch): monkeypatch.setenv("ENLYZE_BASE_URL", "https://example.invalid/api/") for key, value in { "HOST": "localhost", "PORT": "5432", "DB": "analytics", "USER": "user", "PASSWORD": "environment-password", }.items(): monkeypatch.setenv(f"POSTGRES_{key}", value) secrets = tmp_path / "secrets.env" secrets.write_text("ENLYZE_API_KEY=secret-from-file\nPOSTGRES_PASSWORD=file-password\n") calculation = load_channel_material_calculation(EXAMPLE) with patch( "production_analytics.service.channel_material_runtime.ChannelMaterialPollingService" ) as service: runner = build_channel_material_runner( calculation, poll_interval_seconds=7, state_directory=tmp_path / "state", secrets_file=secrets, ) kwargs = service.call_args.kwargs assert kwargs["machine_id"] == calculation.machine_ref assert kwargs["channels"] == calculation.channels assert kwargs["max_sample_gap_seconds"] == calculation.max_sample_gap_seconds assert kwargs["percentage_tolerance"] == calculation.percentage_tolerance assert kwargs["material_names"] == calculation.material_names assert kwargs["gateway"]._client._settings.api_key == "secret-from-file" assert kwargs["state_store"].directory == tmp_path / "state" assert runner.service is service.return_value assert runner.interval == 7 assert runner.snapshot_writer.settings.password == "file-password" def test_once_executes_one_cycle_and_exits(capsys): with patch( "production_analytics.service.channel_material_runtime.build_channel_material_runner" ) as build: assert main(["run", "channel-material-poll", "--config", str(EXAMPLE), "--once"]) == 0 build.return_value.run_once.assert_called_once_with() build.return_value.run.assert_not_called() assert capsys.readouterr().err == "" def test_no_snapshots_are_written_without_eligible_production_run(): writer = Mock() output = io.StringIO() runner = ChannelMaterialPollingRunner( Mock(poll_once=Mock(return_value=None)), machine_id="machine", calculation_id="calculation", snapshot_writer=writer, poll_interval_seconds=10, clock=lambda: NOW, stdout=output, ) assert not runner.run_once() writer.write.assert_not_called() assert "snapshots=0" in output.getvalue() def test_one_cycle_writes_each_channel_snapshot(): run = EnlyzeProductionRun("run", "machine", None, "order", NOW, None) first = ChannelPollResult( run, "Ex1", "C1", MaterialIntegrationState(1.5, 10), "42", "Material", "MAPPED", True ) second = ChannelPollResult( run, "Ex1", "C2", MaterialIntegrationState(2.5, 10), None, None, "AMBIGUOUS", False ) writer = Mock() output = io.StringIO() runner = ChannelMaterialPollingRunner( Mock(poll_once=Mock(return_value=(first, second))), machine_id="machine", calculation_id="calculation", snapshot_writer=writer, poll_interval_seconds=10, clock=lambda: NOW, stdout=output, ) assert runner.run_once() assert writer.write.call_count == 2 assert writer.write.call_args_list[0].kwargs == { "timestamp": NOW, "calculation_id": "calculation", "machine_id": "machine", "extruder": "Ex1", "channel": "C1", "production_order": "order", "run_id": "run", "cumulative_consumption_kg": 1.5, "material_number": "42", "material_name": "Material", "material_mapping_status": "MAPPED", "percentage_sum_valid": True, } assert "channel snapshots=2 written" in output.getvalue()