From a355c3db9d8a6c7e95b9c35ad9a119a57b4686f5 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 25 Sep 2026 15:34:45 -0700 Subject: [PATCH] feat: Add flag overrides to the contract test services Both contract test services advertise the flag-overrides capability and accept the overrides object in the SDK configuration: filePaths, and the optional duplicateKeysHandling, changeDetection, and pollIntervalMs properties, which configure the file-based override source on the FDv2 data system. An overrides object without a data system configuration is rejected, because overrides are an option of the FDv2 data system only. --- contract-tests/async_client_entity.py | 28 ++++++++++++++++++++++----- contract-tests/async_service.py | 1 + contract-tests/client_entity.py | 19 ++++++++++++++++++ contract-tests/service.py | 1 + 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/contract-tests/async_client_entity.py b/contract-tests/async_client_entity.py index 2569a156..d45f9072 100644 --- a/contract-tests/async_client_entity.py +++ b/contract-tests/async_client_entity.py @@ -26,6 +26,7 @@ ) from ldclient.impl.util import Result from ldclient.integrations import Redis +from ldclient.integrations.overrides import FileOverrideSourceBuilder from ldclient.interfaces import DataStoreMode from ldclient.migrations import ( AsyncMigratorBuilder, @@ -56,8 +57,10 @@ async def start(self): } datasystem_config = config_params.get('dataSystem') + if config_params.get('overrides') is not None and datasystem_config is None: + raise ValueError("flag overrides require the data system to be configured") if datasystem_config is not None: - opts["datasystem_config"] = _build_async_data_system(datasystem_config, opts) + opts["datasystem_config"] = _build_async_data_system(datasystem_config, opts, config_params.get('overrides')) elif config_params.get("streaming") is not None: streaming = config_params["streaming"] if streaming.get("baseUri") is not None: @@ -297,13 +300,13 @@ def _set_optional_value(params_in: dict, name_in: str, func: Callable[[Any], Any func(params_in[name_in]) -def _build_async_data_system(datasystem_config: dict, opts: dict) -> AsyncDataSystemConfig: +def _build_async_data_system(datasystem_config: dict, opts: dict, overrides_config: Optional[dict] = None) -> AsyncDataSystemConfig: """Build an AsyncDataSystemConfig from the harness's dataSystem config. Wires the FDv2 initializers, the ordered synchronizer chain, the FDv1 - fallback synchronizer, the payload filter, and an optional async - persistent store. The async client injects its shared aiohttp session - into these builders when it starts. + fallback synchronizer, the payload filter, an optional async persistent + store, and the optional file-based override source. The async client + injects its shared aiohttp session into these builders when it starts. """ initializers: Optional[list] = None init_configs = datasystem_config.get('initializers') @@ -356,6 +359,9 @@ def _build_async_data_system(datasystem_config: dict, opts: dict) -> AsyncDataSy "fdv1_fallback_synchronizer": fdv1_fallback_synchronizer, } + if overrides_config is not None: + ds_kwargs["override_source"] = _make_override_source(overrides_config) + store_config = datasystem_config.get("store") if store_config is not None: persistent_store_config = store_config.get("persistentDataStore") @@ -370,6 +376,18 @@ def _build_async_data_system(datasystem_config: dict, opts: dict) -> AsyncDataSy return AsyncDataSystemConfig(**ds_kwargs) +def _make_override_source(params: dict) -> FileOverrideSourceBuilder: + """ + Builds the file-based override source from the harness's overrides configuration. The + files are written by the harness on a file system shared with this service. + """ + builder = FileOverrideSourceBuilder(params["filePaths"]) + _set_optional_value(params, "duplicateKeysHandling", builder.duplicate_keys_handling) + _set_optional_value(params, "changeDetection", builder.change_detection) + _set_optional_time(params, "pollIntervalMs", builder.poll_interval) + return builder + + def _create_async_persistent_store(persistent_store_config: dict): """Create an async persistent feature store from the harness config. diff --git a/contract-tests/async_service.py b/contract-tests/async_service.py index b3c04d8a..6a11abc0 100644 --- a/contract-tests/async_service.py +++ b/contract-tests/async_service.py @@ -69,6 +69,7 @@ async def handle_status(request: aiohttp.web.Request) -> aiohttp.web.Response: 'migrations', 'persistent-data-store-redis', 'fdv1-fallback', + 'flag-overrides', ] } return aiohttp.web.Response( diff --git a/contract-tests/client_entity.py b/contract-tests/client_entity.py index f93b5d85..c02fc792 100644 --- a/contract-tests/client_entity.py +++ b/contract-tests/client_entity.py @@ -28,6 +28,7 @@ from ldclient.feature_store import CacheConfig from ldclient.impl.datasourcev2.polling import PollingDataSourceBuilder from ldclient.integrations import Consul, DynamoDB, Redis +from ldclient.integrations.overrides import FileOverrideSourceBuilder from ldclient.interfaces import DataStoreMode @@ -44,6 +45,8 @@ def __init__(self, tag, config): } datasystem_config = config.get('dataSystem') + if config.get('overrides') is not None and datasystem_config is None: + raise ValueError("flag overrides require the data system to be configured") if datasystem_config is not None: datasystem = custom() @@ -107,6 +110,10 @@ def __init__(self, tag, config): datasystem.data_store(store, store_mode) + overrides_config = config.get('overrides') + if overrides_config is not None: + datasystem.overrides(_make_override_source(overrides_config)) + opts["datasystem_config"] = datasystem.build() elif config.get("streaming") is not None: @@ -325,6 +332,18 @@ def _set_optional_value(params_in: dict, name_in: str, func: Callable[[Any], Any func(params_in[name_in]) +def _make_override_source(params: dict) -> FileOverrideSourceBuilder: + """ + Builds the file-based override source from the harness's overrides configuration. The + files are written by the harness on a file system shared with this service. + """ + builder = FileOverrideSourceBuilder(params["filePaths"]) + _set_optional_value(params, "duplicateKeysHandling", builder.duplicate_keys_handling) + _set_optional_value(params, "changeDetection", builder.change_detection) + _set_optional_time(params, "pollIntervalMs", builder.poll_interval) + return builder + + def _create_persistent_store(persistent_store_config: dict): """ Creates a persistent store instance based on the configuration. diff --git a/contract-tests/service.py b/contract-tests/service.py index a8e93674..2583748e 100644 --- a/contract-tests/service.py +++ b/contract-tests/service.py @@ -86,6 +86,7 @@ def status(): 'flag-change-listeners', 'flag-value-change-listeners', 'fdv1-fallback', + 'flag-overrides', ] } return json.dumps(body), 200, {'Content-type': 'application/json'}