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,144 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'test_helper'
4
- require 'semantic_logger'
5
-
6
- # Verifies the SemanticLoggerFilter: ONE Quonfig config gates many loggers.
7
- # The filter injects the native SemanticLogger logger name under the
8
- # `quonfig-sdk-logging` context (keyed at `.key`) so customer rules can target
9
- # `PROP_STARTS_WITH_ONE_OF MyApp::` etc.
10
- #
11
- # Breaking change in 0.0.5: context key was renamed from `quonfig.logger-name`
12
- # (dotted snake_case, flat) to `quonfig-sdk-logging.key` (nested, verbatim
13
- # class name). Normalization was removed — logger names are passed through
14
- # as-is.
15
- class TestSemanticLoggerFilter < Minitest::Test
16
- CONFIG_KEY = 'log-levels.my-app'
17
-
18
- # FakeClient lets us assert the exact key + context the filter passes to
19
- # the SDK without standing up a full datadir.
20
- class FakeClient
21
- attr_reader :calls
22
-
23
- def initialize(level)
24
- @level = level
25
- @calls = []
26
- end
27
-
28
- def get(key, default = nil, context = nil)
29
- @calls << { key: key, default: default, context: context }
30
- @level.nil? ? default : @level
31
- end
32
- end
33
-
34
- def make_log(name, level)
35
- SemanticLogger::Log.new(name, level).tap { |log| log.level = level }
36
- end
37
-
38
- def filter_for(level)
39
- client = FakeClient.new(level)
40
- [Quonfig::SemanticLoggerFilter.new(client, config_key: CONFIG_KEY), client]
41
- end
42
-
43
- def test_calls_single_config_key_with_logger_name_in_context
44
- filter, client = filter_for(:info)
45
- filter.call(make_log('MyApp::Foo::Bar', :warn))
46
-
47
- assert_equal 1, client.calls.size
48
- assert_equal CONFIG_KEY, client.calls.first[:key]
49
- ctx = client.calls.first[:context]
50
- assert_equal({ 'quonfig-sdk-logging' => { 'key' => 'MyApp::Foo::Bar' } }, ctx)
51
- end
52
-
53
- def test_passes_through_when_level_meets_configured_minimum
54
- filter, _ = filter_for(:info)
55
-
56
- assert_equal true, filter.call(make_log('Anything', :info))
57
- assert_equal true, filter.call(make_log('Anything', :warn))
58
- assert_equal true, filter.call(make_log('Anything', :error))
59
- assert_equal true, filter.call(make_log('Anything', :fatal))
60
- end
61
-
62
- def test_suppresses_below_configured_minimum
63
- filter, _ = filter_for(:warn)
64
-
65
- assert_equal false, filter.call(make_log('Anything', :trace))
66
- assert_equal false, filter.call(make_log('Anything', :debug))
67
- assert_equal false, filter.call(make_log('Anything', :info))
68
- assert_equal true, filter.call(make_log('Anything', :warn))
69
- end
70
-
71
- def test_missing_key_falls_through_to_semantic_logger_default
72
- filter, _ = filter_for(nil) # FakeClient returns the default (nil) when configured level is nil
73
-
74
- assert_equal true, filter.call(make_log('Anything', :trace))
75
- assert_equal true, filter.call(make_log('Anything', :debug))
76
- end
77
-
78
- def test_logger_name_passed_through_verbatim
79
- # Normalization is gone. Native Ruby class names are preserved as-is,
80
- # which matches how sdk-node and sdk-go pass the logger path.
81
- filter, client = filter_for(:debug)
82
-
83
- [
84
- 'MyApp::Foo::Bar',
85
- 'HTMLParser',
86
- 'foo',
87
- 'A::B::CDPath',
88
- 'MyApp::Services::Auth'
89
- ].each do |raw|
90
- client.calls.clear
91
- filter.call(make_log(raw, :info))
92
- assert_equal raw, client.calls.first[:context]['quonfig-sdk-logging']['key'],
93
- "logger name should be passed through verbatim: #{raw.inspect}"
94
- end
95
- end
96
-
97
- def test_no_dotted_path_traversal_or_get_log_level
98
- # Verifies the legacy hierarchical walk is gone — the filter must NOT
99
- # synthesize keys like "log-levels.my_app" or call any `get_log_level`.
100
- refute Quonfig::SemanticLoggerFilter.instance_methods.include?(:get_log_level)
101
-
102
- filter, client = filter_for(:info)
103
- filter.call(make_log('MyApp::Foo::Bar', :info))
104
-
105
- keys = client.calls.map { |c| c[:key] }
106
- assert_equal [CONFIG_KEY], keys.uniq,
107
- 'Filter should call exactly the configured key, never derived per-logger keys'
108
- end
109
-
110
- def test_normalize_method_is_gone
111
- # The old normalize() method converted "MyApp::Foo" → "my_app.foo".
112
- # It is intentionally removed so callers see native Ruby class names in
113
- # context telemetry and rule matching.
114
- refute Quonfig::SemanticLoggerFilter.instance_methods.include?(:normalize),
115
- 'normalize() should be removed — logger names are passed through as-is'
116
- end
117
-
118
- def test_context_key_constant_is_new_shape
119
- # Sanity check that the context key constant exposes the new shape.
120
- assert_equal 'quonfig-sdk-logging', Quonfig::SemanticLoggerFilter::LOGGER_CONTEXT_NAME
121
- assert_equal 'key', Quonfig::SemanticLoggerFilter::LOGGER_CONTEXT_KEY_PROP
122
- end
123
-
124
- def test_all_six_levels_mapped_correctly
125
- expected = { trace: 0, debug: 1, info: 2, warn: 3, error: 4, fatal: 5 }
126
- assert_equal expected, Quonfig::SemanticLoggerFilter::LEVELS
127
- end
128
-
129
- def test_string_level_from_config
130
- filter, _ = filter_for('warn')
131
-
132
- assert_equal false, filter.call(make_log('Anything', :info))
133
- assert_equal true, filter.call(make_log('Anything', :warn))
134
- end
135
-
136
- def test_raises_loaderror_when_semantic_logger_missing
137
- Quonfig::SemanticLoggerFilter.stub(:semantic_logger_loaded?, false) do
138
- err = assert_raises(LoadError) do
139
- Quonfig::SemanticLoggerFilter.new(FakeClient.new(:info), config_key: CONFIG_KEY)
140
- end
141
- assert_match(/semantic_logger/i, err.message)
142
- end
143
- end
144
- end
data/test/test_semver.rb DELETED
@@ -1,108 +0,0 @@
1
- require 'test_helper'
2
- class TestSemanticVersion < Minitest::Test
3
- def test_parse_valid_version
4
- version = SemanticVersion.parse('1.2.3')
5
- assert_equal 1, version.major
6
- assert_equal 2, version.minor
7
- assert_equal 3, version.patch
8
- assert_nil version.prerelease
9
- assert_nil version.build_metadata
10
- end
11
-
12
- def test_parse_version_with_prerelease
13
- version = SemanticVersion.parse('1.2.3-alpha.1')
14
- assert_equal 1, version.major
15
- assert_equal 2, version.minor
16
- assert_equal 3, version.patch
17
- assert_equal 'alpha.1', version.prerelease
18
- assert_nil version.build_metadata
19
- end
20
-
21
- def test_parse_version_with_build_metadata
22
- version = SemanticVersion.parse('1.2.3+build.123')
23
- assert_equal 1, version.major
24
- assert_equal 2, version.minor
25
- assert_equal 3, version.patch
26
- assert_nil version.prerelease
27
- assert_equal 'build.123', version.build_metadata
28
- end
29
-
30
- def test_parse_full_version
31
- version = SemanticVersion.parse('1.2.3-alpha.1+build.123')
32
- assert_equal 1, version.major
33
- assert_equal 2, version.minor
34
- assert_equal 3, version.patch
35
- assert_equal 'alpha.1', version.prerelease
36
- assert_equal 'build.123', version.build_metadata
37
- end
38
-
39
- def test_parse_invalid_version
40
- assert_raises(ArgumentError) { SemanticVersion.parse('invalid') }
41
- assert_raises(ArgumentError) { SemanticVersion.parse('1.2') }
42
- assert_raises(ArgumentError) { SemanticVersion.parse('1.2.3.4') }
43
- assert_raises(ArgumentError) { SemanticVersion.parse('') }
44
- end
45
-
46
- def test_parse_quietly
47
- assert_nil SemanticVersion.parse_quietly('invalid')
48
- refute_nil SemanticVersion.parse_quietly('1.2.3')
49
- end
50
-
51
- def test_to_string
52
- assert_equal '1.2.3', SemanticVersion.parse('1.2.3').to_s
53
- assert_equal '1.2.3-alpha.1', SemanticVersion.parse('1.2.3-alpha.1').to_s
54
- assert_equal '1.2.3+build.123', SemanticVersion.parse('1.2.3+build.123').to_s
55
- assert_equal '1.2.3-alpha.1+build.123', SemanticVersion.parse('1.2.3-alpha.1+build.123').to_s
56
- end
57
-
58
- def test_equality
59
- v1 = SemanticVersion.parse('1.2.3')
60
- v2 = SemanticVersion.parse('1.2.3')
61
- v3 = SemanticVersion.parse('1.2.4')
62
- v4 = SemanticVersion.parse('1.2.3-alpha')
63
- v5 = SemanticVersion.parse('1.2.3+build.123')
64
-
65
- assert_equal v1, v2
66
- refute_equal v1, v3
67
- refute_equal v1, v4
68
- assert_equal v1, v5 # build metadata is ignored in equality
69
- end
70
-
71
- def test_comparison
72
- versions = [
73
- '1.0.0-alpha',
74
- '1.0.0-alpha.1',
75
- '1.0.0-beta.2',
76
- '1.0.0-beta.11',
77
- '1.0.0-rc.1',
78
- '1.0.0',
79
- '2.0.0',
80
- '2.1.0',
81
- '2.1.1'
82
- ].map { |v| SemanticVersion.parse(v) }
83
-
84
- # Test that each version is less than the next version
85
- (versions.length - 1).times do |i|
86
- assert versions[i] < versions[i + 1], "Expected #{versions[i]} < #{versions[i + 1]}"
87
- end
88
- end
89
-
90
- def test_prerelease_comparison
91
- # Test specific prerelease comparison cases
92
- cases = [
93
- ['1.0.0-alpha', '1.0.0-alpha.1', -1],
94
- ['1.0.0-alpha.1', '1.0.0-alpha.beta', -1],
95
- ['1.0.0-alpha.beta', '1.0.0-beta', -1],
96
- ['1.0.0-beta', '1.0.0-beta.2', -1],
97
- ['1.0.0-beta.2', '1.0.0-beta.11', -1],
98
- ['1.0.0-beta.11', '1.0.0-rc.1', -1],
99
- ['1.0.0-rc.1', '1.0.0', -1]
100
- ]
101
-
102
- cases.each do |v1_str, v2_str, expected|
103
- v1 = SemanticVersion.parse(v1_str)
104
- v2 = SemanticVersion.parse(v2_str)
105
- assert_equal expected, (v1 <=> v2), "Expected #{v1} <=> #{v2} to be #{expected}"
106
- end
107
- end
108
- end
@@ -1,186 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'test_helper'
4
-
5
- # Verifies the client-level should_log?(logger_path:, desired_level:, contexts:)
6
- # API — a Reforge-style convenience built on top of the primitive get() that
7
- # uses the client's `logger_key` option as the config key and injects the
8
- # logger path under `quonfig-sdk-logging.key`. Parallels sdk-node's
9
- # shouldLog({loggerPath}) and sdk-go's ShouldLogPath.
10
- class TestShouldLog < Minitest::Test
11
- LOG_LEVEL_KEY = 'log-level.my-app'
12
-
13
- # Minimal config fixture mirroring what ConfigStore expects: a string
14
- # config whose rule returns the configured log level.
15
- def make_log_level_config(key:, level:)
16
- {
17
- 'id' => '1',
18
- 'key' => key,
19
- 'type' => 'config',
20
- 'valueType' => 'string',
21
- 'sendToClientSdk' => false,
22
- 'default' => {
23
- 'rules' => [
24
- { 'criteria' => [{ 'operator' => 'ALWAYS_TRUE' }],
25
- 'value' => { 'type' => 'string', 'value' => level } }
26
- ]
27
- },
28
- 'environment' => nil
29
- }
30
- end
31
-
32
- def store_with(*configs)
33
- store = Quonfig::ConfigStore.new
34
- configs.each { |c| store.set(c['key'], c) }
35
- store
36
- end
37
-
38
- def client_with(store, **options)
39
- Quonfig::Client.new(Quonfig::Options.new(**options), store: store)
40
- end
41
-
42
- # ---- logger_key option surface ---------------------------------------
43
-
44
- def test_logger_key_option_defaults_to_nil
45
- assert_nil Quonfig::Options.new.logger_key
46
- end
47
-
48
- def test_logger_key_option_accepts_value
49
- opts = Quonfig::Options.new(logger_key: LOG_LEVEL_KEY)
50
- assert_equal LOG_LEVEL_KEY, opts.logger_key
51
- end
52
-
53
- def test_client_exposes_logger_key_from_options
54
- client = client_with(Quonfig::ConfigStore.new, logger_key: LOG_LEVEL_KEY)
55
- assert_equal LOG_LEVEL_KEY, client.logger_key
56
- end
57
-
58
- # ---- should_log? requires logger_key ---------------------------------
59
-
60
- def test_should_log_raises_without_logger_key
61
- client = client_with(Quonfig::ConfigStore.new)
62
- err = assert_raises(Quonfig::Error) do
63
- client.should_log?(logger_path: 'MyApp::Foo', desired_level: :info)
64
- end
65
- assert_match(/logger_key/, err.message)
66
- end
67
-
68
- # ---- should_log? gating ----------------------------------------------
69
-
70
- def test_should_log_true_when_desired_at_or_above_configured
71
- store = store_with(make_log_level_config(key: LOG_LEVEL_KEY, level: 'info'))
72
- client = client_with(store, logger_key: LOG_LEVEL_KEY)
73
-
74
- assert_equal true, client.should_log?(logger_path: 'MyApp::Foo', desired_level: :info)
75
- assert_equal true, client.should_log?(logger_path: 'MyApp::Foo', desired_level: :warn)
76
- assert_equal true, client.should_log?(logger_path: 'MyApp::Foo', desired_level: :error)
77
- assert_equal true, client.should_log?(logger_path: 'MyApp::Foo', desired_level: :fatal)
78
- end
79
-
80
- def test_should_log_false_when_desired_below_configured
81
- store = store_with(make_log_level_config(key: LOG_LEVEL_KEY, level: 'warn'))
82
- client = client_with(store, logger_key: LOG_LEVEL_KEY)
83
-
84
- assert_equal false, client.should_log?(logger_path: 'MyApp::Foo', desired_level: :trace)
85
- assert_equal false, client.should_log?(logger_path: 'MyApp::Foo', desired_level: :debug)
86
- assert_equal false, client.should_log?(logger_path: 'MyApp::Foo', desired_level: :info)
87
- assert_equal true, client.should_log?(logger_path: 'MyApp::Foo', desired_level: :warn)
88
- end
89
-
90
- def test_should_log_accepts_string_desired_level
91
- store = store_with(make_log_level_config(key: LOG_LEVEL_KEY, level: 'warn'))
92
- client = client_with(store, logger_key: LOG_LEVEL_KEY)
93
-
94
- assert_equal true, client.should_log?(logger_path: 'MyApp::Foo', desired_level: 'warn')
95
- assert_equal false, client.should_log?(logger_path: 'MyApp::Foo', desired_level: 'info')
96
- end
97
-
98
- def test_should_log_returns_true_when_no_config_found
99
- # Missing config key → log everything (match go/node).
100
- client = client_with(Quonfig::ConfigStore.new, logger_key: LOG_LEVEL_KEY)
101
- assert_equal true, client.should_log?(logger_path: 'MyApp::Foo', desired_level: :trace)
102
- end
103
-
104
- # ---- context injection -----------------------------------------------
105
-
106
- # Capture what context reaches get() by injecting a spy client that wraps
107
- # a real store-backed client.
108
- class ContextCapturingClient
109
- attr_reader :captured_contexts
110
-
111
- def initialize(delegate)
112
- @delegate = delegate
113
- @captured_contexts = []
114
- end
115
-
116
- def logger_key
117
- @delegate.logger_key
118
- end
119
-
120
- def get(key, default = Quonfig::NO_DEFAULT_PROVIDED, jit_context = Quonfig::NO_DEFAULT_PROVIDED)
121
- @captured_contexts << jit_context
122
- @delegate.get(key, default, jit_context)
123
- end
124
- end
125
-
126
- def test_should_log_injects_logger_path_under_quonfig_sdk_logging_key
127
- store = store_with(make_log_level_config(key: LOG_LEVEL_KEY, level: 'trace'))
128
- client = client_with(store, logger_key: LOG_LEVEL_KEY)
129
-
130
- # Reach into the context that get() sees. We do this by asserting on the
131
- # resolver via a fake — simplest path: call should_log? with a sentinel
132
- # path and verify the evaluator would see it. We assert via the public
133
- # contract: context reaches get(), so we patch get() temporarily.
134
- captured = []
135
- client.define_singleton_method(:get) do |key, default = nil, jit_context = nil|
136
- captured << { key: key, jit_context: jit_context }
137
- 'trace'
138
- end
139
-
140
- client.should_log?(logger_path: 'MyApp::Services::Auth', desired_level: :info)
141
-
142
- assert_equal 1, captured.size
143
- assert_equal LOG_LEVEL_KEY, captured.first[:key]
144
- ctx = captured.first[:jit_context]
145
- assert_equal({ 'quonfig-sdk-logging' => { 'key' => 'MyApp::Services::Auth' } }, ctx)
146
- end
147
-
148
- def test_should_log_merges_caller_contexts_with_logger_context
149
- store = store_with(make_log_level_config(key: LOG_LEVEL_KEY, level: 'trace'))
150
- client = client_with(store, logger_key: LOG_LEVEL_KEY)
151
-
152
- captured = []
153
- client.define_singleton_method(:get) do |key, default = nil, jit_context = nil|
154
- captured << jit_context
155
- 'trace'
156
- end
157
-
158
- client.should_log?(
159
- logger_path: 'MyApp::Foo',
160
- desired_level: :info,
161
- contexts: { 'user' => { 'id' => 'u1' } }
162
- )
163
-
164
- assert_equal(
165
- {
166
- 'user' => { 'id' => 'u1' },
167
- 'quonfig-sdk-logging' => { 'key' => 'MyApp::Foo' }
168
- },
169
- captured.first
170
- )
171
- end
172
-
173
- def test_should_log_logger_path_verbatim_no_normalization
174
- store = store_with(make_log_level_config(key: LOG_LEVEL_KEY, level: 'trace'))
175
- client = client_with(store, logger_key: LOG_LEVEL_KEY)
176
-
177
- captured = []
178
- client.define_singleton_method(:get) do |key, default = nil, jit_context = nil|
179
- captured << jit_context
180
- 'trace'
181
- end
182
-
183
- client.should_log?(logger_path: 'HTMLParser', desired_level: :info)
184
- assert_equal 'HTMLParser', captured.first['quonfig-sdk-logging']['key']
185
- end
186
- end