from datetime import UTC, datetime, timedelta import pytest from app.core.orchestration.migration.rollout import ( MigrationBatch, ObservationWindow, RolloutEntryGates, RolloutExitGates, RolloutPolicy, ) def test_batches_are_fixed_from_low_to_high_risk(): assert [batch.value for batch in MigrationBatch] == [ "read_only_low_frequency", "idempotent_partition_write", "core_multi_node", "special_high_risk", ] def test_entry_gates_require_shadow_failure_recovery_and_capacity(): gates = RolloutEntryGates( inventory_complete=True, workflow_spec_valid=True, policy_complete=True, rollback_target_present=True, shadow_runs=2, required_shadow_runs=3, failure_recovery_observed=True, reconciliation_passed=True, connection_budget_passed=True, peak_concurrency_passed=True, sla_passed=True, ) with pytest.raises(ValueError, match="shadow_runs"): RolloutPolicy.assert_entry(gates) def test_observation_window_fails_closed_before_deadline_or_on_incident(): now = datetime(2026, 7, 19, tzinfo=UTC) window = ObservationWindow( started_at=now - timedelta(hours=2), required_duration=timedelta(hours=4), unexplained_differences=0, duplicate_writes=0, critical_sla_breaches=0, rollback_requests=0, ) with pytest.raises(ValueError, match="observation_window_complete"): RolloutPolicy.assert_observation(window, now=now) incident = ObservationWindow( started_at=now - timedelta(hours=5), required_duration=timedelta(hours=4), unexplained_differences=0, duplicate_writes=1, critical_sla_breaches=0, rollback_requests=0, ) with pytest.raises(ValueError, match="duplicate_writes"): RolloutPolicy.assert_observation(incident, now=now) def test_exit_gates_require_recovery_and_unchanged_n8n_standby(): gates = RolloutExitGates( observation_window_complete=True, no_unexplained_differences=True, no_duplicate_writes=True, sla_passed=True, pause_retry_rollback_exercised=False, n8n_standby=True, n8n_unauthorized_changes=0, ) with pytest.raises(ValueError, match="pause_retry_rollback_exercised"): RolloutPolicy.assert_exit(gates)