From 2442d5d51b0a48cc265529d1b3dc679e0ac46d42 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Fri, 7 Aug 2026 23:21:21 -0700 Subject: [PATCH 01/10] feat: Add FDv2 support to the contract test service --- Makefile | 8 +- contract-tests/src/client_entity.rs | 126 +++++++++++++++++- contract-tests/src/main.rs | 24 ++++ .../testharness-suppressions-fdv2.txt | 22 +++ 4 files changed, 174 insertions(+), 6 deletions(-) create mode 100644 contract-tests/testharness-suppressions-fdv2.txt diff --git a/Makefile b/Makefile index c45bedb8..83992ebf 100644 --- a/Makefile +++ b/Makefile @@ -17,4 +17,10 @@ run-contract-tests: contract-tests: build-contract-tests start-contract-test-service-bg run-contract-tests -.PHONY: build-contract-tests start-contract-test-service run-contract-tests contract-tests +run-contract-tests-fdv2: + @curl -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/main/downloader/run.sh \ + | VERSION=v3 PARAMS="-url http://localhost:8000 -debug -stop-service-at-end -skip-from ./contract-tests/testharness-suppressions-fdv2.txt $(TEST_HARNESS_PARAMS)" sh + +contract-tests-fdv2: build-contract-tests start-contract-test-service-bg run-contract-tests-fdv2 + +.PHONY: build-contract-tests start-contract-test-service run-contract-tests contract-tests run-contract-tests-fdv2 contract-tests-fdv2 diff --git a/contract-tests/src/client_entity.rs b/contract-tests/src/client_entity.rs index fdf59088..aa04dd3b 100644 --- a/contract-tests/src/client_entity.rs +++ b/contract-tests/src/client_entity.rs @@ -9,10 +9,12 @@ const DEFAULT_POLLING_BASE_URL: &str = "https://sdk.launchdarkly.com"; const DEFAULT_STREAM_BASE_URL: &str = "https://stream.launchdarkly.com"; const DEFAULT_EVENTS_BASE_URL: &str = "https://events.launchdarkly.com"; +use launchdarkly_sdk_transport::HttpTransport; use launchdarkly_server_sdk::{ - ApplicationInfo, BuildError, Client, ConfigBuilder, Detail, EventProcessorBuilder, - FlagDetailConfig, FlagFilter, FlagValue, NullEventProcessorBuilder, PollingDataSourceBuilder, - ServiceEndpointsBuilder, StreamingDataSourceBuilder, + ApplicationInfo, BuildError, Client, ConfigBuilder, DataSystemBuilder, Detail, + EventProcessorBuilder, FDv2PollingBuilder, FDv2StreamingBuilder, FlagDetailConfig, FlagFilter, + FlagValue, NullEventProcessorBuilder, PollingDataSourceBuilder, ServiceEndpointsBuilder, + StreamingDataSourceBuilder, }; #[cfg(any(feature = "crypto-aws-lc-rs", feature = "crypto-openssl"))] @@ -27,9 +29,116 @@ use crate::{ CommandParams, CommandResponse, EvaluateAllFlagsParams, EvaluateAllFlagsResponse, EvaluateFlagParams, EvaluateFlagResponse, }, - CreateInstanceParams, + CreateInstanceParams, DataSynchronizerParams, DataSystemParams, }; +/// Builds an FDv2 data system from the contract test's data system params. The +/// FDv1 fallback is always polling and reads its base URL from the polling +/// service endpoint, so it is set here alongside the fallback source. +fn build_fdv2_data_system( + params: DataSystemParams, + transport: T, + service_endpoints: &mut ServiceEndpointsBuilder, +) -> DataSystemBuilder { + let mut builder = DataSystemBuilder::custom(); + + let synchronizers = params.synchronizers.unwrap_or_default(); + for sync in &synchronizers { + if let Some(streaming) = &sync.streaming { + let mut source = FDv2StreamingBuilder::::new(); + if let Some(base_uri) = &streaming.base_uri { + source.base_url(base_uri); + } + if let Some(delay) = streaming.initial_retry_delay_ms { + source.initial_reconnect_delay(Duration::from_millis(delay)); + } + source.transport(transport.clone()); + builder.streaming_synchronizer(source); + } else if let Some(polling) = &sync.polling { + let mut source = FDv2PollingBuilder::::new(); + if let Some(base_uri) = &polling.base_uri { + source.base_url(base_uri); + } + if let Some(interval) = polling.poll_interval_ms { + source.poll_interval(Duration::from_millis(interval)); + } + source.transport(transport.clone()); + builder.polling_synchronizer(source); + } + } + + for init in params.initializers.unwrap_or_default() { + if let Some(polling) = &init.polling { + let mut source = FDv2PollingBuilder::::new(); + if let Some(base_uri) = &polling.base_uri { + source.base_url(base_uri); + } + if let Some(interval) = polling.poll_interval_ms { + source.poll_interval(Duration::from_millis(interval)); + } + source.transport(transport.clone()); + builder.initializer(source); + } + } + + // Use the configured FDv1 fallback if present, otherwise derive one from the + // synchronizers. The fallback is always polling. + let fallback = match params.fdv1_fallback { + Some(fallback) => Some(( + fallback + .base_uri + .or_else(|| derive_fallback_base_url(&synchronizers)), + fallback.poll_interval_ms, + )), + None => select_fallback_synchronizer(&synchronizers).map(|sync| { + if let Some(polling) = &sync.polling { + (polling.base_uri.clone(), polling.poll_interval_ms) + } else if let Some(streaming) = &sync.streaming { + (streaming.base_uri.clone(), None) + } else { + (None, None) + } + }), + }; + + if let Some((base_url, poll_interval)) = fallback { + if let Some(base_url) = base_url { + service_endpoints.polling_base_url(&base_url); + } + let mut fallback_builder = PollingDataSourceBuilder::::new(); + if let Some(interval) = poll_interval { + fallback_builder.poll_interval(Duration::from_millis(interval)); + } + fallback_builder.transport(transport.clone()); + builder.fdv1_fallback(&fallback_builder); + } + + builder +} + +/// Selects the synchronizer to derive an FDv1 fallback from: the first polling +/// synchronizer, otherwise the first synchronizer. +fn select_fallback_synchronizer( + synchronizers: &[DataSynchronizerParams], +) -> Option<&DataSynchronizerParams> { + synchronizers + .iter() + .find(|sync| sync.polling.is_some()) + .or_else(|| synchronizers.first()) +} + +/// Derives an FDv1 fallback base URL from the synchronizers. +fn derive_fallback_base_url(synchronizers: &[DataSynchronizerParams]) -> Option { + let sync = select_fallback_synchronizer(synchronizers)?; + if let Some(polling) = &sync.polling { + polling.base_uri.clone() + } else if let Some(streaming) = &sync.streaming { + streaming.base_uri.clone() + } else { + None + } +} + pub struct ClientEntity { client: Arc, } @@ -87,7 +196,14 @@ impl ClientEntity { } } - if let Some(streaming) = create_instance_params.configuration.streaming { + if let Some(data_system) = create_instance_params.configuration.data_system { + let data_system_builder = build_fdv2_data_system( + data_system, + transport.clone(), + &mut service_endpoints_builder, + ); + config_builder = config_builder.data_system(&data_system_builder); + } else if let Some(streaming) = create_instance_params.configuration.streaming { if let Some(base_uri) = streaming.base_uri { service_endpoints_builder.streaming_base_url(&base_uri); } diff --git a/contract-tests/src/main.rs b/contract-tests/src/main.rs index 87136b27..09a1f2fc 100644 --- a/contract-tests/src/main.rs +++ b/contract-tests/src/main.rs @@ -68,6 +68,27 @@ pub struct ServiceEndpointParameters { pub events: Option, } +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct DataInitializerParams { + pub polling: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct DataSynchronizerParams { + pub streaming: Option, + pub polling: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct DataSystemParams { + pub initializers: Option>, + pub synchronizers: Option>, + pub fdv1_fallback: Option, +} + #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Configuration { @@ -89,6 +110,8 @@ pub struct Configuration { pub tags: Option, pub service_endpoints: Option, + + pub data_system: Option, } #[derive(Deserialize, Debug)] @@ -123,6 +146,7 @@ async fn status() -> impl Responder { "event-gzip".to_string(), "optional-event-gzip".to_string(), "instance-id".to_string(), + "fdv1-fallback".to_string(), ], }) } diff --git a/contract-tests/testharness-suppressions-fdv2.txt b/contract-tests/testharness-suppressions-fdv2.txt new file mode 100644 index 00000000..9a74eb00 --- /dev/null +++ b/contract-tests/testharness-suppressions-fdv2.txt @@ -0,0 +1,22 @@ +# Suppressions for the FDv2 (v3) harness run. The v3 harness runs both the FDv1 +# and FDv2 suites, so this file carries the FDv1 suppressions as well. + +# FDv1 suppressions (also run under the v3 harness). +evaluation/all flags state/client not ready +evaluation/client not ready +streaming/validation/drop and reconnect if stream event has malformed JSON/delete event +streaming/validation/drop and reconnect if stream event has malformed JSON/patch event +streaming/validation/drop and reconnect if stream event has malformed JSON/put event +streaming/validation/drop and reconnect if stream event has well-formed JSON not matching schema/delete event +streaming/validation/drop and reconnect if stream event has well-formed JSON not matching schema/patch event +streaming/validation/drop and reconnect if stream event has well-formed JSON not matching schema/put event + +# Rust honors the TTL-based FDv1 fallback directive (SDK-2527); this scenario +# tests the terminal semantics instead. +streaming/fdv2/FDv1 fallback directive/directive without FDv1 fallback configured halts the data system + +# Rust exposes no FDv2 payload filter API (SDK-2575). +streaming/requests/URL path is computed correctly/environment_filter_key="encoding_not_necessary"/base URI has no trailing slash/GET +streaming/requests/URL path is computed correctly/environment_filter_key="encoding_not_necessary"/base URI has a trailing slash/GET +polling/requests/URL path is computed correctly/environment_filter_key="encoding_not_necessary"/base URI has no trailing slash/GET +polling/requests/URL path is computed correctly/environment_filter_key="encoding_not_necessary"/base URI has a trailing slash/GET From 9d9c25bd4c8444e3b5a2f4f04135260e7a1259d9 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Sun, 9 Aug 2026 13:35:12 -0700 Subject: [PATCH 02/10] fix: Build a fresh transport per component in the contract tests --- contract-tests/src/client_entity.rs | 52 ++++++++++++++++------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/contract-tests/src/client_entity.rs b/contract-tests/src/client_entity.rs index aa04dd3b..1c381a6f 100644 --- a/contract-tests/src/client_entity.rs +++ b/contract-tests/src/client_entity.rs @@ -35,11 +35,15 @@ use crate::{ /// Builds an FDv2 data system from the contract test's data system params. The /// FDv1 fallback is always polling and reads its base URL from the polling /// service endpoint, so it is set here alongside the fallback source. -fn build_fdv2_data_system( +fn build_fdv2_data_system( params: DataSystemParams, - transport: T, + make_transport: F, service_endpoints: &mut ServiceEndpointsBuilder, -) -> DataSystemBuilder { +) -> Result +where + T: HttpTransport + Clone + Send + Sync + 'static, + F: Fn() -> Result, +{ let mut builder = DataSystemBuilder::custom(); let synchronizers = params.synchronizers.unwrap_or_default(); @@ -52,7 +56,7 @@ fn build_fdv2_data_system( if let Some(delay) = streaming.initial_retry_delay_ms { source.initial_reconnect_delay(Duration::from_millis(delay)); } - source.transport(transport.clone()); + source.transport(make_transport()?); builder.streaming_synchronizer(source); } else if let Some(polling) = &sync.polling { let mut source = FDv2PollingBuilder::::new(); @@ -62,7 +66,7 @@ fn build_fdv2_data_system( if let Some(interval) = polling.poll_interval_ms { source.poll_interval(Duration::from_millis(interval)); } - source.transport(transport.clone()); + source.transport(make_transport()?); builder.polling_synchronizer(source); } } @@ -76,7 +80,7 @@ fn build_fdv2_data_system( if let Some(interval) = polling.poll_interval_ms { source.poll_interval(Duration::from_millis(interval)); } - source.transport(transport.clone()); + source.transport(make_transport()?); builder.initializer(source); } } @@ -109,11 +113,11 @@ fn build_fdv2_data_system( if let Some(interval) = poll_interval { fallback_builder.poll_interval(Duration::from_millis(interval)); } - fallback_builder.transport(transport.clone()); + fallback_builder.transport(make_transport()?); builder.fdv1_fallback(&fallback_builder); } - builder + Ok(builder) } /// Selects the synchronizer to derive an FDv1 fallback from: the first polling @@ -154,15 +158,17 @@ impl ClientEntity { .unwrap_or_default() .http_proxy .unwrap_or_default(); - let mut transport_builder = launchdarkly_sdk_transport::HyperTransport::builder(); - if !proxy.is_empty() { - transport_builder = transport_builder.proxy_url(proxy.clone()); - } - - // Create fresh transports for this client to avoid shared connection pool issues - let transport = transport_builder - .build_with_connector(connector.clone()) - .map_err(|e| BuildError::InvalidConfig(e.to_string()))?; + // Build a fresh transport per component, as the SDK normally does. Only the + // connector under test is shared across them. + let make_transport = || { + let mut builder = launchdarkly_sdk_transport::HyperTransport::builder(); + if !proxy.is_empty() { + builder = builder.proxy_url(proxy.clone()); + } + builder + .build_with_connector(connector.clone()) + .map_err(|e| BuildError::InvalidConfig(e.to_string())) + }; let mut config_builder = ConfigBuilder::new(&create_instance_params.configuration.credential); @@ -199,9 +205,9 @@ impl ClientEntity { if let Some(data_system) = create_instance_params.configuration.data_system { let data_system_builder = build_fdv2_data_system( data_system, - transport.clone(), + make_transport, &mut service_endpoints_builder, - ); + )?; config_builder = config_builder.data_system(&data_system_builder); } else if let Some(streaming) = create_instance_params.configuration.streaming { if let Some(base_uri) = streaming.base_uri { @@ -212,7 +218,7 @@ impl ClientEntity { if let Some(delay) = streaming.initial_retry_delay_ms { streaming_builder.initial_reconnect_delay(Duration::from_millis(delay)); } - streaming_builder.transport(transport.clone()); + streaming_builder.transport(make_transport()?); config_builder = config_builder.data_source(&streaming_builder); } else if let Some(polling) = create_instance_params.configuration.polling { @@ -224,7 +230,7 @@ impl ClientEntity { if let Some(delay) = polling.poll_interval_ms { polling_builder.poll_interval(Duration::from_millis(delay)); } - polling_builder.transport(transport.clone()); + polling_builder.transport(make_transport()?); config_builder = config_builder.data_source(&polling_builder); } else { @@ -232,7 +238,7 @@ impl ClientEntity { // customization we provide is the transport to support testing multiple // transport implementations. let mut streaming_builder = StreamingDataSourceBuilder::new(); - streaming_builder.transport(transport.clone()); + streaming_builder.transport(make_transport()?); config_builder = config_builder.data_source(&streaming_builder); } @@ -258,7 +264,7 @@ impl ClientEntity { if let Some(attributes) = events.global_private_attributes { processor_builder.private_attributes(attributes); } - processor_builder.transport(transport); + processor_builder.transport(make_transport()?); processor_builder.omit_anonymous_contexts(events.omit_anonymous_contexts); config_builder.event_processor(&processor_builder) From eeeb473ccccd22754382418e73c4a6f816d2c3c5 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Sun, 9 Aug 2026 16:17:31 -0700 Subject: [PATCH 03/10] chore: Run the FDv2 contract tests in CI --- .github/actions/contract-tests/action.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/actions/contract-tests/action.yml b/.github/actions/contract-tests/action.yml index 4222eb41..0e224341 100644 --- a/.github/actions/contract-tests/action.yml +++ b/.github/actions/contract-tests/action.yml @@ -19,8 +19,20 @@ runs: shell: bash run: CARGO_FLAGS="${{ inputs.cargo-flags }}" make start-contract-test-service-bg - - uses: launchdarkly/gh-actions/actions/contract-tests@contract-tests-v1.0.2 + - name: Run FDv1 contract tests + uses: launchdarkly/gh-actions/actions/contract-tests@contract-tests-v1.3.0 with: test_service_port: 8000 token: ${{ inputs.token }} extra_params: "-skip-from ./contract-tests/testharness-suppressions.txt" + enable_persistence_tests: false + stop_service: false + + - name: Run FDv2 contract tests + uses: launchdarkly/gh-actions/actions/contract-tests@contract-tests-v1.3.0 + with: + test_service_port: 8000 + token: ${{ inputs.token }} + version: v3 + extra_params: "-skip-from ./contract-tests/testharness-suppressions-fdv2.txt" + enable_persistence_tests: false From ae5280403bb8de0cf46cafe09b4945d37b16619f Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Fri, 14 Aug 2026 16:34:30 -0700 Subject: [PATCH 04/10] chore: Suppress the event attribute-reference redaction contract tests --- contract-tests/testharness-suppressions-fdv2.txt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/contract-tests/testharness-suppressions-fdv2.txt b/contract-tests/testharness-suppressions-fdv2.txt index 9a74eb00..45dc7aa1 100644 --- a/contract-tests/testharness-suppressions-fdv2.txt +++ b/contract-tests/testharness-suppressions-fdv2.txt @@ -20,3 +20,19 @@ streaming/requests/URL path is computed correctly/environment_filter_key="encodi streaming/requests/URL path is computed correctly/environment_filter_key="encoding_not_necessary"/base URI has a trailing slash/GET polling/requests/URL path is computed correctly/environment_filter_key="encoding_not_necessary"/base URI has no trailing slash/GET polling/requests/URL path is computed correctly/environment_filter_key="encoding_not_necessary"/base URI has a trailing slash/GET + +# Rust does not escape `/`- or `~`-prefixed redacted attribute names as attribute references in events (SDK-2923). +events/context properties/single-kind, allAttributesPrivate, slash-prefixed attribute name/debug event +events/context properties/single-kind, allAttributesPrivate, slash-prefixed attribute name/identify event +events/context properties/single-kind, allAttributesPrivate, slash-prefixed attribute name/index event from custom event +events/context properties/single-kind, allAttributesPrivate, slash-prefixed attribute name/index event from evaluation +events/feature events/multi-kind with anonymous context redacts attributes appropriately/type: any +events/feature events/multi-kind with anonymous context redacts attributes appropriately/type: bool +events/feature events/multi-kind with anonymous context redacts attributes appropriately/type: double +events/feature events/multi-kind with anonymous context redacts attributes appropriately/type: int +events/feature events/multi-kind with anonymous context redacts attributes appropriately/type: string +events/feature events/single-kind anonymous context redacts all attributes/type: any +events/feature events/single-kind anonymous context redacts all attributes/type: bool +events/feature events/single-kind anonymous context redacts all attributes/type: double +events/feature events/single-kind anonymous context redacts all attributes/type: int +events/feature events/single-kind anonymous context redacts all attributes/type: string From cd0c3a0ba458eaa7ad8e9c3cef5bd25c299eda47 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Wed, 19 Aug 2026 16:25:23 -0700 Subject: [PATCH 05/10] fix: Bump evaluation to 2.2.1 and re-enable the redaction event tests --- .../testharness-suppressions-fdv2.txt | 28 +++++++++---------- launchdarkly-server-sdk/Cargo.toml | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/contract-tests/testharness-suppressions-fdv2.txt b/contract-tests/testharness-suppressions-fdv2.txt index 45dc7aa1..9338738e 100644 --- a/contract-tests/testharness-suppressions-fdv2.txt +++ b/contract-tests/testharness-suppressions-fdv2.txt @@ -22,17 +22,17 @@ polling/requests/URL path is computed correctly/environment_filter_key="encoding polling/requests/URL path is computed correctly/environment_filter_key="encoding_not_necessary"/base URI has a trailing slash/GET # Rust does not escape `/`- or `~`-prefixed redacted attribute names as attribute references in events (SDK-2923). -events/context properties/single-kind, allAttributesPrivate, slash-prefixed attribute name/debug event -events/context properties/single-kind, allAttributesPrivate, slash-prefixed attribute name/identify event -events/context properties/single-kind, allAttributesPrivate, slash-prefixed attribute name/index event from custom event -events/context properties/single-kind, allAttributesPrivate, slash-prefixed attribute name/index event from evaluation -events/feature events/multi-kind with anonymous context redacts attributes appropriately/type: any -events/feature events/multi-kind with anonymous context redacts attributes appropriately/type: bool -events/feature events/multi-kind with anonymous context redacts attributes appropriately/type: double -events/feature events/multi-kind with anonymous context redacts attributes appropriately/type: int -events/feature events/multi-kind with anonymous context redacts attributes appropriately/type: string -events/feature events/single-kind anonymous context redacts all attributes/type: any -events/feature events/single-kind anonymous context redacts all attributes/type: bool -events/feature events/single-kind anonymous context redacts all attributes/type: double -events/feature events/single-kind anonymous context redacts all attributes/type: int -events/feature events/single-kind anonymous context redacts all attributes/type: string +# events/context properties/single-kind, allAttributesPrivate, slash-prefixed attribute name/debug event +# events/context properties/single-kind, allAttributesPrivate, slash-prefixed attribute name/identify event +# events/context properties/single-kind, allAttributesPrivate, slash-prefixed attribute name/index event from custom event +# events/context properties/single-kind, allAttributesPrivate, slash-prefixed attribute name/index event from evaluation +# events/feature events/multi-kind with anonymous context redacts attributes appropriately/type: any +# events/feature events/multi-kind with anonymous context redacts attributes appropriately/type: bool +# events/feature events/multi-kind with anonymous context redacts attributes appropriately/type: double +# events/feature events/multi-kind with anonymous context redacts attributes appropriately/type: int +# events/feature events/multi-kind with anonymous context redacts attributes appropriately/type: string +# events/feature events/single-kind anonymous context redacts all attributes/type: any +# events/feature events/single-kind anonymous context redacts all attributes/type: bool +# events/feature events/single-kind anonymous context redacts all attributes/type: double +# events/feature events/single-kind anonymous context redacts all attributes/type: int +# events/feature events/single-kind anonymous context redacts all attributes/type: string diff --git a/launchdarkly-server-sdk/Cargo.toml b/launchdarkly-server-sdk/Cargo.toml index 61782b41..4041f5fe 100644 --- a/launchdarkly-server-sdk/Cargo.toml +++ b/launchdarkly-server-sdk/Cargo.toml @@ -27,7 +27,7 @@ log = "0.4.14" lru = { version = "0.16.3", default-features = false } # Pulled without default features so that the float-roundtrip feature below is what decides # whether the evaluation engine parses floats the way Go does. -launchdarkly-server-sdk-evaluation = { version = "2.2.0", default-features = false } +launchdarkly-server-sdk-evaluation = { version = "2.2.1", default-features = false } serde = { version = "1.0.132", features = ["derive"] } serde_json = { version = "1.0.73" } thiserror = "2.0" From af373c0145bcd9910c4cd6c8330fd6b3d9577135 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Wed, 19 Aug 2026 17:32:26 -0700 Subject: [PATCH 06/10] chore: Remove obsolete FDv2 payload-filter and redaction suppressions --- .../testharness-suppressions-fdv2.txt | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/contract-tests/testharness-suppressions-fdv2.txt b/contract-tests/testharness-suppressions-fdv2.txt index 9338738e..60ed4c6e 100644 --- a/contract-tests/testharness-suppressions-fdv2.txt +++ b/contract-tests/testharness-suppressions-fdv2.txt @@ -14,25 +14,3 @@ streaming/validation/drop and reconnect if stream event has well-formed JSON not # Rust honors the TTL-based FDv1 fallback directive (SDK-2527); this scenario # tests the terminal semantics instead. streaming/fdv2/FDv1 fallback directive/directive without FDv1 fallback configured halts the data system - -# Rust exposes no FDv2 payload filter API (SDK-2575). -streaming/requests/URL path is computed correctly/environment_filter_key="encoding_not_necessary"/base URI has no trailing slash/GET -streaming/requests/URL path is computed correctly/environment_filter_key="encoding_not_necessary"/base URI has a trailing slash/GET -polling/requests/URL path is computed correctly/environment_filter_key="encoding_not_necessary"/base URI has no trailing slash/GET -polling/requests/URL path is computed correctly/environment_filter_key="encoding_not_necessary"/base URI has a trailing slash/GET - -# Rust does not escape `/`- or `~`-prefixed redacted attribute names as attribute references in events (SDK-2923). -# events/context properties/single-kind, allAttributesPrivate, slash-prefixed attribute name/debug event -# events/context properties/single-kind, allAttributesPrivate, slash-prefixed attribute name/identify event -# events/context properties/single-kind, allAttributesPrivate, slash-prefixed attribute name/index event from custom event -# events/context properties/single-kind, allAttributesPrivate, slash-prefixed attribute name/index event from evaluation -# events/feature events/multi-kind with anonymous context redacts attributes appropriately/type: any -# events/feature events/multi-kind with anonymous context redacts attributes appropriately/type: bool -# events/feature events/multi-kind with anonymous context redacts attributes appropriately/type: double -# events/feature events/multi-kind with anonymous context redacts attributes appropriately/type: int -# events/feature events/multi-kind with anonymous context redacts attributes appropriately/type: string -# events/feature events/single-kind anonymous context redacts all attributes/type: any -# events/feature events/single-kind anonymous context redacts all attributes/type: bool -# events/feature events/single-kind anonymous context redacts all attributes/type: double -# events/feature events/single-kind anonymous context redacts all attributes/type: int -# events/feature events/single-kind anonymous context redacts all attributes/type: string From edb2b669ec0fb86e2e0b9fe08aed49c05b87c1d2 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Wed, 2 Sep 2026 20:30:48 -0700 Subject: [PATCH 07/10] fix: Update the contract test service for the generic synchronizer API --- contract-tests/src/client_entity.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contract-tests/src/client_entity.rs b/contract-tests/src/client_entity.rs index 1c381a6f..847a7476 100644 --- a/contract-tests/src/client_entity.rs +++ b/contract-tests/src/client_entity.rs @@ -57,7 +57,7 @@ where source.initial_reconnect_delay(Duration::from_millis(delay)); } source.transport(make_transport()?); - builder.streaming_synchronizer(source); + builder.synchronizer(source); } else if let Some(polling) = &sync.polling { let mut source = FDv2PollingBuilder::::new(); if let Some(base_uri) = &polling.base_uri { @@ -67,7 +67,7 @@ where source.poll_interval(Duration::from_millis(interval)); } source.transport(make_transport()?); - builder.polling_synchronizer(source); + builder.synchronizer(source); } } From 011aa5273275fe9afec81fe011379f53cda8736b Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Thu, 3 Sep 2026 12:16:12 -0700 Subject: [PATCH 08/10] docs: Split the FDv1-fallback suppression comment into sentences --- contract-tests/testharness-suppressions-fdv2.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contract-tests/testharness-suppressions-fdv2.txt b/contract-tests/testharness-suppressions-fdv2.txt index 60ed4c6e..e2574444 100644 --- a/contract-tests/testharness-suppressions-fdv2.txt +++ b/contract-tests/testharness-suppressions-fdv2.txt @@ -11,6 +11,6 @@ streaming/validation/drop and reconnect if stream event has well-formed JSON not streaming/validation/drop and reconnect if stream event has well-formed JSON not matching schema/patch event streaming/validation/drop and reconnect if stream event has well-formed JSON not matching schema/put event -# Rust honors the TTL-based FDv1 fallback directive (SDK-2527); this scenario -# tests the terminal semantics instead. +# Rust honors the TTL-based FDv1 fallback directive (SDK-2527). +# This scenario tests the terminal semantics instead. streaming/fdv2/FDv1 fallback directive/directive without FDv1 fallback configured halts the data system From 4c869534b2010317d7a269b897c57a55638935d5 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Thu, 3 Sep 2026 13:32:44 -0700 Subject: [PATCH 09/10] refactor: Only set the FDv1 fallback when the harness provides one --- contract-tests/src/client_entity.rs | 51 +++-------------------------- 1 file changed, 4 insertions(+), 47 deletions(-) diff --git a/contract-tests/src/client_entity.rs b/contract-tests/src/client_entity.rs index 847a7476..dbbcc5c3 100644 --- a/contract-tests/src/client_entity.rs +++ b/contract-tests/src/client_entity.rs @@ -29,7 +29,7 @@ use crate::{ CommandParams, CommandResponse, EvaluateAllFlagsParams, EvaluateAllFlagsResponse, EvaluateFlagParams, EvaluateFlagResponse, }, - CreateInstanceParams, DataSynchronizerParams, DataSystemParams, + CreateInstanceParams, DataSystemParams, }; /// Builds an FDv2 data system from the contract test's data system params. The @@ -85,32 +85,12 @@ where } } - // Use the configured FDv1 fallback if present, otherwise derive one from the - // synchronizers. The fallback is always polling. - let fallback = match params.fdv1_fallback { - Some(fallback) => Some(( - fallback - .base_uri - .or_else(|| derive_fallback_base_url(&synchronizers)), - fallback.poll_interval_ms, - )), - None => select_fallback_synchronizer(&synchronizers).map(|sync| { - if let Some(polling) = &sync.polling { - (polling.base_uri.clone(), polling.poll_interval_ms) - } else if let Some(streaming) = &sync.streaming { - (streaming.base_uri.clone(), None) - } else { - (None, None) - } - }), - }; - - if let Some((base_url, poll_interval)) = fallback { - if let Some(base_url) = base_url { + if let Some(fallback) = params.fdv1_fallback { + if let Some(base_url) = fallback.base_uri { service_endpoints.polling_base_url(&base_url); } let mut fallback_builder = PollingDataSourceBuilder::::new(); - if let Some(interval) = poll_interval { + if let Some(interval) = fallback.poll_interval_ms { fallback_builder.poll_interval(Duration::from_millis(interval)); } fallback_builder.transport(make_transport()?); @@ -120,29 +100,6 @@ where Ok(builder) } -/// Selects the synchronizer to derive an FDv1 fallback from: the first polling -/// synchronizer, otherwise the first synchronizer. -fn select_fallback_synchronizer( - synchronizers: &[DataSynchronizerParams], -) -> Option<&DataSynchronizerParams> { - synchronizers - .iter() - .find(|sync| sync.polling.is_some()) - .or_else(|| synchronizers.first()) -} - -/// Derives an FDv1 fallback base URL from the synchronizers. -fn derive_fallback_base_url(synchronizers: &[DataSynchronizerParams]) -> Option { - let sync = select_fallback_synchronizer(synchronizers)?; - if let Some(polling) = &sync.polling { - polling.base_uri.clone() - } else if let Some(streaming) = &sync.streaming { - streaming.base_uri.clone() - } else { - None - } -} - pub struct ClientEntity { client: Arc, } From 9edc163b506790eb0bbaaaa9e63bd21184583705 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Fri, 4 Sep 2026 10:33:48 -0700 Subject: [PATCH 10/10] chore: Remove the obsolete FDv1 fallback directive suppression --- contract-tests/testharness-suppressions-fdv2.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/contract-tests/testharness-suppressions-fdv2.txt b/contract-tests/testharness-suppressions-fdv2.txt index e2574444..d814dc25 100644 --- a/contract-tests/testharness-suppressions-fdv2.txt +++ b/contract-tests/testharness-suppressions-fdv2.txt @@ -10,7 +10,3 @@ streaming/validation/drop and reconnect if stream event has malformed JSON/put e streaming/validation/drop and reconnect if stream event has well-formed JSON not matching schema/delete event streaming/validation/drop and reconnect if stream event has well-formed JSON not matching schema/patch event streaming/validation/drop and reconnect if stream event has well-formed JSON not matching schema/put event - -# Rust honors the TTL-based FDv1 fallback directive (SDK-2527). -# This scenario tests the terminal semantics instead. -streaming/fdv2/FDv1 fallback directive/directive without FDv1 fallback configured halts the data system