11"""Tests for the rolling update FSM evaluation (BEP-1049).
22
33Tests cover:
4- - FSM state transitions: PROVISIONING, PROGRESSING, ROLLED_BACK, COMPLETED
4+ - FSM state transitions: PROVISIONING and COMPLETED
55- max_surge / max_unavailable budget calculations
66- Multi-cycle progression and termination priority
77- Edge cases and boundary conditions
8+
9+ Note: Rollback is not decided by the FSM — the coordinator's timeout
10+ sweep handles it. The FSM only returns PROVISIONING (with or without
11+ route mutations) or COMPLETED.
812"""
913
1014from __future__ import annotations
@@ -136,13 +140,13 @@ class TestBasicFSMStates:
136140 """Test fundamental FSM transitions."""
137141
138142 def test_no_routes_creates_new (self ) -> None :
139- """First cycle with 0 routes → PROGRESSING, creates desired count ."""
143+ """First cycle with 0 routes → PROVISIONING with route creation ."""
140144 deployment = make_deployment (desired = 1 )
141145 spec = RollingUpdateSpec (max_surge = 1 , max_unavailable = 0 )
142146
143147 result = RollingUpdateStrategy (spec ).evaluate_cycle (deployment , [])
144148
145- assert result .sub_step == DeploymentSubStep .PROGRESSING
149+ assert result .sub_step == DeploymentSubStep .PROVISIONING
146150 assert len (result .route_changes .rollout_specs ) == 1
147151 assert len (result .route_changes .drain_route_ids ) == 0
148152
@@ -181,8 +185,11 @@ def test_completed_when_all_new_healthy_and_no_old(self) -> None:
181185 pytest .param (RouteStatus .TERMINATED , id = "terminated" ),
182186 ],
183187 )
184- def test_rollback_when_all_new_in_terminal_state (self , failed_status : RouteStatus ) -> None :
185- """All new routes in terminal state (with old still present) → ROLLED_BACK."""
188+ def test_all_new_failed_retries_creation (self , failed_status : RouteStatus ) -> None :
189+ """All new routes failed → FSM retries by creating new routes.
190+
191+ Rollback is handled by the coordinator's timeout sweep, not the FSM.
192+ """
186193 deployment = make_deployment (desired = 1 )
187194 spec = RollingUpdateSpec (max_surge = 1 , max_unavailable = 0 )
188195 routes = [
@@ -192,7 +199,7 @@ def test_rollback_when_all_new_in_terminal_state(self, failed_status: RouteStatu
192199
193200 result = RollingUpdateStrategy (spec ).evaluate_cycle (deployment , routes )
194201
195- assert result .sub_step == DeploymentSubStep .ROLLED_BACK
202+ assert result .sub_step == DeploymentSubStep .PROVISIONING
196203
197204
198205# ===========================================================================
@@ -388,7 +395,7 @@ def test_not_completed_when_old_still_exists(self) -> None:
388395
389396 result = RollingUpdateStrategy (spec ).evaluate_cycle (deployment , routes )
390397
391- assert result .sub_step == DeploymentSubStep .PROGRESSING
398+ assert result .sub_step == DeploymentSubStep .PROVISIONING
392399 assert len (result .route_changes .drain_route_ids ) == 1
393400
394401
@@ -400,11 +407,8 @@ def test_not_completed_when_old_still_exists(self) -> None:
400407class TestRouteStatusClassification :
401408 """Test how different route statuses affect classification."""
402409
403- # FIXME: Code classifies DEGRADED as new_provisioning (same as PROVISIONING),
404- # so this test should expect DeploymentSubStep.PROVISIONING, not ROLLED_BACK.
405- # Verify the intended behavior and update accordingly.
406- def test_degraded_new_triggers_rollback (self ) -> None :
407- """DEGRADED new routes — see FIXME above for classification concern."""
410+ def test_degraded_new_waits_provisioning (self ) -> None :
411+ """DEGRADED new routes are treated as PROVISIONING (still warming up)."""
408412 deployment = make_deployment (desired = 1 )
409413 spec = RollingUpdateSpec (max_surge = 1 , max_unavailable = 0 )
410414 routes = [
@@ -413,10 +417,10 @@ def test_degraded_new_triggers_rollback(self) -> None:
413417
414418 result = RollingUpdateStrategy (spec ).evaluate_cycle (deployment , routes )
415419
416- assert result .sub_step == DeploymentSubStep .ROLLED_BACK
420+ assert result .sub_step == DeploymentSubStep .PROVISIONING
417421
418- def test_unhealthy_new_triggers_rollback (self ) -> None :
419- """All new UNHEALTHY (none healthy, none provisioning) → ROLLED_BACK ."""
422+ def test_unhealthy_new_retries (self ) -> None :
423+ """All new UNHEALTHY → PROVISIONING (retries, timeout handles rollback) ."""
420424 deployment = make_deployment (desired = 1 )
421425 spec = RollingUpdateSpec (max_surge = 1 , max_unavailable = 0 )
422426 routes = [
@@ -425,7 +429,7 @@ def test_unhealthy_new_triggers_rollback(self) -> None:
425429
426430 result = RollingUpdateStrategy (spec ).evaluate_cycle (deployment , routes )
427431
428- assert result .sub_step == DeploymentSubStep .ROLLED_BACK
432+ assert result .sub_step == DeploymentSubStep .PROVISIONING
429433
430434 @pytest .mark .parametrize (
431435 "inactive_status" ,
@@ -459,7 +463,7 @@ def test_partial_new_failure_continues_progress(self) -> None:
459463
460464 result = RollingUpdateStrategy (spec ).evaluate_cycle (deployment , routes )
461465
462- assert result .sub_step == DeploymentSubStep .PROGRESSING
466+ assert result .sub_step == DeploymentSubStep .PROVISIONING
463467
464468 def test_old_provisioning_counted_as_active (self ) -> None :
465469 """Old routes in PROVISIONING are counted as old_active."""
@@ -472,7 +476,7 @@ def test_old_provisioning_counted_as_active(self) -> None:
472476
473477 result = RollingUpdateStrategy (spec ).evaluate_cycle (deployment , routes )
474478
475- assert result .sub_step == DeploymentSubStep .PROGRESSING
479+ assert result .sub_step == DeploymentSubStep .PROVISIONING
476480
477481
478482# ===========================================================================
@@ -548,7 +552,7 @@ def test_more_new_healthy_than_desired_still_completes(self) -> None:
548552 assert result .sub_step == DeploymentSubStep .COMPLETED
549553
550554 def test_only_failed_new_no_old_rolls_back (self ) -> None :
551- """Only failed new routes, no old → ROLLED_BACK ."""
555+ """Only failed new routes, no old → PROVISIONING (retries creation) ."""
552556 deployment = make_deployment (desired = 2 )
553557 spec = RollingUpdateSpec (max_surge = 1 , max_unavailable = 0 )
554558 routes = [
@@ -558,7 +562,7 @@ def test_only_failed_new_no_old_rolls_back(self) -> None:
558562
559563 result = RollingUpdateStrategy (spec ).evaluate_cycle (deployment , routes )
560564
561- assert result .sub_step == DeploymentSubStep .ROLLED_BACK
565+ assert result .sub_step == DeploymentSubStep .PROVISIONING
562566
563567 def test_all_old_inactive_no_new_creates_desired (self ) -> None :
564568 """All old routes are terminated, no new → create desired."""
@@ -579,7 +583,7 @@ def test_deploying_rev_none_rejected(self) -> None:
579583 spec = RollingUpdateSpec (max_surge = 1 , max_unavailable = 0 )
580584 routes = [make_route (revision_id = OLD_REV , status = RouteStatus .HEALTHY )]
581585
582- with pytest .raises (ValueError , match = "deploying_revision_id must not be None" ):
586+ with pytest .raises (Exception ): # InvalidEndpointState
583587 RollingUpdateStrategy (spec ).evaluate_cycle (deployment , routes )
584588
585589 def test_route_without_revision_classified_as_old (self ) -> None :
@@ -676,7 +680,7 @@ def test_step_by_step_rolling_update(self) -> None:
676680 r4 = RollingUpdateStrategy (spec ).evaluate_cycle (deployment , routes_c4 )
677681 assert len (r4 .route_changes .rollout_specs ) == 0
678682 assert len (r4 .route_changes .drain_route_ids ) == 1
679- assert r4 .sub_step == DeploymentSubStep .PROGRESSING
683+ assert r4 .sub_step == DeploymentSubStep .PROVISIONING
680684
681685 # Cycle 5: 0 old, 5 new healthy → completed
682686 routes_c5 = [make_route (revision_id = NEW_REV , status = RouteStatus .HEALTHY ) for _ in range (5 )]
0 commit comments