From 882c1d5d18bdee8810a2b5d6e92ae5ff65bc2074 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Thu, 30 Apr 2026 17:35:33 +0600 Subject: [PATCH 01/10] feat: add FeatureGates struct with localHoldouts flag Default false to suppress local holdout logic until backend is ready. Co-Authored-By: Claude Opus 4.6 --- Sources/Utils/FeatureGates.swift | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 Sources/Utils/FeatureGates.swift diff --git a/Sources/Utils/FeatureGates.swift b/Sources/Utils/FeatureGates.swift new file mode 100644 index 00000000..1f9da939 --- /dev/null +++ b/Sources/Utils/FeatureGates.swift @@ -0,0 +1,21 @@ +// +// Copyright 2026, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct FeatureGates { + static var localHoldouts = false +} From a725bd6b20892f5ecc422cf34bbc6663027de933 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Thu, 30 Apr 2026 17:40:56 +0600 Subject: [PATCH 02/10] feat: guard local holdout evaluation behind FeatureGates.localHoldouts Both experiment rule and delivery rule local holdout checks are now gated. When false, these blocks are skipped entirely. Global holdout evaluation in getDecisionForFlag() is unaffected. Co-Authored-By: Claude Opus 4.6 --- .claude/settings.local.json | 14 + FeatureGates.d | 1 + FeatureGates.o | Bin 0 -> 15376 bytes FeatureGates.swiftdeps | Bin 0 -> 18288 bytes .../DefaultDecisionService.swift | 52 +-- .../2026-04-30-local-holdout-feature-gate.md | 366 ++++++++++++++++++ 6 files changed, 409 insertions(+), 24 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 FeatureGates.d create mode 100644 FeatureGates.o create mode 100644 FeatureGates.swiftdeps create mode 100644 docs/superpowers/plans/2026-04-30-local-holdout-feature-gate.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..67ad1a5d --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,14 @@ +{ + "permissions": { + "allow": [ + "mcp__jira__getConfluencePage", + "mcp__jira__createJiraIssue", + "mcp__jira__searchConfluenceUsingCql", + "Bash(open:*)" + ] + }, + "enabledMcpjsonServers": [ + "jira", + "github" + ] +} diff --git a/FeatureGates.d b/FeatureGates.d new file mode 100644 index 00000000..5eaa20ae --- /dev/null +++ b/FeatureGates.d @@ -0,0 +1 @@ +FeatureGates.o : /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/MurmurHash3.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely/OptimizelyJSON+ObjC.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely/OptimizelyConfig+ObjC.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely/OptimizelyClient+ObjC.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely+Decide/OptimizelyUserContext+ObjC.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely/OptimizelyJSON.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Cmab.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/CMAB/CmabService.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Customization/Protocols/OPTUserProfileService.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Customization/DefaultUserProfileService.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Protocols/OPTDecisionService.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/DefaultDecisionService.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/HandlerRegistryService.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Audience/Audience.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/LogMessage.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/ODP/LruCache.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Variable.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/FeatureVariable.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/Datastore/DataStoreFile.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/ExperimentCore.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Protocols/OPTDataStore.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/DecisionResponse.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Attribute.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Audience/UserAttribute.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Audience/AttributeValue.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Audience/ConditionLeaf.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/FeatureFlag.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/CMAB/CmabConfig.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/ODP/OdpConfig.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/ProjectConfig.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/HoldoutConfig.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely/OptimizelyConfig.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/DispatchEvents/EventForDispatch.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Protocols/DataStoreQueueStack.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely/OptimizelyLogLevel.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/Datastore/DataStoreQueueStackImpl.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely+Decide/OptimizelyDecision.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Extensions/ArrayEventForDispatch+Extension.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Extensions/DataStoreQueueStackImpl+Extension.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Extensions/OptimizelyClient+Extension.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Extensions/Array+Extension.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/SDKVersion.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Audience/SemanticVersion.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/TrafficAllocation.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Variation.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Integration.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely+Decide/OptimizelyDecideOption.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/ODP/OptimizelySegmentOption.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/DecisionInfo.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Group.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/Events/BatchEventBuilder.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Audience/ConditionHolder.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely/VuidManager.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/ODP/OdpSegmentApiManager.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/ODP/OdpEventApiManager.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/ODP/OdpManager.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/ODP/OdpSegmentManager.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/ODP/OdpEventManager.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Customization/Protocols/OPTLogger.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/ThreadSafeLogger.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Customization/DefaultLogger.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Customization/Protocols/OPTEventDispatcher.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Customization/DefaultEventDispatcher.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/watchOS/WatchBackgroundNotifier.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/UserProfileTracker.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Customization/Protocols/OPTDatafileHandler.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Customization/DefaultDatafileHandler.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Protocols/OPTBucketer.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/DefaultBucketer.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Protocols/OPTNotificationCenter.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/DefaultNotificationCenter.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely/OptimizelyError.swift /Users/muzahidul.islam/workspace/swift-sdk/.build/arm64-apple-macosx/debug/Optimizely.build/DerivedSources/resource_bundle_accessor.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/FeatureGates.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/ODP/OptimizelySdkSettings.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Protocols/BackgroundingCallbacks.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/Utils.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/Notifications.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/DecisionReasons.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/Datastore/DataStoreUserDefaults.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/Constants.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Project.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely/OptimizelyResult.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/CMAB/CmabClient.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely/OptimizelyClient.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Experiment.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Event.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/DispatchEvents/BatchEvent.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/ODP/OdpEvent.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Holdout.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Rollout.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely+Decide/OptimizelyUserContext.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/AtomicArray.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/RetryStrategy.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/AtomicDictionary.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/Datastore/DataStoreMemory.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/NetworkReachability.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/AtomicProperty.swift /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/XPC.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/ObjectiveC.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/CoreData.framework/Modules/CoreData.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/Distributed.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/unistd.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/CoreImage.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/CoreTransferable.framework/Modules/CoreTransferable.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/_time.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/sys_time.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/Combine.framework/Modules/Combine.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/QuartzCore.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/_StringProcessing.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/OSLog.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/Dispatch.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/_math.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/Network.framework/Modules/Network.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/_signal.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/Metal.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/System.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/Darwin.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/Foundation.framework/Modules/Foundation.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/CoreFoundation.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/Observation.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/_stdio.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/_errno.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/CoreGraphics.framework/Modules/CoreGraphics.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/Symbols.framework/Modules/Symbols.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/os.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/UniformTypeIdentifiers.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/_Builtin_float.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/Swift.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/IOKit.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/AppKit.framework/Modules/AppKit.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/SwiftOnoneSupport.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/DeveloperToolsSupport.framework/Modules/DeveloperToolsSupport.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/CoreText.framework/Modules/CoreText.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/_Concurrency.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/Accessibility.framework/Modules/Accessibility.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/XPC.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/ObjectiveC.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/CoreData.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Distributed.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/unistd.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/CoreImage.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/CoreTransferable.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/_time.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/sys_time.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Combine.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/QuartzCore.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/_StringProcessing.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/OSLog.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Dispatch.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/_math.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Network.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/_signal.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Metal.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/System.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Darwin.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Foundation.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/CoreFoundation.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Observation.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/_stdio.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/_errno.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/CoreGraphics.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Symbols.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/os.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/UniformTypeIdentifiers.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/_Builtin_float.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Swift.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/IOKit.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/AppKit.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/SwiftOnoneSupport.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/DeveloperToolsSupport.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/CoreText.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/_Concurrency.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Accessibility.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/include/ObjectiveC.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/CoreData.framework/Headers/CoreData.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/CoreImage.framework/Headers/CoreImage.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/include/_time.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/QuartzCore.framework/Headers/QuartzCore.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/include/dispatch/Dispatch.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/Network.framework/Headers/Network.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/Metal.framework/Headers/Metal.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/Foundation.framework/Headers/Foundation.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/CoreGraphics.framework/Headers/CoreGraphics.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/ApplicationServices.framework/Headers/ApplicationServices.apinotes /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/apinotes/os.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/UniformTypeIdentifiers.framework/Headers/UniformTypeIdentifiers.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/AppKit.framework/Headers/AppKit.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/CoreText.framework/Headers/CoreText.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/Security.framework/Headers/Security.apinotes diff --git a/FeatureGates.o b/FeatureGates.o new file mode 100644 index 0000000000000000000000000000000000000000..d10497738ab35688ba938a0b4c59457191ba668b GIT binary patch literal 15376 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 CEfo;} literal 0 HcmV?d00001 diff --git a/Sources/Implementation/DefaultDecisionService.swift b/Sources/Implementation/DefaultDecisionService.swift index f2210635..7036d5cd 100644 --- a/Sources/Implementation/DefaultDecisionService.swift +++ b/Sources/Implementation/DefaultDecisionService.swift @@ -658,18 +658,20 @@ class DefaultDecisionService: OPTDecisionService { } // check local holdouts targeting this rule - let localHoldouts = config.getHoldoutsForRule(ruleId: rule.id) - for holdout in localHoldouts { - let holdoutDecision = getVariationForHoldout(config: config, - flagKey: flagKey, - holdout: holdout, - user: user, - options: options) - reasons.merge(holdoutDecision.reasons) - if let variation = holdoutDecision.result { - // User is in holdout — return holdout variation immediately, skip this rule - let variationDecision = VariationDecision(variation: variation, holdout: holdout) - return DecisionResponse(result: variationDecision, reasons: reasons) + if FeatureGates.localHoldouts { + let localHoldouts = config.getHoldoutsForRule(ruleId: rule.id) + for holdout in localHoldouts { + let holdoutDecision = getVariationForHoldout(config: config, + flagKey: flagKey, + holdout: holdout, + user: user, + options: options) + reasons.merge(holdoutDecision.reasons) + if let variation = holdoutDecision.result { + // User is in holdout — return holdout variation immediately, skip this rule + let variationDecision = VariationDecision(variation: variation, holdout: holdout) + return DecisionResponse(result: variationDecision, reasons: reasons) + } } } @@ -716,18 +718,20 @@ class DefaultDecisionService: OPTDecisionService { } // check local holdouts targeting this delivery rule - let localHoldouts = config.getHoldoutsForRule(ruleId: rule.id) - for holdout in localHoldouts { - let holdoutDecision = getVariationForHoldout(config: config, - flagKey: flagKey, - holdout: holdout, - user: user, - options: options) - reasons.merge(holdoutDecision.reasons) - if let variation = holdoutDecision.result { - // User is in holdout — return holdout variation with holdout info - let decision = DeliveryRuleDecision(variation: variation, skipToEveryoneElse: skipToEveryoneElse, holdout: holdout) - return DecisionResponse(result: decision, reasons: reasons) + if FeatureGates.localHoldouts { + let localHoldouts = config.getHoldoutsForRule(ruleId: rule.id) + for holdout in localHoldouts { + let holdoutDecision = getVariationForHoldout(config: config, + flagKey: flagKey, + holdout: holdout, + user: user, + options: options) + reasons.merge(holdoutDecision.reasons) + if let variation = holdoutDecision.result { + // User is in holdout — return holdout variation with holdout info + let decision = DeliveryRuleDecision(variation: variation, skipToEveryoneElse: skipToEveryoneElse, holdout: holdout) + return DecisionResponse(result: decision, reasons: reasons) + } } } diff --git a/docs/superpowers/plans/2026-04-30-local-holdout-feature-gate.md b/docs/superpowers/plans/2026-04-30-local-holdout-feature-gate.md new file mode 100644 index 00000000..57b36626 --- /dev/null +++ b/docs/superpowers/plans/2026-04-30-local-holdout-feature-gate.md @@ -0,0 +1,366 @@ +# Local Holdout Feature Gate Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Suppress local holdout logic behind a feature gate so the feature-rollout release ships without local holdout support. When the backend is ready, flip the gate to `true`. + +**Architecture:** Add a standalone `FeatureGates` struct with a `static var localHoldouts` flag (default `false`). Guard the two local holdout evaluation blocks in `DefaultDecisionService.swift` behind this flag. Test setUp/tearDown overrides the flag to `true` so all existing tests pass without modification. + +**Tech Stack:** Swift, XCTest + +--- + +## File Map + +| File | Action | Responsibility | +|------|--------|----------------| +| `Sources/Utils/FeatureGates.swift` | Create | Standalone `FeatureGates` struct with `localHoldouts` flag | +| `Sources/Implementation/DefaultDecisionService.swift` | Modify | Guard local holdout checks in experiment and delivery rule methods behind gate | +| `Tests/OptimizelyTests-Common/DecisionServiceTests_LocalHoldouts.swift` | Modify | Override gate to `true` in setUp/tearDown | +| `Tests/OptimizelyTests-DataModel/HoldoutConfigTests.swift` | Modify | Override gate to `true` in setUp/tearDown | +| `Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Holdouts.swift` | Modify | Override gate to `true` in setUp/tearDown | +| `Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift` | Modify | Override gate to `true` in setUp/tearDown | +| `Tests/OptimizelyTests-Common/DecisionListenerTest_Holdouts.swift` | Modify | Override gate to `true` in setUp/tearDown | + +--- + +### Task 1: Create `FeatureGates` struct + +**Files:** +- Create: `Sources/Utils/FeatureGates.swift` + +- [ ] **Step 1: Create the FeatureGates file** + +Create `Sources/Utils/FeatureGates.swift`: + +```swift +// +// Copyright 2026, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct FeatureGates { + static var localHoldouts = false +} +``` + +- [ ] **Step 2: Build to verify no compilation errors** + +Run: `swift build 2>&1 | tail -5` +Expected: Build succeeds. + +- [ ] **Step 3: Commit** + +```bash +git add Sources/Utils/FeatureGates.swift +git commit -m "feat: add FeatureGates struct with localHoldouts flag + +Default false to suppress local holdout logic until backend is ready. + +Co-Authored-By: Claude Opus 4.6 " +``` + +--- + +### Task 2: Guard local holdout checks in DefaultDecisionService + +**Files:** +- Modify: `Sources/Implementation/DefaultDecisionService.swift:660-674` (experiment rule local holdout block) +- Modify: `Sources/Implementation/DefaultDecisionService.swift:718-732` (delivery rule local holdout block) + +- [ ] **Step 1: Guard experiment rule local holdout block** + +In `getVariationFromExperimentRule()`, wrap the local holdout block (lines 660-674) with the feature gate. + +Replace: + +```swift + // check local holdouts targeting this rule + let localHoldouts = config.getHoldoutsForRule(ruleId: rule.id) + for holdout in localHoldouts { + let holdoutDecision = getVariationForHoldout(config: config, + flagKey: flagKey, + holdout: holdout, + user: user, + options: options) + reasons.merge(holdoutDecision.reasons) + if let variation = holdoutDecision.result { + // User is in holdout — return holdout variation immediately, skip this rule + let variationDecision = VariationDecision(variation: variation, holdout: holdout) + return DecisionResponse(result: variationDecision, reasons: reasons) + } + } +``` + +With: + +```swift + // check local holdouts targeting this rule + if FeatureGates.localHoldouts { + let localHoldouts = config.getHoldoutsForRule(ruleId: rule.id) + for holdout in localHoldouts { + let holdoutDecision = getVariationForHoldout(config: config, + flagKey: flagKey, + holdout: holdout, + user: user, + options: options) + reasons.merge(holdoutDecision.reasons) + if let variation = holdoutDecision.result { + // User is in holdout — return holdout variation immediately, skip this rule + let variationDecision = VariationDecision(variation: variation, holdout: holdout) + return DecisionResponse(result: variationDecision, reasons: reasons) + } + } + } +``` + +- [ ] **Step 2: Guard delivery rule local holdout block** + +In `getVariationFromDeliveryRule()`, wrap the local holdout block (lines 718-732) with the same gate. + +Replace: + +```swift + // check local holdouts targeting this delivery rule + let localHoldouts = config.getHoldoutsForRule(ruleId: rule.id) + for holdout in localHoldouts { + let holdoutDecision = getVariationForHoldout(config: config, + flagKey: flagKey, + holdout: holdout, + user: user, + options: options) + reasons.merge(holdoutDecision.reasons) + if let variation = holdoutDecision.result { + // User is in holdout — return holdout variation with holdout info + let decision = DeliveryRuleDecision(variation: variation, skipToEveryoneElse: skipToEveryoneElse, holdout: holdout) + return DecisionResponse(result: decision, reasons: reasons) + } + } +``` + +With: + +```swift + // check local holdouts targeting this delivery rule + if FeatureGates.localHoldouts { + let localHoldouts = config.getHoldoutsForRule(ruleId: rule.id) + for holdout in localHoldouts { + let holdoutDecision = getVariationForHoldout(config: config, + flagKey: flagKey, + holdout: holdout, + user: user, + options: options) + reasons.merge(holdoutDecision.reasons) + if let variation = holdoutDecision.result { + // User is in holdout — return holdout variation with holdout info + let decision = DeliveryRuleDecision(variation: variation, skipToEveryoneElse: skipToEveryoneElse, holdout: holdout) + return DecisionResponse(result: decision, reasons: reasons) + } + } + } +``` + +- [ ] **Step 3: Build to verify no compilation errors** + +Run: `swift build 2>&1 | tail -5` +Expected: Build succeeds. + +- [ ] **Step 4: Commit** + +```bash +git add Sources/Implementation/DefaultDecisionService.swift +git commit -m "feat: guard local holdout evaluation behind FeatureGates.localHoldouts + +Both experiment rule and delivery rule local holdout checks are now +gated. When false, these blocks are skipped entirely. Global holdout +evaluation in getDecisionForFlag() is unaffected. + +Co-Authored-By: Claude Opus 4.6 " +``` + +--- + +### Task 3: Override feature gate in test setUp/tearDown + +**Files:** +- Modify: `Tests/OptimizelyTests-Common/DecisionServiceTests_LocalHoldouts.swift:53-61` +- Modify: `Tests/OptimizelyTests-DataModel/HoldoutConfigTests.swift:19` +- Modify: `Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Holdouts.swift:46` +- Modify: `Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift:45` +- Modify: `Tests/OptimizelyTests-Common/DecisionListenerTest_Holdouts.swift:62` + +- [ ] **Step 1: Add gate override to DecisionServiceTests_LocalHoldouts** + +Add `FeatureGates.localHoldouts = true` at the start of `setUp()`, and add a `tearDown()` method to reset it. This class has no existing `tearDown`. + +The existing `setUp()` becomes: + +```swift + override func setUp() { + super.setUp() + FeatureGates.localHoldouts = true + + // Load a real datafile for testing + optimizely = OTUtils.createOptimizely(datafileName: "decide_datafile", + clearUserProfileService: true) + config = optimizely.config! + decisionService = optimizely.decisionService as? DefaultDecisionService + } + + override func tearDown() { + FeatureGates.localHoldouts = false + super.tearDown() + } +``` + +- [ ] **Step 2: Add gate override to HoldoutConfigTests** + +This class has no existing `setUp`/`tearDown`. Add them right after the class declaration (line 19): + +```swift +class HoldoutConfigTests: XCTestCase { + override func setUp() { + super.setUp() + FeatureGates.localHoldouts = true + } + + override func tearDown() { + FeatureGates.localHoldouts = false + super.tearDown() + } + + func testEmptyHoldouts_shouldHaveEmptyMaps() { + // ... existing code unchanged +``` + +- [ ] **Step 3: Add gate override to OptimizelyUserContextTests_Decide_Holdouts** + +Add the gate override at the start of the existing `setUp()` (after `super.setUp()`) and add a `tearDown()`. The class has no existing `tearDown`. + +```swift + override func setUp() { + super.setUp() + FeatureGates.localHoldouts = true + // ... rest of existing setUp code unchanged + } + + override func tearDown() { + FeatureGates.localHoldouts = false + super.tearDown() + } +``` + +- [ ] **Step 4: Add gate override to OptimizelyUserContextTests_Decide_With_Holdouts_Reasons** + +Add the gate override at the start of the existing `setUp()` (after `super.setUp()`) and add a `tearDown()`. The class has no existing `tearDown`. + +```swift + override func setUp() { + super.setUp() + FeatureGates.localHoldouts = true + // ... rest of existing setUp code unchanged + } + + override func tearDown() { + FeatureGates.localHoldouts = false + super.tearDown() + } +``` + +- [ ] **Step 5: Add gate override to DecisionListenerTests_Holdouts** + +Add the gate override at the start of the existing `setUp()` (after `super.setUp()`) and add a `tearDown()`. The class has no existing `tearDown`. + +```swift + override func setUp() { + super.setUp() + FeatureGates.localHoldouts = true + // ... rest of existing setUp code unchanged + } + + override func tearDown() { + FeatureGates.localHoldouts = false + super.tearDown() + } +``` + +- [ ] **Step 6: Build and run tests** + +Run: `swift test 2>&1 | tail -20` +Expected: All tests pass. Local holdout tests pass because the gate is overridden to `true` in setUp. Non-holdout tests are unaffected because the gate defaults to `false`. + +- [ ] **Step 7: Commit** + +```bash +git add Tests/OptimizelyTests-Common/DecisionServiceTests_LocalHoldouts.swift \ + Tests/OptimizelyTests-DataModel/HoldoutConfigTests.swift \ + Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Holdouts.swift \ + Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift \ + Tests/OptimizelyTests-Common/DecisionListenerTest_Holdouts.swift +git commit -m "test: override FeatureGates.localHoldouts in holdout test setUp/tearDown + +All holdout test classes set the gate to true in setUp and reset to false +in tearDown. Existing test methods are unchanged — they pass because the +gate enables the local holdout code paths during test execution. + +Co-Authored-By: Claude Opus 4.6 " +``` + +--- + +### Task 4: Verify end-to-end behavior + +**Files:** None (verification only) + +- [ ] **Step 1: Verify global holdouts still work with gate off** + +Run the global holdout tests specifically: + +```bash +xcodebuild test \ + -workspace OptimizelySwiftSDK.xcworkspace \ + -scheme OptimizelySwiftSDK-iOS \ + -destination 'platform=iOS Simulator,name=iPhone 16' \ + -only-testing:OptimizelyTests-Common-iOS/DecisionServiceTests_Holdouts 2>&1 | tail -20 +``` + +Expected: All global holdout tests pass (they don't depend on the local holdout gate). + +- [ ] **Step 2: Run full test suite** + +```bash +xcodebuild test \ + -workspace OptimizelySwiftSDK.xcworkspace \ + -scheme OptimizelySwiftSDK-iOS \ + -destination 'platform=iOS Simulator,name=iPhone 16' 2>&1 | grep -E "Test Suite|Executed|failed" +``` + +Expected: All test suites pass with 0 failures. + +--- + +## Future: Enabling Local Holdouts + +When the backend is ready, the only change needed is: + +```swift +// In FeatureGates.swift +struct FeatureGates { + static var localHoldouts = true // ← flip from false to true +} +``` + +Then remove the `setUp`/`tearDown` overrides from the 5 test files (they become no-ops but are unnecessary cleanup). From 99f66d79c71f81bd77a02a7e3604efda68479f98 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Thu, 30 Apr 2026 17:44:27 +0600 Subject: [PATCH 03/10] test: override FeatureGates.localHoldouts in holdout test setUp/tearDown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All holdout test classes set the gate to true in setUp and reset to false in tearDown. Existing test methods are unchanged — they pass because the gate enables the local holdout code paths during test execution. Co-Authored-By: Claude Opus 4.6 --- .../DecisionListenerTest_Holdouts.swift | 12 +++++++++--- .../DecisionServiceTests_LocalHoldouts.swift | 6 ++++++ .../OptimizelyUserContextTests_Decide_Holdouts.swift | 10 ++++++++-- ...erContextTests_Decide_With_Holdouts_Reasons.swift | 10 ++++++++-- .../HoldoutConfigTests.swift | 10 ++++++++++ 5 files changed, 41 insertions(+), 7 deletions(-) diff --git a/Tests/OptimizelyTests-Common/DecisionListenerTest_Holdouts.swift b/Tests/OptimizelyTests-Common/DecisionListenerTest_Holdouts.swift index 0470af28..c5bf8e56 100644 --- a/Tests/OptimizelyTests-Common/DecisionListenerTest_Holdouts.swift +++ b/Tests/OptimizelyTests-Common/DecisionListenerTest_Holdouts.swift @@ -61,13 +61,14 @@ class DecisionListenerTests_Holdouts: XCTestCase { override func setUp() { super.setUp() - + FeatureGates.localHoldouts = true + optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, eventDispatcher: eventDispatcher, userProfileService: OTUtils.createClearUserProfileService()) - + try! optimizely.start(datafile: OTUtils.loadJSONDatafile("decide_datafile")!) - + var holdout = try! OTUtils.model(from: sampleHoldout) as Holdout // Audience "13389130056" requires "country" = "US" holdout.audienceIds = ["13389130056"] @@ -79,6 +80,11 @@ class DecisionListenerTests_Holdouts: XCTestCase { self.notificationCenter = self.optimizely.notificationCenter! } + + override func tearDown() { + FeatureGates.localHoldouts = false + super.tearDown() + } func testDecisionListenerDecideWithUserInHoldout() { let exp = expectation(description: "x") diff --git a/Tests/OptimizelyTests-Common/DecisionServiceTests_LocalHoldouts.swift b/Tests/OptimizelyTests-Common/DecisionServiceTests_LocalHoldouts.swift index 0d39787a..e5d64fd1 100644 --- a/Tests/OptimizelyTests-Common/DecisionServiceTests_LocalHoldouts.swift +++ b/Tests/OptimizelyTests-Common/DecisionServiceTests_LocalHoldouts.swift @@ -52,6 +52,7 @@ class DecisionServiceTests_LocalHoldouts: XCTestCase { override func setUp() { super.setUp() + FeatureGates.localHoldouts = true // Load a real datafile for testing optimizely = OTUtils.createOptimizely(datafileName: "decide_datafile", @@ -60,6 +61,11 @@ class DecisionServiceTests_LocalHoldouts: XCTestCase { decisionService = optimizely.decisionService as? DefaultDecisionService } + override func tearDown() { + FeatureGates.localHoldouts = false + super.tearDown() + } + // MARK: - Global Holdouts Tests func testGlobalHoldout_EvaluatedBeforeAllRules() { diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Holdouts.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Holdouts.swift index a21ca3c0..2a33db0b 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Holdouts.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Holdouts.swift @@ -45,13 +45,19 @@ class OptimizelyUserContextTests_Decide_Holdouts: XCTestCase { override func setUp() { super.setUp() - + FeatureGates.localHoldouts = true + optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, eventDispatcher: eventDispatcher, userProfileService: OTUtils.createClearUserProfileService()) - + try! optimizely.start(datafile: OTUtils.loadJSONDatafile("decide_datafile")!) } + + override func tearDown() { + FeatureGates.localHoldouts = false + super.tearDown() + } func test_decide_with_global_holdout_audience_matched() { let featureKey = "feature_1" diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift index f05d7ba0..e459b4a1 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift @@ -44,12 +44,18 @@ class OptimizelyUserContextTests_Decide_With_Holdouts_Reasons: XCTestCase { override func setUp() { super.setUp() - + FeatureGates.localHoldouts = true + optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, userProfileService: OTUtils.createClearUserProfileService()) - + try! optimizely.start(datafile: OTUtils.loadJSONDatafile("decide_datafile")!) } + + override func tearDown() { + FeatureGates.localHoldouts = false + super.tearDown() + } /// Test when user is bucketed into the global holdout func testDecideReasons_userBucketedIntoGlobalHoldout() { diff --git a/Tests/OptimizelyTests-DataModel/HoldoutConfigTests.swift b/Tests/OptimizelyTests-DataModel/HoldoutConfigTests.swift index acc9f804..96d70d24 100644 --- a/Tests/OptimizelyTests-DataModel/HoldoutConfigTests.swift +++ b/Tests/OptimizelyTests-DataModel/HoldoutConfigTests.swift @@ -17,6 +17,16 @@ import XCTest class HoldoutConfigTests: XCTestCase { + override func setUp() { + super.setUp() + FeatureGates.localHoldouts = true + } + + override func tearDown() { + FeatureGates.localHoldouts = false + super.tearDown() + } + func testEmptyHoldouts_shouldHaveEmptyMaps() { let config = HoldoutConfig(allholdouts: []) From 15644501e1a96bbd1b103a9a2d7b0a28ad3a3814 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Thu, 30 Apr 2026 17:48:51 +0600 Subject: [PATCH 04/10] fix: move FeatureGates into Constants.swift for Xcode project compatibility FeatureGates.swift was not registered in the Xcode project pbxproj file, causing build failures. Moving into Constants.swift which is already included in all build targets. Co-Authored-By: Claude Opus 4.6 --- Sources/Utils/Constants.swift | 4 ++++ Sources/Utils/FeatureGates.swift | 21 --------------------- 2 files changed, 4 insertions(+), 21 deletions(-) delete mode 100644 Sources/Utils/FeatureGates.swift diff --git a/Sources/Utils/Constants.swift b/Sources/Utils/Constants.swift index 0c106b2a..db04f22c 100644 --- a/Sources/Utils/Constants.swift +++ b/Sources/Utils/Constants.swift @@ -16,6 +16,10 @@ import Foundation +struct FeatureGates { + static var localHoldouts = false +} + struct Constants { struct Attributes { static let reservedBucketIdAttribute = "$opt_bucketing_id" diff --git a/Sources/Utils/FeatureGates.swift b/Sources/Utils/FeatureGates.swift deleted file mode 100644 index 1f9da939..00000000 --- a/Sources/Utils/FeatureGates.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Copyright 2026, Optimizely, Inc. and contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -struct FeatureGates { - static var localHoldouts = false -} From 7eb16a9f0abbedb9d68b823953fe2667a92b9563 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Thu, 30 Apr 2026 17:58:19 +0600 Subject: [PATCH 05/10] test: add FeatureGates override to DecisionServiceTests_Holdouts This test class also uses local holdout data (includedRules), so it needs the gate enabled during test execution. Co-Authored-By: Claude Opus 4.6 --- .../DecisionServiceTests_Holdouts.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Tests/OptimizelyTests-Common/DecisionServiceTests_Holdouts.swift b/Tests/OptimizelyTests-Common/DecisionServiceTests_Holdouts.swift index 31f2e857..5c24fa62 100644 --- a/Tests/OptimizelyTests-Common/DecisionServiceTests_Holdouts.swift +++ b/Tests/OptimizelyTests-Common/DecisionServiceTests_Holdouts.swift @@ -189,7 +189,7 @@ class DecisionServiceTests_Holdouts: XCTestCase { override func setUp() { super.setUp() - + FeatureGates.localHoldouts = true self.optimizely = OTUtils.createOptimizely(datafileName: "empty_datafile", clearUserProfileService: true) self.config = self.optimizely.config! @@ -212,6 +212,10 @@ class DecisionServiceTests_Holdouts: XCTestCase { self.config.holdoutConfig.allHoldouts = [holdout] } + override func tearDown() { + FeatureGates.localHoldouts = false + } + } // MARK: - Test doesMeetAudienceConditions() From c56ddbf12f63037a5ff1fc08e00b34d4b41c6e5c Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Thu, 30 Apr 2026 17:59:30 +0600 Subject: [PATCH 06/10] chore: remove accidentally committed build artifacts Co-Authored-By: Claude Opus 4.6 --- FeatureGates.d | 1 - FeatureGates.o | Bin 15376 -> 0 bytes FeatureGates.swiftdeps | Bin 18288 -> 0 bytes 3 files changed, 1 deletion(-) delete mode 100644 FeatureGates.d delete mode 100644 FeatureGates.o delete mode 100644 FeatureGates.swiftdeps diff --git a/FeatureGates.d b/FeatureGates.d deleted file mode 100644 index 5eaa20ae..00000000 --- a/FeatureGates.d +++ /dev/null @@ -1 +0,0 @@ -FeatureGates.o : /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/MurmurHash3.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely/OptimizelyJSON+ObjC.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely/OptimizelyConfig+ObjC.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely/OptimizelyClient+ObjC.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely+Decide/OptimizelyUserContext+ObjC.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely/OptimizelyJSON.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Cmab.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/CMAB/CmabService.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Customization/Protocols/OPTUserProfileService.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Customization/DefaultUserProfileService.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Protocols/OPTDecisionService.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/DefaultDecisionService.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/HandlerRegistryService.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Audience/Audience.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/LogMessage.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/ODP/LruCache.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Variable.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/FeatureVariable.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/Datastore/DataStoreFile.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/ExperimentCore.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Protocols/OPTDataStore.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/DecisionResponse.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Attribute.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Audience/UserAttribute.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Audience/AttributeValue.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Audience/ConditionLeaf.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/FeatureFlag.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/CMAB/CmabConfig.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/ODP/OdpConfig.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/ProjectConfig.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/HoldoutConfig.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely/OptimizelyConfig.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/DispatchEvents/EventForDispatch.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Protocols/DataStoreQueueStack.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely/OptimizelyLogLevel.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/Datastore/DataStoreQueueStackImpl.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely+Decide/OptimizelyDecision.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Extensions/ArrayEventForDispatch+Extension.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Extensions/DataStoreQueueStackImpl+Extension.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Extensions/OptimizelyClient+Extension.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Extensions/Array+Extension.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/SDKVersion.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Audience/SemanticVersion.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/TrafficAllocation.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Variation.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Integration.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely+Decide/OptimizelyDecideOption.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/ODP/OptimizelySegmentOption.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/DecisionInfo.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Group.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/Events/BatchEventBuilder.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Audience/ConditionHolder.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely/VuidManager.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/ODP/OdpSegmentApiManager.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/ODP/OdpEventApiManager.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/ODP/OdpManager.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/ODP/OdpSegmentManager.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/ODP/OdpEventManager.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Customization/Protocols/OPTLogger.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/ThreadSafeLogger.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Customization/DefaultLogger.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Customization/Protocols/OPTEventDispatcher.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Customization/DefaultEventDispatcher.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/watchOS/WatchBackgroundNotifier.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/UserProfileTracker.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Customization/Protocols/OPTDatafileHandler.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Customization/DefaultDatafileHandler.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Protocols/OPTBucketer.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/DefaultBucketer.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Protocols/OPTNotificationCenter.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/DefaultNotificationCenter.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely/OptimizelyError.swift /Users/muzahidul.islam/workspace/swift-sdk/.build/arm64-apple-macosx/debug/Optimizely.build/DerivedSources/resource_bundle_accessor.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/FeatureGates.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/ODP/OptimizelySdkSettings.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Protocols/BackgroundingCallbacks.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/Utils.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/Notifications.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/DecisionReasons.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/Datastore/DataStoreUserDefaults.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/Constants.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Project.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely/OptimizelyResult.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/CMAB/CmabClient.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely/OptimizelyClient.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Experiment.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Event.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/DispatchEvents/BatchEvent.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/ODP/OdpEvent.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Holdout.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Data\ Model/Rollout.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Optimizely+Decide/OptimizelyUserContext.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/AtomicArray.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/RetryStrategy.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/AtomicDictionary.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Implementation/Datastore/DataStoreMemory.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/NetworkReachability.swift /Users/muzahidul.islam/workspace/swift-sdk/Sources/Utils/AtomicProperty.swift /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/XPC.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/ObjectiveC.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/CoreData.framework/Modules/CoreData.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/Distributed.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/unistd.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/CoreImage.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/CoreTransferable.framework/Modules/CoreTransferable.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/_time.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/sys_time.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/Combine.framework/Modules/Combine.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/QuartzCore.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/_StringProcessing.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/OSLog.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/Dispatch.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/_math.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/Network.framework/Modules/Network.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/_signal.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/Metal.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/System.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/Darwin.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/Foundation.framework/Modules/Foundation.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/CoreFoundation.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/Observation.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/_stdio.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/_errno.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/CoreGraphics.framework/Modules/CoreGraphics.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/Symbols.framework/Modules/Symbols.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/os.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/UniformTypeIdentifiers.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/_Builtin_float.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/Swift.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/IOKit.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/AppKit.framework/Modules/AppKit.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/SwiftOnoneSupport.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/DeveloperToolsSupport.framework/Modules/DeveloperToolsSupport.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/CoreText.framework/Modules/CoreText.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/lib/swift/_Concurrency.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/Accessibility.framework/Modules/Accessibility.swiftmodule/arm64e-apple-macos.swiftinterface /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/XPC.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/ObjectiveC.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/CoreData.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Distributed.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/unistd.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/CoreImage.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/CoreTransferable.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/_time.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/sys_time.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Combine.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/QuartzCore.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/_StringProcessing.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/OSLog.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Dispatch.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/_math.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Network.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/_signal.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Metal.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/System.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Darwin.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Foundation.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/CoreFoundation.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Observation.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/_stdio.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/_errno.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/CoreGraphics.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Symbols.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/os.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/UniformTypeIdentifiers.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/_Builtin_float.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Swift.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/IOKit.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/AppKit.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/SwiftOnoneSupport.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/DeveloperToolsSupport.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/CoreText.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/_Concurrency.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/Accessibility.swiftmodule/arm64e-apple-macos.swiftmodule /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/include/ObjectiveC.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/CoreData.framework/Headers/CoreData.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/CoreImage.framework/Headers/CoreImage.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/include/_time.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/QuartzCore.framework/Headers/QuartzCore.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/usr/include/dispatch/Dispatch.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/Network.framework/Headers/Network.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/Metal.framework/Headers/Metal.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/Foundation.framework/Headers/Foundation.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/CoreGraphics.framework/Headers/CoreGraphics.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/ApplicationServices.framework/Headers/ApplicationServices.apinotes /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/apinotes/os.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/UniformTypeIdentifiers.framework/Headers/UniformTypeIdentifiers.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/AppKit.framework/Headers/AppKit.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/CoreText.framework/Headers/CoreText.apinotes /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/Security.framework/Headers/Security.apinotes diff --git a/FeatureGates.o b/FeatureGates.o deleted file mode 100644 index d10497738ab35688ba938a0b4c59457191ba668b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15376 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 CEfo;} From b925fd88617010ac1aba2a87e67334c60984146b Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Thu, 30 Apr 2026 18:00:07 +0600 Subject: [PATCH 07/10] chore: remove local settings and plan doc from tracking Co-Authored-By: Claude Opus 4.6 --- .claude/settings.local.json | 14 - .../2026-04-30-local-holdout-feature-gate.md | 366 ------------------ 2 files changed, 380 deletions(-) delete mode 100644 .claude/settings.local.json delete mode 100644 docs/superpowers/plans/2026-04-30-local-holdout-feature-gate.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 67ad1a5d..00000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "permissions": { - "allow": [ - "mcp__jira__getConfluencePage", - "mcp__jira__createJiraIssue", - "mcp__jira__searchConfluenceUsingCql", - "Bash(open:*)" - ] - }, - "enabledMcpjsonServers": [ - "jira", - "github" - ] -} diff --git a/docs/superpowers/plans/2026-04-30-local-holdout-feature-gate.md b/docs/superpowers/plans/2026-04-30-local-holdout-feature-gate.md deleted file mode 100644 index 57b36626..00000000 --- a/docs/superpowers/plans/2026-04-30-local-holdout-feature-gate.md +++ /dev/null @@ -1,366 +0,0 @@ -# Local Holdout Feature Gate Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Suppress local holdout logic behind a feature gate so the feature-rollout release ships without local holdout support. When the backend is ready, flip the gate to `true`. - -**Architecture:** Add a standalone `FeatureGates` struct with a `static var localHoldouts` flag (default `false`). Guard the two local holdout evaluation blocks in `DefaultDecisionService.swift` behind this flag. Test setUp/tearDown overrides the flag to `true` so all existing tests pass without modification. - -**Tech Stack:** Swift, XCTest - ---- - -## File Map - -| File | Action | Responsibility | -|------|--------|----------------| -| `Sources/Utils/FeatureGates.swift` | Create | Standalone `FeatureGates` struct with `localHoldouts` flag | -| `Sources/Implementation/DefaultDecisionService.swift` | Modify | Guard local holdout checks in experiment and delivery rule methods behind gate | -| `Tests/OptimizelyTests-Common/DecisionServiceTests_LocalHoldouts.swift` | Modify | Override gate to `true` in setUp/tearDown | -| `Tests/OptimizelyTests-DataModel/HoldoutConfigTests.swift` | Modify | Override gate to `true` in setUp/tearDown | -| `Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Holdouts.swift` | Modify | Override gate to `true` in setUp/tearDown | -| `Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift` | Modify | Override gate to `true` in setUp/tearDown | -| `Tests/OptimizelyTests-Common/DecisionListenerTest_Holdouts.swift` | Modify | Override gate to `true` in setUp/tearDown | - ---- - -### Task 1: Create `FeatureGates` struct - -**Files:** -- Create: `Sources/Utils/FeatureGates.swift` - -- [ ] **Step 1: Create the FeatureGates file** - -Create `Sources/Utils/FeatureGates.swift`: - -```swift -// -// Copyright 2026, Optimizely, Inc. and contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -struct FeatureGates { - static var localHoldouts = false -} -``` - -- [ ] **Step 2: Build to verify no compilation errors** - -Run: `swift build 2>&1 | tail -5` -Expected: Build succeeds. - -- [ ] **Step 3: Commit** - -```bash -git add Sources/Utils/FeatureGates.swift -git commit -m "feat: add FeatureGates struct with localHoldouts flag - -Default false to suppress local holdout logic until backend is ready. - -Co-Authored-By: Claude Opus 4.6 " -``` - ---- - -### Task 2: Guard local holdout checks in DefaultDecisionService - -**Files:** -- Modify: `Sources/Implementation/DefaultDecisionService.swift:660-674` (experiment rule local holdout block) -- Modify: `Sources/Implementation/DefaultDecisionService.swift:718-732` (delivery rule local holdout block) - -- [ ] **Step 1: Guard experiment rule local holdout block** - -In `getVariationFromExperimentRule()`, wrap the local holdout block (lines 660-674) with the feature gate. - -Replace: - -```swift - // check local holdouts targeting this rule - let localHoldouts = config.getHoldoutsForRule(ruleId: rule.id) - for holdout in localHoldouts { - let holdoutDecision = getVariationForHoldout(config: config, - flagKey: flagKey, - holdout: holdout, - user: user, - options: options) - reasons.merge(holdoutDecision.reasons) - if let variation = holdoutDecision.result { - // User is in holdout — return holdout variation immediately, skip this rule - let variationDecision = VariationDecision(variation: variation, holdout: holdout) - return DecisionResponse(result: variationDecision, reasons: reasons) - } - } -``` - -With: - -```swift - // check local holdouts targeting this rule - if FeatureGates.localHoldouts { - let localHoldouts = config.getHoldoutsForRule(ruleId: rule.id) - for holdout in localHoldouts { - let holdoutDecision = getVariationForHoldout(config: config, - flagKey: flagKey, - holdout: holdout, - user: user, - options: options) - reasons.merge(holdoutDecision.reasons) - if let variation = holdoutDecision.result { - // User is in holdout — return holdout variation immediately, skip this rule - let variationDecision = VariationDecision(variation: variation, holdout: holdout) - return DecisionResponse(result: variationDecision, reasons: reasons) - } - } - } -``` - -- [ ] **Step 2: Guard delivery rule local holdout block** - -In `getVariationFromDeliveryRule()`, wrap the local holdout block (lines 718-732) with the same gate. - -Replace: - -```swift - // check local holdouts targeting this delivery rule - let localHoldouts = config.getHoldoutsForRule(ruleId: rule.id) - for holdout in localHoldouts { - let holdoutDecision = getVariationForHoldout(config: config, - flagKey: flagKey, - holdout: holdout, - user: user, - options: options) - reasons.merge(holdoutDecision.reasons) - if let variation = holdoutDecision.result { - // User is in holdout — return holdout variation with holdout info - let decision = DeliveryRuleDecision(variation: variation, skipToEveryoneElse: skipToEveryoneElse, holdout: holdout) - return DecisionResponse(result: decision, reasons: reasons) - } - } -``` - -With: - -```swift - // check local holdouts targeting this delivery rule - if FeatureGates.localHoldouts { - let localHoldouts = config.getHoldoutsForRule(ruleId: rule.id) - for holdout in localHoldouts { - let holdoutDecision = getVariationForHoldout(config: config, - flagKey: flagKey, - holdout: holdout, - user: user, - options: options) - reasons.merge(holdoutDecision.reasons) - if let variation = holdoutDecision.result { - // User is in holdout — return holdout variation with holdout info - let decision = DeliveryRuleDecision(variation: variation, skipToEveryoneElse: skipToEveryoneElse, holdout: holdout) - return DecisionResponse(result: decision, reasons: reasons) - } - } - } -``` - -- [ ] **Step 3: Build to verify no compilation errors** - -Run: `swift build 2>&1 | tail -5` -Expected: Build succeeds. - -- [ ] **Step 4: Commit** - -```bash -git add Sources/Implementation/DefaultDecisionService.swift -git commit -m "feat: guard local holdout evaluation behind FeatureGates.localHoldouts - -Both experiment rule and delivery rule local holdout checks are now -gated. When false, these blocks are skipped entirely. Global holdout -evaluation in getDecisionForFlag() is unaffected. - -Co-Authored-By: Claude Opus 4.6 " -``` - ---- - -### Task 3: Override feature gate in test setUp/tearDown - -**Files:** -- Modify: `Tests/OptimizelyTests-Common/DecisionServiceTests_LocalHoldouts.swift:53-61` -- Modify: `Tests/OptimizelyTests-DataModel/HoldoutConfigTests.swift:19` -- Modify: `Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Holdouts.swift:46` -- Modify: `Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift:45` -- Modify: `Tests/OptimizelyTests-Common/DecisionListenerTest_Holdouts.swift:62` - -- [ ] **Step 1: Add gate override to DecisionServiceTests_LocalHoldouts** - -Add `FeatureGates.localHoldouts = true` at the start of `setUp()`, and add a `tearDown()` method to reset it. This class has no existing `tearDown`. - -The existing `setUp()` becomes: - -```swift - override func setUp() { - super.setUp() - FeatureGates.localHoldouts = true - - // Load a real datafile for testing - optimizely = OTUtils.createOptimizely(datafileName: "decide_datafile", - clearUserProfileService: true) - config = optimizely.config! - decisionService = optimizely.decisionService as? DefaultDecisionService - } - - override func tearDown() { - FeatureGates.localHoldouts = false - super.tearDown() - } -``` - -- [ ] **Step 2: Add gate override to HoldoutConfigTests** - -This class has no existing `setUp`/`tearDown`. Add them right after the class declaration (line 19): - -```swift -class HoldoutConfigTests: XCTestCase { - override func setUp() { - super.setUp() - FeatureGates.localHoldouts = true - } - - override func tearDown() { - FeatureGates.localHoldouts = false - super.tearDown() - } - - func testEmptyHoldouts_shouldHaveEmptyMaps() { - // ... existing code unchanged -``` - -- [ ] **Step 3: Add gate override to OptimizelyUserContextTests_Decide_Holdouts** - -Add the gate override at the start of the existing `setUp()` (after `super.setUp()`) and add a `tearDown()`. The class has no existing `tearDown`. - -```swift - override func setUp() { - super.setUp() - FeatureGates.localHoldouts = true - // ... rest of existing setUp code unchanged - } - - override func tearDown() { - FeatureGates.localHoldouts = false - super.tearDown() - } -``` - -- [ ] **Step 4: Add gate override to OptimizelyUserContextTests_Decide_With_Holdouts_Reasons** - -Add the gate override at the start of the existing `setUp()` (after `super.setUp()`) and add a `tearDown()`. The class has no existing `tearDown`. - -```swift - override func setUp() { - super.setUp() - FeatureGates.localHoldouts = true - // ... rest of existing setUp code unchanged - } - - override func tearDown() { - FeatureGates.localHoldouts = false - super.tearDown() - } -``` - -- [ ] **Step 5: Add gate override to DecisionListenerTests_Holdouts** - -Add the gate override at the start of the existing `setUp()` (after `super.setUp()`) and add a `tearDown()`. The class has no existing `tearDown`. - -```swift - override func setUp() { - super.setUp() - FeatureGates.localHoldouts = true - // ... rest of existing setUp code unchanged - } - - override func tearDown() { - FeatureGates.localHoldouts = false - super.tearDown() - } -``` - -- [ ] **Step 6: Build and run tests** - -Run: `swift test 2>&1 | tail -20` -Expected: All tests pass. Local holdout tests pass because the gate is overridden to `true` in setUp. Non-holdout tests are unaffected because the gate defaults to `false`. - -- [ ] **Step 7: Commit** - -```bash -git add Tests/OptimizelyTests-Common/DecisionServiceTests_LocalHoldouts.swift \ - Tests/OptimizelyTests-DataModel/HoldoutConfigTests.swift \ - Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Holdouts.swift \ - Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift \ - Tests/OptimizelyTests-Common/DecisionListenerTest_Holdouts.swift -git commit -m "test: override FeatureGates.localHoldouts in holdout test setUp/tearDown - -All holdout test classes set the gate to true in setUp and reset to false -in tearDown. Existing test methods are unchanged — they pass because the -gate enables the local holdout code paths during test execution. - -Co-Authored-By: Claude Opus 4.6 " -``` - ---- - -### Task 4: Verify end-to-end behavior - -**Files:** None (verification only) - -- [ ] **Step 1: Verify global holdouts still work with gate off** - -Run the global holdout tests specifically: - -```bash -xcodebuild test \ - -workspace OptimizelySwiftSDK.xcworkspace \ - -scheme OptimizelySwiftSDK-iOS \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - -only-testing:OptimizelyTests-Common-iOS/DecisionServiceTests_Holdouts 2>&1 | tail -20 -``` - -Expected: All global holdout tests pass (they don't depend on the local holdout gate). - -- [ ] **Step 2: Run full test suite** - -```bash -xcodebuild test \ - -workspace OptimizelySwiftSDK.xcworkspace \ - -scheme OptimizelySwiftSDK-iOS \ - -destination 'platform=iOS Simulator,name=iPhone 16' 2>&1 | grep -E "Test Suite|Executed|failed" -``` - -Expected: All test suites pass with 0 failures. - ---- - -## Future: Enabling Local Holdouts - -When the backend is ready, the only change needed is: - -```swift -// In FeatureGates.swift -struct FeatureGates { - static var localHoldouts = true // ← flip from false to true -} -``` - -Then remove the `setUp`/`tearDown` overrides from the 5 test files (they become no-ops but are unnecessary cleanup). From dd8442da27da9774916c30b64f98bf307d8f4777 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Thu, 30 Apr 2026 18:12:33 +0600 Subject: [PATCH 08/10] fix: add missing super.tearDown() and remove unnecessary gate override - Added super.tearDown() to DecisionServiceTests_Holdouts tearDown - Removed FeatureGates override from HoldoutConfigTests since it tests the data model directly, not the gated decision service path Co-Authored-By: Claude Opus 4.6 --- .../DecisionServiceTests_Holdouts.swift | 1 + .../HoldoutConfigTests.swift | 11 +---------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/Tests/OptimizelyTests-Common/DecisionServiceTests_Holdouts.swift b/Tests/OptimizelyTests-Common/DecisionServiceTests_Holdouts.swift index 5c24fa62..c6028120 100644 --- a/Tests/OptimizelyTests-Common/DecisionServiceTests_Holdouts.swift +++ b/Tests/OptimizelyTests-Common/DecisionServiceTests_Holdouts.swift @@ -214,6 +214,7 @@ class DecisionServiceTests_Holdouts: XCTestCase { override func tearDown() { FeatureGates.localHoldouts = false + super.tearDown() } } diff --git a/Tests/OptimizelyTests-DataModel/HoldoutConfigTests.swift b/Tests/OptimizelyTests-DataModel/HoldoutConfigTests.swift index 96d70d24..b9d02535 100644 --- a/Tests/OptimizelyTests-DataModel/HoldoutConfigTests.swift +++ b/Tests/OptimizelyTests-DataModel/HoldoutConfigTests.swift @@ -17,16 +17,7 @@ import XCTest class HoldoutConfigTests: XCTestCase { - override func setUp() { - super.setUp() - FeatureGates.localHoldouts = true - } - - override func tearDown() { - FeatureGates.localHoldouts = false - super.tearDown() - } - + func testEmptyHoldouts_shouldHaveEmptyMaps() { let config = HoldoutConfig(allholdouts: []) From 29fea983d7bc231f00c54b71d0f5ce7fd6696b70 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Mon, 4 May 2026 19:13:38 +0600 Subject: [PATCH 09/10] [FSSDK-12524] Add base test class for holdout tests with feature gate override Created BaseHoldoutTests class that automatically enables the FeatureGates.localHoldouts flag in setUp() and resets it in tearDown(). Updated all holdout test classes to inherit from this base class, removing duplicate feature gate setup code from individual test files. This centralizes feature gate management and ensures all holdout tests run with the feature enabled consistently. Co-Authored-By: Claude Sonnet 4.5 --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 10 +++++++ .../BaseHoldoutTests.swift | 27 +++++++++++++++++++ .../DecisionListenerTest_Holdouts.swift | 5 +--- .../DecisionServiceTests_Holdouts.swift | 4 +-- .../DecisionServiceTests_LocalHoldouts.swift | 5 +--- ...zelyUserContextTests_Decide_Holdouts.swift | 5 +--- ...xtTests_Decide_With_Holdouts_Reasons.swift | 4 +-- .../HoldoutConfigTests.swift | 1 - 8 files changed, 42 insertions(+), 19 deletions(-) create mode 100644 Tests/OptimizelyTests-Common/BaseHoldoutTests.swift diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 828d69b5..320a03c8 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -2121,6 +2121,10 @@ 98AC985F2DBA6721001405DD /* OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC985D2DBA6721001405DD /* OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift */; }; 98C2DF242F900669003F2443 /* DecisionServiceTests_LocalHoldouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C2DF232F900669003F2443 /* DecisionServiceTests_LocalHoldouts.swift */; }; 98C2DF252F900669003F2443 /* DecisionServiceTests_LocalHoldouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C2DF232F900669003F2443 /* DecisionServiceTests_LocalHoldouts.swift */; }; + 98C2DF752FA8D055003F2443 /* BaseHoldoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C2DF742FA8D055003F2443 /* BaseHoldoutTests.swift */; }; + 98C2DF762FA8D055003F2443 /* BaseHoldoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C2DF742FA8D055003F2443 /* BaseHoldoutTests.swift */; }; + 98C2DF772FA8D1FF003F2443 /* BaseHoldoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C2DF742FA8D055003F2443 /* BaseHoldoutTests.swift */; }; + 98C2DF782FA8D207003F2443 /* BaseHoldoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C2DF742FA8D055003F2443 /* BaseHoldoutTests.swift */; }; 98D5AE842DBB91C0000D5844 /* OptimizelyUserContextTests_Decide_Holdouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98D5AE832DBB91C0000D5844 /* OptimizelyUserContextTests_Decide_Holdouts.swift */; }; 98D5AE852DBB91C0000D5844 /* OptimizelyUserContextTests_Decide_Holdouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98D5AE832DBB91C0000D5844 /* OptimizelyUserContextTests_Decide_Holdouts.swift */; }; 98F28A1D2E01940500A86546 /* Cmab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F28A1C2E01940500A86546 /* Cmab.swift */; }; @@ -2638,6 +2642,7 @@ 98AC98482DB8FC29001405DD /* DecisionServiceTests_Holdouts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecisionServiceTests_Holdouts.swift; sourceTree = ""; }; 98AC985D2DBA6721001405DD /* OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift; sourceTree = ""; }; 98C2DF232F900669003F2443 /* DecisionServiceTests_LocalHoldouts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecisionServiceTests_LocalHoldouts.swift; sourceTree = ""; }; + 98C2DF742FA8D055003F2443 /* BaseHoldoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseHoldoutTests.swift; sourceTree = ""; }; 98D5AE832DBB91C0000D5844 /* OptimizelyUserContextTests_Decide_Holdouts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Decide_Holdouts.swift; sourceTree = ""; }; 98F28A1C2E01940500A86546 /* Cmab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cmab.swift; sourceTree = ""; }; 98F28A2D2E01968000A86546 /* CmabTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmabTests.swift; sourceTree = ""; }; @@ -3169,6 +3174,7 @@ 6E75198022C5211100B2B157 /* DecisionServiceTests_Experiments.swift */, 6E75199122C5211100B2B157 /* DecisionServiceTests_Features.swift */, 98AC98482DB8FC29001405DD /* DecisionServiceTests_Holdouts.swift */, + 98C2DF742FA8D055003F2443 /* BaseHoldoutTests.swift */, 6E75199422C5211100B2B157 /* DecisionServiceTests_Others.swift */, 6E75198622C5211100B2B157 /* DecisionServiceTests_UserProfiles.swift */, 6E75198822C5211100B2B157 /* DefaultLoggerTests.swift */, @@ -5124,6 +5130,7 @@ 6E75192D22C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, 6E7516D322C520D400B2B157 /* OPTLogger.swift in Sources */, 6E75180122C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, + 98C2DF752FA8D055003F2443 /* BaseHoldoutTests.swift in Sources */, 98F28A672E05220300A86546 /* CmabServiceTests.swift in Sources */, 98AC98472DB7B762001405DD /* BucketTests_HoldoutToVariation.swift in Sources */, 6E75175722C520D400B2B157 /* LogMessage.swift in Sources */, @@ -5294,6 +5301,7 @@ 6E7516EC22C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E75181A22C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6EF8DE2624BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, + 98C2DF782FA8D207003F2443 /* BaseHoldoutTests.swift in Sources */, 8464087E28130D3200CCF97D /* Integration.swift in Sources */, 6E9B119722C5488300C22D81 /* ConditionLeafTests.swift in Sources */, 6E75184A22C520D400B2B157 /* Event.swift in Sources */, @@ -5428,6 +5436,7 @@ 6E75193322C520D500B2B157 /* OPTDataStore.swift in Sources */, 84861811286D0B8900B7F41B /* OdpSegmentManagerTests.swift in Sources */, 6E7517EF22C520D400B2B157 /* DataStoreMemory.swift in Sources */, + 98C2DF762FA8D055003F2443 /* BaseHoldoutTests.swift in Sources */, 98F28A682E05220300A86546 /* CmabServiceTests.swift in Sources */, 6E75194B22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E75195722C520D500B2B157 /* OPTBucketer.swift in Sources */, @@ -5572,6 +5581,7 @@ 6E75193522C520D500B2B157 /* OPTDataStore.swift in Sources */, 6EC6DD4824ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, 6E75182122C520D400B2B157 /* BatchEventBuilder.swift in Sources */, + 98C2DF772FA8D1FF003F2443 /* BaseHoldoutTests.swift in Sources */, 983F81852F801E7500CDBC8D /* FeatureRolloutTests.swift in Sources */, 6E86CEA924FDC847005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E9B118322C5488100C22D81 /* UserAttributeTests_Evaluate.swift in Sources */, diff --git a/Tests/OptimizelyTests-Common/BaseHoldoutTests.swift b/Tests/OptimizelyTests-Common/BaseHoldoutTests.swift new file mode 100644 index 00000000..4a64c1d3 --- /dev/null +++ b/Tests/OptimizelyTests-Common/BaseHoldoutTests.swift @@ -0,0 +1,27 @@ +// +// Copyright 2022, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +class BaseHoldoutTests: XCTestCase { + override func setUp() { + FeatureGates.localHoldouts = true + } + + override func tearDown() { + FeatureGates.localHoldouts = false + } +} diff --git a/Tests/OptimizelyTests-Common/DecisionListenerTest_Holdouts.swift b/Tests/OptimizelyTests-Common/DecisionListenerTest_Holdouts.swift index c5bf8e56..1f9bc14d 100644 --- a/Tests/OptimizelyTests-Common/DecisionListenerTest_Holdouts.swift +++ b/Tests/OptimizelyTests-Common/DecisionListenerTest_Holdouts.swift @@ -16,7 +16,7 @@ import XCTest -class DecisionListenerTests_Holdouts: XCTestCase { +class DecisionListenerTests_Holdouts: BaseHoldoutTests { let kUserId = "11111" var optimizely: OptimizelyClient! var notificationCenter: OPTNotificationCenter! @@ -61,8 +61,6 @@ class DecisionListenerTests_Holdouts: XCTestCase { override func setUp() { super.setUp() - FeatureGates.localHoldouts = true - optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, eventDispatcher: eventDispatcher, userProfileService: OTUtils.createClearUserProfileService()) @@ -82,7 +80,6 @@ class DecisionListenerTests_Holdouts: XCTestCase { } override func tearDown() { - FeatureGates.localHoldouts = false super.tearDown() } diff --git a/Tests/OptimizelyTests-Common/DecisionServiceTests_Holdouts.swift b/Tests/OptimizelyTests-Common/DecisionServiceTests_Holdouts.swift index c6028120..62907151 100644 --- a/Tests/OptimizelyTests-Common/DecisionServiceTests_Holdouts.swift +++ b/Tests/OptimizelyTests-Common/DecisionServiceTests_Holdouts.swift @@ -16,7 +16,7 @@ import XCTest -class DecisionServiceTests_Holdouts: XCTestCase { +class DecisionServiceTests_Holdouts: BaseHoldoutTests { var optimizely: OptimizelyClient! var config: ProjectConfig! @@ -189,7 +189,6 @@ class DecisionServiceTests_Holdouts: XCTestCase { override func setUp() { super.setUp() - FeatureGates.localHoldouts = true self.optimizely = OTUtils.createOptimizely(datafileName: "empty_datafile", clearUserProfileService: true) self.config = self.optimizely.config! @@ -213,7 +212,6 @@ class DecisionServiceTests_Holdouts: XCTestCase { } override func tearDown() { - FeatureGates.localHoldouts = false super.tearDown() } diff --git a/Tests/OptimizelyTests-Common/DecisionServiceTests_LocalHoldouts.swift b/Tests/OptimizelyTests-Common/DecisionServiceTests_LocalHoldouts.swift index e5d64fd1..f652b766 100644 --- a/Tests/OptimizelyTests-Common/DecisionServiceTests_LocalHoldouts.swift +++ b/Tests/OptimizelyTests-Common/DecisionServiceTests_LocalHoldouts.swift @@ -19,7 +19,7 @@ import XCTest /// Integration tests for Local Holdouts functionality /// Tests that local holdouts are correctly evaluated at the rule level /// and global holdouts are evaluated at the flag level before any rules -class DecisionServiceTests_LocalHoldouts: XCTestCase { +class DecisionServiceTests_LocalHoldouts: BaseHoldoutTests { var optimizely: OptimizelyClient! var config: ProjectConfig! @@ -52,8 +52,6 @@ class DecisionServiceTests_LocalHoldouts: XCTestCase { override func setUp() { super.setUp() - FeatureGates.localHoldouts = true - // Load a real datafile for testing optimizely = OTUtils.createOptimizely(datafileName: "decide_datafile", clearUserProfileService: true) @@ -62,7 +60,6 @@ class DecisionServiceTests_LocalHoldouts: XCTestCase { } override func tearDown() { - FeatureGates.localHoldouts = false super.tearDown() } diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Holdouts.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Holdouts.swift index 2a33db0b..b4993be6 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Holdouts.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Holdouts.swift @@ -16,7 +16,7 @@ import XCTest -class OptimizelyUserContextTests_Decide_Holdouts: XCTestCase { +class OptimizelyUserContextTests_Decide_Holdouts: BaseHoldoutTests { let kUserId = "tester" var optimizely: OptimizelyClient! var eventDispatcher = MockEventDispatcher() @@ -45,8 +45,6 @@ class OptimizelyUserContextTests_Decide_Holdouts: XCTestCase { override func setUp() { super.setUp() - FeatureGates.localHoldouts = true - optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, eventDispatcher: eventDispatcher, userProfileService: OTUtils.createClearUserProfileService()) @@ -55,7 +53,6 @@ class OptimizelyUserContextTests_Decide_Holdouts: XCTestCase { } override func tearDown() { - FeatureGates.localHoldouts = false super.tearDown() } diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift index e459b4a1..6e57e7bd 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift @@ -16,7 +16,7 @@ import XCTest -class OptimizelyUserContextTests_Decide_With_Holdouts_Reasons: XCTestCase { +class OptimizelyUserContextTests_Decide_With_Holdouts_Reasons: BaseHoldoutTests { let kUserId = "tester" var optimizely: OptimizelyClient! @@ -44,7 +44,6 @@ class OptimizelyUserContextTests_Decide_With_Holdouts_Reasons: XCTestCase { override func setUp() { super.setUp() - FeatureGates.localHoldouts = true optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, userProfileService: OTUtils.createClearUserProfileService()) @@ -53,7 +52,6 @@ class OptimizelyUserContextTests_Decide_With_Holdouts_Reasons: XCTestCase { } override func tearDown() { - FeatureGates.localHoldouts = false super.tearDown() } diff --git a/Tests/OptimizelyTests-DataModel/HoldoutConfigTests.swift b/Tests/OptimizelyTests-DataModel/HoldoutConfigTests.swift index b9d02535..acc9f804 100644 --- a/Tests/OptimizelyTests-DataModel/HoldoutConfigTests.swift +++ b/Tests/OptimizelyTests-DataModel/HoldoutConfigTests.swift @@ -17,7 +17,6 @@ import XCTest class HoldoutConfigTests: XCTestCase { - func testEmptyHoldouts_shouldHaveEmptyMaps() { let config = HoldoutConfig(allholdouts: []) From 899b77fb2cc9b67c3489c5030e699f4b5bb42f6b Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Mon, 4 May 2026 21:57:24 +0600 Subject: [PATCH 10/10] [FSSDK-12524] Add feature gate verification tests Created FeatureGateTests_LocalHoldouts with 5 tests verifying: - Local holdouts are skipped when FeatureGates.localHoldouts = false - Local holdouts are evaluated when FeatureGates.localHoldouts = true - Global holdouts work regardless of flag state Updated CLAUDE.md with concise guidance on properly linking new test files to Xcode targets to prevent integration issues. Addresses PR #631 review feedback from Raju. Co-Authored-By: Claude Sonnet 4.5 --- CLAUDE.md | 16 ++ OptimizelySwiftSDK.xcodeproj/project.pbxproj | 6 + .../FeatureGateTests_LocalHoldouts.swift | 224 ++++++++++++++++++ 3 files changed, 246 insertions(+) create mode 100644 Tests/OptimizelyTests-Common/FeatureGateTests_LocalHoldouts.swift diff --git a/CLAUDE.md b/CLAUDE.md index da0c60fb..b9625ed5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -174,6 +174,22 @@ Similar test targets exist for tvOS and other platforms. - Use JSON fixtures from `Tests/TestData/` for consistent test data - Each test should use unique file names for persistent storage +### Adding New Test Files + +**CRITICAL**: New test files must be manually added to `OptimizelySwiftSDK.xcodeproj/project.pbxproj` - file creation alone is insufficient. + +**Steps**: +1. Create test file in appropriate directory (`Tests/OptimizelyTests-Common/`, etc.) +2. Generate unique IDs: `uuidgen | tr 'A-F' 'a-f' | tr -d '-' | cut -c1-24 | awk '{print toupper($0)}'` +3. Edit `project.pbxproj` following pattern of similar files (e.g., `DecisionServiceTests_LocalHoldouts.swift`): + - Add PBXBuildFile entries (~line 2120) - one per target (iOS, tvOS) + - Add PBXFileReference entry (~line 2640) + - Add to file group listing (~line 3180) + - Add to PBXSourcesBuildPhase for each target (~lines 5200, 5510) +4. Verify: `swift build && swift test --filter YourTestClass` + +**Pattern**: Common tests need 2 targets (iOS + tvOS); base classes may need 4 targets. + ## Development Workflow ### Branch Strategy diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 320a03c8..27dbcf31 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -2121,6 +2121,8 @@ 98AC985F2DBA6721001405DD /* OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC985D2DBA6721001405DD /* OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift */; }; 98C2DF242F900669003F2443 /* DecisionServiceTests_LocalHoldouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C2DF232F900669003F2443 /* DecisionServiceTests_LocalHoldouts.swift */; }; 98C2DF252F900669003F2443 /* DecisionServiceTests_LocalHoldouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C2DF232F900669003F2443 /* DecisionServiceTests_LocalHoldouts.swift */; }; + 0921CDD1BC7F41F59B2D4CC3 /* FeatureGateTests_LocalHoldouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C5D873BB4C44181B07ECC15 /* FeatureGateTests_LocalHoldouts.swift */; }; + BB21E5C3E29D4F0C892E523C /* FeatureGateTests_LocalHoldouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C5D873BB4C44181B07ECC15 /* FeatureGateTests_LocalHoldouts.swift */; }; 98C2DF752FA8D055003F2443 /* BaseHoldoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C2DF742FA8D055003F2443 /* BaseHoldoutTests.swift */; }; 98C2DF762FA8D055003F2443 /* BaseHoldoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C2DF742FA8D055003F2443 /* BaseHoldoutTests.swift */; }; 98C2DF772FA8D1FF003F2443 /* BaseHoldoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C2DF742FA8D055003F2443 /* BaseHoldoutTests.swift */; }; @@ -2642,6 +2644,7 @@ 98AC98482DB8FC29001405DD /* DecisionServiceTests_Holdouts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecisionServiceTests_Holdouts.swift; sourceTree = ""; }; 98AC985D2DBA6721001405DD /* OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift; sourceTree = ""; }; 98C2DF232F900669003F2443 /* DecisionServiceTests_LocalHoldouts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecisionServiceTests_LocalHoldouts.swift; sourceTree = ""; }; + 7C5D873BB4C44181B07ECC15 /* FeatureGateTests_LocalHoldouts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureGateTests_LocalHoldouts.swift; sourceTree = ""; }; 98C2DF742FA8D055003F2443 /* BaseHoldoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseHoldoutTests.swift; sourceTree = ""; }; 98D5AE832DBB91C0000D5844 /* OptimizelyUserContextTests_Decide_Holdouts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Decide_Holdouts.swift; sourceTree = ""; }; 98F28A1C2E01940500A86546 /* Cmab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cmab.swift; sourceTree = ""; }; @@ -3187,6 +3190,7 @@ 6E75198B22C5211100B2B157 /* NotificationCenterTests.swift */, 84861810286D0B8900B7F41B /* OdpEventManagerTests.swift */, 98C2DF232F900669003F2443 /* DecisionServiceTests_LocalHoldouts.swift */, + 7C5D873BB4C44181B07ECC15 /* FeatureGateTests_LocalHoldouts.swift */, 8486180E286D0B8900B7F41B /* OdpManagerTests.swift */, 8486180D286D0B8900B7F41B /* OdpSegmentManagerTests.swift */, 98F28A512E02E81500A86546 /* CMABClientTests.swift */, @@ -5203,6 +5207,7 @@ 6E75187922C520D400B2B157 /* Variation.swift in Sources */, 6E75191522C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, 98C2DF242F900669003F2443 /* DecisionServiceTests_LocalHoldouts.swift in Sources */, + 0921CDD1BC7F41F59B2D4CC3 /* FeatureGateTests_LocalHoldouts.swift in Sources */, 6E75195D22C520D500B2B157 /* OPTBucketer.swift in Sources */, 6E9B117622C5487100C22D81 /* DatafileHandlerTests.swift in Sources */, 84E2E97F2855875E001114AB /* OdpEventManager.swift in Sources */, @@ -5509,6 +5514,7 @@ 6E7516FD22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E75187322C520D400B2B157 /* Variation.swift in Sources */, 98C2DF252F900669003F2443 /* DecisionServiceTests_LocalHoldouts.swift in Sources */, + BB21E5C3E29D4F0C892E523C /* FeatureGateTests_LocalHoldouts.swift in Sources */, 6E7517E322C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E75179922C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E9B115C22C5486E00C22D81 /* DatafileHandlerTests.swift in Sources */, diff --git a/Tests/OptimizelyTests-Common/FeatureGateTests_LocalHoldouts.swift b/Tests/OptimizelyTests-Common/FeatureGateTests_LocalHoldouts.swift new file mode 100644 index 00000000..6081cef2 --- /dev/null +++ b/Tests/OptimizelyTests-Common/FeatureGateTests_LocalHoldouts.swift @@ -0,0 +1,224 @@ +// +// Copyright 2026, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +/// Tests to verify FeatureGates.localHoldouts flag behavior +/// These tests do NOT inherit from BaseHoldoutTests to control the flag state directly +class FeatureGateTests_LocalHoldouts: XCTestCase { + + var optimizely: OptimizelyClient! + var config: ProjectConfig! + + let userId = "test_user" + let flagKey = "feature_1" + let experimentRuleId = "10390977673" // From decide_datafile + let deliveryRuleId = "3332020515" // From decide_datafile + + var sampleHoldout: [String: Any] { + return [ + "status": "Running", + "id": "holdout_test_id", + "key": "holdout_test_key", + "trafficAllocation": [ + ["entityId": "holdout_variation_id", "endOfRange": 5000] // 50% traffic + ], + "audienceIds": [], + "variations": [ + [ + "variables": [], + "id": "holdout_variation_id", + "key": "holdout_variation_key", + "featureEnabled": false + ] + ] + ] + } + + override func setUp() { + super.setUp() + + optimizely = OTUtils.createOptimizely(datafileName: "decide_datafile", + clearUserProfileService: true) + config = optimizely.config! + } + + override func tearDown() { + // Always reset flag to false to prevent test pollution + FeatureGates.localHoldouts = false + super.tearDown() + } + + // MARK: - Flag OFF Tests (Local Holdouts Should Be Skipped) + + func testLocalHoldoutsSkippedWhenFlagOff_ExperimentRule() { + // Setup: Flag is OFF + FeatureGates.localHoldouts = false + + // Create local holdout targeting experiment rule + var holdout = try! OTUtils.model(from: sampleHoldout) as Holdout + holdout.includedRules = [experimentRuleId] + config.project.holdouts = [holdout] + config.holdoutConfig.allHoldouts = [holdout] + + // Mock bucketer to ensure user WOULD bucket into holdout if it were evaluated + let mockBucketer = MockBucketer(mockBucketValue: 2500) // Within holdout range (0-5000) + let mockDecisionService = DefaultDecisionService( + userProfileService: OTUtils.createClearUserProfileService(), + bucketer: mockBucketer + ) + optimizely.decisionService = mockDecisionService + + // Execute decision + let user = optimizely.createUserContext(userId: userId) + let decision = user.decide(key: flagKey) + + // Verify: User did NOT get holdout variation (flag is off, so holdout skipped) + XCTAssertNotEqual(decision.variationKey, "holdout_variation_key", + "Local holdout should be skipped when FeatureGates.localHoldouts = false") + XCTAssertNotEqual(decision.ruleKey, "holdout_test_key", + "Should get experiment rule, not holdout") + } + + func testLocalHoldoutsSkippedWhenFlagOff_DeliveryRule() { + // Setup: Flag is OFF + FeatureGates.localHoldouts = false + + // Create local holdout targeting delivery rule + var holdout = try! OTUtils.model(from: sampleHoldout) as Holdout + holdout.includedRules = [deliveryRuleId] + config.project.holdouts = [holdout] + config.holdoutConfig.allHoldouts = [holdout] + + // Mock bucketer to ensure user WOULD bucket into holdout if it were evaluated + let mockBucketer = MockBucketer(mockBucketValue: 2500) + let mockDecisionService = DefaultDecisionService( + userProfileService: OTUtils.createClearUserProfileService(), + bucketer: mockBucketer + ) + optimizely.decisionService = mockDecisionService + + // Execute decision + let user = optimizely.createUserContext(userId: userId) + let decision = user.decide(key: flagKey) + + // Verify: User did NOT get holdout variation + XCTAssertNotEqual(decision.variationKey, "holdout_variation_key", + "Local holdout should be skipped when FeatureGates.localHoldouts = false") + XCTAssertNotEqual(decision.ruleKey, "holdout_test_key", + "Should get delivery rule, not holdout") + } + + // MARK: - Flag ON Tests (Local Holdouts Should Be Evaluated) + + func testLocalHoldoutsEvaluatedWhenFlagOn_ExperimentRule() { + // Setup: Flag is ON + FeatureGates.localHoldouts = true + + // Create local holdout targeting experiment rule + var holdout = try! OTUtils.model(from: sampleHoldout) as Holdout + holdout.includedRules = [experimentRuleId] + config.project.holdouts = [holdout] + config.holdoutConfig.allHoldouts = [holdout] + + // Mock bucketer to bucket user into holdout + let mockBucketer = MockBucketer(mockBucketValue: 2500) + let mockDecisionService = DefaultDecisionService( + userProfileService: OTUtils.createClearUserProfileService(), + bucketer: mockBucketer + ) + optimizely.decisionService = mockDecisionService + + // Execute decision + let user = optimizely.createUserContext(userId: userId) + let decision = user.decide(key: flagKey) + + // Verify: User DID get holdout variation (flag is on) + XCTAssertEqual(decision.variationKey, "holdout_variation_key", + "Local holdout should be evaluated when FeatureGates.localHoldouts = true") + XCTAssertEqual(decision.ruleKey, "holdout_test_key", + "Should get holdout, not experiment rule") + XCTAssertFalse(decision.enabled, "Holdout variation has featureEnabled: false") + } + + func testLocalHoldoutsEvaluatedWhenFlagOn_DeliveryRule() { + // Setup: Flag is ON + FeatureGates.localHoldouts = true + + // Create local holdout targeting delivery rule + var holdout = try! OTUtils.model(from: sampleHoldout) as Holdout + holdout.includedRules = [deliveryRuleId] + config.project.holdouts = [holdout] + config.holdoutConfig.allHoldouts = [holdout] + + // Mock bucketer to bucket user into holdout + let mockBucketer = MockBucketer(mockBucketValue: 2500) + let mockDecisionService = DefaultDecisionService( + userProfileService: OTUtils.createClearUserProfileService(), + bucketer: mockBucketer + ) + optimizely.decisionService = mockDecisionService + + // Execute decision + let user = optimizely.createUserContext(userId: userId) + let decision = user.decide(key: flagKey) + + // Verify: User DID get holdout variation (flag is on) + XCTAssertEqual(decision.variationKey, "holdout_variation_key", + "Local holdout should be evaluated when FeatureGates.localHoldouts = true") + XCTAssertEqual(decision.ruleKey, "holdout_test_key", + "Should get holdout, not delivery rule") + XCTAssertFalse(decision.enabled, "Holdout variation has featureEnabled: false") + } + + // MARK: - Global Holdouts (Flag State Should Not Matter) + + func testGlobalHoldoutsWorkRegardlessOfFlagState() { + // Create global holdout (no includedRules) + var holdout = try! OTUtils.model(from: sampleHoldout) as Holdout + holdout.includedRules = nil // Global holdout + config.project.holdouts = [holdout] + config.holdoutConfig.allHoldouts = [holdout] + + // Mock bucketer to bucket user into holdout + let mockBucketer = MockBucketer(mockBucketValue: 2500) + let mockDecisionService = DefaultDecisionService( + userProfileService: OTUtils.createClearUserProfileService(), + bucketer: mockBucketer + ) + optimizely.decisionService = mockDecisionService + + // Test with flag OFF + FeatureGates.localHoldouts = false + let user1 = optimizely.createUserContext(userId: userId) + let decision1 = user1.decide(key: flagKey) + + XCTAssertEqual(decision1.variationKey, "holdout_variation_key", + "Global holdout should work when flag is OFF") + XCTAssertEqual(decision1.ruleKey, "holdout_test_key", + "Should get global holdout regardless of flag state") + + // Test with flag ON + FeatureGates.localHoldouts = true + let user2 = optimizely.createUserContext(userId: userId + "_2") + let decision2 = user2.decide(key: flagKey) + + XCTAssertEqual(decision2.variationKey, "holdout_variation_key", + "Global holdout should work when flag is ON") + XCTAssertEqual(decision2.ruleKey, "holdout_test_key", + "Should get global holdout regardless of flag state") + } +}