From b05bd8e14677079d0bb9ca6f52e875a998a16a31 Mon Sep 17 00:00:00 2001 From: Chris Busillo Date: Mon, 25 May 2026 18:30:47 -0400 Subject: [PATCH] Fix inherited rollback plan availability --- .../product_environment_read_model.py | 3 ++ tests/test_product_environment_read_model.py | 45 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/control_plane/contracts/product_environment_read_model.py b/control_plane/contracts/product_environment_read_model.py index 6dc4337..21020b4 100644 --- a/control_plane/contracts/product_environment_read_model.py +++ b/control_plane/contracts/product_environment_read_model.py @@ -1037,6 +1037,8 @@ def _product_action_authorization_context( return "" if action.route_path == "/v1/drivers/generic-web/prod-promotion-workflow": return _lane_context_for_instance(profile=profile, preferred_instances=("testing", "prod")) + if action.route_path == "/v1/drivers/generic-web/prod-rollback-plan": + return _lane_context_if_present(profile=profile, instance="prod") if action.route_path in { "/v1/drivers/odoo/prod-backup-gate", "/v1/drivers/odoo/prod-promotion", @@ -1331,6 +1333,7 @@ def _action_support_reason( return "" if action.route_path in { "/v1/drivers/generic-web/prod-promotion-workflow", + "/v1/drivers/generic-web/prod-rollback-plan", "/v1/drivers/odoo/prod-backup-gate", "/v1/drivers/odoo/prod-promotion", "/v1/drivers/odoo/prod-rollback", diff --git a/tests/test_product_environment_read_model.py b/tests/test_product_environment_read_model.py index a1dba96..2bdd8ea 100644 --- a/tests/test_product_environment_read_model.py +++ b/tests/test_product_environment_read_model.py @@ -552,6 +552,7 @@ def action_allowed(action: str, product: str, context: str) -> bool: in { "generic_web_prod_promotion.dispatch", "generic_web_prod_promotion.execute", + "generic_web_prod_rollback.plan", } and context == "example-site-prod" ) @@ -565,8 +566,52 @@ def action_allowed(action: str, product: str, context: str) -> bool: actions = {action.action_id: action for action in overview.available_actions} self.assertTrue(actions["prod_promotion_workflow"].enabled) self.assertTrue(actions["prod_promotion"].enabled) + self.assertTrue(actions["prod_rollback_plan"].enabled) self.assertFalse(actions["preview_refresh"].enabled) + def test_odoo_product_site_overview_uses_prod_context_for_inherited_rollback_plan( + self, + ) -> None: + profile = LaunchplaneProductProfileRecord.model_validate(_odoo_profile_payload()) + seen_contexts: list[tuple[str, str]] = [] + + def action_allowed(action: str, _product: str, context: str) -> bool: + if action == "generic_web_prod_rollback.plan": + seen_contexts.append((action, context)) + return context == "cm" + return True + + overview = build_product_site_overview( + record_store=_PreviewRecordStore(profile, ()), + product=profile.product, + action_allowed=action_allowed, + ) + + actions = {action.action_id: action for action in overview.available_actions} + self.assertEqual( + actions["prod_rollback_plan"].route_path, + "/v1/drivers/generic-web/prod-rollback-plan", + ) + self.assertTrue(actions["prod_rollback_plan"].enabled) + self.assertTrue(seen_contexts) + self.assertEqual({context for _action, context in seen_contexts}, {"cm"}) + + def test_inherited_rollback_plan_is_disabled_without_prod_lane(self) -> None: + payload = _odoo_profile_payload() + lanes = cast(tuple[dict[str, object], ...], payload["lanes"]) + payload["lanes"] = tuple(lane for lane in lanes if lane["instance"] != "prod") + profile = LaunchplaneProductProfileRecord.model_validate(payload) + + overview = build_product_site_overview( + record_store=_PreviewRecordStore(profile, ()), + product=profile.product, + action_allowed=lambda *_: True, + ) + + actions = {action.action_id: action for action in overview.available_actions} + self.assertFalse(actions["prod_rollback_plan"].enabled) + self.assertIn("prod lane", actions["prod_rollback_plan"].disabled_reasons[0]) + def test_product_site_overview_uses_testing_context_for_deploy_actions(self) -> None: profile = LaunchplaneProductProfileRecord.model_validate( _site_profile_payload(preview_enabled=False)