From f7f5388eb9a7103ffff18038b0a0e4e82627eb92 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 30 Oct 2026 23:49:34 +0000 Subject: [PATCH 1/2] feat: Support the file and test data source capabilities in the contract test service --- .../server/contract-tests/Representations.cs | 23 +++++++ .../server/contract-tests/SdkClientEntity.cs | 65 ++++++++++++++++++- pkgs/sdk/server/contract-tests/TestService.cs | 5 +- 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/pkgs/sdk/server/contract-tests/Representations.cs b/pkgs/sdk/server/contract-tests/Representations.cs index 9efeb5ce7..1750af807 100644 --- a/pkgs/sdk/server/contract-tests/Representations.cs +++ b/pkgs/sdk/server/contract-tests/Representations.cs @@ -208,12 +208,28 @@ public class SdkConfigPersistentCacheParams public class SdkConfigDataInitializerParams { public SdkConfigPollingParams Polling { get; set; } + public SdkConfigFileDataParams File { get; set; } + public SdkConfigTestDataParams TestData { get; set; } } public class SdkConfigDataSynchronizerParams { public SdkConfigStreamingParams Streaming { get; set; } public SdkConfigPollingParams Polling { get; set; } + public SdkConfigFileDataParams File { get; set; } + public SdkConfigTestDataParams TestData { get; set; } + } + + public class SdkConfigFileDataParams + { + public string[] FilePaths { get; set; } + public bool? Persist { get; set; } + } + + public class SdkConfigTestDataParams + { + public Dictionary Flags { get; set; } + public bool? Persist { get; set; } } public class SdkConfigPollingParams @@ -244,6 +260,7 @@ public class CommandParams public RegisterFlagChangeListenerParams RegisterFlagChangeListener { get; set; } public RegisterFlagValueChangeListenerParams RegisterFlagValueChangeListener { get; set; } public UnregisterListenerParams UnregisterListener { get; set; } + public UpdateTestDataParams UpdateTestData { get; set; } } public class ContextComparisonPairParams @@ -304,6 +321,12 @@ public class UnregisterListenerParams public string ListenerId { get; set; } } + public class UpdateTestDataParams + { + public string FlagKey { get; set; } + public LdValue Value { get; set; } + } + public class ListenerNotification { public string ListenerId { get; set; } diff --git a/pkgs/sdk/server/contract-tests/SdkClientEntity.cs b/pkgs/sdk/server/contract-tests/SdkClientEntity.cs index 2007cca9d..9ac5dfbac 100644 --- a/pkgs/sdk/server/contract-tests/SdkClientEntity.cs +++ b/pkgs/sdk/server/contract-tests/SdkClientEntity.cs @@ -23,6 +23,9 @@ public class SdkClientEntity private readonly Logger _log; private readonly Dictionary> _flagListeners = new Dictionary>(); + // The test data source of this client, if the configuration has a testData data source. Every + // testData entry of the configuration uses this one instance. + private TestData _testData; public SdkClientEntity( SdkConfigParams sdkParams, @@ -170,6 +173,17 @@ public bool DoCommand(CommandParams command, out object response) break; } + case "updateTestData": + { + if (_testData == null) + { + throw new InvalidOperationException("the client has no test data source"); + } + var p = command.UpdateTestData; + _testData.Update(_testData.Flag(p.FlagKey).ValueForAll(p.Value)); + break; + } + default: return false; } @@ -376,7 +390,7 @@ private ContextBuildResponse DoContextConvert(ContextConvertParams p) } } - private static Configuration BuildSdkConfig(SdkConfigParams sdkParams, ILogAdapter logAdapter, string tag) + private Configuration BuildSdkConfig(SdkConfigParams sdkParams, ILogAdapter logAdapter, string tag) { var builder = Configuration.Builder(sdkParams.Credential); @@ -617,6 +631,14 @@ private static Configuration BuildSdkConfig(SdkConfigParams sdkParams, ILogAdapt } initializers.Add(pollingBuilder); } + else if (initializer.File != null) + { + initializers.Add(CreateFileDataSource(initializer.File, false)); + } + else if (initializer.TestData != null) + { + initializers.Add(TestDataFor(initializer.TestData)); + } } if (initializers.Count > 0) { @@ -721,7 +743,37 @@ private static FileOverrideSourceBuilder BuildOverrideSource(SdkConfigOverridesP return source; } - private static IComponentConfigurer CreateSynchronizer( + private static FileDataSourceBuilder CreateFileDataSource(SdkConfigFileDataParams p, bool autoUpdate) + { + var builder = FileData.DataSource().FilePaths(p.FilePaths ?? new string[0]).AutoUpdate(autoUpdate); + if (p.Persist.HasValue) + { + builder.ShouldPersist(p.Persist.Value); + } + return builder; + } + + private TestData TestDataFor(SdkConfigTestDataParams p) + { + if (_testData == null) + { + _testData = TestData.DataSource(); + } + if (p.Persist.HasValue) + { + _testData.ShouldPersist(p.Persist.Value); + } + if (p.Flags != null) + { + foreach (var flag in p.Flags) + { + _testData.Update(_testData.Flag(flag.Key).ValueForAll(flag.Value)); + } + } + return _testData; + } + + private IComponentConfigurer CreateSynchronizer( SdkConfigDataSynchronizerParams synchronizer, string payloadFilter) { @@ -763,6 +815,15 @@ private static IComponentConfigurer CreateSynchronizer( } return streamingBuilder; } + else if (synchronizer.File != null) + { + // A file synchronizer reloads the files when they change. + return CreateFileDataSource(synchronizer.File, true); + } + else if (synchronizer.TestData != null) + { + return TestDataFor(synchronizer.TestData); + } return null; } diff --git a/pkgs/sdk/server/contract-tests/TestService.cs b/pkgs/sdk/server/contract-tests/TestService.cs index 9ce3be0b2..20d66e88f 100644 --- a/pkgs/sdk/server/contract-tests/TestService.cs +++ b/pkgs/sdk/server/contract-tests/TestService.cs @@ -52,7 +52,10 @@ public class Webapp "instance-id", "retry-conformance-fdv1-streaming", "retry-conformance-fdv1-polling", - "flag-overrides" + "flag-overrides", + "file-data-source", + "test-data-source", + "data-source-persist-opt-in" }; public readonly Handler Handler; From e8c9f4ad2c7f750ff9973719bdf63e1bf2b9837b Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:03:04 +0000 Subject: [PATCH 2/2] fix: Validate the file and test data source configuration in the contract test service From the review of this change: - testData is a synchronizer only, and a configuration may have one: the updateTestData command addresses the client's test data source, and a test data initializer is disposed when the initializer phase ends. - A file data source needs at least one file path. With none it would load an empty data set, which hides LaunchDarkly data in memory or, when persisted, empties the store. - updateTestData without a flag key is rejected. --- .../server/contract-tests/Representations.cs | 1 - .../server/contract-tests/SdkClientEntity.cs | 26 ++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/pkgs/sdk/server/contract-tests/Representations.cs b/pkgs/sdk/server/contract-tests/Representations.cs index 1750af807..77e91f81c 100644 --- a/pkgs/sdk/server/contract-tests/Representations.cs +++ b/pkgs/sdk/server/contract-tests/Representations.cs @@ -209,7 +209,6 @@ public class SdkConfigDataInitializerParams { public SdkConfigPollingParams Polling { get; set; } public SdkConfigFileDataParams File { get; set; } - public SdkConfigTestDataParams TestData { get; set; } } public class SdkConfigDataSynchronizerParams diff --git a/pkgs/sdk/server/contract-tests/SdkClientEntity.cs b/pkgs/sdk/server/contract-tests/SdkClientEntity.cs index 9ac5dfbac..861c7fe6f 100644 --- a/pkgs/sdk/server/contract-tests/SdkClientEntity.cs +++ b/pkgs/sdk/server/contract-tests/SdkClientEntity.cs @@ -23,8 +23,8 @@ public class SdkClientEntity private readonly Logger _log; private readonly Dictionary> _flagListeners = new Dictionary>(); - // The test data source of this client, if the configuration has a testData data source. Every - // testData entry of the configuration uses this one instance. + // The test data source of this client, if the configuration has a testData synchronizer. The + // harness sends at most one. private TestData _testData; public SdkClientEntity( @@ -180,6 +180,10 @@ public bool DoCommand(CommandParams command, out object response) throw new InvalidOperationException("the client has no test data source"); } var p = command.UpdateTestData; + if (p?.FlagKey == null) + { + throw new ArgumentException("updateTestData requires a flag key"); + } _testData.Update(_testData.Flag(p.FlagKey).ValueForAll(p.Value)); break; } @@ -635,10 +639,6 @@ private Configuration BuildSdkConfig(SdkConfigParams sdkParams, ILogAdapter logA { initializers.Add(CreateFileDataSource(initializer.File, false)); } - else if (initializer.TestData != null) - { - initializers.Add(TestDataFor(initializer.TestData)); - } } if (initializers.Count > 0) { @@ -745,7 +745,13 @@ private static FileOverrideSourceBuilder BuildOverrideSource(SdkConfigOverridesP private static FileDataSourceBuilder CreateFileDataSource(SdkConfigFileDataParams p, bool autoUpdate) { - var builder = FileData.DataSource().FilePaths(p.FilePaths ?? new string[0]).AutoUpdate(autoUpdate); + // A file data source with no files would load an empty data set, which hides LaunchDarkly data + // in memory or, when persisted, empties the store. + if (p.FilePaths == null || p.FilePaths.Length == 0 || p.FilePaths.Any(string.IsNullOrEmpty)) + { + throw new ArgumentException("a file data source requires at least one file path"); + } + var builder = FileData.DataSource().FilePaths(p.FilePaths).AutoUpdate(autoUpdate); if (p.Persist.HasValue) { builder.ShouldPersist(p.Persist.Value); @@ -755,10 +761,12 @@ private static FileDataSourceBuilder CreateFileDataSource(SdkConfigFileDataParam private TestData TestDataFor(SdkConfigTestDataParams p) { - if (_testData == null) + // The updateTestData command addresses the client's test data source, so there is only one. + if (_testData != null) { - _testData = TestData.DataSource(); + throw new ArgumentException("the configuration has more than one testData synchronizer"); } + _testData = TestData.DataSource(); if (p.Persist.HasValue) { _testData.ShouldPersist(p.Persist.Value);