@@ -1074,6 +1074,95 @@ def test_get_variation_cmab_experiment_with_whitelisted_variation(self):
10741074 mock_bucket .assert_not_called ()
10751075 mock_cmab_decision .assert_not_called ()
10761076
1077+ def test_get_variation_cmab_experiment_excludes_user_profile_service (self ):
1078+ """Test that CMAB experiments do not use User Profile Service for sticky bucketing.
1079+
1080+ CMAB decisions should be dynamic and not rely on UPS, which maintains decisions
1081+ across the experiment lifetime without considering TTL or user attributes.
1082+ """
1083+
1084+ # Create a user context
1085+ user = optimizely_user_context .OptimizelyUserContext (
1086+ optimizely_client = None ,
1087+ logger = None ,
1088+ user_id = "test_user" ,
1089+ user_attributes = {}
1090+ )
1091+
1092+ # Create a user profile service and tracker
1093+ user_profile_service = user_profile .UserProfileService ()
1094+ user_profile_tracker = user_profile .UserProfileTracker (user .user_id , user_profile_service )
1095+
1096+ # Pre-populate the user profile with a stored decision (simulating previous bucketing)
1097+ user_profile_tracker .update_user_profile (
1098+ entities .Experiment ('111150' , 'cmab_experiment' , 'Running' , '111150' , [], {},
1099+ [entities .Variation ('111151' , 'variation_1' )],
1100+ [{'entityId' : '111151' , 'endOfRange' : 10000 }],
1101+ cmab = {'trafficAllocation' : 5000 }),
1102+ entities .Variation ('111151' , 'variation_1' )
1103+ )
1104+
1105+ # Create a CMAB experiment
1106+ cmab_experiment = entities .Experiment (
1107+ '111150' ,
1108+ 'cmab_experiment' ,
1109+ 'Running' ,
1110+ '111150' ,
1111+ [],
1112+ {},
1113+ [
1114+ entities .Variation ('111151' , 'variation_1' ),
1115+ entities .Variation ('111152' , 'variation_2' )
1116+ ],
1117+ [
1118+ {'entityId' : '111151' , 'endOfRange' : 5000 },
1119+ {'entityId' : '111152' , 'endOfRange' : 10000 }
1120+ ],
1121+ cmab = {'trafficAllocation' : 5000 }
1122+ )
1123+
1124+ with mock .patch ('optimizely.helpers.experiment.is_experiment_running' , return_value = True ), \
1125+ mock .patch ('optimizely.helpers.audience.does_user_meet_audience_conditions' , return_value = [True , []]), \
1126+ mock .patch .object (self .decision_service .bucketer , 'bucket_to_entity_id' ,
1127+ return_value = ['$' , []]), \
1128+ mock .patch .object (self .decision_service , 'cmab_service' ) as mock_cmab_service , \
1129+ mock .patch .object (self .project_config , 'get_variation_from_id' ,
1130+ return_value = entities .Variation ('111152' , 'variation_2' )), \
1131+ mock .patch .object (self .decision_service , 'get_stored_variation' ) as mock_get_stored_variation , \
1132+ mock .patch .object (user_profile_tracker , 'update_user_profile' ) as mock_update_profile :
1133+
1134+ # Configure CMAB service to return a different variation than what's stored
1135+ mock_cmab_service .get_decision .return_value = (
1136+ {
1137+ 'variation_id' : '111152' ,
1138+ 'cmab_uuid' : 'test-cmab-uuid-456'
1139+ },
1140+ []
1141+ )
1142+
1143+ # Call get_variation with user profile tracker
1144+ variation_result = self .decision_service .get_variation (
1145+ self .project_config ,
1146+ cmab_experiment ,
1147+ user ,
1148+ user_profile_tracker
1149+ )
1150+ variation = variation_result ['variation' ]
1151+ cmab_uuid = variation_result ['cmab_uuid' ]
1152+
1153+ # Verify that get_stored_variation was NOT called (UPS retrieval skipped for CMAB)
1154+ mock_get_stored_variation .assert_not_called ()
1155+
1156+ # Verify that update_user_profile was NOT called (UPS storage skipped for CMAB)
1157+ mock_update_profile .assert_not_called ()
1158+
1159+ # Verify that CMAB decision was used (variation_2, not the stored variation_1)
1160+ self .assertEqual (entities .Variation ('111152' , 'variation_2' ), variation )
1161+ self .assertEqual ('test-cmab-uuid-456' , cmab_uuid )
1162+
1163+ # Verify CMAB service was called
1164+ mock_cmab_service .get_decision .assert_called_once ()
1165+
10771166
10781167class FeatureFlagDecisionTests (base .BaseTest ):
10791168 def setUp (self ):
0 commit comments