@@ -2102,7 +2102,7 @@ OptimizelyDecision callDecideWithIncludeReasons(String flagKey) {
2102
2102
}
2103
2103
2104
2104
@ Test
2105
- public void decide_holdoutApplied_basic () throws Exception {
2105
+ public void decide_with_holdout () throws Exception {
2106
2106
Optimizely optWithHoldout = createOptimizelyWithHoldouts ();
2107
2107
// pick a flag that is eligible for basic_holdout. Using boolean_feature from config.
2108
2108
String flagKey = "boolean_feature" ;
@@ -2139,4 +2139,114 @@ public void decide_holdoutApplied_basic() throws Exception {
2139
2139
.build ();
2140
2140
eventHandler .expectImpression (experimentId , variationId , userId , Collections .emptyMap (), metadata );
2141
2141
}
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
+ }
2142
2252
}
0 commit comments