@@ -2102,7 +2102,7 @@ OptimizelyDecision callDecideWithIncludeReasons(String flagKey) {
21022102 }
21032103
21042104 @ Test
2105- public void decide_holdoutApplied_basic () throws Exception {
2105+ public void decide_with_holdout () throws Exception {
21062106 Optimizely optWithHoldout = createOptimizelyWithHoldouts ();
21072107 // pick a flag that is eligible for basic_holdout. Using boolean_feature from config.
21082108 String flagKey = "boolean_feature" ;
@@ -2139,4 +2139,114 @@ public void decide_holdoutApplied_basic() throws Exception {
21392139 .build ();
21402140 eventHandler .expectImpression (experimentId , variationId , userId , Collections .emptyMap (), metadata );
21412141 }
2142+
2143+ @ Test
2144+ public void decide_for_keys_with_holdout () throws Exception {
2145+ Optimizely optWithHoldout = createOptimizelyWithHoldouts ();
2146+ String userId = "user123" ;
2147+ Map <String , Object > attrs = new HashMap <>();
2148+ attrs .put ("$opt_bucketing_id" , "ppid300002" ); // deterministic bucketing used in prior holdout test
2149+ OptimizelyUserContext user = optWithHoldout .createUserContext (userId , attrs );
2150+
2151+ List <String > flagKeys = Arrays .asList (
2152+ "boolean_feature" , // previously validated basic_holdout membership
2153+ "double_single_variable_feature" , // also subject to global/basic holdout
2154+ "integer_single_variable_feature" // also subject to global/basic holdout
2155+ );
2156+
2157+ Map <String , OptimizelyDecision > decisions = user .decideForKeys (flagKeys , Collections .singletonList (OptimizelyDecideOption .INCLUDE_REASONS ));
2158+ assertEquals (3 , decisions .size ());
2159+
2160+ String holdoutExperimentId = "10075323428" ; // basic_holdout id
2161+ String variationId = "$opt_dummy_variation_id" ;
2162+ String variationKey = "ho_off_key" ;
2163+ String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (basic_holdout)." ;
2164+
2165+ for (String flagKey : flagKeys ) {
2166+ OptimizelyDecision d = decisions .get (flagKey );
2167+ assertNotNull (d );
2168+ assertEquals (flagKey , d .getFlagKey ());
2169+ assertEquals (variationKey , d .getVariationKey ());
2170+ assertFalse (d .getEnabled ());
2171+ assertTrue ("Expected holdout reason for flag " + flagKey , d .getReasons ().contains (expectedReason ));
2172+ DecisionMetadata metadata = new DecisionMetadata .Builder ()
2173+ .setFlagKey (flagKey )
2174+ .setRuleKey ("basic_holdout" )
2175+ .setRuleType ("holdout" )
2176+ .setVariationKey (variationKey )
2177+ .setEnabled (false )
2178+ .build ();
2179+ // attributes map expected empty (reserved $opt_ attribute filtered out)
2180+ eventHandler .expectImpression (holdoutExperimentId , variationId , userId , Collections .emptyMap (), metadata );
2181+ }
2182+
2183+ // At least one log message confirming holdout membership
2184+ logbackVerifier .expectMessage (Level .INFO , expectedReason );
2185+ }
2186+
2187+ @ Test
2188+ public void decide_all_with_holdout () throws Exception {
2189+
2190+ Optimizely optWithHoldout = createOptimizelyWithHoldouts ();
2191+ String userId = "user123" ;
2192+ Map <String , Object > attrs = new HashMap <>();
2193+ // ppid120000 buckets user into holdout_included_flags
2194+ attrs .put ("$opt_bucketing_id" , "ppid120000" );
2195+ OptimizelyUserContext user = optWithHoldout .createUserContext (userId , attrs );
2196+
2197+ // All flag keys present in holdouts-project-config.json
2198+ List <String > allFlagKeys = Arrays .asList (
2199+ "boolean_feature" ,
2200+ "double_single_variable_feature" ,
2201+ "integer_single_variable_feature" ,
2202+ "boolean_single_variable_feature" ,
2203+ "string_single_variable_feature" ,
2204+ "multi_variate_feature" ,
2205+ "multi_variate_future_feature" ,
2206+ "mutex_group_feature"
2207+ );
2208+
2209+ // Flags INCLUDED in holdout_included_flags (only these should be holdout decisions)
2210+ List <String > includedInHoldout = Arrays .asList (
2211+ "boolean_feature" ,
2212+ "double_single_variable_feature" ,
2213+ "integer_single_variable_feature"
2214+ );
2215+
2216+ Map <String , OptimizelyDecision > decisions = user .decideAll (Arrays .asList (
2217+ OptimizelyDecideOption .INCLUDE_REASONS ,
2218+ OptimizelyDecideOption .DISABLE_DECISION_EVENT
2219+ ));
2220+ assertEquals (allFlagKeys .size (), decisions .size ());
2221+
2222+ String holdoutExperimentId = "1007543323427" ; // holdout_included_flags id
2223+ String variationId = "$opt_dummy_variation_id" ;
2224+ String variationKey = "ho_off_key" ;
2225+ String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (holdout_included_flags)." ;
2226+
2227+ int holdoutCount = 0 ;
2228+ for (String flagKey : allFlagKeys ) {
2229+ OptimizelyDecision d = decisions .get (flagKey );
2230+ assertNotNull ("Missing decision for flag " + flagKey , d );
2231+ if (includedInHoldout .contains (flagKey )) {
2232+ // Should be holdout decision
2233+ assertEquals (variationKey , d .getVariationKey ());
2234+ assertFalse (d .getEnabled ());
2235+ assertTrue ("Expected holdout reason for flag " + flagKey , d .getReasons ().contains (expectedReason ));
2236+ DecisionMetadata metadata = new DecisionMetadata .Builder ()
2237+ .setFlagKey (flagKey )
2238+ .setRuleKey ("holdout_included_flags" )
2239+ .setRuleType ("holdout" )
2240+ .setVariationKey (variationKey )
2241+ .setEnabled (false )
2242+ .build ();
2243+ holdoutCount ++;
2244+ } else {
2245+ // Should NOT be a holdout decision
2246+ assertFalse ("Non-included flag should not have holdout reason: " + flagKey , d .getReasons ().contains (expectedReason ));
2247+ }
2248+ }
2249+ assertEquals ("Expected exactly the included flags to be in holdout" , includedInHoldout .size (), holdoutCount );
2250+ logbackVerifier .expectMessage (Level .INFO , expectedReason );
2251+ }
21422252}
0 commit comments