@@ -446,6 +446,155 @@ func TestEvaluateHoldoutRunningNoAudience(t *testing.T) {
446446 assert .NotNil (t , reasons )
447447}
448448
449+ func TestEvaluateHoldoutAudienceFails (t * testing.T ) {
450+ rolloutService := NewRolloutService ("" )
451+ mockConfig := new (mockProjectConfig )
452+ userContext := entities.UserContext {ID : "test_user" }
453+ options := & decide.Options {}
454+
455+ holdoutVar := entities.Variation {ID : "var_1" , Key : "control" }
456+ audienceCondTree := & entities.TreeNode {
457+ Operator : "or" ,
458+ Nodes : []* entities.TreeNode {
459+ {Item : "audience_123" },
460+ },
461+ }
462+ holdoutWithAudience := & entities.Holdout {
463+ ID : "holdout_2" ,
464+ Key : "audience_holdout" ,
465+ Status : entities .HoldoutStatusRunning ,
466+ Variations : map [string ]entities.Variation {"var_1" : holdoutVar },
467+ TrafficAllocation : []entities.Range {{EntityID : "var_1" , EndOfRange : 10000 }},
468+ AudienceConditionTree : audienceCondTree ,
469+ }
470+
471+ mockConfig .On ("GetAudienceMap" ).Return (map [string ]entities.Audience {})
472+
473+ decisionContext := FeatureDecisionContext {
474+ ProjectConfig : mockConfig ,
475+ Feature : & testFeatRollout3334 ,
476+ }
477+
478+ decision , reasons := rolloutService .evaluateHoldout (holdoutWithAudience , userContext , decisionContext , options )
479+
480+ // Should return empty decision when audience doesn't match
481+ assert .Nil (t , decision .Variation )
482+ assert .NotNil (t , reasons )
483+ }
484+
485+ func TestEvaluateHoldoutNotBucketed (t * testing.T ) {
486+ rolloutService := NewRolloutService ("" )
487+ mockConfig := new (mockProjectConfig )
488+ userContext := entities.UserContext {ID : "test_user_not_bucketed" }
489+ options := & decide.Options {}
490+
491+ holdoutVar := entities.Variation {ID : "var_1" , Key : "control" }
492+ runningHoldout := & entities.Holdout {
493+ ID : "holdout_3" ,
494+ Key : "zero_traffic_holdout" ,
495+ Status : entities .HoldoutStatusRunning ,
496+ Variations : map [string ]entities.Variation {
497+ "var_1" : holdoutVar ,
498+ },
499+ TrafficAllocation : []entities.Range {
500+ {EntityID : "var_1" , EndOfRange : 0 }, // 0% traffic - no one gets bucketed
501+ },
502+ AudienceConditionTree : nil ,
503+ }
504+
505+ mockConfig .On ("GetAudienceMap" ).Return (map [string ]entities.Audience {})
506+
507+ decisionContext := FeatureDecisionContext {
508+ ProjectConfig : mockConfig ,
509+ Feature : & testFeatRollout3334 ,
510+ }
511+
512+ decision , reasons := rolloutService .evaluateHoldout (runningHoldout , userContext , decisionContext , options )
513+
514+ // Should return empty decision when user not bucketed (0% traffic)
515+ assert .Nil (t , decision .Variation )
516+ assert .NotNil (t , reasons )
517+ }
518+
519+ func TestEvaluateHoldoutWithCustomBucketingID (t * testing.T ) {
520+ rolloutService := NewRolloutService ("" )
521+ mockConfig := new (mockProjectConfig )
522+ // User context with custom bucketing ID attribute
523+ userContext := entities.UserContext {
524+ ID : "test_user" ,
525+ Attributes : map [string ]interface {}{
526+ "$opt_bucketing_id" : "custom_bucket_123" ,
527+ },
528+ }
529+ options := & decide.Options {}
530+
531+ holdoutVar := entities.Variation {ID : "var_1" , Key : "control" }
532+ runningHoldout := & entities.Holdout {
533+ ID : "holdout_4" ,
534+ Key : "bucketing_id_holdout" ,
535+ Status : entities .HoldoutStatusRunning ,
536+ Variations : map [string ]entities.Variation {
537+ "var_1" : holdoutVar ,
538+ },
539+ TrafficAllocation : []entities.Range {
540+ {EntityID : "var_1" , EndOfRange : 10000 },
541+ },
542+ AudienceConditionTree : nil ,
543+ }
544+
545+ mockConfig .On ("GetAudienceMap" ).Return (map [string ]entities.Audience {})
546+
547+ decisionContext := FeatureDecisionContext {
548+ ProjectConfig : mockConfig ,
549+ Feature : & testFeatRollout3334 ,
550+ }
551+
552+ _ , reasons := rolloutService .evaluateHoldout (runningHoldout , userContext , decisionContext , options )
553+
554+ // Should use custom bucketing ID and not error
555+ assert .NotNil (t , reasons )
556+ }
557+
558+ func TestEveryoneElseRuleAudienceFails (t * testing.T ) {
559+ rolloutService := NewRolloutService ("" )
560+ mockConfig := new (mockProjectConfig )
561+ userContext := entities.UserContext {ID : "test_user" }
562+ options := & decide.Options {}
563+ reasons := decide .NewDecisionReasons (options )
564+
565+ mockConfig .On ("GetHoldoutsForRule" , testExp1118 .ID ).Return ([]entities.Holdout {})
566+ mockConfig .On ("GetAudienceMap" ).Return (map [string ]entities.Audience {})
567+
568+ featureDecision := FeatureDecision {Source : Rollout }
569+ decisionContext := FeatureDecisionContext {
570+ ProjectConfig : mockConfig ,
571+ Feature : & testFeatRollout3334 ,
572+ }
573+
574+ checkForForcedDecision := func (exp * entities.Experiment ) * FeatureDecision { return nil }
575+ evaluateConditionTree := func (exp * entities.Experiment , loggingKey string ) bool { return false } // Audience fails
576+ getExperimentDecisionContext := func (exp * entities.Experiment ) ExperimentDecisionContext {
577+ return ExperimentDecisionContext {Experiment : exp , ProjectConfig : mockConfig }
578+ }
579+
580+ decision , _ , _ := rolloutService .evaluateEveryoneElseRule (
581+ & testExp1118 ,
582+ & featureDecision ,
583+ & testFeatRollout3334 ,
584+ userContext ,
585+ decisionContext ,
586+ options ,
587+ reasons ,
588+ checkForForcedDecision ,
589+ evaluateConditionTree ,
590+ getExperimentDecisionContext ,
591+ )
592+
593+ // Should return original featureDecision when audience fails
594+ assert .Nil (t , decision .Variation )
595+ assert .Equal (t , Rollout , decision .Source )
596+ }
597+
449598func TestRolloutServiceTestSuite (t * testing.T ) {
450599 suite .Run (t , new (RolloutServiceTestSuite ))
451600}
0 commit comments