@@ -1074,6 +1074,271 @@ 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_skips_ups_lookup (self ):
1078+ """Test that get_variation skips UPS lookup for CMAB experiments even when UPS has a stored decision."""
1079+
1080+ user = optimizely_user_context .OptimizelyUserContext (
1081+ optimizely_client = None ,
1082+ logger = None ,
1083+ user_id = "test_user" ,
1084+ user_attributes = {}
1085+ )
1086+
1087+ # Create a CMAB experiment
1088+ cmab_experiment = entities .Experiment (
1089+ '111150' ,
1090+ 'cmab_experiment' ,
1091+ 'Running' ,
1092+ '111150' ,
1093+ [],
1094+ {},
1095+ [
1096+ entities .Variation ('111151' , 'variation_1' ),
1097+ entities .Variation ('111152' , 'variation_2' )
1098+ ],
1099+ [
1100+ {'entityId' : '111151' , 'endOfRange' : 5000 },
1101+ {'entityId' : '111152' , 'endOfRange' : 10000 }
1102+ ],
1103+ cmab = {'trafficAllocation' : 5000 }
1104+ )
1105+
1106+ # Set up a user profile tracker WITH a stored decision
1107+ user_profile_service = user_profile .UserProfileService ()
1108+ user_profile_tracker = user_profile .UserProfileTracker (user .user_id , user_profile_service )
1109+ # Manually store a variation for this experiment in the user profile
1110+ user_profile_tracker .user_profile .save_variation_for_experiment ('111150' , '111152' )
1111+
1112+ with mock .patch ('optimizely.helpers.experiment.is_experiment_running' , return_value = True ), \
1113+ mock .patch ('optimizely.helpers.audience.does_user_meet_audience_conditions' , return_value = [True , []]), \
1114+ mock .patch .object (self .decision_service .bucketer , 'bucket_to_entity_id' ,
1115+ return_value = ['$' , []]), \
1116+ mock .patch .object (self .decision_service , 'cmab_service' ) as mock_cmab_service , \
1117+ mock .patch .object (self .project_config , 'get_variation_from_id' ,
1118+ return_value = entities .Variation ('111151' , 'variation_1' )), \
1119+ mock .patch (
1120+ "optimizely.decision_service.DecisionService.get_stored_variation"
1121+ ) as mock_get_stored_variation , \
1122+ mock .patch .object (self .decision_service , 'logger' ) as mock_logger :
1123+
1124+ # Configure CMAB service to return a decision
1125+ mock_cmab_service .get_decision .return_value = (
1126+ {
1127+ 'variation_id' : '111151' ,
1128+ 'cmab_uuid' : 'test-cmab-uuid-456'
1129+ },
1130+ []
1131+ )
1132+
1133+ variation_result = self .decision_service .get_variation (
1134+ self .project_config ,
1135+ cmab_experiment ,
1136+ user ,
1137+ user_profile_tracker
1138+ )
1139+ variation = variation_result ['variation' ]
1140+ cmab_uuid = variation_result ['cmab_uuid' ]
1141+ reasons = variation_result ['reasons' ]
1142+
1143+ # UPS lookup should NOT have been called for CMAB experiment
1144+ mock_get_stored_variation .assert_not_called ()
1145+
1146+ # CMAB service should have been called instead
1147+ mock_cmab_service .get_decision .assert_called_once ()
1148+
1149+ # Should return the CMAB decision, not the stored UPS decision
1150+ self .assertEqual (entities .Variation ('111151' , 'variation_1' ), variation )
1151+ self .assertEqual ('test-cmab-uuid-456' , cmab_uuid )
1152+
1153+ # Should include decision reason about skipping UPS
1154+ self .assertIn (
1155+ 'Skipping user profile service for CMAB experiment "cmab_experiment".' ,
1156+ reasons
1157+ )
1158+
1159+ # Should log the UPS skip
1160+ mock_logger .debug .assert_any_call (
1161+ 'Skipping user profile service for CMAB experiment "cmab_experiment".'
1162+ )
1163+
1164+ def test_get_variation_cmab_experiment_skips_ups_save (self ):
1165+ """Test that get_variation does not save to UPS for CMAB experiments."""
1166+
1167+ user = optimizely_user_context .OptimizelyUserContext (
1168+ optimizely_client = None ,
1169+ logger = None ,
1170+ user_id = "test_user" ,
1171+ user_attributes = {}
1172+ )
1173+
1174+ # Create a CMAB experiment
1175+ cmab_experiment = entities .Experiment (
1176+ '111150' ,
1177+ 'cmab_experiment' ,
1178+ 'Running' ,
1179+ '111150' ,
1180+ [],
1181+ {},
1182+ [
1183+ entities .Variation ('111151' , 'variation_1' ),
1184+ entities .Variation ('111152' , 'variation_2' )
1185+ ],
1186+ [
1187+ {'entityId' : '111151' , 'endOfRange' : 5000 },
1188+ {'entityId' : '111152' , 'endOfRange' : 10000 }
1189+ ],
1190+ cmab = {'trafficAllocation' : 5000 }
1191+ )
1192+
1193+ user_profile_service = user_profile .UserProfileService ()
1194+ user_profile_tracker = user_profile .UserProfileTracker (user .user_id , user_profile_service )
1195+
1196+ with mock .patch ('optimizely.helpers.experiment.is_experiment_running' , return_value = True ), \
1197+ mock .patch ('optimizely.helpers.audience.does_user_meet_audience_conditions' , return_value = [True , []]), \
1198+ mock .patch .object (self .decision_service .bucketer , 'bucket_to_entity_id' ,
1199+ return_value = ['$' , []]), \
1200+ mock .patch .object (self .decision_service , 'cmab_service' ) as mock_cmab_service , \
1201+ mock .patch .object (self .project_config , 'get_variation_from_id' ,
1202+ return_value = entities .Variation ('111151' , 'variation_1' )), \
1203+ mock .patch .object (user_profile_tracker , 'update_user_profile' ) as mock_update_profile , \
1204+ mock .patch .object (self .decision_service , 'logger' ):
1205+
1206+ mock_cmab_service .get_decision .return_value = (
1207+ {
1208+ 'variation_id' : '111151' ,
1209+ 'cmab_uuid' : 'test-cmab-uuid-789'
1210+ },
1211+ []
1212+ )
1213+
1214+ variation_result = self .decision_service .get_variation (
1215+ self .project_config ,
1216+ cmab_experiment ,
1217+ user ,
1218+ user_profile_tracker
1219+ )
1220+ variation = variation_result ['variation' ]
1221+
1222+ # Variation should be returned from CMAB
1223+ self .assertEqual (entities .Variation ('111151' , 'variation_1' ), variation )
1224+
1225+ # UPS should NOT have been updated for CMAB experiment
1226+ mock_update_profile .assert_not_called ()
1227+
1228+ def test_get_variation_non_cmab_experiment_still_uses_ups (self ):
1229+ """Test that non-CMAB experiments still use UPS for lookup and save."""
1230+
1231+ user = optimizely_user_context .OptimizelyUserContext (
1232+ optimizely_client = None ,
1233+ logger = None ,
1234+ user_id = "test_user" ,
1235+ user_attributes = {}
1236+ )
1237+
1238+ experiment = self .project_config .get_experiment_from_key ("test_experiment" )
1239+
1240+ user_profile_service = user_profile .UserProfileService ()
1241+ user_profile_tracker = user_profile .UserProfileTracker (user .user_id , user_profile_service )
1242+
1243+ stored_variation = entities .Variation ("111128" , "control" )
1244+
1245+ with mock .patch (
1246+ "optimizely.decision_service.DecisionService.get_whitelisted_variation" ,
1247+ return_value = [None , []],
1248+ ), mock .patch (
1249+ "optimizely.decision_service.DecisionService.get_stored_variation" ,
1250+ return_value = stored_variation ,
1251+ ) as mock_get_stored_variation :
1252+ variation_result = self .decision_service .get_variation (
1253+ self .project_config , experiment , user , user_profile_tracker
1254+ )
1255+ variation = variation_result ['variation' ]
1256+ reasons = variation_result ['reasons' ]
1257+
1258+ # UPS lookup should have been called for non-CMAB experiment
1259+ mock_get_stored_variation .assert_called_once ()
1260+
1261+ # Should return the stored variation
1262+ self .assertEqual (stored_variation , variation )
1263+
1264+ # Should NOT include the CMAB UPS skip reason
1265+ for reason in reasons :
1266+ self .assertNotIn ('Skipping user profile service for CMAB experiment' , reason )
1267+
1268+ def test_get_variation_cmab_experiment_ups_exclusion_with_ignore_ups_option (self ):
1269+ """Test that when IGNORE_USER_PROFILE_SERVICE option is set with a CMAB experiment,
1270+ UPS is skipped and no CMAB-specific skip reason is added (since UPS was already ignored)."""
1271+
1272+ from optimizely .decision .optimizely_decide_option import OptimizelyDecideOption
1273+
1274+ user = optimizely_user_context .OptimizelyUserContext (
1275+ optimizely_client = None ,
1276+ logger = None ,
1277+ user_id = "test_user" ,
1278+ user_attributes = {}
1279+ )
1280+
1281+ cmab_experiment = entities .Experiment (
1282+ '111150' ,
1283+ 'cmab_experiment' ,
1284+ 'Running' ,
1285+ '111150' ,
1286+ [],
1287+ {},
1288+ [
1289+ entities .Variation ('111151' , 'variation_1' ),
1290+ entities .Variation ('111152' , 'variation_2' )
1291+ ],
1292+ [
1293+ {'entityId' : '111151' , 'endOfRange' : 5000 },
1294+ {'entityId' : '111152' , 'endOfRange' : 10000 }
1295+ ],
1296+ cmab = {'trafficAllocation' : 5000 }
1297+ )
1298+
1299+ user_profile_service = user_profile .UserProfileService ()
1300+ user_profile_tracker = user_profile .UserProfileTracker (user .user_id , user_profile_service )
1301+
1302+ with mock .patch ('optimizely.helpers.experiment.is_experiment_running' , return_value = True ), \
1303+ mock .patch ('optimizely.helpers.audience.does_user_meet_audience_conditions' , return_value = [True , []]), \
1304+ mock .patch .object (self .decision_service .bucketer , 'bucket_to_entity_id' ,
1305+ return_value = ['$' , []]), \
1306+ mock .patch .object (self .decision_service , 'cmab_service' ) as mock_cmab_service , \
1307+ mock .patch .object (self .project_config , 'get_variation_from_id' ,
1308+ return_value = entities .Variation ('111151' , 'variation_1' )), \
1309+ mock .patch (
1310+ "optimizely.decision_service.DecisionService.get_stored_variation"
1311+ ) as mock_get_stored_variation , \
1312+ mock .patch .object (user_profile_tracker , 'update_user_profile' ) as mock_update_profile , \
1313+ mock .patch .object (self .decision_service , 'logger' ):
1314+
1315+ mock_cmab_service .get_decision .return_value = (
1316+ {
1317+ 'variation_id' : '111151' ,
1318+ 'cmab_uuid' : 'test-cmab-uuid'
1319+ },
1320+ []
1321+ )
1322+
1323+ variation_result = self .decision_service .get_variation (
1324+ self .project_config ,
1325+ cmab_experiment ,
1326+ user ,
1327+ user_profile_tracker ,
1328+ options = [OptimizelyDecideOption .IGNORE_USER_PROFILE_SERVICE ]
1329+ )
1330+ reasons = variation_result ['reasons' ]
1331+
1332+ # UPS lookup should NOT have been called
1333+ mock_get_stored_variation .assert_not_called ()
1334+
1335+ # UPS save should NOT have been called
1336+ mock_update_profile .assert_not_called ()
1337+
1338+ # CMAB-specific skip message should NOT appear since UPS was already ignored via option
1339+ for reason in reasons :
1340+ self .assertNotIn ('Skipping user profile service for CMAB experiment' , reason )
1341+
10771342
10781343class FeatureFlagDecisionTests (base .BaseTest ):
10791344 def setUp (self ):
0 commit comments