quonfig 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -0
  3. data/README.md +4 -4
  4. data/lib/quonfig/evaluation_details.rb +60 -0
  5. data/lib/quonfig/options.rb +37 -16
  6. data/lib/quonfig/sse_config_client.rb +1 -1
  7. data/lib/quonfig/version.rb +5 -0
  8. data/lib/quonfig.rb +2 -1
  9. data/quonfig.gemspec +30 -163
  10. metadata +29 -182
  11. data/.claude/rules/constitution.md +0 -81
  12. data/.claude/rules/git-safety.md +0 -11
  13. data/.claude/rules/issue-tracking.md +0 -13
  14. data/.claude/rules/testing-workflow.md +0 -28
  15. data/.envrc.sample +0 -3
  16. data/.github/CODEOWNERS +0 -2
  17. data/.github/pull_request_template.md +0 -8
  18. data/.github/workflows/release.yml +0 -49
  19. data/.github/workflows/ruby.yml +0 -60
  20. data/.github/workflows/test.yaml +0 -40
  21. data/.rubocop.yml +0 -13
  22. data/.tool-versions +0 -1
  23. data/CLAUDE.md +0 -29
  24. data/CODEOWNERS +0 -1
  25. data/Gemfile +0 -26
  26. data/Gemfile.lock +0 -177
  27. data/Rakefile +0 -64
  28. data/VERSION +0 -1
  29. data/dev/allocation_stats +0 -60
  30. data/dev/benchmark +0 -40
  31. data/dev/console +0 -12
  32. data/dev/script_setup.rb +0 -18
  33. data/test/fixtures/datafile.json +0 -87
  34. data/test/integration/test_context_precedence.rb +0 -112
  35. data/test/integration/test_datadir_environment.rb +0 -54
  36. data/test/integration/test_dev_overrides.rb +0 -40
  37. data/test/integration/test_enabled.rb +0 -478
  38. data/test/integration/test_enabled_with_contexts.rb +0 -64
  39. data/test/integration/test_get.rb +0 -136
  40. data/test/integration/test_get_feature_flag.rb +0 -28
  41. data/test/integration/test_get_or_raise.rb +0 -60
  42. data/test/integration/test_get_weighted_values.rb +0 -34
  43. data/test/integration/test_helpers.rb +0 -667
  44. data/test/integration/test_helpers_test.rb +0 -73
  45. data/test/integration/test_post.rb +0 -44
  46. data/test/integration/test_telemetry.rb +0 -170
  47. data/test/support/common_helpers.rb +0 -106
  48. data/test/support/mock_base_client.rb +0 -27
  49. data/test/support/mock_config_loader.rb +0 -1
  50. data/test/test_bound_client.rb +0 -109
  51. data/test/test_caching_http_connection.rb +0 -218
  52. data/test/test_client.rb +0 -255
  53. data/test/test_client_network_mode.rb +0 -136
  54. data/test/test_client_telemetry.rb +0 -175
  55. data/test/test_config_loader.rb +0 -70
  56. data/test/test_context.rb +0 -139
  57. data/test/test_context_shape.rb +0 -37
  58. data/test/test_context_shape_aggregator.rb +0 -126
  59. data/test/test_datadir.rb +0 -203
  60. data/test/test_dev_context.rb +0 -163
  61. data/test/test_duration.rb +0 -37
  62. data/test/test_encryption.rb +0 -16
  63. data/test/test_evaluation_summaries_aggregator.rb +0 -180
  64. data/test/test_evaluator.rb +0 -285
  65. data/test/test_example_contexts_aggregator.rb +0 -119
  66. data/test/test_exponential_backoff.rb +0 -44
  67. data/test/test_fixed_size_hash.rb +0 -119
  68. data/test/test_helper.rb +0 -17
  69. data/test/test_http_connection.rb +0 -79
  70. data/test/test_internal_logger.rb +0 -34
  71. data/test/test_options.rb +0 -167
  72. data/test/test_rate_limit_cache.rb +0 -44
  73. data/test/test_reason.rb +0 -79
  74. data/test/test_rename.rb +0 -65
  75. data/test/test_resolver.rb +0 -291
  76. data/test/test_semantic_logger_filter.rb +0 -144
  77. data/test/test_semver.rb +0 -108
  78. data/test/test_should_log.rb +0 -186
  79. data/test/test_sse_config_client.rb +0 -297
  80. data/test/test_stdlib_formatter.rb +0 -195
  81. data/test/test_telemetry_reporter.rb +0 -209
  82. data/test/test_typed_getters.rb +0 -131
  83. data/test/test_types.rb +0 -141
  84. data/test/test_weighted_value_resolver.rb +0 -84
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'test_helper'
4
-
5
- class TestInternalLogger < Minitest::Test
6
-
7
- def teardown
8
- # using_quonfig_log_filter! mutates the shared @@instances list — restore
9
- # the default :warn level so it doesn't bleed into other tests' $logs.
10
- Quonfig::InternalLogger.class_variable_get(:@@instances).each do |logger|
11
- logger.level = :warn
12
- end
13
- super
14
- end
15
-
16
- def test_levels
17
- logger_a = Quonfig::InternalLogger.new(A)
18
- logger_b = Quonfig::InternalLogger.new(B)
19
-
20
- assert_equal :warn, logger_a.level
21
- assert_equal :warn, logger_b.level
22
-
23
- Quonfig::InternalLogger.using_quonfig_log_filter!
24
- assert_equal :trace, logger_a.level
25
- assert_equal :trace, logger_b.level
26
- end
27
-
28
- end
29
-
30
- class A
31
- end
32
-
33
- class B
34
- end
data/test/test_options.rb DELETED
@@ -1,167 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'test_helper'
4
-
5
- class TestOptions < Minitest::Test
6
- API_KEY = 'abcdefg'
7
-
8
- def test_api_urls_override_env_var
9
- assert_equal Quonfig::Options::DEFAULT_API_URLS, Quonfig::Options.new.api_urls
10
-
11
- # blank doesn't take effect
12
- with_env('QUONFIG_API_URLS', '') do
13
- assert_equal Quonfig::Options::DEFAULT_API_URLS, Quonfig::Options.new.api_urls
14
- end
15
-
16
- # non-blank does take effect
17
- with_env('QUONFIG_API_URLS', 'https://override.example.com') do
18
- assert_equal ["https://override.example.com"], Quonfig::Options.new.api_urls
19
- end
20
- end
21
-
22
- def test_default_api_urls_point_to_quonfig
23
- assert_equal [
24
- 'https://primary.quonfig.com',
25
- ], Quonfig::Options::DEFAULT_API_URLS
26
- end
27
-
28
- def test_overriding_api_urls
29
- assert_equal Quonfig::Options::DEFAULT_API_URLS, Quonfig::Options.new.api_urls
30
-
31
- # a plain string ends up wrapped in an array
32
- api_url = 'https://example.com'
33
- assert_equal [api_url], Quonfig::Options.new(api_urls: api_url).api_urls
34
-
35
- api_urls = ['https://example.com', 'https://example2.com']
36
- assert_equal api_urls, Quonfig::Options.new(api_urls: api_urls).api_urls
37
- end
38
-
39
- def test_derive_stream_url_prepends_stream_to_hostname
40
- assert_equal 'https://stream.primary.quonfig.com',
41
- Quonfig::Options.derive_stream_url('https://primary.quonfig.com')
42
- end
43
-
44
- def test_derive_stream_url_preserves_port
45
- assert_equal 'http://stream.localhost:6550',
46
- Quonfig::Options.derive_stream_url('http://localhost:6550')
47
- end
48
-
49
- def test_derive_stream_url_preserves_scheme_and_path
50
- assert_equal 'http://stream.api.example.com/base',
51
- Quonfig::Options.derive_stream_url('http://api.example.com/base')
52
- end
53
-
54
- def test_derive_stream_url_with_eu_subdomain
55
- assert_equal 'https://stream.primary.eu.quonfig.com',
56
- Quonfig::Options.derive_stream_url('https://primary.eu.quonfig.com')
57
- end
58
-
59
- def test_works_with_named_arguments
60
- assert_equal API_KEY, Quonfig::Options.new(sdk_key: API_KEY).sdk_key
61
- end
62
-
63
- def test_works_with_hash
64
- assert_equal API_KEY, Quonfig::Options.new({ sdk_key: API_KEY }).sdk_key
65
- end
66
-
67
- def test_sdk_key_reads_from_quonfig_backend_sdk_key
68
- with_env('QUONFIG_BACKEND_SDK_KEY', 'env-key') do
69
- assert_equal 'env-key', Quonfig::Options.new.sdk_key
70
- end
71
- end
72
-
73
- def test_environment_reads_from_quonfig_environment
74
- with_env('QUONFIG_ENVIRONMENT', 'staging') do
75
- assert_equal 'staging', Quonfig::Options.new.environment
76
- end
77
- end
78
-
79
- def test_environment_explicit_overrides_env_var
80
- with_env('QUONFIG_ENVIRONMENT', 'staging') do
81
- assert_equal 'production', Quonfig::Options.new(environment: 'production').environment
82
- end
83
- end
84
-
85
- def test_enable_sse_defaults_true
86
- assert_equal true, Quonfig::Options.new.enable_sse
87
- assert_equal false, Quonfig::Options.new(enable_sse: false).enable_sse
88
- end
89
-
90
- def test_enable_polling_defaults_true
91
- assert_equal true, Quonfig::Options.new.enable_polling
92
- assert_equal false, Quonfig::Options.new(enable_polling: false).enable_polling
93
- end
94
-
95
- def test_datadir_reads_from_quonfig_dir_env
96
- with_env('QUONFIG_DIR', '/tmp/some/workspace') do
97
- assert_equal '/tmp/some/workspace', Quonfig::Options.new.datadir
98
- end
99
- end
100
-
101
- def test_datadir_explicit_overrides_env_var
102
- with_env('QUONFIG_DIR', '/tmp/env/workspace') do
103
- assert_equal '/tmp/explicit', Quonfig::Options.new(datadir: '/tmp/explicit').datadir
104
- end
105
- end
106
-
107
- def test_datadir_predicate
108
- assert_equal false, Quonfig::Options.new.datadir?
109
- assert_equal true, Quonfig::Options.new(datadir: '/tmp/ws').datadir?
110
- end
111
-
112
- def test_local_only_uses_datadir_presence
113
- refute Quonfig::Options.new.local_only?
114
- assert Quonfig::Options.new(datadir: '/tmp/ws').local_only?
115
- end
116
-
117
- def test_collect_max_paths
118
- assert_equal 1000, Quonfig::Options.new.collect_max_paths
119
- assert_equal 100, Quonfig::Options.new(collect_max_paths: 100).collect_max_paths
120
- end
121
-
122
- def test_collect_max_evaluation_summaries
123
- assert_equal 100_000, Quonfig::Options.new.collect_max_evaluation_summaries
124
- assert_equal 0, Quonfig::Options.new(collect_evaluation_summaries: false).collect_max_evaluation_summaries
125
- assert_equal 3,
126
- Quonfig::Options.new(collect_max_evaluation_summaries: 3).collect_max_evaluation_summaries
127
- end
128
-
129
- def test_context_upload_mode_periodic
130
- options = Quonfig::Options.new(context_upload_mode: :periodic_example, context_max_size: 100)
131
- assert_equal 100, options.collect_max_example_contexts
132
-
133
- options = Quonfig::Options.new(context_upload_mode: :none)
134
- assert_equal 0, options.collect_max_example_contexts
135
- end
136
-
137
- def test_context_upload_mode_shapes_only
138
- options = Quonfig::Options.new(context_upload_mode: :shapes_only, context_max_size: 100)
139
- assert_equal 100, options.collect_max_shapes
140
-
141
- options = Quonfig::Options.new(context_upload_mode: :none)
142
- assert_equal 0, options.collect_max_shapes
143
- end
144
-
145
- def test_context_upload_mode_none
146
- options = Quonfig::Options.new(context_upload_mode: :none)
147
- assert_equal 0, options.collect_max_example_contexts
148
-
149
- options = Quonfig::Options.new(context_upload_mode: :none)
150
- assert_equal 0, options.collect_max_shapes
151
- end
152
-
153
- def test_telemetry_destination_reads_env_first
154
- with_env('QUONFIG_TELEMETRY_URL', 'https://custom-telemetry.example.com') do
155
- assert_equal 'https://custom-telemetry.example.com', Quonfig::Options.new.telemetry_destination
156
- end
157
- end
158
-
159
- def test_telemetry_destination_derives_from_default_sources
160
- assert_equal 'https://telemetry.quonfig.com', Quonfig::Options.new.telemetry_destination
161
- end
162
-
163
- def test_telemetry_destination_derives_from_custom_quonfig_api_urls
164
- options = Quonfig::Options.new(api_urls: ['https://primary.eu.quonfig.com'])
165
- assert_equal 'https://telemetry.eu.quonfig.com', options.telemetry_destination
166
- end
167
- end
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'test_helper'
4
- require 'timecop'
5
-
6
- class RateLimitCacheTest < Minitest::Test
7
- def test_set_and_fresh
8
- cache = Quonfig::RateLimitCache.new(5)
9
- cache.set('key')
10
- assert cache.fresh?('key')
11
- end
12
-
13
- def test_fresh_with_no_set
14
- cache = Quonfig::RateLimitCache.new(5)
15
- refute cache.fresh?('key')
16
- end
17
-
18
- def test_get_after_expiration
19
- cache = Quonfig::RateLimitCache.new(5)
20
-
21
- Timecop.freeze(Time.now - 6) do
22
- cache.set('key')
23
- assert cache.fresh?('key')
24
- end
25
-
26
- refute cache.fresh?('key')
27
-
28
- # but the data is still there
29
- assert cache.data.get('key')
30
- end
31
-
32
- def test_prune
33
- cache = Quonfig::RateLimitCache.new(5)
34
-
35
- Timecop.freeze(Time.now - 6) do
36
- cache.set('key')
37
- assert cache.fresh?('key')
38
- end
39
-
40
- cache.prune
41
-
42
- refute cache.fresh?('key')
43
- end
44
- end
data/test/test_reason.rb DELETED
@@ -1,79 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'minitest/autorun'
4
- require 'quonfig/reason'
5
-
6
- # Tests Quonfig::Reason in isolation — uses plain Structs so the test does not
7
- # depend on PrefabProto (which is not loaded by the current bootstrap).
8
- class TestReason < Minitest::Test
9
- FakeCriterion = Struct.new(:operator)
10
- FakeConditionalValue = Struct.new(:criteria)
11
- FakeRow = Struct.new(:values, :project_env_id)
12
- FakeConfig = Struct.new(:rows)
13
-
14
- ALWAYS_TRUE = FakeCriterion.new(:ALWAYS_TRUE)
15
- PROP_MATCH = FakeCriterion.new(:PROP_IS_ONE_OF)
16
-
17
- DEFAULT_CV = FakeConditionalValue.new([])
18
- ALWAYS_TRUE_CV = FakeConditionalValue.new([ALWAYS_TRUE])
19
- RULE_CV = FakeConditionalValue.new([PROP_MATCH])
20
-
21
- def default_only_config
22
- FakeConfig.new([FakeRow.new([DEFAULT_CV], 0)])
23
- end
24
-
25
- def targeted_config
26
- FakeConfig.new([
27
- FakeRow.new([DEFAULT_CV], 0),
28
- FakeRow.new([RULE_CV, ALWAYS_TRUE_CV], 1)
29
- ])
30
- end
31
-
32
- def test_default_for_default_only_config
33
- reason = Quonfig::Reason.compute(
34
- config: default_only_config,
35
- conditional_value: DEFAULT_CV
36
- )
37
- assert_equal :DEFAULT, reason
38
- end
39
-
40
- def test_rule_match_when_targeting_rule_matched
41
- reason = Quonfig::Reason.compute(
42
- config: targeted_config,
43
- conditional_value: RULE_CV
44
- )
45
- assert_equal :RULE_MATCH, reason
46
- end
47
-
48
- def test_rule_match_when_falling_back_to_always_true_in_targeted_config
49
- reason = Quonfig::Reason.compute(
50
- config: targeted_config,
51
- conditional_value: ALWAYS_TRUE_CV
52
- )
53
- assert_equal :RULE_MATCH, reason
54
- end
55
-
56
- def test_split_when_weighted_value_index_positive
57
- reason = Quonfig::Reason.compute(
58
- config: default_only_config,
59
- conditional_value: DEFAULT_CV,
60
- weighted_value_index: 2
61
- )
62
- assert_equal :SPLIT, reason
63
- end
64
-
65
- def test_weighted_value_index_zero_is_not_split
66
- reason = Quonfig::Reason.compute(
67
- config: default_only_config,
68
- conditional_value: DEFAULT_CV,
69
- weighted_value_index: 0
70
- )
71
- assert_equal :DEFAULT, reason
72
- end
73
-
74
- def test_default_when_only_always_true_criteria_and_no_targeting_rules
75
- config = FakeConfig.new([FakeRow.new([ALWAYS_TRUE_CV], 0)])
76
- reason = Quonfig::Reason.compute(config: config, conditional_value: ALWAYS_TRUE_CV)
77
- assert_equal :DEFAULT, reason
78
- end
79
- end
data/test/test_rename.rb DELETED
@@ -1,65 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'test_helper'
4
-
5
- class TestRename < Minitest::Test
6
- def test_quonfig_module_is_defined
7
- assert defined?(Quonfig), 'Quonfig module must be defined after rename'
8
- end
9
-
10
- def test_reforge_module_is_gone
11
- # NOTE: 'Reforge' intentionally not renamed — this test guards against the old constant leaking back in.
12
- refute Object.const_defined?(:Reforge), 'Reforge constant must be removed after rename'
13
- end
14
-
15
- def test_quonfig_options_exists
16
- assert defined?(Quonfig::Options), 'Quonfig::Options must be defined'
17
- end
18
-
19
- def test_quonfig_client_exists
20
- assert defined?(Quonfig::Client), 'Quonfig::Client must be defined'
21
- end
22
-
23
- def test_quonfig_sdk_key_env_var
24
- # NOTE: the REFORGE_/PREFAB_ keys below are intentionally spelled out in string literals
25
- # so the bulk rename tool does not touch them — we are asserting that the NEW env var name
26
- # is the one the SDK reads.
27
- old_key_a = 'REFORGE_' + 'BACKEND_SDK_KEY'
28
- old_key_b = 'PREFAB_' + 'API_KEY'
29
- original = ENV.to_h.slice('QUONFIG_BACKEND_SDK_KEY', old_key_a, old_key_b)
30
- ENV.delete(old_key_a)
31
- ENV.delete(old_key_b)
32
- ENV['QUONFIG_BACKEND_SDK_KEY'] = 'quonfig-test-key-123'
33
- options = Quonfig::Options.new
34
- assert_equal 'quonfig-test-key-123', options.sdk_key
35
- ensure
36
- ENV.delete('QUONFIG_BACKEND_SDK_KEY')
37
- original&.each { |k, v| ENV[k] = v }
38
- end
39
-
40
- def test_gemspec_file_is_quonfig
41
- root = File.expand_path('..', __dir__)
42
- assert File.exist?(File.join(root, 'quonfig.gemspec')),
43
- 'quonfig.gemspec must exist at repo root'
44
- refute File.exist?(File.join(root, 'sdk-reforge.gemspec')),
45
- 'sdk-reforge.gemspec must be removed'
46
- end
47
-
48
- def test_gemspec_name_is_quonfig
49
- root = File.expand_path('..', __dir__)
50
- spec = Gem::Specification.load(File.join(root, 'quonfig.gemspec'))
51
- assert_equal 'quonfig', spec.name
52
- end
53
-
54
- def test_lib_entrypoint_renamed
55
- root = File.expand_path('..', __dir__)
56
- assert File.exist?(File.join(root, 'lib', 'quonfig.rb')),
57
- 'lib/quonfig.rb must exist'
58
- assert Dir.exist?(File.join(root, 'lib', 'quonfig')),
59
- 'lib/quonfig/ directory must exist'
60
- refute File.exist?(File.join(root, 'lib', 'sdk-reforge.rb')),
61
- 'lib/sdk-reforge.rb must be removed'
62
- refute Dir.exist?(File.join(root, 'lib', 'reforge')),
63
- 'lib/reforge/ directory must be removed'
64
- end
65
- end
@@ -1,291 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'test_helper'
4
-
5
- # Tests for the new public API trio introduced by qfg-dk6.9:
6
- # store = Quonfig::ConfigStore.new(configs_hash)
7
- # evaluator = Quonfig::Evaluator.new(store)
8
- # resolver = Quonfig::Resolver.new(store, evaluator)
9
- # result = resolver.get('my.flag', context)
10
- #
11
- # Mirrors the sdk-node pattern so the integration test suite (qfg-dk6.22-24)
12
- # can construct these directly without a full Client.
13
- #
14
- # We deliberately do NOT use PrefabProto — the protobuf gem was dropped in
15
- # qfg-dk6.4 and JSON Criterion types land in qfg-dk6.5 / operators port in
16
- # qfg-dk6.10. These tests use minimal Struct doubles that satisfy the
17
- # duck-typed shape the current CriteriaEvaluator reads.
18
- class TestResolverTrio < Minitest::Test
19
- CONFIG_KEY = 'my.flag'
20
- DEFAULT_VALUE = 'default_value'
21
-
22
- # qfg-dk6.10 — configs are now plain ConfigResponse-shaped hashes (symbol
23
- # top-level keys + string keys inside rules/criteria). Matches what
24
- # Quonfig::Datadir.to_config_response and
25
- # IntegrationTestHelpers.to_config_response emit.
26
- def make_default_config(key: CONFIG_KEY, value: DEFAULT_VALUE)
27
- {
28
- id: '1',
29
- key: key,
30
- type: 'config',
31
- value_type: 'string',
32
- send_to_client_sdk: false,
33
- default: {
34
- 'rules' => [
35
- {
36
- 'criteria' => [{ 'operator' => 'ALWAYS_TRUE' }],
37
- 'value' => { 'type' => 'string', 'value' => value }
38
- }
39
- ]
40
- },
41
- environment: nil
42
- }
43
- end
44
-
45
- def base_client
46
- MockBaseClient.new(Quonfig::Options.new)
47
- end
48
-
49
- # ---- ConfigStore ------------------------------------------------------
50
-
51
- def test_config_store_constructs_with_hash
52
- cfg = make_default_config
53
- store = Quonfig::ConfigStore.new({ CONFIG_KEY => cfg })
54
-
55
- assert_equal cfg, store.get(CONFIG_KEY)
56
- assert_equal [CONFIG_KEY], store.keys
57
- end
58
-
59
- def test_config_store_constructs_empty
60
- store = Quonfig::ConfigStore.new
61
- assert_nil store.get('missing')
62
- assert_empty store.keys
63
- end
64
-
65
- def test_config_store_set_and_get
66
- store = Quonfig::ConfigStore.new
67
- cfg = make_default_config
68
- store.set(CONFIG_KEY, cfg)
69
- assert_equal cfg, store.get(CONFIG_KEY)
70
- end
71
-
72
- def test_config_store_all_configs_returns_hash
73
- cfg = make_default_config
74
- store = Quonfig::ConfigStore.new({ CONFIG_KEY => cfg })
75
-
76
- all = store.all_configs
77
- assert_kind_of Hash, all
78
- assert_equal cfg, all[CONFIG_KEY]
79
- end
80
-
81
- def test_config_store_all_configs_is_a_copy
82
- cfg = make_default_config
83
- store = Quonfig::ConfigStore.new({ CONFIG_KEY => cfg })
84
- store.all_configs['mutated'] = :nope
85
- assert_nil store.get('mutated')
86
- end
87
-
88
- def test_config_store_clear_empties_the_store
89
- store = Quonfig::ConfigStore.new({ CONFIG_KEY => make_default_config })
90
- store.clear
91
- assert_nil store.get(CONFIG_KEY)
92
- assert_empty store.keys
93
- end
94
-
95
- # ---- Evaluator --------------------------------------------------------
96
-
97
- def test_evaluator_accepts_store
98
- store = Quonfig::ConfigStore.new
99
- Quonfig::Evaluator.new(store, base_client: base_client)
100
- end
101
-
102
- # ---- Resolver ---------------------------------------------------------
103
-
104
- def test_resolver_raw_returns_config_from_store
105
- cfg = make_default_config
106
- store = Quonfig::ConfigStore.new({ CONFIG_KEY => cfg })
107
- evaluator = Quonfig::Evaluator.new(store, base_client: base_client)
108
- resolver = Quonfig::Resolver.new(store, evaluator)
109
-
110
- assert_equal cfg, resolver.raw(CONFIG_KEY)
111
- end
112
-
113
- def test_resolver_raw_returns_nil_for_missing_key
114
- store = Quonfig::ConfigStore.new
115
- evaluator = Quonfig::Evaluator.new(store, base_client: base_client)
116
- resolver = Quonfig::Resolver.new(store, evaluator)
117
-
118
- assert_nil resolver.raw('nope')
119
- end
120
-
121
- def test_resolver_get_returns_evaluation_for_default_row_with_empty_criteria
122
- cfg = make_default_config
123
- store = Quonfig::ConfigStore.new({ CONFIG_KEY => cfg })
124
- evaluator = Quonfig::Evaluator.new(store, base_client: base_client)
125
- resolver = Quonfig::Resolver.new(store, evaluator)
126
-
127
- result = resolver.get(CONFIG_KEY, Quonfig::Context.new({}))
128
-
129
- refute_nil result
130
- assert_kind_of Quonfig::EvalResult, result
131
- # The EvalResult exposes both the raw JSON value hash (#value) and the
132
- # coerced Ruby value (#unwrapped_value). Prefer unwrapped_value for
133
- # assertions — it mirrors what the real Client returns.
134
- assert_equal DEFAULT_VALUE, result.unwrapped_value
135
- end
136
-
137
- def test_resolver_get_raises_missing_default_for_missing_key
138
- store = Quonfig::ConfigStore.new
139
- evaluator = Quonfig::Evaluator.new(store, base_client: base_client)
140
- resolver = Quonfig::Resolver.new(store, evaluator)
141
-
142
- # Resolver.get raises Quonfig::Errors::MissingDefaultError when no
143
- # config exists for the key (qfg-9x7 alignment with the shared YAML
144
- # get_or_raise.yaml suite). Client.get catches this and folds it into
145
- # the on_no_default policy / caller-supplied default.
146
- assert_raises(Quonfig::Errors::MissingDefaultError) do
147
- resolver.get('nope', Quonfig::Context.new({}))
148
- end
149
- end
150
-
151
- # ---- ENV_VAR provided value resolution (qfg-08q) ---------------------
152
-
153
- # Build a config whose value comes from a `provided` ENV_VAR lookup.
154
- # value_type drives coercion of the env var string back to the SDK type
155
- # (mirrors sdk-node/sdk-go behavior).
156
- def make_provided_config(key:, value_type:, lookup:)
157
- {
158
- id: '1',
159
- key: key,
160
- type: 'config',
161
- value_type: value_type,
162
- send_to_client_sdk: false,
163
- default: {
164
- 'rules' => [
165
- {
166
- 'criteria' => [{ 'operator' => 'ALWAYS_TRUE' }],
167
- 'value' => {
168
- 'type' => 'provided',
169
- 'value' => { 'source' => 'ENV_VAR', 'lookup' => lookup }
170
- }
171
- }
172
- ]
173
- },
174
- environment: nil
175
- }
176
- end
177
-
178
- def with_env(name, value)
179
- original = ENV[name]
180
- ENV[name] = value
181
- yield
182
- ensure
183
- if original.nil?
184
- ENV.delete(name)
185
- else
186
- ENV[name] = original
187
- end
188
- end
189
-
190
- def build_resolver(cfg)
191
- store = Quonfig::ConfigStore.new({ cfg[:key] => cfg })
192
- evaluator = Quonfig::Evaluator.new(store, base_client: base_client)
193
- Quonfig::Resolver.new(store, evaluator)
194
- end
195
-
196
- def test_resolver_get_resolves_provided_env_var_as_string
197
- cfg = make_provided_config(key: 'a.string', value_type: 'string', lookup: 'QFG_TEST_STRING')
198
- resolver = build_resolver(cfg)
199
-
200
- with_env('QFG_TEST_STRING', 'hello') do
201
- result = resolver.get('a.string', Quonfig::Context.new({}))
202
- assert_equal 'hello', result.unwrapped_value
203
- assert_equal 'string', result.value_type
204
- end
205
- end
206
-
207
- def test_resolver_get_resolves_provided_env_var_as_int
208
- cfg = make_provided_config(key: 'a.number', value_type: 'int', lookup: 'QFG_TEST_INT')
209
- resolver = build_resolver(cfg)
210
-
211
- with_env('QFG_TEST_INT', '1234') do
212
- result = resolver.get('a.number', Quonfig::Context.new({}))
213
- assert_equal 1234, result.unwrapped_value
214
- assert_equal 'int', result.value_type
215
- end
216
- end
217
-
218
- def test_resolver_get_resolves_provided_env_var_as_double
219
- cfg = make_provided_config(key: 'a.double', value_type: 'double', lookup: 'QFG_TEST_DOUBLE')
220
- resolver = build_resolver(cfg)
221
-
222
- with_env('QFG_TEST_DOUBLE', '3.14') do
223
- result = resolver.get('a.double', Quonfig::Context.new({}))
224
- assert_in_delta 3.14, result.unwrapped_value, 0.0001
225
- assert_equal 'double', result.value_type
226
- end
227
- end
228
-
229
- def test_resolver_get_resolves_provided_env_var_as_bool
230
- cfg = make_provided_config(key: 'a.bool', value_type: 'bool', lookup: 'QFG_TEST_BOOL')
231
- resolver = build_resolver(cfg)
232
-
233
- with_env('QFG_TEST_BOOL', 'true') do
234
- result = resolver.get('a.bool', Quonfig::Context.new({}))
235
- assert_equal true, result.unwrapped_value
236
- assert_equal 'bool', result.value_type
237
- end
238
-
239
- with_env('QFG_TEST_BOOL', 'no') do
240
- result = resolver.get('a.bool', Quonfig::Context.new({}))
241
- assert_equal false, result.unwrapped_value
242
- end
243
- end
244
-
245
- def test_resolver_get_resolves_provided_env_var_as_string_list
246
- cfg = make_provided_config(key: 'a.list', value_type: 'string_list', lookup: 'QFG_TEST_LIST')
247
- resolver = build_resolver(cfg)
248
-
249
- with_env('QFG_TEST_LIST', 'a, b ,c') do
250
- result = resolver.get('a.list', Quonfig::Context.new({}))
251
- assert_equal %w[a b c], result.unwrapped_value
252
- assert_equal 'string_list', result.value_type
253
- end
254
- end
255
-
256
- def test_resolver_get_raises_missing_env_var_error_when_unset
257
- cfg = make_provided_config(key: 'a.missing', value_type: 'string', lookup: 'QFG_DEFINITELY_UNSET')
258
- resolver = build_resolver(cfg)
259
- ENV.delete('QFG_DEFINITELY_UNSET')
260
-
261
- err = assert_raises(Quonfig::Errors::MissingEnvVarError) do
262
- resolver.get('a.missing', Quonfig::Context.new({}))
263
- end
264
- assert_match(/QFG_DEFINITELY_UNSET/, err.message)
265
- assert_match(/a\.missing/, err.message)
266
- end
267
-
268
- def test_resolver_get_raises_env_var_parse_error_on_bad_int
269
- cfg = make_provided_config(key: 'a.number', value_type: 'int', lookup: 'QFG_TEST_BAD_INT')
270
- resolver = build_resolver(cfg)
271
-
272
- with_env('QFG_TEST_BAD_INT', 'not_a_number') do
273
- err = assert_raises(Quonfig::Errors::EnvVarParseError) do
274
- resolver.get('a.number', Quonfig::Context.new({}))
275
- end
276
- assert_match(/a\.number/, err.message)
277
- assert_match(/not_a_number/, err.message)
278
- end
279
- end
280
-
281
- def test_resolver_get_raises_env_var_parse_error_on_bad_double
282
- cfg = make_provided_config(key: 'a.double', value_type: 'double', lookup: 'QFG_TEST_BAD_DOUBLE')
283
- resolver = build_resolver(cfg)
284
-
285
- with_env('QFG_TEST_BAD_DOUBLE', 'not_a_number') do
286
- assert_raises(Quonfig::Errors::EnvVarParseError) do
287
- resolver.get('a.double', Quonfig::Context.new({}))
288
- end
289
- end
290
- end
291
- end