package dev.openfeature.sdk; import static dev.openfeature.sdk.DoSomethingProvider.DEFAULT_METADATA; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.*; import dev.openfeature.sdk.exceptions.GeneralError; import dev.openfeature.sdk.fixtures.HookFixtures; import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; import dev.openfeature.sdk.testutils.TestEventsProvider; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import lombok.SneakyThrows; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.simplify4u.slf4jmock.LoggerMock; import org.slf4j.Logger; class FlagEvaluationSpecTest implements HookFixtures { private Logger logger; private OpenFeatureAPI api; private Client _client() { FeatureProviderTestUtils.setFeatureProvider(new NoOpProvider()); return api.getClient(); } @SneakyThrows private Client _initializedClient() { TestEventsProvider provider = new TestEventsProvider(); provider.initialize(null); FeatureProviderTestUtils.setFeatureProvider(provider); return api.getClient(); } @BeforeEach void getApiInstance() { api = OpenFeatureAPI.getInstance(); } @AfterEach void reset_ctx() { api.setEvaluationContext(null); } @BeforeEach void set_logger() { logger = Mockito.mock(Logger.class); LoggerMock.setMock(OpenFeatureClient.class, logger); } @AfterEach void reset_logs() { LoggerMock.setMock(OpenFeatureClient.class, logger); } @Specification( number = "1.1.1", text = "The API, and any state it maintains SHOULD exist as a global singleton, even in cases wherein multiple versions of the API are present at runtime.") @Test void global_singleton() { assertSame(OpenFeatureAPI.getInstance(), OpenFeatureAPI.getInstance()); } @Specification( number = "1.1.2.1", text = "The API MUST define a provider mutator, a function to set the default provider, which accepts an API-conformant provider implementation.") @Test void provider() { FeatureProvider mockProvider = mock(FeatureProvider.class); FeatureProviderTestUtils.setFeatureProvider(mockProvider); assertThat(api.getProvider()).isEqualTo(mockProvider); } @SneakyThrows @Specification( number = "1.1.8", text = "The API SHOULD provide functions to set a provider and wait for the initialize function to return or throw.") @Test void providerAndWait() { FeatureProvider provider = new TestEventsProvider(500); OpenFeatureAPI.getInstance().setProviderAndWait(provider); Client client = api.getClient(); assertThat(client.getProviderState()).isEqualTo(ProviderState.READY); provider = new TestEventsProvider(500); String providerName = "providerAndWait"; OpenFeatureAPI.getInstance().setProviderAndWait(providerName, provider); Client client2 = api.getClient(providerName); assertThat(client2.getProviderState()).isEqualTo(ProviderState.READY); } @SneakyThrows @Specification( number = "1.1.8", text = "The API SHOULD provide functions to set a provider and wait for the initialize function to return or throw.") @Test void providerAndWaitError() { FeatureProvider provider1 = new TestEventsProvider(500, true, "fake error"); assertThrows(GeneralError.class, () -> api.setProviderAndWait(provider1)); FeatureProvider provider2 = new TestEventsProvider(500, true, "fake error"); String providerName = "providerAndWaitError"; assertThrows(GeneralError.class, () -> api.setProviderAndWait(providerName, provider2)); } @Specification( number = "2.4.5", text = "The provider SHOULD indicate an error if flag resolution is attempted before the provider is ready.") @Test void shouldReturnNotReadyIfNotInitialized() { FeatureProvider provider = new TestEventsProvider(100); String providerName = "shouldReturnNotReadyIfNotInitialized"; OpenFeatureAPI.getInstance().setProvider(providerName, provider); Client client = OpenFeatureAPI.getInstance().getClient(providerName); FlagEvaluationDetails details = client.getBooleanDetails("return_error_when_not_initialized", false); assertEquals(ErrorCode.PROVIDER_NOT_READY, details.getErrorCode()); assertEquals(Reason.ERROR.toString(), details.getReason()); } @Specification( number = "1.1.5", text = "The API MUST provide a function for retrieving the metadata field of the configured provider.") @Test void provider_metadata() { FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider()); assertThat(api.getProviderMetadata().getName()).isEqualTo(DoSomethingProvider.name); } @Specification( number = "1.1.4", text = "The API MUST provide a function to add hooks which accepts one or more API-conformant hooks, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.") @Test void hook_addition() { Hook h1 = mock(Hook.class); Hook h2 = mock(Hook.class); api.addHooks(h1); assertEquals(1, api.getHooks().size()); assertEquals(h1, api.getHooks().get(0)); api.addHooks(h2); assertEquals(2, api.getHooks().size()); assertEquals(h2, api.getHooks().get(1)); } @Specification( number = "1.1.6", text = "The API MUST provide a function for creating a client which accepts the following options: - domain (optional): A logical string identifier for binding clients to provider.") @Test void domainName() { assertNull(api.getClient().getMetadata().getDomain()); String domain = "Sir Calls-a-lot"; Client clientForDomain = api.getClient(domain); assertEquals(domain, clientForDomain.getMetadata().getDomain()); } @Specification( number = "1.2.1", text = "The client MUST provide a method to add hooks which accepts one or more API-conformant hooks, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.") @Test void hookRegistration() { Client c = _client(); Hook m1 = mock(Hook.class); Hook m2 = mock(Hook.class); c.addHooks(m1); c.addHooks(m2); List hooks = c.getHooks(); assertEquals(2, hooks.size()); assertTrue(hooks.contains(m1)); assertTrue(hooks.contains(m2)); } @Specification( number = "1.3.1.1", text = "The client MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure, with parameters flag key (string, required), default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation options (optional), which returns the flag value.") @Specification( number = "1.3.3.1", text = "The client SHOULD provide functions for floating-point numbers and integers, consistent with language idioms.") @Test void value_flags() { FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider()); Client c = api.getClient(); String key = "key"; assertEquals(true, c.getBooleanValue(key, false)); assertEquals(true, c.getBooleanValue(key, false, new ImmutableContext())); assertEquals( true, c.getBooleanValue( key, false, new ImmutableContext(), FlagEvaluationOptions.builder().build())); assertEquals("gnirts-ym", c.getStringValue(key, "my-string")); assertEquals("gnirts-ym", c.getStringValue(key, "my-string", new ImmutableContext())); assertEquals( "gnirts-ym", c.getStringValue( key, "my-string", new ImmutableContext(), FlagEvaluationOptions.builder().build())); assertEquals(400, c.getIntegerValue(key, 4)); assertEquals(400, c.getIntegerValue(key, 4, new ImmutableContext())); assertEquals( 400, c.getIntegerValue( key, 4, new ImmutableContext(), FlagEvaluationOptions.builder().build())); assertEquals(40.0, c.getDoubleValue(key, .4)); assertEquals(40.0, c.getDoubleValue(key, .4, new ImmutableContext())); assertEquals( 40.0, c.getDoubleValue( key, .4, new ImmutableContext(), FlagEvaluationOptions.builder().build())); assertEquals(null, c.getObjectValue(key, new Value())); assertEquals(null, c.getObjectValue(key, new Value(), new ImmutableContext())); assertEquals( null, c.getObjectValue( key, new Value(), new ImmutableContext(), FlagEvaluationOptions.builder().build())); } @Specification( number = "1.4.1.1", text = "The client MUST provide methods for detailed flag value evaluation with parameters flag key (string, required), default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation options (optional), which returns an evaluation details structure.") @Specification( number = "1.4.3", text = "The evaluation details structure's value field MUST contain the evaluated flag value.") @Specification( number = "1.4.4.1", text = "The evaluation details structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped value field.") @Specification( number = "1.4.5", text = "The evaluation details structure's flag key field MUST contain the flag key argument passed to the detailed flag evaluation method.") @Specification( number = "1.4.6", text = "In cases of normal execution, the evaluation details structure's variant field MUST contain the value of the variant field in the flag resolution structure returned by the configured provider, if the field is set.") @Specification( number = "1.4.7", text = "In cases of normal execution, the `evaluation details` structure's `reason` field MUST contain the value of the `reason` field in the `flag resolution` structure returned by the configured `provider`, if the field is set.") @Test void detail_flags() { FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider()); Client c = api.getClient(); String key = "key"; FlagEvaluationDetails bd = FlagEvaluationDetails.builder() .flagKey(key) .value(false) .variant(null) .flagMetadata(DEFAULT_METADATA) .build(); assertEquals(bd, c.getBooleanDetails(key, true)); assertEquals(bd, c.getBooleanDetails(key, true, new ImmutableContext())); assertEquals( bd, c.getBooleanDetails( key, true, new ImmutableContext(), FlagEvaluationOptions.builder().build())); FlagEvaluationDetails sd = FlagEvaluationDetails.builder() .flagKey(key) .value("tset") .variant(null) .flagMetadata(DEFAULT_METADATA) .build(); assertEquals(sd, c.getStringDetails(key, "test")); assertEquals(sd, c.getStringDetails(key, "test", new ImmutableContext())); assertEquals( sd, c.getStringDetails( key, "test", new ImmutableContext(), FlagEvaluationOptions.builder().build())); FlagEvaluationDetails id = FlagEvaluationDetails.builder() .flagKey(key) .value(400) .flagMetadata(DEFAULT_METADATA) .build(); assertEquals(id, c.getIntegerDetails(key, 4)); assertEquals(id, c.getIntegerDetails(key, 4, new ImmutableContext())); assertEquals( id, c.getIntegerDetails( key, 4, new ImmutableContext(), FlagEvaluationOptions.builder().build())); FlagEvaluationDetails dd = FlagEvaluationDetails.builder() .flagKey(key) .value(40.0) .flagMetadata(DEFAULT_METADATA) .build(); assertEquals(dd, c.getDoubleDetails(key, .4)); assertEquals(dd, c.getDoubleDetails(key, .4, new ImmutableContext())); assertEquals( dd, c.getDoubleDetails( key, .4, new ImmutableContext(), FlagEvaluationOptions.builder().build())); // TODO: Structure detail tests. } @Specification( number = "1.5.1", text = "The evaluation options structure's hooks field denotes an ordered collection of hooks that the client MUST execute for the respective flag evaluation, in addition to those already configured.") @SneakyThrows @Test void hooks() { Client c = _initializedClient(); Hook clientHook = mockBooleanHook(); Hook invocationHook = mockBooleanHook(); c.addHooks(clientHook); c.getBooleanValue( "key", false, null, FlagEvaluationOptions.builder().hook(invocationHook).build()); verify(clientHook, times(1)).before(any(), any()); verify(invocationHook, times(1)).before(any(), any()); } @Specification( number = "1.4.8", text = "In cases of abnormal execution, the `evaluation details` structure's `error code` field **MUST** contain an `error code`.") @Specification( number = "1.4.9", text = "In cases of abnormal execution (network failure, unhandled error, etc) the `reason` field in the `evaluation details` SHOULD indicate an error.") @Specification( number = "1.4.10", text = "Methods, functions, or operations on the client MUST NOT throw exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the `default value` in the event of abnormal execution. Exceptions include functions or methods for the purposes for configuration or setup.") @Specification( number = "1.4.13", text = "In cases of abnormal execution, the `evaluation details` structure's `error message` field **MAY** contain a string containing additional details about the nature of the error.") @Test void broken_provider() { FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider()); Client c = api.getClient(); boolean defaultValue = false; assertFalse(c.getBooleanValue("key", defaultValue)); FlagEvaluationDetails details = c.getBooleanDetails("key", defaultValue); assertEquals(ErrorCode.FLAG_NOT_FOUND, details.getErrorCode()); assertEquals(TestConstants.BROKEN_MESSAGE, details.getErrorMessage()); assertEquals(Reason.ERROR.toString(), details.getReason()); assertEquals(defaultValue, details.getValue()); } @Specification( number = "1.4.8", text = "In cases of abnormal execution, the `evaluation details` structure's `error code` field **MUST** contain an `error code`.") @Specification( number = "1.4.9", text = "In cases of abnormal execution (network failure, unhandled error, etc) the `reason` field in the `evaluation details` SHOULD indicate an error.") @Specification( number = "1.4.10", text = "Methods, functions, or operations on the client MUST NOT throw exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the `default value` in the event of abnormal execution. Exceptions include functions or methods for the purposes for configuration or setup.") @Specification( number = "1.4.13", text = "In cases of abnormal execution, the `evaluation details` structure's `error message` field **MAY** contain a string containing additional details about the nature of the error.") @Test void broken_provider_withDetails() { FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenWithDetailsProvider()); Client c = api.getClient(); boolean defaultValue = false; assertFalse(c.getBooleanValue("key", defaultValue)); FlagEvaluationDetails details = c.getBooleanDetails("key", defaultValue); assertEquals(ErrorCode.FLAG_NOT_FOUND, details.getErrorCode()); assertEquals(TestConstants.BROKEN_MESSAGE, details.getErrorMessage()); assertEquals(Reason.ERROR.toString(), details.getReason()); assertEquals(defaultValue, details.getValue()); } @Specification( number = "1.4.11", text = "Methods, functions, or operations on the client SHOULD NOT write log messages.") @Test void log_on_error() throws NotImplementedException { FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider()); Client c = api.getClient(); FlagEvaluationDetails result = c.getBooleanDetails("test", false); assertEquals(Reason.ERROR.toString(), result.getReason()); Mockito.verify(logger, never()).error(any(String.class), any(), any()); } @Specification( number = "1.2.2", text = "The client interface MUST define a metadata member or accessor, containing an immutable domain field or accessor of type string, which corresponds to the domain value supplied during client creation. In previous drafts, this property was called name. For backwards compatibility, implementations should consider name an alias to domain.") @Test void clientMetadata() { Client c = _client(); assertNull(c.getMetadata().getName()); assertNull(c.getMetadata().getDomain()); String domainName = "test domain"; FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider()); Client c2 = api.getClient(domainName); assertEquals(domainName, c2.getMetadata().getName()); assertEquals(domainName, c2.getMetadata().getDomain()); } @Specification( number = "1.4.9", text = "In cases of abnormal execution (network failure, unhandled error, etc) the reason field in the evaluation details SHOULD indicate an error.") @Test void reason_is_error_when_there_are_errors() { FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider()); Client c = api.getClient(); FlagEvaluationDetails result = c.getBooleanDetails("test", false); assertEquals(Reason.ERROR.toString(), result.getReason()); } @Specification( number = "1.4.14", text = "If the flag metadata field in the flag resolution structure returned by the configured provider is set, the evaluation details structure's flag metadata field MUST contain that value. Otherwise, it MUST contain an empty record.") @Test void flag_metadata_passed() { FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider(null)); Client c = api.getClient(); FlagEvaluationDetails result = c.getBooleanDetails("test", false); assertNotNull(result.getFlagMetadata()); } @Specification(number = "3.2.2.1", text = "The API MUST have a method for setting the global evaluation context.") @Test void api_context() { String contextKey = "some-key"; String contextValue = "some-value"; DoSomethingProvider provider = spy(new DoSomethingProvider()); FeatureProviderTestUtils.setFeatureProvider(provider); Map attributes = new HashMap<>(); attributes.put(contextKey, new Value(contextValue)); EvaluationContext apiCtx = new ImmutableContext(attributes); // set the global context api.setEvaluationContext(apiCtx); Client client = api.getClient(); client.getBooleanValue("any-flag", false); // assert that the value from the global context was passed to the provider verify(provider).getBooleanEvaluation(any(), any(), argThat((arg) -> arg.getValue(contextKey) .asString() .equals(contextValue))); } @Specification( number = "3.2.1.1", text = "The API, Client and invocation MUST have a method for supplying evaluation context.") @Specification( number = "3.2.3", text = "Evaluation context MUST be merged in the order: API (global; lowest precedence) -> transaction -> client -> invocation -> before hooks (highest precedence), with duplicate values being overwritten.") @Test void multi_layer_context_merges_correctly() { DoSomethingProvider provider = spy(new DoSomethingProvider()); FeatureProviderTestUtils.setFeatureProvider(provider); TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator(); api.setTransactionContextPropagator(transactionContextPropagator); Hook hook = spy(new Hook() { @Override public Optional before(HookContext ctx, Map hints) { Map attrs = ctx.getCtx().asMap(); attrs.put("before", new Value("5")); attrs.put("common7", new Value("5")); return Optional.ofNullable(new ImmutableContext(attrs)); } @Override public void after( HookContext ctx, FlagEvaluationDetails details, Map hints) { Hook.super.after(ctx, details, hints); } }); Map apiAttributes = new HashMap<>(); apiAttributes.put("common1", new Value("1")); apiAttributes.put("common2", new Value("1")); apiAttributes.put("common3", new Value("1")); apiAttributes.put("common7", new Value("1")); apiAttributes.put("api", new Value("1")); EvaluationContext apiCtx = new ImmutableContext(apiAttributes); api.setEvaluationContext(apiCtx); Map transactionAttributes = new HashMap<>(); // overwrite value from api context transactionAttributes.put("common1", new Value("2")); transactionAttributes.put("common4", new Value("2")); transactionAttributes.put("common5", new Value("2")); transactionAttributes.put("transaction", new Value("2")); EvaluationContext transactionCtx = new ImmutableContext(transactionAttributes); api.setTransactionContext(transactionCtx); Client c = api.getClient(); Map clientAttributes = new HashMap<>(); // overwrite value from api context clientAttributes.put("common2", new Value("3")); // overwrite value from transaction context clientAttributes.put("common4", new Value("3")); clientAttributes.put("common6", new Value("3")); clientAttributes.put("client", new Value("3")); EvaluationContext clientCtx = new ImmutableContext(clientAttributes); c.setEvaluationContext(clientCtx); Map invocationAttributes = new HashMap<>(); // overwrite value from api context invocationAttributes.put("common3", new Value("4")); // overwrite value from transaction context invocationAttributes.put("common5", new Value("4")); // overwrite value from api client context invocationAttributes.put("common6", new Value("4")); invocationAttributes.put("invocation", new Value("4")); EvaluationContext invocationCtx = new ImmutableContext(invocationAttributes); c.getBooleanValue( "key", false, invocationCtx, FlagEvaluationOptions.builder().hook(hook).build()); // assert the correct overrides in before hook verify(hook) .before( argThat((arg) -> { EvaluationContext evaluationContext = arg.getCtx(); return evaluationContext.getValue("api").asString().equals("1") && evaluationContext .getValue("transaction") .asString() .equals("2") && evaluationContext .getValue("client") .asString() .equals("3") && evaluationContext .getValue("invocation") .asString() .equals("4") && evaluationContext .getValue("common1") .asString() .equals("2") && evaluationContext .getValue("common2") .asString() .equals("3") && evaluationContext .getValue("common3") .asString() .equals("4") && evaluationContext .getValue("common4") .asString() .equals("3") && evaluationContext .getValue("common5") .asString() .equals("4") && evaluationContext .getValue("common6") .asString() .equals("4"); }), any()); // assert the correct overrides in evaluation verify(provider).getBooleanEvaluation(any(), any(), argThat((arg) -> { return arg.getValue("api").asString().equals("1") && arg.getValue("transaction").asString().equals("2") && arg.getValue("client").asString().equals("3") && arg.getValue("invocation").asString().equals("4") && arg.getValue("before").asString().equals("5") && arg.getValue("common1").asString().equals("2") && arg.getValue("common2").asString().equals("3") && arg.getValue("common3").asString().equals("4") && arg.getValue("common4").asString().equals("3") && arg.getValue("common5").asString().equals("4") && arg.getValue("common6").asString().equals("4") && arg.getValue("common7").asString().equals("5"); })); // assert the correct overrides in after hook verify(hook) .after( argThat((arg) -> { EvaluationContext evaluationContext = arg.getCtx(); return evaluationContext.getValue("api").asString().equals("1") && evaluationContext .getValue("transaction") .asString() .equals("2") && evaluationContext .getValue("client") .asString() .equals("3") && evaluationContext .getValue("invocation") .asString() .equals("4") && evaluationContext .getValue("before") .asString() .equals("5") && evaluationContext .getValue("common1") .asString() .equals("2") && evaluationContext .getValue("common2") .asString() .equals("3") && evaluationContext .getValue("common3") .asString() .equals("4") && evaluationContext .getValue("common4") .asString() .equals("3") && evaluationContext .getValue("common5") .asString() .equals("4") && evaluationContext .getValue("common6") .asString() .equals("4") && evaluationContext .getValue("common7") .asString() .equals("5"); }), any(), any()); } @Specification( number = "3.3.1.1", text = "The API SHOULD have a method for setting a transaction context propagator.") @Test void setting_transaction_context_propagator() { DoSomethingProvider provider = new DoSomethingProvider(); FeatureProviderTestUtils.setFeatureProvider(provider); TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator(); api.setTransactionContextPropagator(transactionContextPropagator); assertEquals(transactionContextPropagator, api.getTransactionContextPropagator()); } @Specification( number = "3.3.1.2.1", text = "The API MUST have a method for setting the evaluation context of the transaction context propagator for the current transaction.") @Test void setting_transaction_context() { DoSomethingProvider provider = new DoSomethingProvider(); FeatureProviderTestUtils.setFeatureProvider(provider); TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator(); api.setTransactionContextPropagator(transactionContextPropagator); Map attributes = new HashMap<>(); attributes.put("common", new Value("1")); EvaluationContext transactionContext = new ImmutableContext(attributes); api.setTransactionContext(transactionContext); assertEquals(transactionContext, transactionContextPropagator.getTransactionContext()); } @Specification( number = "3.3.1.2.2", text = "A transaction context propagator MUST have a method for setting the evaluation context of the current transaction.") @Specification( number = "3.3.1.2.3", text = "A transaction context propagator MUST have a method for getting the evaluation context of the current transaction.") @Test void transaction_context_propagator_setting_context() { TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator(); Map attributes = new HashMap<>(); attributes.put("common", new Value("1")); EvaluationContext transactionContext = new ImmutableContext(attributes); transactionContextPropagator.setTransactionContext(transactionContext); assertEquals(transactionContext, transactionContextPropagator.getTransactionContext()); } @Specification( number = "1.3.4", text = "The client SHOULD guarantee the returned value of any typed flag evaluation method is of the expected type. If the value returned by the underlying provider implementation does not match the expected type, it's to be considered abnormal execution, and the supplied default value should be returned.") @Test void type_system_prevents_this() {} @Specification( number = "1.1.7", text = "The client creation function MUST NOT throw, or otherwise abnormally terminate.") @Test void constructor_does_not_throw() {} @Specification( number = "1.4.12", text = "The client SHOULD provide asynchronous or non-blocking mechanisms for flag evaluation.") @Test void one_thread_per_request_model() {} @Specification(number = "1.4.14.1", text = "Condition: Flag metadata MUST be immutable.") @Test void compiler_enforced() {} @Specification( number = "1.4.2.1", text = "The client MUST provide methods for detailed flag value evaluation with parameters flag key (string, required), default value (boolean | number | string | structure, required), and evaluation options (optional), which returns an evaluation details structure.") @Specification( number = "1.3.2.1", text = "The client MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure, with parameters flag key (string, required), default value (boolean | number | string | structure, required), and evaluation options (optional), which returns the flag value.") @Specification( number = "3.2.2.2", text = "The Client and invocation MUST NOT have a method for supplying evaluation context.") @Specification( number = "3.2.4.1", text = "When the global evaluation context is set, the on context changed handler MUST run.") @Specification( number = "3.3.2.1", text = "The API MUST NOT have a method for setting a transaction context propagator.") @Test void not_applicable_for_dynamic_context() {} }