@@ -1074,6 +1074,180 @@ 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_does_not_save_user_profile (self ):
1078+ """Test that CMAB experiments do not save bucketing decisions to user profile."""
1079+
1080+ # Create a user context
1081+ user = optimizely_user_context .OptimizelyUserContext (
1082+ optimizely_client = None ,
1083+ logger = None ,
1084+ user_id = "test_user" ,
1085+ user_attributes = {}
1086+ )
1087+
1088+ # Create a user profile service and tracker
1089+ user_profile_service = user_profile .UserProfileService ()
1090+ user_profile_tracker = user_profile .UserProfileTracker (user .user_id , user_profile_service )
1091+
1092+ # Create a CMAB experiment
1093+ cmab_experiment = entities .Experiment (
1094+ '111150' ,
1095+ 'cmab_experiment' ,
1096+ 'Running' ,
1097+ '111150' ,
1098+ [], # No audience IDs
1099+ {},
1100+ [
1101+ entities .Variation ('111151' , 'variation_1' ),
1102+ entities .Variation ('111152' , 'variation_2' )
1103+ ],
1104+ [
1105+ {'entityId' : '111151' , 'endOfRange' : 5000 },
1106+ {'entityId' : '111152' , 'endOfRange' : 10000 }
1107+ ],
1108+ cmab = {'trafficAllocation' : 5000 }
1109+ )
1110+
1111+ with mock .patch ('optimizely.helpers.experiment.is_experiment_running' , return_value = True ), \
1112+ mock .patch ('optimizely.helpers.audience.does_user_meet_audience_conditions' , return_value = [True , []]), \
1113+ mock .patch .object (self .decision_service .bucketer , 'bucket_to_entity_id' ,
1114+ return_value = ['$' , []]), \
1115+ mock .patch .object (self .decision_service , 'cmab_service' ) as mock_cmab_service , \
1116+ mock .patch .object (self .project_config , 'get_variation_from_id' ,
1117+ return_value = entities .Variation ('111151' , 'variation_1' )), \
1118+ mock .patch .object (user_profile_tracker , 'update_user_profile' ) as mock_update_profile , \
1119+ mock .patch .object (self .decision_service , 'logger' ) as mock_logger :
1120+
1121+ # Configure CMAB service to return a decision
1122+ mock_cmab_service .get_decision .return_value = (
1123+ {
1124+ 'variation_id' : '111151' ,
1125+ 'cmab_uuid' : 'test-cmab-uuid-123'
1126+ },
1127+ [] # reasons list
1128+ )
1129+
1130+ # Call get_variation with the CMAB experiment and user profile tracker
1131+ variation_result = self .decision_service .get_variation (
1132+ self .project_config ,
1133+ cmab_experiment ,
1134+ user ,
1135+ user_profile_tracker
1136+ )
1137+ variation = variation_result ['variation' ]
1138+ cmab_uuid = variation_result ['cmab_uuid' ]
1139+
1140+ # Verify the variation and cmab_uuid are returned
1141+ self .assertEqual (entities .Variation ('111151' , 'variation_1' ), variation )
1142+ self .assertEqual ('test-cmab-uuid-123' , cmab_uuid )
1143+
1144+ # Verify user profile was NOT updated for CMAB experiment
1145+ mock_update_profile .assert_not_called ()
1146+
1147+ # Verify debug log was called to explain CMAB exclusion
1148+ mock_logger .debug .assert_called_with (
1149+ 'Skipping user profile update for CMAB experiment "cmab_experiment". '
1150+ 'CMAB decisions are dynamic and not stored for sticky bucketing.'
1151+ )
1152+
1153+ def test_get_variation_standard_experiment_saves_user_profile (self ):
1154+ """Test that standard (non-CMAB) experiments DO save bucketing decisions to user profile."""
1155+
1156+ user = optimizely_user_context .OptimizelyUserContext (
1157+ optimizely_client = None ,
1158+ logger = None ,
1159+ user_id = "test_user" ,
1160+ user_attributes = {}
1161+ )
1162+
1163+ # Create a user profile service and tracker
1164+ user_profile_service = user_profile .UserProfileService ()
1165+ user_profile_tracker = user_profile .UserProfileTracker (user .user_id , user_profile_service )
1166+
1167+ # Get a standard (non-CMAB) experiment
1168+ experiment = self .project_config .get_experiment_from_key ("test_experiment" )
1169+
1170+ with mock .patch ('optimizely.decision_service.DecisionService.get_whitelisted_variation' ,
1171+ return_value = [None , []]), \
1172+ mock .patch ('optimizely.decision_service.DecisionService.get_stored_variation' ,
1173+ return_value = None ), \
1174+ mock .patch ('optimizely.helpers.audience.does_user_meet_audience_conditions' ,
1175+ return_value = [True , []]), \
1176+ mock .patch ('optimizely.bucketer.Bucketer.bucket' ,
1177+ return_value = [entities .Variation ("111129" , "variation" ), []]), \
1178+ mock .patch .object (user_profile_tracker , 'update_user_profile' ) as mock_update_profile :
1179+
1180+ # Call get_variation with standard experiment and user profile tracker
1181+ variation_result = self .decision_service .get_variation (
1182+ self .project_config ,
1183+ experiment ,
1184+ user ,
1185+ user_profile_tracker
1186+ )
1187+ variation = variation_result ['variation' ]
1188+
1189+ # Verify variation was returned
1190+ self .assertEqual (entities .Variation ("111129" , "variation" ), variation )
1191+
1192+ # Verify user profile WAS updated for standard experiment
1193+ mock_update_profile .assert_called_once_with (experiment , variation )
1194+
1195+ def test_get_variation_cmab_experiment_with_ignore_ups_option (self ):
1196+ """Test that CMAB experiments with IGNORE_USER_PROFILE_SERVICE option don't attempt profile update."""
1197+
1198+ user = optimizely_user_context .OptimizelyUserContext (
1199+ optimizely_client = None ,
1200+ logger = None ,
1201+ user_id = "test_user" ,
1202+ user_attributes = {}
1203+ )
1204+
1205+ # Create a user profile tracker
1206+ user_profile_service = user_profile .UserProfileService ()
1207+ user_profile_tracker = user_profile .UserProfileTracker (user .user_id , user_profile_service )
1208+
1209+ # Create a CMAB experiment
1210+ cmab_experiment = entities .Experiment (
1211+ '111150' ,
1212+ 'cmab_experiment' ,
1213+ 'Running' ,
1214+ '111150' ,
1215+ [],
1216+ {},
1217+ [entities .Variation ('111151' , 'variation_1' )],
1218+ [{'entityId' : '111151' , 'endOfRange' : 10000 }],
1219+ cmab = {'trafficAllocation' : 5000 }
1220+ )
1221+
1222+ with mock .patch ('optimizely.helpers.experiment.is_experiment_running' , return_value = True ), \
1223+ mock .patch ('optimizely.helpers.audience.does_user_meet_audience_conditions' , return_value = [True , []]), \
1224+ mock .patch .object (self .decision_service .bucketer , 'bucket_to_entity_id' ,
1225+ return_value = ['$' , []]), \
1226+ mock .patch .object (self .decision_service , 'cmab_service' ) as mock_cmab_service , \
1227+ mock .patch .object (self .project_config , 'get_variation_from_id' ,
1228+ return_value = entities .Variation ('111151' , 'variation_1' )), \
1229+ mock .patch .object (user_profile_tracker , 'update_user_profile' ) as mock_update_profile , \
1230+ mock .patch .object (self .decision_service , 'logger' ):
1231+
1232+ mock_cmab_service .get_decision .return_value = (
1233+ {'variation_id' : '111151' , 'cmab_uuid' : 'test-uuid' },
1234+ []
1235+ )
1236+
1237+ # Call with IGNORE_USER_PROFILE_SERVICE option
1238+ variation_result = self .decision_service .get_variation (
1239+ self .project_config ,
1240+ cmab_experiment ,
1241+ user ,
1242+ user_profile_tracker ,
1243+ [],
1244+ options = ['IGNORE_USER_PROFILE_SERVICE' ]
1245+ )
1246+
1247+ # Verify variation returned but profile not updated
1248+ self .assertIsNotNone (variation_result ['variation' ])
1249+ mock_update_profile .assert_not_called ()
1250+
10771251
10781252class FeatureFlagDecisionTests (base .BaseTest ):
10791253 def setUp (self ):
0 commit comments