Skip to content

Commit ec661d0

Browse files
Mat001claude
andcommitted
[FSSDK-12368] Add tests for local holdouts in feature experiments
- Add TestGetDecisionWithLocalHoldout: user bucketed into holdout - Add TestGetDecisionWithLocalHoldoutNotBucketed: 0% traffic falls through - Add TestGetDecisionWithLocalHoldoutNotRunning: draft holdout skipped - Improves coverage for evaluateHoldout method (77 uncovered lines) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 4c70fee commit ec661d0

1 file changed

Lines changed: 132 additions & 0 deletions

File tree

pkg/decision/feature_experiment_service_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
298430
func TestFeatureExperimentServiceTestSuite(t *testing.T) {
299431
suite.Run(t, new(FeatureExperimentServiceTestSuite))
300432
}

0 commit comments

Comments
 (0)