test_workflow_migration_rollout.py 2.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. from datetime import UTC, datetime, timedelta
  2. import pytest
  3. from app.core.orchestration.migration.rollout import (
  4. MigrationBatch,
  5. ObservationWindow,
  6. RolloutEntryGates,
  7. RolloutExitGates,
  8. RolloutPolicy,
  9. )
  10. def test_batches_are_fixed_from_low_to_high_risk():
  11. assert [batch.value for batch in MigrationBatch] == [
  12. "read_only_low_frequency",
  13. "idempotent_partition_write",
  14. "core_multi_node",
  15. "special_high_risk",
  16. ]
  17. def test_entry_gates_require_shadow_failure_recovery_and_capacity():
  18. gates = RolloutEntryGates(
  19. inventory_complete=True,
  20. workflow_spec_valid=True,
  21. policy_complete=True,
  22. rollback_target_present=True,
  23. shadow_runs=2,
  24. required_shadow_runs=3,
  25. failure_recovery_observed=True,
  26. reconciliation_passed=True,
  27. connection_budget_passed=True,
  28. peak_concurrency_passed=True,
  29. sla_passed=True,
  30. )
  31. with pytest.raises(ValueError, match="shadow_runs"):
  32. RolloutPolicy.assert_entry(gates)
  33. def test_observation_window_fails_closed_before_deadline_or_on_incident():
  34. now = datetime(2026, 7, 19, tzinfo=UTC)
  35. window = ObservationWindow(
  36. started_at=now - timedelta(hours=2),
  37. required_duration=timedelta(hours=4),
  38. unexplained_differences=0,
  39. duplicate_writes=0,
  40. critical_sla_breaches=0,
  41. rollback_requests=0,
  42. )
  43. with pytest.raises(ValueError, match="observation_window_complete"):
  44. RolloutPolicy.assert_observation(window, now=now)
  45. incident = ObservationWindow(
  46. started_at=now - timedelta(hours=5),
  47. required_duration=timedelta(hours=4),
  48. unexplained_differences=0,
  49. duplicate_writes=1,
  50. critical_sla_breaches=0,
  51. rollback_requests=0,
  52. )
  53. with pytest.raises(ValueError, match="duplicate_writes"):
  54. RolloutPolicy.assert_observation(incident, now=now)
  55. def test_exit_gates_require_recovery_and_unchanged_n8n_standby():
  56. gates = RolloutExitGates(
  57. observation_window_complete=True,
  58. no_unexplained_differences=True,
  59. no_duplicate_writes=True,
  60. sla_passed=True,
  61. pause_retry_rollback_exercised=False,
  62. n8n_standby=True,
  63. n8n_unauthorized_changes=0,
  64. )
  65. with pytest.raises(ValueError, match="pause_retry_rollback_exercised"):
  66. RolloutPolicy.assert_exit(gates)