@@ -295,6 +295,138 @@ func (s *FeatureExperimentServiceTestSuite) TestGetDecisionWithCmabError() {
295295 s .mockExperimentService .AssertExpectations (s .T ())
296296}
297297
298+ func (s * FeatureExperimentServiceTestSuite ) TestGetDecisionWithLocalHoldout () {
299+ testUserContext := entities.UserContext {
300+ ID : "test_user_1" ,
301+ }
302+
303+ // Create local holdout targeting the first experiment
304+ localHoldout := entities.Holdout {
305+ ID : "local_holdout_1" ,
306+ Key : "local_holdout_key" ,
307+ Status : entities .HoldoutStatusRunning ,
308+ Variations : []entities.Variation {
309+ {ID : "holdout_var_1" , Key : "holdout_off" },
310+ },
311+ TrafficAllocation : []entities.Range {
312+ {EntityID : "holdout_var_1" , EndOfRange : 10000 },
313+ },
314+ IncludedRules : & []string {testExp1113 .ID }, // Target first experiment
315+ }
316+
317+ // Mock GetHoldoutsForRule to return our local holdout
318+ s .mockConfig .On ("GetHoldoutsForRule" , testExp1113 .ID ).Return ([]entities.Holdout {localHoldout })
319+ s .mockConfig .On ("GetAudienceMap" ).Return (map [string ]entities.Audience {})
320+
321+ featureExperimentService := & FeatureExperimentService {
322+ compositeExperimentService : s .mockExperimentService ,
323+ logger : logging .GetLogger ("sdkKey" , "FeatureExperimentService" ),
324+ }
325+
326+ expectedHoldoutVariation := localHoldout .Variations [0 ]
327+ expectedFeatureDecision := FeatureDecision {
328+ Experiment : entities.Experiment {
329+ ID : testExp1113 .ID ,
330+ Key : testExp1113 .Key ,
331+ Variations : localHoldout .Variations ,
332+ TrafficAllocation : localHoldout .TrafficAllocation ,
333+ },
334+ Variation : & expectedHoldoutVariation ,
335+ Source : Holdout ,
336+ }
337+
338+ decision , _ , err := featureExperimentService .GetDecision (s .testFeatureDecisionContext , testUserContext , s .options )
339+ s .NoError (err )
340+ s .Equal (Holdout , decision .Source )
341+ s .Equal (& expectedHoldoutVariation , decision .Variation )
342+ s .mockConfig .AssertExpectations (s .T ())
343+ }
344+
345+ func (s * FeatureExperimentServiceTestSuite ) TestGetDecisionWithLocalHoldoutNotBucketed () {
346+ testUserContext := entities.UserContext {
347+ ID : "test_user_1" ,
348+ }
349+
350+ // Create local holdout with 0% traffic
351+ localHoldout := entities.Holdout {
352+ ID : "local_holdout_1" ,
353+ Key : "local_holdout_key" ,
354+ Status : entities .HoldoutStatusRunning ,
355+ Variations : []entities.Variation {
356+ {ID : "holdout_var_1" , Key : "holdout_off" },
357+ },
358+ TrafficAllocation : []entities.Range {
359+ {EntityID : "holdout_var_1" , EndOfRange : 0 }, // 0% traffic
360+ },
361+ IncludedRules : & []string {testExp1113 .ID },
362+ }
363+
364+ s .mockConfig .On ("GetHoldoutsForRule" , testExp1113 .ID ).Return ([]entities.Holdout {localHoldout })
365+ s .mockConfig .On ("GetAudienceMap" ).Return (map [string ]entities.Audience {})
366+
367+ // User should fall through to normal experiment evaluation
368+ expectedVariation := testExp1113 .Variations ["2223" ]
369+ returnExperimentDecision := ExperimentDecision {
370+ Variation : & expectedVariation ,
371+ }
372+ testExperimentDecisionContext := ExperimentDecisionContext {
373+ Experiment : & testExp1113 ,
374+ ProjectConfig : s .mockConfig ,
375+ }
376+ s .mockExperimentService .On ("GetDecision" , testExperimentDecisionContext , testUserContext , s .options ).Return (returnExperimentDecision , s .reasons , nil )
377+
378+ featureExperimentService := & FeatureExperimentService {
379+ compositeExperimentService : s .mockExperimentService ,
380+ logger : logging .GetLogger ("sdkKey" , "FeatureExperimentService" ),
381+ }
382+
383+ decision , _ , err := featureExperimentService .GetDecision (s .testFeatureDecisionContext , testUserContext , s .options )
384+ s .NoError (err )
385+ s .Equal (FeatureTest , decision .Source ) // Should be from feature test, not holdout
386+ s .mockExperimentService .AssertExpectations (s .T ())
387+ s .mockConfig .AssertExpectations (s .T ())
388+ }
389+
390+ func (s * FeatureExperimentServiceTestSuite ) TestGetDecisionWithLocalHoldoutNotRunning () {
391+ testUserContext := entities.UserContext {
392+ ID : "test_user_1" ,
393+ }
394+
395+ // Create local holdout that's not running
396+ localHoldout := entities.Holdout {
397+ ID : "local_holdout_1" ,
398+ Key : "local_holdout_key" ,
399+ Status : "Draft" , // Not running
400+ Variations : []entities.Variation {{ID : "holdout_var_1" , Key : "holdout_off" }},
401+ TrafficAllocation : []entities.Range {{EntityID : "holdout_var_1" , EndOfRange : 10000 }},
402+ IncludedRules : & []string {testExp1113 .ID },
403+ }
404+
405+ s .mockConfig .On ("GetHoldoutsForRule" , testExp1113 .ID ).Return ([]entities.Holdout {localHoldout })
406+
407+ // User should fall through to normal experiment evaluation
408+ expectedVariation := testExp1113 .Variations ["2223" ]
409+ returnExperimentDecision := ExperimentDecision {
410+ Variation : & expectedVariation ,
411+ }
412+ testExperimentDecisionContext := ExperimentDecisionContext {
413+ Experiment : & testExp1113 ,
414+ ProjectConfig : s .mockConfig ,
415+ }
416+ s .mockExperimentService .On ("GetDecision" , testExperimentDecisionContext , testUserContext , s .options ).Return (returnExperimentDecision , s .reasons , nil )
417+
418+ featureExperimentService := & FeatureExperimentService {
419+ compositeExperimentService : s .mockExperimentService ,
420+ logger : logging .GetLogger ("sdkKey" , "FeatureExperimentService" ),
421+ }
422+
423+ decision , _ , err := featureExperimentService .GetDecision (s .testFeatureDecisionContext , testUserContext , s .options )
424+ s .NoError (err )
425+ s .Equal (FeatureTest , decision .Source )
426+ s .mockExperimentService .AssertExpectations (s .T ())
427+ s .mockConfig .AssertExpectations (s .T ())
428+ }
429+
298430func TestFeatureExperimentServiceTestSuite (t * testing.T ) {
299431 suite .Run (t , new (FeatureExperimentServiceTestSuite ))
300432}
0 commit comments