@@ -1704,6 +1704,103 @@ public void getVariationCmabExperimentUserNotInTrafficAllocation() {
17041704 verify (mockBucketer , times (1 )).bucket (any (Experiment .class ), anyString (), any (ProjectConfig .class ), any (DecisionPath .class ));
17051705 }
17061706
1707+ /**
1708+ * Verify that CMAB experiments do NOT save bucketing decisions to user profile.
1709+ * CMAB decisions are dynamic and should not be stored for sticky bucketing.
1710+ */
1711+ @ Test
1712+ public void getVariationCmabExperimentDoesNotSaveUserProfile () throws Exception {
1713+ // Create a CMAB experiment
1714+ Experiment cmabExperiment = createMockCmabExperiment ();
1715+ Variation variation1 = cmabExperiment .getVariations ().get (0 );
1716+
1717+ // Setup user profile service and tracker
1718+ UserProfileService mockUserProfileService = mock (UserProfileService .class );
1719+ when (mockUserProfileService .lookup (genericUserId )).thenReturn (null );
1720+
1721+ // Setup bucketer to return a variation (pass traffic allocation)
1722+ Bucketer mockBucketer = mock (Bucketer .class );
1723+ when (mockBucketer .bucket (eq (cmabExperiment ), anyString (), eq (v4ProjectConfig ), any (DecisionPath .class )))
1724+ .thenReturn (DecisionResponse .responseNoReasons (variation1 ));
1725+
1726+ // Setup CMAB service to return a decision
1727+ CmabDecision mockCmabDecision = mock (CmabDecision .class );
1728+ when (mockCmabDecision .getVariationId ()).thenReturn (variation1 .getId ());
1729+ when (mockCmabDecision .getCmabUuid ()).thenReturn ("test-cmab-uuid-123" );
1730+ when (mockCmabService .getDecision (any (), any (), any (), any ()))
1731+ .thenReturn (mockCmabDecision );
1732+
1733+ DecisionService decisionServiceWithUPS = new DecisionService (
1734+ mockBucketer ,
1735+ mockErrorHandler ,
1736+ mockUserProfileService ,
1737+ mockCmabService
1738+ );
1739+
1740+ // Call getVariation with CMAB experiment
1741+ DecisionResponse <Variation > result = decisionServiceWithUPS .getVariation (
1742+ cmabExperiment ,
1743+ optimizely .createUserContext (genericUserId , Collections .emptyMap ()),
1744+ v4ProjectConfig
1745+ );
1746+
1747+ // Verify variation and cmab_uuid are returned
1748+ assertEquals (variation1 , result .getResult ());
1749+ assertEquals ("test-cmab-uuid-123" , result .getCmabUuid ());
1750+
1751+ // Verify user profile service was NEVER called to save
1752+ verify (mockUserProfileService , never ()).save (anyMapOf (String .class , Object .class ));
1753+
1754+ // Verify debug log was called to explain CMAB exclusion
1755+ logbackVerifier .expectMessage (Level .DEBUG ,
1756+ "Skipping user profile service for CMAB experiment \" cmab_experiment\" . " +
1757+ "CMAB decisions are dynamic and not stored for sticky bucketing." );
1758+ }
1759+
1760+ /**
1761+ * Verify that standard (non-CMAB) experiments DO save bucketing decisions to user profile.
1762+ * Standard experiments should use sticky bucketing.
1763+ */
1764+ @ Test
1765+ public void getVariationStandardExperimentSavesUserProfile () throws Exception {
1766+ final Experiment experiment = noAudienceProjectConfig .getExperiments ().get (0 );
1767+ final Variation variation = experiment .getVariations ().get (0 );
1768+ final Decision decision = new Decision (variation .getId ());
1769+
1770+ UserProfileService mockUserProfileService = mock (UserProfileService .class );
1771+ when (mockUserProfileService .lookup (genericUserId )).thenReturn (null );
1772+
1773+ Bucketer mockBucketer = mock (Bucketer .class );
1774+ when (mockBucketer .bucket (eq (experiment ), eq (genericUserId ), eq (noAudienceProjectConfig ), any (DecisionPath .class )))
1775+ .thenReturn (DecisionResponse .responseNoReasons (variation ));
1776+
1777+ DecisionService decisionServiceWithUPS = new DecisionService (
1778+ mockBucketer ,
1779+ mockErrorHandler ,
1780+ mockUserProfileService ,
1781+ null // No CMAB service for standard experiment
1782+ );
1783+
1784+ // Call getVariation with standard experiment
1785+ DecisionResponse <Variation > result = decisionServiceWithUPS .getVariation (
1786+ experiment ,
1787+ optimizely .createUserContext (genericUserId , Collections .emptyMap ()),
1788+ noAudienceProjectConfig
1789+ );
1790+
1791+ // Verify variation was returned
1792+ assertEquals (variation , result .getResult ());
1793+
1794+ // Verify user profile WAS saved for standard experiment
1795+ UserProfile expectedUserProfile = new UserProfile (genericUserId ,
1796+ Collections .singletonMap (experiment .getId (), decision ));
1797+ verify (mockUserProfileService , times (1 )).save (eq (expectedUserProfile .toMap ()));
1798+
1799+ // Verify appropriate logging
1800+ logbackVerifier .expectMessage (Level .INFO ,
1801+ String .format ("Saved user profile of user \" %s\" ." , genericUserId ));
1802+ }
1803+
17071804 private Experiment createMockCmabExperiment () {
17081805 List <Variation > variations = Arrays .asList (
17091806 new Variation ("111151" , "variation_1" ),
0 commit comments