From 0cff19ae363c5281cd8b2b2a7acff66d869ec2e5 Mon Sep 17 00:00:00 2001 From: Matjaz Pirnovar Date: Thu, 11 Dec 2025 15:16:10 -0800 Subject: [PATCH 1/4] Add holdouts to OptimizelyConfig This change exposes holdouts in the OptimizelyConfig API, allowing clients to retrieve holdout information via the config endpoint. Changes: - Added OptimizelyHoldout type with id, key, audiences, and variationsMap - Added Holdouts field to OptimizelyConfig struct - Added GetHoldoutList() method to ProjectConfig interface - Implemented getHoldouts() helper to convert holdout entities to OptimizelyHoldouts - Populated holdouts in NewOptimizelyConfig constructor This enables Agent and other clients to expose holdout configuration through their /config endpoints. --- pkg/config/datafileprojectconfig/config.go | 5 +++ pkg/config/interface.go | 1 + pkg/config/optimizely_config.go | 40 ++++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/pkg/config/datafileprojectconfig/config.go b/pkg/config/datafileprojectconfig/config.go index 13526875..cb1bd8ec 100644 --- a/pkg/config/datafileprojectconfig/config.go +++ b/pkg/config/datafileprojectconfig/config.go @@ -292,6 +292,11 @@ func (c DatafileProjectConfig) GetHoldoutsForFlag(featureKey string) []entities. return []entities.Holdout{} } +// GetHoldoutList returns all holdouts in the project +func (c DatafileProjectConfig) GetHoldoutList() []entities.Holdout { + return c.holdouts +} + // NewDatafileProjectConfig initializes a new datafile from a json byte array using the default JSON datafile parser func NewDatafileProjectConfig(jsonDatafile []byte, logger logging.OptimizelyLogProducer) (*DatafileProjectConfig, error) { datafile, err := Parse(jsonDatafile) diff --git a/pkg/config/interface.go b/pkg/config/interface.go index 6d45c14a..3c58e143 100644 --- a/pkg/config/interface.go +++ b/pkg/config/interface.go @@ -57,6 +57,7 @@ type ProjectConfig interface { GetFlagVariationsMap() map[string][]entities.Variation GetRegion() string GetHoldoutsForFlag(featureKey string) []entities.Holdout + GetHoldoutList() []entities.Holdout } // ProjectConfigManager maintains an instance of the ProjectConfig diff --git a/pkg/config/optimizely_config.go b/pkg/config/optimizely_config.go index 289e90e6..43fee411 100644 --- a/pkg/config/optimizely_config.go +++ b/pkg/config/optimizely_config.go @@ -42,6 +42,7 @@ type OptimizelyConfig struct { Attributes []OptimizelyAttribute `json:"attributes"` Audiences []OptimizelyAudience `json:"audiences"` Events []OptimizelyEvent `json:"events"` + Holdouts []OptimizelyHoldout `json:"holdouts"` datafile string } @@ -78,6 +79,14 @@ type OptimizelyEvent struct { ExperimentIds []string `json:"experimentIds"` } +// OptimizelyHoldout has holdout info +type OptimizelyHoldout struct { + ID string `json:"id"` + Key string `json:"key"` + Audiences string `json:"audiences"` + VariationsMap map[string]OptimizelyVariation `json:"variationsMap"` +} + // OptimizelyFeature has feature info type OptimizelyFeature struct { ID string `json:"id"` @@ -365,6 +374,35 @@ func getFeaturesMap(audiencesByID map[string]entities.Audience, mappedExperiment return featuresMap } +func getHoldoutAudiences(holdout entities.Holdout, audiencesByID map[string]entities.Audience) string { + return getSerializedAudiences(holdout.AudienceConditions, audiencesByID) +} + +func getHoldouts(audiencesByID map[string]entities.Audience, holdouts []entities.Holdout) []OptimizelyHoldout { + optimizelyHoldouts := []OptimizelyHoldout{} + + for _, holdout := range holdouts { + // Create variations map for this holdout + variationsMap := map[string]OptimizelyVariation{} + for _, variation := range holdout.Variations { + variationsMap[variation.Key] = OptimizelyVariation{ + ID: variation.ID, + Key: variation.Key, + FeatureEnabled: variation.FeatureEnabled, + VariablesMap: map[string]OptimizelyVariable{}, // Holdouts don't have feature variables + } + } + + optimizelyHoldouts = append(optimizelyHoldouts, OptimizelyHoldout{ + ID: holdout.ID, + Key: holdout.Key, + Audiences: getHoldoutAudiences(holdout, audiencesByID), + VariationsMap: variationsMap, + }) + } + return optimizelyHoldouts +} + // NewOptimizelyConfig constructs OptimizelyConfig object func NewOptimizelyConfig(projConfig ProjectConfig) *OptimizelyConfig { @@ -405,6 +443,8 @@ func NewOptimizelyConfig(projConfig ProjectConfig) *OptimizelyConfig { variableByIDMap := getVariableByIDMap(featuresList) optimizelyConfig.FeaturesMap = getFeaturesMap(projConfig.GetAudienceMap(), mappedExperiments, featuresList, rolloutIDMap, variableByIDMap) + optimizelyConfig.Holdouts = getHoldouts(projConfig.GetAudienceMap(), projConfig.GetHoldoutList()) + optimizelyConfig.datafile = projConfig.GetDatafile() return optimizelyConfig From def69a1705201bd0f92b510361a3d9094b893b88 Mon Sep 17 00:00:00 2001 From: Matjaz Pirnovar Date: Fri, 12 Dec 2025 14:39:05 -0800 Subject: [PATCH 2/4] Add unit test for holdouts in OptimizelyConfig This test verifies that: - Holdouts field is present in OptimizelyConfig - Holdouts field is initialized as empty array when no holdouts exist - Holdouts field correctly marshals to JSON --- pkg/config/optimizely_config_test.go | 84 +++++++ pkg/config/testdata/holdouts_datafile.json | 241 +++++++++++++++++++++ 2 files changed, 325 insertions(+) create mode 100644 pkg/config/testdata/holdouts_datafile.json diff --git a/pkg/config/optimizely_config_test.go b/pkg/config/optimizely_config_test.go index cee05ca3..d090e07b 100644 --- a/pkg/config/optimizely_config_test.go +++ b/pkg/config/optimizely_config_test.go @@ -182,6 +182,90 @@ func (s *OptimizelyConfigTestSuite) TestOptlyConfigGetDatafile() { s.Equal(string(datafile), optimizelyConfig.GetDatafile()) } +func (s *OptimizelyConfigTestSuite) TestOptlyConfigIncludesHoldouts() { + // Test with minimal datafile (no holdouts) + datafile := []byte(`{"version":"4"}`) + projectMgr := NewStaticProjectConfigManagerWithOptions("", WithInitialDatafile(datafile)) + optimizelyConfig := NewOptimizelyConfig(projectMgr.projectConfig) + + // Verify holdouts field exists and is empty + s.NotNil(optimizelyConfig.Holdouts) + s.Empty(optimizelyConfig.Holdouts) + + // Verify it marshals to JSON correctly + var jsonMap map[string]interface{} + bytesData, _ := json.Marshal(optimizelyConfig) + json.Unmarshal(bytesData, &jsonMap) + + holdouts, exists := jsonMap["holdouts"] + s.True(exists, "holdouts field should exist in JSON") + s.IsType([]interface{}{}, holdouts, "holdouts should be an array") +} + +func (s *OptimizelyConfigTestSuite) TestOptlyConfigWithHoldouts() { + // Test with datafile containing holdouts + rootDirectory := "testdata/" + dataFile, err := os.ReadFile(rootDirectory + "holdouts_datafile.json") + if err != nil { + s.Fail("error opening holdouts_datafile.json") + } + + projectMgr := NewStaticProjectConfigManagerWithOptions("", WithInitialDatafile(dataFile)) + projConfig, err := projectMgr.GetConfig() + if err != nil { + s.Fail(err.Error()) + } + optimizelyConfig := NewOptimizelyConfig(projConfig) + + // Verify holdouts are present + s.NotNil(optimizelyConfig.Holdouts) + s.Len(optimizelyConfig.Holdouts, 4, "Should have 4 holdouts") + + // Verify holdout structure + expectedHoldoutKeys := map[string]bool{ + "holdout_3": false, + "holdout_5": false, + "holdouts_4": false, + "holdout_6": false, + } + + for _, holdout := range optimizelyConfig.Holdouts { + // Verify required fields + s.NotEmpty(holdout.ID) + s.NotEmpty(holdout.Key) + s.NotEmpty(holdout.Audiences) + s.NotNil(holdout.VariationsMap) + + // Mark key as found + if _, exists := expectedHoldoutKeys[holdout.Key]; exists { + expectedHoldoutKeys[holdout.Key] = true + } + + // Verify variation structure + s.Contains(holdout.VariationsMap, "off", "Holdout should have 'off' variation") + offVariation := holdout.VariationsMap["off"] + s.Equal("$opt_dummy_variation_id", offVariation.ID) + s.Equal("off", offVariation.Key) + s.False(offVariation.FeatureEnabled) + } + + // Verify all expected holdout keys were found + for key, found := range expectedHoldoutKeys { + s.True(found, "Holdout with key %s should be present", key) + } + + // Verify JSON marshaling + var jsonMap map[string]interface{} + bytesData, _ := json.Marshal(optimizelyConfig) + json.Unmarshal(bytesData, &jsonMap) + + holdoutsJSON, exists := jsonMap["holdouts"] + s.True(exists) + holdoutsArray, ok := holdoutsJSON.([]interface{}) + s.True(ok) + s.Len(holdoutsArray, 4) +} + func TestOptimizelyConfigTestSuite(t *testing.T) { suite.Run(t, new(OptimizelyConfigTestSuite)) } diff --git a/pkg/config/testdata/holdouts_datafile.json b/pkg/config/testdata/holdouts_datafile.json new file mode 100644 index 00000000..30bd5380 --- /dev/null +++ b/pkg/config/testdata/holdouts_datafile.json @@ -0,0 +1,241 @@ +{ + "accountId": "12133785640", + "projectId": "6460519658291200", + "revision": "12", + "attributes": [ + { + "id": "5502380200951808", + "key": "all" + }, + { + "id": "5750214343000064", + "key": "ho" + } + ], + "audiences": [ + { + "name": "ho_3_aud", + "conditions": "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + "id": "5435551013142528" + }, + { + "name": "ho_6_aud", + "conditions": "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + "id": "5841838209236992" + }, + { + "name": "ho_4_aud", + "conditions": "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + "id": "6043616745881600" + }, + { + "name": "ho_5_aud", + "conditions": "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + "id": "6410995866796032" + }, + { + "id": "$opt_dummy_audience", + "name": "Optimizely-Generated Audience for Backwards Compatibility", + "conditions": "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]" + } + ], + "version": "4", + "events": [ + { + "id": "6554438379241472", + "experimentIds": [], + "key": "event1" + } + ], + "integrations": [], + "holdouts": [ + { + "id": "1673115", + "key": "holdout_6", + "status": "Running", + "variations": [ + { + "id": "$opt_dummy_variation_id", + "key": "off", + "featureEnabled": false, + "variables": [] + } + ], + "trafficAllocation": [ + { + "entityId": "$opt_dummy_variation_id", + "endOfRange": 4000 + } + ], + "audienceIds": ["5841838209236992"], + "audienceConditions": ["or", "5841838209236992"] + }, + { + "id": "1673114", + "key": "holdout_5", + "status": "Running", + "variations": [ + { + "id": "$opt_dummy_variation_id", + "key": "off", + "featureEnabled": false, + "variables": [] + } + ], + "trafficAllocation": [ + { + "entityId": "$opt_dummy_variation_id", + "endOfRange": 2000 + } + ], + "audienceIds": ["6410995866796032"], + "audienceConditions": ["or", "6410995866796032"] + }, + { + "id": "1673113", + "key": "holdouts_4", + "status": "Running", + "variations": [ + { + "id": "$opt_dummy_variation_id", + "key": "off", + "featureEnabled": false, + "variables": [] + } + ], + "trafficAllocation": [ + { + "entityId": "$opt_dummy_variation_id", + "endOfRange": 5000 + } + ], + "audienceIds": ["6043616745881600"], + "audienceConditions": ["or", "6043616745881600"] + }, + { + "id": "1673112", + "key": "holdout_3", + "status": "Running", + "variations": [ + { + "id": "$opt_dummy_variation_id", + "key": "off", + "featureEnabled": false, + "variables": [] + } + ], + "trafficAllocation": [ + { + "entityId": "$opt_dummy_variation_id", + "endOfRange": 1000 + } + ], + "audienceIds": ["5435551013142528"], + "audienceConditions": ["or", "5435551013142528"] + } + ], + "anonymizeIP": true, + "botFiltering": false, + "typedAudiences": [ + { + "name": "ho_3_aud", + "conditions": ["and", ["or", ["or", {"match": "exact", "name": "ho", "type": "custom_attribute", "value": 3}]], ["or", {"match": "le", "name": "all", "type": "custom_attribute", "value": 3}]], + "id": "5435551013142528" + }, + { + "name": "ho_6_aud", + "conditions": ["and", ["or", ["or", {"match": "exact", "name": "ho", "type": "custom_attribute", "value": 6}]], ["or", {"match": "le", "name": "all", "type": "custom_attribute", "value": 6}]], + "id": "5841838209236992" + }, + { + "name": "ho_4_aud", + "conditions": ["and", ["or", ["or", {"match": "exact", "name": "ho", "type": "custom_attribute", "value": 4}]], ["or", {"match": "le", "name": "all", "type": "custom_attribute", "value": 4}]], + "id": "6043616745881600" + }, + { + "name": "ho_5_aud", + "conditions": ["and", ["or", ["or", {"match": "exact", "name": "ho", "type": "custom_attribute", "value": 5}]], ["or", {"match": "le", "name": "all", "type": "custom_attribute", "value": 5}]], + "id": "6410995866796032" + } + ], + "variables": [], + "environmentKey": "production", + "sdkKey": "BLsSFScP7tSY5SCYuKn8c", + "featureFlags": [ + { + "id": "497759", + "key": "flag1", + "rolloutId": "rollout-497759-631765411405174", + "experimentIds": [], + "variables": [] + }, + { + "id": "497760", + "key": "flag2", + "rolloutId": "rollout-497760-631765411405174", + "experimentIds": [], + "variables": [] + } + ], + "rollouts": [ + { + "id": "rollout-497759-631765411405174", + "experiments": [ + { + "id": "default-rollout-497759-631765411405174", + "key": "default-rollout-497759-631765411405174", + "status": "Running", + "layerId": "rollout-497759-631765411405174", + "variations": [ + { + "id": "1583341", + "key": "variation_1", + "featureEnabled": true, + "variables": [] + } + ], + "trafficAllocation": [ + { + "entityId": "1583341", + "endOfRange": 10000 + } + ], + "forcedVariations": {}, + "audienceIds": [], + "audienceConditions": [] + } + ] + }, + { + "id": "rollout-497760-631765411405174", + "experiments": [ + { + "id": "default-rollout-497760-631765411405174", + "key": "default-rollout-497760-631765411405174", + "status": "Running", + "layerId": "rollout-497760-631765411405174", + "variations": [ + { + "id": "1583340", + "key": "variation_2", + "featureEnabled": true, + "variables": [] + } + ], + "trafficAllocation": [ + { + "entityId": "1583340", + "endOfRange": 10000 + } + ], + "forcedVariations": {}, + "audienceIds": [], + "audienceConditions": [] + } + ] + } + ], + "experiments": [], + "groups": [], + "region": "US" +} From 4d777b60494ab2dc8ea5ff274b885a2c6e1c8a64 Mon Sep 17 00:00:00 2001 From: Matjaz Pirnovar Date: Fri, 12 Dec 2025 14:45:30 -0800 Subject: [PATCH 3/4] Add GetHoldoutList() to MockProjectConfig implementations Updated all MockProjectConfig implementations to include the GetHoldoutList() method required by the config.ProjectConfig interface: - pkg/cmab/service_test.go: Added GetHoldoutList() to MockProjectConfig - pkg/decision/evaluator/audience_evaluator_test.go: Added GetHoldoutList() to MockProjectConfig - pkg/client/fixtures_test.go: Added GetHoldoutList() and GetHoldoutsForFlag() to MockProjectConfig - pkg/config/static_manager_test.go: Updated test assertion to expect empty Holdouts array instead of nil This fixes compilation errors in CMAB and audience evaluator tests where the mock implementations were missing the new interface method. --- pkg/client/fixtures_test.go | 8 ++++++++ pkg/cmab/service_test.go | 5 +++++ pkg/config/static_manager_test.go | 2 +- pkg/decision/evaluator/audience_evaluator_test.go | 5 +++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pkg/client/fixtures_test.go b/pkg/client/fixtures_test.go index 13816697..a8ecac52 100644 --- a/pkg/client/fixtures_test.go +++ b/pkg/client/fixtures_test.go @@ -86,6 +86,14 @@ func (c *MockProjectConfig) GetRegion() string { return "US" } +func (c *MockProjectConfig) GetHoldoutList() []entities.Holdout { + return []entities.Holdout{} +} + +func (c *MockProjectConfig) GetHoldoutsForFlag(featureKey string) []entities.Holdout { + return []entities.Holdout{} +} + type MockProjectConfigManager struct { projectConfig config.ProjectConfig mock.Mock diff --git a/pkg/cmab/service_test.go b/pkg/cmab/service_test.go index 4675d773..71a9a235 100644 --- a/pkg/cmab/service_test.go +++ b/pkg/cmab/service_test.go @@ -235,6 +235,11 @@ func (m *MockProjectConfig) GetHoldoutsForFlag(featureKey string) []entities.Hol return args.Get(0).([]entities.Holdout) } +func (m *MockProjectConfig) GetHoldoutList() []entities.Holdout { + args := m.Called() + return args.Get(0).([]entities.Holdout) +} + type CmabServiceTestSuite struct { suite.Suite mockClient *MockCmabClient diff --git a/pkg/config/static_manager_test.go b/pkg/config/static_manager_test.go index b318f386..55333264 100644 --- a/pkg/config/static_manager_test.go +++ b/pkg/config/static_manager_test.go @@ -59,7 +59,7 @@ func TestStaticGetOptimizelyConfig(t *testing.T) { assert.NotNil(t, configManager.optimizelyConfig) assert.Equal(t, &OptimizelyConfig{ExperimentsMap: map[string]OptimizelyExperiment{}, - FeaturesMap: map[string]OptimizelyFeature{}, Attributes: []OptimizelyAttribute{}, Audiences: []OptimizelyAudience{}, Events: []OptimizelyEvent{}, datafile: "{\"accountId\":\"42\",\"projectId\":\"123\",\"version\":\"4\"}"}, optimizelyConfig) + FeaturesMap: map[string]OptimizelyFeature{}, Attributes: []OptimizelyAttribute{}, Audiences: []OptimizelyAudience{}, Events: []OptimizelyEvent{}, Holdouts: []OptimizelyHoldout{}, datafile: "{\"accountId\":\"42\",\"projectId\":\"123\",\"version\":\"4\"}"}, optimizelyConfig) } func TestNewStaticProjectConfigManagerFromURL(t *testing.T) { diff --git a/pkg/decision/evaluator/audience_evaluator_test.go b/pkg/decision/evaluator/audience_evaluator_test.go index 75897aed..1eb6dadf 100644 --- a/pkg/decision/evaluator/audience_evaluator_test.go +++ b/pkg/decision/evaluator/audience_evaluator_test.go @@ -205,6 +205,11 @@ func (m *MockProjectConfig) GetHoldoutsForFlag(featureKey string) []entities.Hol return args.Get(0).([]entities.Holdout) } +func (m *MockProjectConfig) GetHoldoutList() []entities.Holdout { + args := m.Called() + return args.Get(0).([]entities.Holdout) +} + // MockLogger is a mock implementation of OptimizelyLogProducer // (This declaration has been removed to resolve the redeclaration error) From e70a8f37a76addae165755ef46b27701e907a464 Mon Sep 17 00:00:00 2001 From: Matjaz Pirnovar Date: Fri, 12 Dec 2025 15:11:47 -0800 Subject: [PATCH 4/4] Remove Holdouts field from OptimizelyConfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Holdouts field was not part of the original TDD requirements and is not present in JavaScript or C# SDKs. This field was causing FSC test failures. Agent doesn't need holdouts exposed in OptimizelyConfig because: - Decision logic (GetHoldoutsForFlag, GetHoldoutList) works internally - Agent's /v1/decide endpoint uses go-sdk's decision service which already handles holdouts from v2.3.0 - OptimizelyConfig is metadata, not for decision-making Changes: - Removed Holdouts field from OptimizelyConfig struct - Removed OptimizelyHoldout type - Removed getHoldouts() and getHoldoutAudiences() helper functions - Removed holdout-specific tests (TestOptlyConfigIncludesHoldouts, TestOptlyConfigWithHoldouts) - Updated TestStaticGetOptimizelyConfig to not expect Holdouts field The ProjectConfig interface methods (GetHoldoutList, GetHoldoutsForFlag) remain as they are required by TDD for internal decision logic. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- pkg/config/optimizely_config.go | 40 ------------- pkg/config/optimizely_config_test.go | 84 ---------------------------- pkg/config/static_manager_test.go | 2 +- 3 files changed, 1 insertion(+), 125 deletions(-) diff --git a/pkg/config/optimizely_config.go b/pkg/config/optimizely_config.go index 43fee411..289e90e6 100644 --- a/pkg/config/optimizely_config.go +++ b/pkg/config/optimizely_config.go @@ -42,7 +42,6 @@ type OptimizelyConfig struct { Attributes []OptimizelyAttribute `json:"attributes"` Audiences []OptimizelyAudience `json:"audiences"` Events []OptimizelyEvent `json:"events"` - Holdouts []OptimizelyHoldout `json:"holdouts"` datafile string } @@ -79,14 +78,6 @@ type OptimizelyEvent struct { ExperimentIds []string `json:"experimentIds"` } -// OptimizelyHoldout has holdout info -type OptimizelyHoldout struct { - ID string `json:"id"` - Key string `json:"key"` - Audiences string `json:"audiences"` - VariationsMap map[string]OptimizelyVariation `json:"variationsMap"` -} - // OptimizelyFeature has feature info type OptimizelyFeature struct { ID string `json:"id"` @@ -374,35 +365,6 @@ func getFeaturesMap(audiencesByID map[string]entities.Audience, mappedExperiment return featuresMap } -func getHoldoutAudiences(holdout entities.Holdout, audiencesByID map[string]entities.Audience) string { - return getSerializedAudiences(holdout.AudienceConditions, audiencesByID) -} - -func getHoldouts(audiencesByID map[string]entities.Audience, holdouts []entities.Holdout) []OptimizelyHoldout { - optimizelyHoldouts := []OptimizelyHoldout{} - - for _, holdout := range holdouts { - // Create variations map for this holdout - variationsMap := map[string]OptimizelyVariation{} - for _, variation := range holdout.Variations { - variationsMap[variation.Key] = OptimizelyVariation{ - ID: variation.ID, - Key: variation.Key, - FeatureEnabled: variation.FeatureEnabled, - VariablesMap: map[string]OptimizelyVariable{}, // Holdouts don't have feature variables - } - } - - optimizelyHoldouts = append(optimizelyHoldouts, OptimizelyHoldout{ - ID: holdout.ID, - Key: holdout.Key, - Audiences: getHoldoutAudiences(holdout, audiencesByID), - VariationsMap: variationsMap, - }) - } - return optimizelyHoldouts -} - // NewOptimizelyConfig constructs OptimizelyConfig object func NewOptimizelyConfig(projConfig ProjectConfig) *OptimizelyConfig { @@ -443,8 +405,6 @@ func NewOptimizelyConfig(projConfig ProjectConfig) *OptimizelyConfig { variableByIDMap := getVariableByIDMap(featuresList) optimizelyConfig.FeaturesMap = getFeaturesMap(projConfig.GetAudienceMap(), mappedExperiments, featuresList, rolloutIDMap, variableByIDMap) - optimizelyConfig.Holdouts = getHoldouts(projConfig.GetAudienceMap(), projConfig.GetHoldoutList()) - optimizelyConfig.datafile = projConfig.GetDatafile() return optimizelyConfig diff --git a/pkg/config/optimizely_config_test.go b/pkg/config/optimizely_config_test.go index d090e07b..cee05ca3 100644 --- a/pkg/config/optimizely_config_test.go +++ b/pkg/config/optimizely_config_test.go @@ -182,90 +182,6 @@ func (s *OptimizelyConfigTestSuite) TestOptlyConfigGetDatafile() { s.Equal(string(datafile), optimizelyConfig.GetDatafile()) } -func (s *OptimizelyConfigTestSuite) TestOptlyConfigIncludesHoldouts() { - // Test with minimal datafile (no holdouts) - datafile := []byte(`{"version":"4"}`) - projectMgr := NewStaticProjectConfigManagerWithOptions("", WithInitialDatafile(datafile)) - optimizelyConfig := NewOptimizelyConfig(projectMgr.projectConfig) - - // Verify holdouts field exists and is empty - s.NotNil(optimizelyConfig.Holdouts) - s.Empty(optimizelyConfig.Holdouts) - - // Verify it marshals to JSON correctly - var jsonMap map[string]interface{} - bytesData, _ := json.Marshal(optimizelyConfig) - json.Unmarshal(bytesData, &jsonMap) - - holdouts, exists := jsonMap["holdouts"] - s.True(exists, "holdouts field should exist in JSON") - s.IsType([]interface{}{}, holdouts, "holdouts should be an array") -} - -func (s *OptimizelyConfigTestSuite) TestOptlyConfigWithHoldouts() { - // Test with datafile containing holdouts - rootDirectory := "testdata/" - dataFile, err := os.ReadFile(rootDirectory + "holdouts_datafile.json") - if err != nil { - s.Fail("error opening holdouts_datafile.json") - } - - projectMgr := NewStaticProjectConfigManagerWithOptions("", WithInitialDatafile(dataFile)) - projConfig, err := projectMgr.GetConfig() - if err != nil { - s.Fail(err.Error()) - } - optimizelyConfig := NewOptimizelyConfig(projConfig) - - // Verify holdouts are present - s.NotNil(optimizelyConfig.Holdouts) - s.Len(optimizelyConfig.Holdouts, 4, "Should have 4 holdouts") - - // Verify holdout structure - expectedHoldoutKeys := map[string]bool{ - "holdout_3": false, - "holdout_5": false, - "holdouts_4": false, - "holdout_6": false, - } - - for _, holdout := range optimizelyConfig.Holdouts { - // Verify required fields - s.NotEmpty(holdout.ID) - s.NotEmpty(holdout.Key) - s.NotEmpty(holdout.Audiences) - s.NotNil(holdout.VariationsMap) - - // Mark key as found - if _, exists := expectedHoldoutKeys[holdout.Key]; exists { - expectedHoldoutKeys[holdout.Key] = true - } - - // Verify variation structure - s.Contains(holdout.VariationsMap, "off", "Holdout should have 'off' variation") - offVariation := holdout.VariationsMap["off"] - s.Equal("$opt_dummy_variation_id", offVariation.ID) - s.Equal("off", offVariation.Key) - s.False(offVariation.FeatureEnabled) - } - - // Verify all expected holdout keys were found - for key, found := range expectedHoldoutKeys { - s.True(found, "Holdout with key %s should be present", key) - } - - // Verify JSON marshaling - var jsonMap map[string]interface{} - bytesData, _ := json.Marshal(optimizelyConfig) - json.Unmarshal(bytesData, &jsonMap) - - holdoutsJSON, exists := jsonMap["holdouts"] - s.True(exists) - holdoutsArray, ok := holdoutsJSON.([]interface{}) - s.True(ok) - s.Len(holdoutsArray, 4) -} - func TestOptimizelyConfigTestSuite(t *testing.T) { suite.Run(t, new(OptimizelyConfigTestSuite)) } diff --git a/pkg/config/static_manager_test.go b/pkg/config/static_manager_test.go index 55333264..b318f386 100644 --- a/pkg/config/static_manager_test.go +++ b/pkg/config/static_manager_test.go @@ -59,7 +59,7 @@ func TestStaticGetOptimizelyConfig(t *testing.T) { assert.NotNil(t, configManager.optimizelyConfig) assert.Equal(t, &OptimizelyConfig{ExperimentsMap: map[string]OptimizelyExperiment{}, - FeaturesMap: map[string]OptimizelyFeature{}, Attributes: []OptimizelyAttribute{}, Audiences: []OptimizelyAudience{}, Events: []OptimizelyEvent{}, Holdouts: []OptimizelyHoldout{}, datafile: "{\"accountId\":\"42\",\"projectId\":\"123\",\"version\":\"4\"}"}, optimizelyConfig) + FeaturesMap: map[string]OptimizelyFeature{}, Attributes: []OptimizelyAttribute{}, Audiences: []OptimizelyAudience{}, Events: []OptimizelyEvent{}, datafile: "{\"accountId\":\"42\",\"projectId\":\"123\",\"version\":\"4\"}"}, optimizelyConfig) } func TestNewStaticProjectConfigManagerFromURL(t *testing.T) {