You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[FSSDK-12394] Add local holdouts to swift-sdk (ref sdk) (#628)
* [FSSDK-12394] Implement Local Holdouts support
This commit implements Local Holdouts functionality that allows holdouts
to target specific rules instead of all rules within a flag.
Changes:
- Holdout.swift: Replace includedFlags/excludedFlags with includedRules
- HoldoutConfig.swift: Replace flag-level maps with ruleHoldoutsMap
- ProjectConfig.swift: Add getGlobalHoldouts() and getHoldoutsForRule()
- DefaultDecisionService.swift: Update decision logic for global/local holdouts
* getDecisionForFlag() now uses only global holdouts
* Added local holdout checks to getVariationFromExperimentRule()
* Added local holdout checks to getVariationFromDeliveryRule()
Datafile changes:
- Global holdouts: includedRules == nil (applies to all rules)
- Local holdouts: includedRules == [ruleId, ...] (specific rules only)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* [FSSDK-12394] Update unit tests for Local Holdouts
Updates existing holdout tests to use includedRules instead of
includedFlags/excludedFlags:
- HoldoutTests.swift: Update sample data and decode tests
* Replace sampleDataWithIncludedFlags with sampleDataWithIncludedRules
* Replace sampleDataWithExcludedFlags with sampleDataWithDifferentRules
* Add tests for isGlobal property
- HoldoutConfigTests.swift: Complete rewrite for new model
* Test getGlobalHoldouts() returns only global holdouts
* Test getHoldoutsForRule() returns local holdouts for specific rules
* Test multiple holdouts can target the same rule
* Test rule-to-holdout mapping is built correctly
* Remove tests for removed flag-level targeting functionality
All tests verify the new Local Holdouts behavior:
- Global holdouts: includedRules == nil
- Local holdouts: includedRules == [ruleId, ...]
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* [FSSDK-12394] Add integration tests for Local Holdouts decision logic
Added comprehensive integration tests covering:
- Global holdout evaluation before all rules
- Local holdout evaluation at experiment and delivery rule level
- Multiple holdouts targeting same rule
- Cross-flag holdout targeting
- Global and local holdout interaction
- Edge cases (inactive status, non-existent rules, empty includedRules)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* [FSSDK-12394] Update all tests to use includedRules instead of includedFlags/excludedFlags
Migrated all test files from flag-level to rule-level holdout targeting:
- Replaced includedFlags/excludedFlags with includedRules
- Updated sample data to use rule IDs instead of flag IDs
- Replaced getHoldoutForFlag() calls with getHoldoutsForRule()
- Updated ProjectConfigTests to test new rule-level mapping logic
Migration strategy:
- includedFlags: [] + excludedFlags: [] → omit includedRules (nil = global)
- includedFlags: [flagId] → includedRules: [all rule IDs in that flag]
- excludedFlags: [flagId] → includedRules: [] (empty = local with no rules)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* [FSSDK-12394] Fix test failures by updating HoldoutConfig when modifying holdouts
When tests modify project.holdouts, they must also update holdoutConfig.allHoldouts
to trigger the internal map rebuilding (ruleHoldoutsMap). Without this, local holdouts
are not properly indexed by rule ID and won't be evaluated during decision-making.
Added `config.holdoutConfig.allHoldouts = [...]` after each `config.project.holdouts = [...]`
assignment in all test files.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Fix local holdout test failures
- Fix testDecideAll_with_holdout_excluded_flags: Changed includedRules from
empty array to all feature_1 rule IDs, and updated feature_3 expectations
to nil since feature_3 has no rules for local holdouts to target
- Fix testDecideAll_with_multiple_holdouts: Removed excludedHoldout which
had includedRules=[] (targets no rules), updated feature_3 expectations
to nil since local holdouts cannot apply to features with no rules
- Fix DecisionListenerTest_Holdouts setUp(): Added missing
holdoutConfig.allHoldouts assignment to trigger map rebuild
Feature_3 has no experiments or rollout rules, so local holdouts (rule-level
targeting) cannot apply to it. Only global holdouts can apply to flags with
no rules.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Fix local holdout source and experiment tracking
Problem: When local holdouts returned variations, they were being
converted to FeatureDecisions with the experiment (not holdout) and
source "feature-test" (not "holdout"), causing tests to fail.
Root cause: VariationDecision struct didn't carry information about
whether the variation came from a holdout.
Solution:
- Added 'holdout' field to VariationDecision struct
- Created DeliveryRuleDecision struct for delivery rules with holdout info
- Updated getVariationFromExperimentRule() to set holdout when returning
holdout variation
- Updated getVariationFromDeliveryRule() to use DeliveryRuleDecision and
set holdout field
- Updated getVariationForFeatureExperiments() to check for holdout and
create FeatureDecision with holdout + source "holdout" instead of
experiment + source "feature-test"
- Updated getVariationForFeatureRollout() to handle DeliveryRuleDecision
and check for holdout
This ensures holdout decisions are properly tracked through the decision
flow and returned with correct experiment ID and source.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Fix bucketing boundary issue in global holdout tests
Problem: testGetVariationForFeatureExperiment_NoExperiments and
testGetVariationForFeatureExperiment_InvalidExperimentIds were failing
because users weren't bucketing into global holdouts.
Root cause: Tests used mockBucketValue: 500 (from setUp) but
sampleHoldoutGlobal has endOfRange: 500, meaning the valid range is
0-499 (exclusive of 500). Bucket value 500 is outside this range.
Solution: Create new MockDecisionService instances in these tests with
mockBucketValue: 400, which is within the global holdout range.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* [FSSDK-12394] Fix stub tests to fail by default instead of passing
Replace XCTAssertTrue(true, ...) with XCTFail(...) for all
unimplemented test stubs. This ensures unimplemented tests are
visible in test results rather than silently passing.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* [FSSDK-12394] Add DecisionServiceTests_LocalHoldouts to Xcode project
Add the test file to OptimizelySwiftSDK.xcodeproj so tests run in
Xcode builds. File was created on filesystem but not linked in the
Xcode project, causing tests to be skipped in Xcode.
- Added PBXBuildFile entries for both test targets
- Added PBXFileReference entry
- Added to test target sources
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* [FSSDK-12394] Implement Local Holdouts integration tests
Implemented 13 integration tests for local holdouts functionality:
Global Holdouts:
- testGlobalHoldout_EvaluatedBeforeAllRules
- testGlobalHoldout_MissAllowsRuleEvaluation
Local Holdouts - Experiment Rules:
- testLocalHoldout_ExperimentRule_UserBucketed
- testLocalHoldout_ExperimentRule_UserNotBucketed
- testLocalHoldout_ExperimentRule_AudienceMismatch
Local Holdouts - Delivery Rules:
- testLocalHoldout_DeliveryRule_UserBucketed
- testLocalHoldout_DeliveryRule_UserNotBucketed
Multiple Holdouts:
- testMultipleLocalHoldouts_SameRule_FirstMatchWins
- testMultipleLocalHoldouts_DifferentRules_EachEvaluated
Cross-Flag & Precedence:
- testLocalHoldout_CrossFlag_OnlyTargetedRulesAffected
- testGlobalAndLocalHoldouts_GlobalEvaluatedFirst
- testLocalHoldout_EvaluatedAfterForcedDecision
Edge Cases:
- testLocalHoldout_InactiveStatus_NotEvaluated
Each test uses decide_datafile, MockBucketer for controlled bucketing,
and asserts correct decision behavior.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* [FSSDK-12394] Fix compilation errors in LocalHoldouts tests
Fixed two compilation errors:
1. Changed .paused to .draft (valid Status enum value)
2. Fixed testLocalHoldout_EmptyIncludedRules_TreatedAsGlobal to use
OTUtils.model instead of direct Holdout initializer
Holdout Status enum values: .draft, .running, .concluded, .archived
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: muzahidul-opti <muzahidul.islam@Optimizely.com>
0 commit comments