Skip to content

Commit 1d08491

Browse files
josecolellaclaude
andauthored
fix: close remaining MUST-level spec compliance gaps (#238)
Signed-off-by: Jose Colella <jose.colella@gusto.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0b2c609 commit 1d08491

4 files changed

Lines changed: 157 additions & 2 deletions

File tree

lib/open_feature/sdk/api.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ def build_client(domain: nil, evaluation_context: nil)
5454
Client.new(provider: Provider::NoOpProvider.new, evaluation_context:)
5555
end
5656

57+
def provider_metadata(domain: nil)
58+
prov = provider(domain: domain)
59+
prov.metadata if prov&.respond_to?(:metadata)
60+
end
61+
5762
def add_handler(event_type, handler)
5863
configuration.add_handler(event_type, handler)
5964
end

lib/open_feature/sdk/client.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,17 @@ def fetch_details(type:, flag_key:, default_value:, evaluation_context: nil, inv
9797
return EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution)
9898
end
9999

100-
return evaluate_flag(type: type, flag_key: flag_key, default_value: default_value, evaluation_context: built_context)
100+
begin
101+
return evaluate_flag(type: type, flag_key: flag_key, default_value: default_value, evaluation_context: built_context)
102+
rescue => e
103+
resolution = Provider::ResolutionDetails.new(
104+
value: default_value,
105+
error_code: Provider::ErrorCode::GENERAL,
106+
reason: Provider::Reason::ERROR,
107+
error_message: e.message
108+
)
109+
return EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution)
110+
end
101111
end
102112

103113
hook_context = Hooks::HookContext.new(

lib/open_feature/sdk/configuration.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ def shutdown
9393

9494
providers_to_shutdown.each do |prov|
9595
# Spec 1.7.9: Set provider state to NOT_READY before shutdown
96-
@provider_state_registry.update_state_from_event(prov, ProviderEvent::PROVIDER_READY)
9796
@provider_state_registry.set_initial_state(prov, ProviderState::NOT_READY)
9897
prov.shutdown if prov.respond_to?(:shutdown)
9998
rescue => e
@@ -114,6 +113,9 @@ def reset
114113
@provider_mutex.synchronize do
115114
@providers.clear
116115
end
116+
@hooks.clear
117+
@evaluation_context = nil
118+
@transaction_context_propagator = nil
117119
end
118120

119121
def set_provider_internal(provider, domain:, wait_for_init:)

spec/specification/flag_evaluation_api_spec.rb

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,88 @@
370370

371371
expect(OpenFeature::SDK.provider).to be_nil
372372
end
373+
374+
specify "After shutdown, hooks are cleared." do
375+
hook = Class.new { include OpenFeature::SDK::Hooks::Hook }.new
376+
OpenFeature::SDK.add_hooks(hook)
377+
expect(OpenFeature::SDK.hooks).not_to be_empty
378+
379+
OpenFeature::SDK.shutdown
380+
381+
expect(OpenFeature::SDK.hooks).to be_empty
382+
end
383+
384+
specify "After shutdown, evaluation context is cleared." do
385+
OpenFeature::SDK.configure do |config|
386+
config.evaluation_context = OpenFeature::SDK::EvaluationContext.new(targeting_key: "user-1")
387+
end
388+
expect(OpenFeature::SDK.evaluation_context).not_to be_nil
389+
390+
OpenFeature::SDK.shutdown
391+
392+
expect(OpenFeature::SDK.evaluation_context).to be_nil
393+
end
394+
395+
specify "After shutdown, transaction context propagator is cleared." do
396+
propagator = OpenFeature::SDK::ThreadLocalTransactionContextPropagator.new
397+
OpenFeature::SDK.set_transaction_context_propagator(propagator)
398+
expect(OpenFeature::SDK.configuration.transaction_context_propagator).not_to be_nil
399+
400+
OpenFeature::SDK.shutdown
401+
402+
expect(OpenFeature::SDK.configuration.transaction_context_propagator).to be_nil
403+
end
404+
end
405+
end
406+
407+
context "1.4 - Flag Evaluation" do
408+
context "Requirement 1.4.10" do
409+
specify "client must not throw exceptions even when no hooks are registered and provider raises" do
410+
error_provider = Class.new do
411+
def metadata
412+
OpenFeature::SDK::Provider::ProviderMetadata.new(name: "Error Provider")
413+
end
414+
415+
def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
416+
raise "provider internal error"
417+
end
418+
419+
def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
420+
raise "provider internal error"
421+
end
422+
423+
def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
424+
raise "provider internal error"
425+
end
426+
427+
def fetch_integer_value(flag_key:, default_value:, evaluation_context: nil)
428+
raise "provider internal error"
429+
end
430+
431+
def fetch_float_value(flag_key:, default_value:, evaluation_context: nil)
432+
raise "provider internal error"
433+
end
434+
435+
def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
436+
raise "provider internal error"
437+
end
438+
end.new
439+
440+
OpenFeature::SDK.set_provider_and_wait(error_provider)
441+
client = OpenFeature::SDK.build_client
442+
443+
# Ensure no hooks are registered (fast path)
444+
OpenFeature::SDK.hooks.clear
445+
446+
result = nil
447+
expect {
448+
result = client.fetch_boolean_details(flag_key: "flag", default_value: false)
449+
}.not_to raise_error
450+
451+
expect(result.value).to eq(false)
452+
expect(result.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::GENERAL)
453+
expect(result.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR)
454+
end
373455
end
374456
end
375457

@@ -470,6 +552,62 @@
470552
end
471553
end
472554

555+
context "1.7 - Provider Lifecycle (shutdown)" do
556+
context "Requirement 1.7.9" do
557+
specify "shutdown must not emit PROVIDER_READY before transitioning to NOT_READY" do
558+
provider = OpenFeature::SDK::Provider::InMemoryProvider.new
559+
OpenFeature::SDK.set_provider_and_wait(provider)
560+
561+
states_observed = []
562+
handler = ->(event_details) { states_observed << :ready }
563+
564+
OpenFeature::SDK.add_handler(OpenFeature::SDK::ProviderEvent::PROVIDER_READY, handler)
565+
566+
# Clear any immediate handler invocation from registration
567+
states_observed.clear
568+
569+
OpenFeature::SDK.shutdown
570+
571+
expect(states_observed).not_to include(:ready)
572+
end
573+
end
574+
end
575+
576+
context "1.1 - API Metadata" do
577+
context "Requirement 1.1.5" do
578+
specify "The API must provide a function for retrieving the metadata of the configured provider" do
579+
provider = OpenFeature::SDK::Provider::InMemoryProvider.new
580+
OpenFeature::SDK.set_provider_and_wait(provider)
581+
582+
expect(OpenFeature::SDK).to respond_to(:provider_metadata)
583+
expect(OpenFeature::SDK.provider_metadata.name).to eq("In-memory Provider")
584+
end
585+
586+
specify "provider_metadata accepts a domain parameter" do
587+
default_provider = OpenFeature::SDK::Provider::InMemoryProvider.new
588+
domain_provider = OpenFeature::SDK::Provider::NoOpProvider.new
589+
590+
OpenFeature::SDK.set_provider_and_wait(default_provider)
591+
OpenFeature::SDK.set_provider_and_wait(domain_provider, domain: "my-domain")
592+
593+
expect(OpenFeature::SDK.provider_metadata(domain: "my-domain").name).to eq("No-op Provider")
594+
end
595+
596+
specify "provider_metadata falls back to default provider when domain has no binding" do
597+
default_provider = OpenFeature::SDK::Provider::InMemoryProvider.new
598+
OpenFeature::SDK.set_provider_and_wait(default_provider)
599+
600+
expect(OpenFeature::SDK.provider_metadata(domain: "unknown").name).to eq("In-memory Provider")
601+
end
602+
603+
specify "provider_metadata returns nil when no provider is configured" do
604+
OpenFeature::SDK.shutdown
605+
606+
expect(OpenFeature::SDK.provider_metadata).to be_nil
607+
end
608+
end
609+
end
610+
473611
context "Logger Methods" do
474612
specify "delegates logger getter to configuration" do
475613
logger = double("Logger")

0 commit comments

Comments
 (0)