Skip to content

Commit 640746e

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 640746e

1 file changed

Lines changed: 140 additions & 0 deletions

File tree

pkg/decision/feature_experiment_service_test.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,146 @@ 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 holdout variation
304+
holdoutVariation := entities.Variation{
305+
ID: "holdout_var_1",
306+
Key: "holdout_off",
307+
}
308+
309+
// Create local holdout targeting the first experiment
310+
localHoldout := entities.Holdout{
311+
ID: "local_holdout_1",
312+
Key: "local_holdout_key",
313+
Status: entities.HoldoutStatusRunning,
314+
Variations: map[string]entities.Variation{
315+
"holdout_var_1": holdoutVariation,
316+
},
317+
TrafficAllocation: []entities.Range{
318+
{EntityID: "holdout_var_1", EndOfRange: 10000},
319+
},
320+
IncludedRules: []string{testExp1113.ID}, // Target first experiment
321+
}
322+
323+
// Mock GetHoldoutsForRule to return our local holdout
324+
s.mockConfig.On("GetHoldoutsForRule", testExp1113.ID).Return([]entities.Holdout{localHoldout})
325+
s.mockConfig.On("GetAudienceMap").Return(map[string]entities.Audience{})
326+
327+
featureExperimentService := &FeatureExperimentService{
328+
compositeExperimentService: s.mockExperimentService,
329+
logger: logging.GetLogger("sdkKey", "FeatureExperimentService"),
330+
}
331+
332+
decision, _, err := featureExperimentService.GetDecision(s.testFeatureDecisionContext, testUserContext, s.options)
333+
s.NoError(err)
334+
s.Equal(Holdout, decision.Source)
335+
s.Equal(&holdoutVariation, decision.Variation)
336+
s.mockConfig.AssertExpectations(s.T())
337+
}
338+
339+
func (s *FeatureExperimentServiceTestSuite) TestGetDecisionWithLocalHoldoutNotBucketed() {
340+
testUserContext := entities.UserContext{
341+
ID: "test_user_1",
342+
}
343+
344+
// Create holdout variation
345+
holdoutVariation := entities.Variation{
346+
ID: "holdout_var_1",
347+
Key: "holdout_off",
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: map[string]entities.Variation{
356+
"holdout_var_1": holdoutVariation,
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 holdout variation
396+
holdoutVariation := entities.Variation{
397+
ID: "holdout_var_1",
398+
Key: "holdout_off",
399+
}
400+
401+
// Create local holdout that's not running
402+
localHoldout := entities.Holdout{
403+
ID: "local_holdout_1",
404+
Key: "local_holdout_key",
405+
Status: "Draft", // Not running
406+
Variations: map[string]entities.Variation{
407+
"holdout_var_1": holdoutVariation,
408+
},
409+
TrafficAllocation: []entities.Range{{EntityID: "holdout_var_1", EndOfRange: 10000}},
410+
IncludedRules: []string{testExp1113.ID},
411+
}
412+
413+
s.mockConfig.On("GetHoldoutsForRule", testExp1113.ID).Return([]entities.Holdout{localHoldout})
414+
415+
// User should fall through to normal experiment evaluation
416+
expectedVariation := testExp1113.Variations["2223"]
417+
returnExperimentDecision := ExperimentDecision{
418+
Variation: &expectedVariation,
419+
}
420+
testExperimentDecisionContext := ExperimentDecisionContext{
421+
Experiment: &testExp1113,
422+
ProjectConfig: s.mockConfig,
423+
}
424+
s.mockExperimentService.On("GetDecision", testExperimentDecisionContext, testUserContext, s.options).Return(returnExperimentDecision, s.reasons, nil)
425+
426+
featureExperimentService := &FeatureExperimentService{
427+
compositeExperimentService: s.mockExperimentService,
428+
logger: logging.GetLogger("sdkKey", "FeatureExperimentService"),
429+
}
430+
431+
decision, _, err := featureExperimentService.GetDecision(s.testFeatureDecisionContext, testUserContext, s.options)
432+
s.NoError(err)
433+
s.Equal(FeatureTest, decision.Source)
434+
s.mockExperimentService.AssertExpectations(s.T())
435+
s.mockConfig.AssertExpectations(s.T())
436+
}
437+
298438
func TestFeatureExperimentServiceTestSuite(t *testing.T) {
299439
suite.Run(t, new(FeatureExperimentServiceTestSuite))
300440
}

0 commit comments

Comments
 (0)