quonfig 0.0.10 → 0.0.12

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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/README.md +94 -0
  4. data/lib/quonfig/caching_http_connection.rb +3 -3
  5. data/lib/quonfig/client.rb +22 -27
  6. data/lib/quonfig/config_loader.rb +5 -1
  7. data/lib/quonfig/config_store.rb +10 -6
  8. data/lib/quonfig/context.rb +5 -4
  9. data/lib/quonfig/datadir.rb +4 -10
  10. data/lib/quonfig/dev_context.rb +4 -2
  11. data/lib/quonfig/duration.rb +2 -2
  12. data/lib/quonfig/encryption.rb +12 -16
  13. data/lib/quonfig/errors/invalid_environment_error.rb +1 -3
  14. data/lib/quonfig/errors/invalid_sdk_key_error.rb +6 -7
  15. data/lib/quonfig/errors/missing_env_var_error.rb +0 -3
  16. data/lib/quonfig/errors/missing_environment_error.rb +1 -1
  17. data/lib/quonfig/errors/uninitialized_error.rb +1 -1
  18. data/lib/quonfig/evaluation.rb +11 -8
  19. data/lib/quonfig/evaluator.rb +34 -37
  20. data/lib/quonfig/fixed_size_hash.rb +1 -0
  21. data/lib/quonfig/http_connection.rb +2 -4
  22. data/lib/quonfig/internal_logger.rb +63 -27
  23. data/lib/quonfig/murmer3.rb +2 -2
  24. data/lib/quonfig/options.rb +62 -75
  25. data/lib/quonfig/periodic_sync.rb +1 -1
  26. data/lib/quonfig/quonfig.rb +3 -3
  27. data/lib/quonfig/reason.rb +2 -1
  28. data/lib/quonfig/resolver.rb +8 -9
  29. data/lib/quonfig/semantic_logger_filter.rb +4 -3
  30. data/lib/quonfig/semver.rb +6 -8
  31. data/lib/quonfig/sse_config_client.rb +14 -15
  32. data/lib/quonfig/stdlib_formatter.rb +3 -3
  33. data/lib/quonfig/telemetry/context_shape_aggregator.rb +2 -3
  34. data/lib/quonfig/telemetry/example_contexts_aggregator.rb +1 -1
  35. data/lib/quonfig/telemetry/telemetry_reporter.rb +1 -0
  36. data/lib/quonfig/time_helpers.rb +2 -0
  37. data/lib/quonfig/version.rb +5 -0
  38. data/lib/quonfig.rb +2 -1
  39. data/quonfig.gemspec +29 -165
  40. metadata +24 -193
  41. data/.claude/rules/constitution.md +0 -81
  42. data/.claude/rules/git-safety.md +0 -11
  43. data/.claude/rules/issue-tracking.md +0 -13
  44. data/.claude/rules/testing-workflow.md +0 -28
  45. data/.envrc.sample +0 -3
  46. data/.github/CODEOWNERS +0 -2
  47. data/.github/pull_request_template.md +0 -8
  48. data/.github/workflows/release.yml +0 -49
  49. data/.github/workflows/ruby.yml +0 -60
  50. data/.github/workflows/test.yaml +0 -40
  51. data/.rubocop.yml +0 -13
  52. data/.tool-versions +0 -1
  53. data/CLAUDE.md +0 -29
  54. data/CODEOWNERS +0 -1
  55. data/Gemfile +0 -26
  56. data/Gemfile.lock +0 -177
  57. data/Rakefile +0 -64
  58. data/VERSION +0 -1
  59. data/dev/allocation_stats +0 -60
  60. data/dev/benchmark +0 -40
  61. data/dev/console +0 -12
  62. data/dev/script_setup.rb +0 -18
  63. data/test/fixtures/datafile.json +0 -87
  64. data/test/integration/test_context_precedence.rb +0 -112
  65. data/test/integration/test_datadir_environment.rb +0 -54
  66. data/test/integration/test_dev_overrides.rb +0 -40
  67. data/test/integration/test_enabled.rb +0 -478
  68. data/test/integration/test_enabled_with_contexts.rb +0 -64
  69. data/test/integration/test_get.rb +0 -136
  70. data/test/integration/test_get_feature_flag.rb +0 -28
  71. data/test/integration/test_get_or_raise.rb +0 -60
  72. data/test/integration/test_get_weighted_values.rb +0 -34
  73. data/test/integration/test_helpers.rb +0 -667
  74. data/test/integration/test_helpers_test.rb +0 -73
  75. data/test/integration/test_post.rb +0 -44
  76. data/test/integration/test_telemetry.rb +0 -170
  77. data/test/support/common_helpers.rb +0 -106
  78. data/test/support/mock_base_client.rb +0 -27
  79. data/test/support/mock_config_loader.rb +0 -1
  80. data/test/test_bound_client.rb +0 -109
  81. data/test/test_caching_http_connection.rb +0 -218
  82. data/test/test_client.rb +0 -255
  83. data/test/test_client_network_mode.rb +0 -136
  84. data/test/test_client_telemetry.rb +0 -175
  85. data/test/test_config_loader.rb +0 -70
  86. data/test/test_context.rb +0 -139
  87. data/test/test_context_shape.rb +0 -37
  88. data/test/test_context_shape_aggregator.rb +0 -126
  89. data/test/test_datadir.rb +0 -203
  90. data/test/test_details_getters.rb +0 -242
  91. data/test/test_dev_context.rb +0 -163
  92. data/test/test_duration.rb +0 -37
  93. data/test/test_encryption.rb +0 -16
  94. data/test/test_evaluation_summaries_aggregator.rb +0 -180
  95. data/test/test_evaluator.rb +0 -285
  96. data/test/test_example_contexts_aggregator.rb +0 -119
  97. data/test/test_exponential_backoff.rb +0 -44
  98. data/test/test_fixed_size_hash.rb +0 -119
  99. data/test/test_helper.rb +0 -17
  100. data/test/test_http_connection.rb +0 -81
  101. data/test/test_internal_logger.rb +0 -34
  102. data/test/test_options.rb +0 -198
  103. data/test/test_rate_limit_cache.rb +0 -44
  104. data/test/test_reason.rb +0 -79
  105. data/test/test_rename.rb +0 -65
  106. data/test/test_resolver.rb +0 -291
  107. data/test/test_semantic_logger_filter.rb +0 -144
  108. data/test/test_semver.rb +0 -108
  109. data/test/test_should_log.rb +0 -186
  110. data/test/test_sse_config_client.rb +0 -297
  111. data/test/test_stdlib_formatter.rb +0 -195
  112. data/test/test_telemetry_reporter.rb +0 -209
  113. data/test/test_typed_getters.rb +0 -131
  114. data/test/test_types.rb +0 -141
  115. data/test/test_weighted_value_resolver.rb +0 -84
@@ -1,209 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'test_helper'
4
-
5
- class TestTelemetryReporter < Minitest::Test
6
- # Minimal stand-in for Quonfig::HttpConnection that records POSTs.
7
- class FakeHttpConnection
8
- FakeResponse = Struct.new(:status)
9
-
10
- attr_reader :posts
11
-
12
- def initialize
13
- @posts = []
14
- end
15
-
16
- def post(path, body)
17
- @posts << [path, body]
18
- FakeResponse.new(200)
19
- end
20
- end
21
-
22
- def make_options(telemetry_destination: 'https://telemetry.example.com',
23
- sdk_key: 'qf_sk_development_abc_deadbeef')
24
- # Build minimal Options bypassing env-var lookups via explicit overrides.
25
- Quonfig::Options.new(
26
- sdk_key: sdk_key,
27
- environment: 'development',
28
- api_urls: ['https://primary.example.com'],
29
- enable_sse: false,
30
- enable_polling: false,
31
- on_init_failure: Quonfig::Options::ON_INITIALIZATION_FAILURE::RETURN,
32
- context_upload_mode: :periodic_example
33
- ).tap do |opts|
34
- opts.instance_variable_set(:@telemetry_destination, telemetry_destination)
35
- end
36
- end
37
-
38
- def test_sync_posts_combined_events_in_api_telemetry_wire_shape
39
- options = make_options
40
- fake = FakeHttpConnection.new
41
-
42
- shape_agg = Quonfig::Telemetry::ContextShapeAggregator.new(max_shapes: 100)
43
- example_agg = Quonfig::Telemetry::ExampleContextsAggregator.new(max_contexts: 100)
44
-
45
- reporter = Quonfig::Telemetry::TelemetryReporter.new(
46
- options: options,
47
- instance_hash: 'fake-instance-hash',
48
- context_shape_aggregator: shape_agg,
49
- example_contexts_aggregator: example_agg,
50
- http_connection: fake
51
- )
52
-
53
- ctx = Quonfig::Context.new('user' => { 'key' => 'abc', 'age' => 33 })
54
- shape_agg.push(ctx)
55
- example_agg.record(ctx)
56
-
57
- reporter.sync
58
-
59
- assert_equal 1, fake.posts.size
60
- path, body = fake.posts.first
61
- assert_equal '/api/v1/telemetry/', path
62
-
63
- # Wire shape: TelemetryEventsSchema
64
- assert_equal 'fake-instance-hash', body['instanceHash']
65
- assert_kind_of Array, body['events']
66
- assert_equal 2, body['events'].size
67
-
68
- shape_event = body['events'].find { |e| e.key?('contextShapes') }
69
- example_event = body['events'].find { |e| e.key?('exampleContexts') }
70
-
71
- refute_nil shape_event
72
- refute_nil example_event
73
-
74
- # ContextShapesSchema: { shapes: [{ name, fieldTypes }] }
75
- shapes = shape_event['contextShapes']['shapes']
76
- assert_equal 1, shapes.size
77
- assert_equal 'user', shapes[0]['name']
78
- assert_equal({ 'key' => 2, 'age' => 1 }, shapes[0]['fieldTypes'])
79
-
80
- # ExampleContextsSchema: { examples: [{ timestamp, contextSet: { contexts: [...] } }] }
81
- examples = example_event['exampleContexts']['examples']
82
- assert_equal 1, examples.size
83
- assert_kind_of Integer, examples[0]['timestamp']
84
- contexts_list = examples[0]['contextSet']['contexts']
85
- assert_equal 'user', contexts_list[0]['type']
86
- assert_equal 'abc', contexts_list[0]['values']['key']
87
- assert_equal 33, contexts_list[0]['values']['age']
88
- end
89
-
90
- def test_sync_noop_when_aggregators_empty
91
- fake = FakeHttpConnection.new
92
- reporter = Quonfig::Telemetry::TelemetryReporter.new(
93
- options: make_options,
94
- instance_hash: 'h',
95
- context_shape_aggregator: Quonfig::Telemetry::ContextShapeAggregator.new(max_shapes: 10),
96
- example_contexts_aggregator: Quonfig::Telemetry::ExampleContextsAggregator.new(max_contexts: 10),
97
- http_connection: fake
98
- )
99
-
100
- reporter.sync
101
- assert_equal 0, fake.posts.size
102
- end
103
-
104
- def test_enabled_requires_sdk_key_and_destination
105
- options = make_options(sdk_key: '')
106
- reporter = Quonfig::Telemetry::TelemetryReporter.new(
107
- options: options,
108
- instance_hash: 'h',
109
- context_shape_aggregator: Quonfig::Telemetry::ContextShapeAggregator.new(max_shapes: 10)
110
- )
111
- refute reporter.enabled?
112
- end
113
-
114
- def test_record_feeds_both_aggregators
115
- fake = FakeHttpConnection.new
116
- shape_agg = Quonfig::Telemetry::ContextShapeAggregator.new(max_shapes: 100)
117
- example_agg = Quonfig::Telemetry::ExampleContextsAggregator.new(max_contexts: 100)
118
-
119
- reporter = Quonfig::Telemetry::TelemetryReporter.new(
120
- options: make_options,
121
- instance_hash: 'h',
122
- context_shape_aggregator: shape_agg,
123
- example_contexts_aggregator: example_agg,
124
- http_connection: fake
125
- )
126
-
127
- reporter.record(Quonfig::Context.new('user' => { 'key' => 'zzz' }))
128
-
129
- refute_nil shape_agg.drain_event
130
- refute_nil example_agg.drain_event
131
- end
132
-
133
- def test_sync_posts_evaluation_summaries_event
134
- fake = FakeHttpConnection.new
135
- summaries_agg = Quonfig::Telemetry::EvaluationSummariesAggregator.new(max_keys: 100)
136
-
137
- reporter = Quonfig::Telemetry::TelemetryReporter.new(
138
- options: make_options,
139
- instance_hash: 'h',
140
- evaluation_summaries_aggregator: summaries_agg,
141
- http_connection: fake
142
- )
143
-
144
- summaries_agg.record(
145
- config_id: 'cid',
146
- config_key: 'my-key',
147
- config_type: 'config',
148
- conditional_value_index: 0,
149
- selected_value: 'v',
150
- reason: 1
151
- )
152
-
153
- reporter.sync
154
-
155
- assert_equal 1, fake.posts.size
156
- _path, body = fake.posts.first
157
-
158
- summaries_event = body['events'].find { |e| e.key?('summaries') }
159
- refute_nil summaries_event, 'expected a summaries event in the payload'
160
-
161
- inner = summaries_event['summaries']
162
- assert_kind_of Array, inner['summaries']
163
- counter = inner['summaries'][0]['counters'][0]
164
- assert_equal 1, counter['count']
165
- assert_equal 1, counter['reason']
166
- end
167
-
168
- def test_at_exit_final_drain_posts_pending_batch
169
- fake = FakeHttpConnection.new
170
- shape_agg = Quonfig::Telemetry::ContextShapeAggregator.new(max_shapes: 100)
171
-
172
- reporter = Quonfig::Telemetry::TelemetryReporter.new(
173
- options: make_options,
174
- instance_hash: 'h',
175
- context_shape_aggregator: shape_agg,
176
- http_connection: fake
177
- )
178
-
179
- # Simulate evaluations accumulating between sync cycles.
180
- shape_agg.push(Quonfig::Context.new('user' => { 'key' => 'x' }))
181
-
182
- # Simulate a Rails SIGTERM: process exits without Client#stop being
183
- # called. The reporter's at_exit handler must flush the pending batch.
184
- reporter.send(:final_drain_on_exit)
185
-
186
- assert_equal 1, fake.posts.size
187
- _path, body = fake.posts.first
188
- refute_nil body['events'].find { |e| e.key?('contextShapes') }
189
- end
190
-
191
- def test_at_exit_handler_registered_on_start
192
- # Guards against regressing the Kernel.at_exit hookup that catches
193
- # Rails / Passenger worker SIGTERMs where Client#stop isn't called.
194
- fake = FakeHttpConnection.new
195
- reporter = Quonfig::Telemetry::TelemetryReporter.new(
196
- options: make_options,
197
- instance_hash: 'h',
198
- context_shape_aggregator: Quonfig::Telemetry::ContextShapeAggregator.new(max_shapes: 10),
199
- http_connection: fake,
200
- sync_interval: 999 # avoid the background thread firing during the test
201
- )
202
-
203
- refute reporter.at_exit_registered?, 'not registered before start'
204
- reporter.start
205
- assert reporter.at_exit_registered?, 'registered after start'
206
- ensure
207
- reporter&.stop
208
- end
209
- end
@@ -1,131 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'test_helper'
4
-
5
- # Typed getters (get_string / get_int / get_bool / get_string_list /
6
- # get_duration / get_json) on Quonfig::Client. Each verifies both the happy
7
- # path against an injected ConfigStore and the type-mismatch error path.
8
- class TestTypedGetters < Minitest::Test
9
- KEY = 'my.key'
10
-
11
- def make_config(value:, type:)
12
- {
13
- 'id' => '1',
14
- 'key' => KEY,
15
- 'type' => 'config',
16
- 'valueType' => type,
17
- 'sendToClientSdk' => false,
18
- 'default' => {
19
- 'rules' => [
20
- {
21
- 'criteria' => [{ 'operator' => 'ALWAYS_TRUE' }],
22
- 'value' => { 'type' => type, 'value' => value }
23
- }
24
- ]
25
- },
26
- 'environment' => nil
27
- }
28
- end
29
-
30
- def client_with_value(value:, type:)
31
- store = Quonfig::ConfigStore.new
32
- store.set(KEY, make_config(value: value, type: type))
33
- Quonfig::Client.new(Quonfig::Options.new, store: store)
34
- end
35
-
36
- # ---- get_string -------------------------------------------------------
37
-
38
- def test_get_string_returns_string
39
- assert_equal 'hello', client_with_value(value: 'hello', type: 'string').get_string(KEY)
40
- end
41
-
42
- def test_get_string_raises_on_non_string
43
- assert_raises(Quonfig::Errors::TypeMismatchError) do
44
- client_with_value(value: 42, type: 'int').get_string(KEY)
45
- end
46
- end
47
-
48
- def test_get_string_default_when_missing
49
- client = Quonfig::Client.new(Quonfig::Options.new, store: Quonfig::ConfigStore.new)
50
- assert_equal 'fallback', client.get_string('nope', default: 'fallback')
51
- end
52
-
53
- # ---- get_int ----------------------------------------------------------
54
-
55
- def test_get_int_returns_integer
56
- assert_equal 42, client_with_value(value: 42, type: 'int').get_int(KEY)
57
- end
58
-
59
- def test_get_int_raises_on_string
60
- assert_raises(Quonfig::Errors::TypeMismatchError) do
61
- client_with_value(value: 'oops', type: 'string').get_int(KEY)
62
- end
63
- end
64
-
65
- # ---- get_float --------------------------------------------------------
66
-
67
- def test_get_float_returns_float
68
- assert_in_delta 3.14, client_with_value(value: 3.14, type: 'double').get_float(KEY), 0.0001
69
- end
70
-
71
- def test_get_float_raises_on_non_float
72
- assert_raises(Quonfig::Errors::TypeMismatchError) do
73
- client_with_value(value: 'oops', type: 'string').get_float(KEY)
74
- end
75
- end
76
-
77
- # ---- get_bool ---------------------------------------------------------
78
-
79
- def test_get_bool_returns_true
80
- assert_equal true, client_with_value(value: true, type: 'bool').get_bool(KEY)
81
- end
82
-
83
- def test_get_bool_returns_false
84
- assert_equal false, client_with_value(value: false, type: 'bool').get_bool(KEY)
85
- end
86
-
87
- def test_get_bool_raises_on_string
88
- assert_raises(Quonfig::Errors::TypeMismatchError) do
89
- client_with_value(value: 'true', type: 'string').get_bool(KEY)
90
- end
91
- end
92
-
93
- # ---- get_string_list --------------------------------------------------
94
-
95
- def test_get_string_list_returns_array_of_strings
96
- assert_equal %w[a b c], client_with_value(value: %w[a b c], type: 'string_list').get_string_list(KEY)
97
- end
98
-
99
- def test_get_string_list_raises_on_non_array
100
- assert_raises(Quonfig::Errors::TypeMismatchError) do
101
- client_with_value(value: 'a,b,c', type: 'string').get_string_list(KEY)
102
- end
103
- end
104
-
105
- # ---- get_duration -----------------------------------------------------
106
-
107
- def test_get_duration_returns_milliseconds_for_iso_string
108
- # ISO-8601 PT1S -> 1 second -> 1000 ms.
109
- client = client_with_value(value: 'PT1S', type: 'duration')
110
- assert_equal 1000, client.get_duration(KEY)
111
- end
112
-
113
- def test_get_duration_passes_through_numeric
114
- client = client_with_value(value: 5000, type: 'int')
115
- assert_equal 5000, client.get_duration(KEY)
116
- end
117
-
118
- # ---- get_json ---------------------------------------------------------
119
-
120
- def test_get_json_returns_hash_unchanged
121
- payload = { 'a' => 1, 'b' => [1, 2, 3] }
122
- client = client_with_value(value: payload, type: 'json')
123
- assert_equal payload, client.get_json(KEY)
124
- end
125
-
126
- def test_get_json_returns_array_unchanged
127
- payload = [1, 2, 3]
128
- client = client_with_value(value: payload, type: 'json')
129
- assert_equal payload, client.get_json(KEY)
130
- end
131
- end
data/test/test_types.rb DELETED
@@ -1,141 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'test_helper'
4
- require 'json'
5
- require 'quonfig/types'
6
-
7
- class TestTypes < Minitest::Test
8
- def test_value_holds_type_and_value
9
- v = Quonfig::Value.new(type: 'double', value: '9.95')
10
- assert_equal 'double', v.type
11
- assert_equal '9.95', v.value
12
- assert_nil v.confidential
13
- assert_nil v.decrypt_with
14
- end
15
-
16
- def test_criterion_fields_accept_nil
17
- c = Quonfig::Criterion.new(operator: 'ALWAYS_TRUE')
18
- assert_equal 'ALWAYS_TRUE', c.operator
19
- assert_nil c.property_name
20
- assert_nil c.value_to_match
21
- end
22
-
23
- def test_rule_wires_criteria_and_value
24
- value = Quonfig::Value.new(type: 'double', value: '9.95')
25
- criterion = Quonfig::Criterion.new(operator: 'ALWAYS_TRUE')
26
- rule = Quonfig::Rule.new(criteria: [criterion], value: value)
27
- assert_equal [criterion], rule.criteria
28
- assert_same value, rule.value
29
- end
30
-
31
- def test_rule_set_holds_rules_array
32
- rs = Quonfig::RuleSet.new(rules: [])
33
- assert_equal [], rs.rules
34
- end
35
-
36
- def test_environment_holds_id_and_rules
37
- env = Quonfig::Environment.new(id: 'env-1', rules: [])
38
- assert_equal 'env-1', env.id
39
- assert_equal [], env.rules
40
- end
41
-
42
- def test_meta_optional_workspace_id
43
- meta = Quonfig::Meta.new(version: '1', environment: 'production')
44
- assert_nil meta.workspace_id
45
- end
46
-
47
- def test_weighted_value_and_weighted_values_data
48
- wv = Quonfig::WeightedValue.new(weight: 100, value: Quonfig::Value.new(type: 'bool', value: true))
49
- assert_equal 100, wv.weight
50
- wvd = Quonfig::WeightedValuesData.new(weighted_values: [wv])
51
- assert_equal [wv], wvd.weighted_values
52
- assert_nil wvd.hash_by_property_name
53
- end
54
-
55
- def test_schema_data
56
- sd = Quonfig::SchemaData.new(schema_type: 'zod', schema: 'z.string()')
57
- assert_equal 'zod', sd.schema_type
58
- assert_equal 'z.string()', sd.schema
59
- end
60
-
61
- def test_provided_data
62
- pd = Quonfig::ProvidedData.new(source: 'ENV_VAR', lookup: 'DATABASE_URL')
63
- assert_equal 'ENV_VAR', pd.source
64
- assert_equal 'DATABASE_URL', pd.lookup
65
- end
66
-
67
- def test_config_response_from_integration_fixture
68
- path = File.expand_path(
69
- '../../integration-test-data/data/integration-tests/configs/my-double-key.json',
70
- __dir__
71
- )
72
- skip "integration-test-data not present at #{path}" unless File.exist?(path)
73
-
74
- raw = JSON.parse(File.read(path))
75
- default_rules = raw.fetch('default').fetch('rules').map do |rule|
76
- criteria = rule.fetch('criteria').map do |c|
77
- Quonfig::Criterion.new(
78
- property_name: c['propertyName'],
79
- operator: c.fetch('operator'),
80
- value_to_match: c['valueToMatch']
81
- )
82
- end
83
- value = rule.fetch('value')
84
- Quonfig::Rule.new(
85
- criteria: criteria,
86
- value: Quonfig::Value.new(type: value['type'], value: value['value'])
87
- )
88
- end
89
-
90
- response = Quonfig::ConfigResponse.new(
91
- id: raw.fetch('id'),
92
- key: raw.fetch('key'),
93
- type: raw.fetch('type'),
94
- value_type: raw.fetch('valueType'),
95
- send_to_client_sdk: raw.fetch('sendToClientSdk'),
96
- default: Quonfig::RuleSet.new(rules: default_rules)
97
- )
98
-
99
- assert_equal 'my-double-key', response.key
100
- assert_equal 'config', response.type
101
- assert_equal 'double', response.value_type
102
- refute response.send_to_client_sdk
103
- assert_nil response.environment
104
- assert_equal 1, response.default.rules.length
105
-
106
- rule = response.default.rules.first
107
- assert_equal 'ALWAYS_TRUE', rule.criteria.first.operator
108
- assert_equal 'double', rule.value.type
109
- assert_equal '9.95', rule.value.value
110
- end
111
-
112
- def test_config_envelope_carries_configs_and_meta
113
- meta = Quonfig::Meta.new(version: '1', environment: 'production')
114
- envelope = Quonfig::ConfigEnvelope.new(configs: [], meta: meta)
115
- assert_equal [], envelope.configs
116
- assert_same meta, envelope.meta
117
- end
118
-
119
- def test_workspace_config_document_fields
120
- doc = Quonfig::WorkspaceConfigDocument.new(
121
- id: 'id-1',
122
- key: 'feature.x',
123
- type: 'feature_flag',
124
- value_type: 'bool',
125
- send_to_client_sdk: true,
126
- default: Quonfig::RuleSet.new(rules: []),
127
- environments: []
128
- )
129
- assert_equal 'feature.x', doc.key
130
- assert_equal 'feature_flag', doc.type
131
- assert_equal 'bool', doc.value_type
132
- assert doc.send_to_client_sdk
133
- assert_equal [], doc.environments
134
- end
135
-
136
- def test_workspace_environment_fields
137
- we = Quonfig::WorkspaceEnvironment.new(id: 'env-1', rules: [])
138
- assert_equal 'env-1', we.id
139
- assert_equal [], we.rules
140
- end
141
- end
@@ -1,84 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'test_helper'
4
-
5
- class TestWeightedValueResolver < Minitest::Test
6
- KEY = 'config_key'
7
-
8
- def test_resolving_single_value
9
- values = weighted_values([['abc', 1]])
10
- resolver = Quonfig::WeightedValueResolver.new(values, KEY, nil)
11
- assert_equal 'abc', resolver.resolve[0][:value]
12
- assert_equal 0, resolver.resolve[1]
13
- end
14
-
15
- def test_resolving_multiple_values_evenly_distributed
16
- values = weighted_values([['abc', 1], ['def', 1]])
17
-
18
- resolver = Quonfig::WeightedValueResolver.new(values, KEY, 'user:001')
19
- assert_equal 'abc', resolver.resolve[0][:value]
20
- assert_equal 0, resolver.resolve[1]
21
-
22
- resolver = Quonfig::WeightedValueResolver.new(values, KEY, 'user:456')
23
- assert_equal 'def', resolver.resolve[0][:value]
24
- assert_equal 1, resolver.resolve[1]
25
- end
26
-
27
- def test_resolving_multiple_values_unevenly_distributed
28
- values = weighted_values([['abc', 1], ['def', 98], ['ghi', 1]])
29
-
30
- resolver = Quonfig::WeightedValueResolver.new(values, KEY, 'user:456')
31
- assert_equal 'def', resolver.resolve[0][:value]
32
- assert_equal 1, resolver.resolve[1]
33
-
34
- resolver = Quonfig::WeightedValueResolver.new(values, KEY, 'user:103')
35
- assert_equal 'ghi', resolver.resolve[0][:value]
36
- assert_equal 2, resolver.resolve[1]
37
-
38
- resolver = Quonfig::WeightedValueResolver.new(values, KEY, 'user:119')
39
- assert_equal 'abc', resolver.resolve[0][:value]
40
- assert_equal 0, resolver.resolve[1]
41
- end
42
-
43
- def test_resolving_multiple_values_with_simulation
44
- values = weighted_values([['abc', 1], ['def', 98], ['ghi', 1]])
45
- results = {}
46
-
47
- 10_000.times do |i|
48
- result = Quonfig::WeightedValueResolver.new(values, KEY, "user:#{i}").resolve[0][:value]
49
- results[result] ||= 0
50
- results[result] += 1
51
- end
52
-
53
- assert_in_delta 100, results['abc'], 20
54
- assert_in_delta 9800, results['def'], 50
55
- assert_in_delta 100, results['ghi'], 20
56
- end
57
-
58
- def test_string_keyed_weighted_value_hashes
59
- # JSON.parse without symbolize_names yields string keys; the resolver must
60
- # accept that shape too.
61
- values = [
62
- { 'value' => 'abc', 'weight' => 1 },
63
- { 'value' => 'def', 'weight' => 1 }
64
- ]
65
- resolver = Quonfig::WeightedValueResolver.new(values, KEY, 'user:456')
66
- assert_equal 'def', resolver.resolve[0]['value']
67
- assert_equal 1, resolver.resolve[1]
68
- end
69
-
70
- def test_all_zero_weights_returns_last_variant
71
- values = weighted_values([['abc', 0], ['def', 0], ['ghi', 0]])
72
- resolver = Quonfig::WeightedValueResolver.new(values, KEY, 'user:001')
73
- assert_equal 'ghi', resolver.resolve[0][:value]
74
- assert_equal 2, resolver.resolve[1]
75
- end
76
-
77
- private
78
-
79
- def weighted_values(values_and_weights)
80
- values_and_weights.map do |value, weight|
81
- { value: value, weight: weight }
82
- end
83
- end
84
- end