Skip to content

Commit 4c70fee

Browse files
Mat001claude
andcommitted
[FSSDK-12368] Improve test coverage for local holdout evaluation
Added 4 new tests: - TestEvaluateHoldoutAudienceFails: User doesn't meet holdout audience conditions - TestEvaluateHoldoutNotBucketed: User not bucketed (0% traffic) - TestEvaluateHoldoutWithCustomBucketingID: Custom bucketing ID attribute - TestEveryoneElseRuleAudienceFails: Everyone Else rule audience fails Coverage improvements: - evaluateHoldout: 71.0% → 93.5% (+22.5%) - Decision package: 87.4% → 88.5% (+1.1%) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 429a861 commit 4c70fee

1 file changed

Lines changed: 149 additions & 0 deletions

File tree

pkg/decision/rollout_service_test.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
449598
func TestRolloutServiceTestSuite(t *testing.T) {
450599
suite.Run(t, new(RolloutServiceTestSuite))
451600
}

0 commit comments

Comments
 (0)