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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -0
- data/README.md +4 -4
- data/lib/quonfig/evaluation_details.rb +60 -0
- data/lib/quonfig/options.rb +37 -16
- data/lib/quonfig/sse_config_client.rb +1 -1
- data/lib/quonfig/version.rb +5 -0
- data/lib/quonfig.rb +2 -1
- data/quonfig.gemspec +30 -163
- metadata +29 -182
- data/.claude/rules/constitution.md +0 -81
- data/.claude/rules/git-safety.md +0 -11
- data/.claude/rules/issue-tracking.md +0 -13
- data/.claude/rules/testing-workflow.md +0 -28
- data/.envrc.sample +0 -3
- data/.github/CODEOWNERS +0 -2
- data/.github/pull_request_template.md +0 -8
- data/.github/workflows/release.yml +0 -49
- data/.github/workflows/ruby.yml +0 -60
- data/.github/workflows/test.yaml +0 -40
- data/.rubocop.yml +0 -13
- data/.tool-versions +0 -1
- data/CLAUDE.md +0 -29
- data/CODEOWNERS +0 -1
- data/Gemfile +0 -26
- data/Gemfile.lock +0 -177
- data/Rakefile +0 -64
- data/VERSION +0 -1
- data/dev/allocation_stats +0 -60
- data/dev/benchmark +0 -40
- data/dev/console +0 -12
- data/dev/script_setup.rb +0 -18
- data/test/fixtures/datafile.json +0 -87
- data/test/integration/test_context_precedence.rb +0 -112
- data/test/integration/test_datadir_environment.rb +0 -54
- data/test/integration/test_dev_overrides.rb +0 -40
- data/test/integration/test_enabled.rb +0 -478
- data/test/integration/test_enabled_with_contexts.rb +0 -64
- data/test/integration/test_get.rb +0 -136
- data/test/integration/test_get_feature_flag.rb +0 -28
- data/test/integration/test_get_or_raise.rb +0 -60
- data/test/integration/test_get_weighted_values.rb +0 -34
- data/test/integration/test_helpers.rb +0 -667
- data/test/integration/test_helpers_test.rb +0 -73
- data/test/integration/test_post.rb +0 -44
- data/test/integration/test_telemetry.rb +0 -170
- data/test/support/common_helpers.rb +0 -106
- data/test/support/mock_base_client.rb +0 -27
- data/test/support/mock_config_loader.rb +0 -1
- data/test/test_bound_client.rb +0 -109
- data/test/test_caching_http_connection.rb +0 -218
- data/test/test_client.rb +0 -255
- data/test/test_client_network_mode.rb +0 -136
- data/test/test_client_telemetry.rb +0 -175
- data/test/test_config_loader.rb +0 -70
- data/test/test_context.rb +0 -139
- data/test/test_context_shape.rb +0 -37
- data/test/test_context_shape_aggregator.rb +0 -126
- data/test/test_datadir.rb +0 -203
- data/test/test_dev_context.rb +0 -163
- data/test/test_duration.rb +0 -37
- data/test/test_encryption.rb +0 -16
- data/test/test_evaluation_summaries_aggregator.rb +0 -180
- data/test/test_evaluator.rb +0 -285
- data/test/test_example_contexts_aggregator.rb +0 -119
- data/test/test_exponential_backoff.rb +0 -44
- data/test/test_fixed_size_hash.rb +0 -119
- data/test/test_helper.rb +0 -17
- data/test/test_http_connection.rb +0 -79
- data/test/test_internal_logger.rb +0 -34
- data/test/test_options.rb +0 -167
- data/test/test_rate_limit_cache.rb +0 -44
- data/test/test_reason.rb +0 -79
- data/test/test_rename.rb +0 -65
- data/test/test_resolver.rb +0 -291
- data/test/test_semantic_logger_filter.rb +0 -144
- data/test/test_semver.rb +0 -108
- data/test/test_should_log.rb +0 -186
- data/test/test_sse_config_client.rb +0 -297
- data/test/test_stdlib_formatter.rb +0 -195
- data/test/test_telemetry_reporter.rb +0 -209
- data/test/test_typed_getters.rb +0 -131
- data/test/test_types.rb +0 -141
- data/test/test_weighted_value_resolver.rb +0 -84
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
require 'integration/test_helpers'
|
|
5
|
-
|
|
6
|
-
# Verifies the shared helper that generated integration tests (qfg-dk6.23/.24)
|
|
7
|
-
# depend on: fixture loading, resolver construction, env-var scoping,
|
|
8
|
-
# and the assertion helper.
|
|
9
|
-
class TestIntegrationHelpers < Minitest::Test
|
|
10
|
-
def test_data_dir_is_the_integration_tests_sibling_repo
|
|
11
|
-
assert_equal 'integration-tests', File.basename(IntegrationTestHelpers.data_dir)
|
|
12
|
-
assert Dir.exist?(IntegrationTestHelpers.data_dir),
|
|
13
|
-
"integration-test-data sibling repo must exist at #{IntegrationTestHelpers.data_dir}"
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def test_build_store_loads_configs_from_subdirs
|
|
17
|
-
store = IntegrationTestHelpers.build_store('get')
|
|
18
|
-
|
|
19
|
-
assert_kind_of Quonfig::ConfigStore, store
|
|
20
|
-
refute_empty store.keys, 'build_store should load at least one config'
|
|
21
|
-
assert store.keys.include?('my-test-key'),
|
|
22
|
-
"expected 'my-test-key' in store keys (got #{store.keys.first(5).inspect}...)"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def test_build_resolver_wires_store_and_evaluator
|
|
26
|
-
store = IntegrationTestHelpers.build_store('get')
|
|
27
|
-
resolver = IntegrationTestHelpers.build_resolver(store)
|
|
28
|
-
|
|
29
|
-
assert_kind_of Quonfig::Resolver, resolver
|
|
30
|
-
assert_same store, resolver.store
|
|
31
|
-
assert_kind_of Quonfig::Evaluator, resolver.evaluator
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def test_env_vars_for_encryption_and_env_lookups_are_set_at_load
|
|
35
|
-
assert_equal 'c87ba22d8662282abe8a0e4651327b579cb64a454ab0f4c170b45b15f049a221',
|
|
36
|
-
ENV['PREFAB_INTEGRATION_TEST_ENCRYPTION_KEY']
|
|
37
|
-
# IS_A_NUMBER / NOT_A_NUMBER support the env-var lookup integration tests.
|
|
38
|
-
assert_equal '1234', ENV['IS_A_NUMBER']
|
|
39
|
-
assert_equal 'not_a_number', ENV['NOT_A_NUMBER']
|
|
40
|
-
assert_nil ENV['MISSING_ENV_VAR']
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def test_with_env_sets_and_restores
|
|
44
|
-
ENV['ORIGINAL_PRESENT'] = 'keep-me'
|
|
45
|
-
ENV.delete('ORIGINAL_ABSENT')
|
|
46
|
-
|
|
47
|
-
IntegrationTestHelpers.with_env(
|
|
48
|
-
'ORIGINAL_PRESENT' => 'overridden',
|
|
49
|
-
'ORIGINAL_ABSENT' => 'temporary'
|
|
50
|
-
) do
|
|
51
|
-
assert_equal 'overridden', ENV['ORIGINAL_PRESENT']
|
|
52
|
-
assert_equal 'temporary', ENV['ORIGINAL_ABSENT']
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
assert_equal 'keep-me', ENV['ORIGINAL_PRESENT']
|
|
56
|
-
assert_nil ENV['ORIGINAL_ABSENT']
|
|
57
|
-
ensure
|
|
58
|
-
ENV.delete('ORIGINAL_PRESENT')
|
|
59
|
-
ENV.delete('ORIGINAL_ABSENT')
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def test_with_env_restores_after_exception
|
|
63
|
-
ENV.delete('ROLLBACK_ME')
|
|
64
|
-
begin
|
|
65
|
-
IntegrationTestHelpers.with_env('ROLLBACK_ME' => 'set') do
|
|
66
|
-
raise 'boom'
|
|
67
|
-
end
|
|
68
|
-
rescue RuntimeError
|
|
69
|
-
# swallow — we only care that ENV was cleaned up
|
|
70
|
-
end
|
|
71
|
-
assert_nil ENV['ROLLBACK_ME']
|
|
72
|
-
end
|
|
73
|
-
end
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
#
|
|
3
|
-
# AUTO-GENERATED from integration-test-data/tests/eval/post.yaml.
|
|
4
|
-
# Regenerate with:
|
|
5
|
-
# cd integration-test-data/generators && npm run generate -- --target=ruby
|
|
6
|
-
# Source: integration-test-data/generators/src/targets/ruby.ts
|
|
7
|
-
# Do NOT edit by hand — changes will be overwritten.
|
|
8
|
-
|
|
9
|
-
require 'test_helper'
|
|
10
|
-
require 'integration/test_helpers'
|
|
11
|
-
|
|
12
|
-
class TestPost < Minitest::Test
|
|
13
|
-
def setup
|
|
14
|
-
@store = IntegrationTestHelpers.build_store("post")
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# reports context shape aggregation
|
|
18
|
-
def test_reports_context_shape_aggregation
|
|
19
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:context_shape, {"context_upload_mode" => ":shape_only"})
|
|
20
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :context_shape, {"user" => {"name" => "Michael", "age" => 38, "human" => true}, "role" => {"name" => "developer", "admin" => false, "salary" => 15.75, "permissions" => ["read", "write"]}}, contexts: {})
|
|
21
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :context_shape, [{"name" => "user", "field_types" => {"name" => 2, "age" => 1, "human" => 5}}, {"name" => "role", "field_types" => {"name" => 2, "admin" => 5, "salary" => 4, "permissions" => 10}}], endpoint: "/api/v1/context-shapes")
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# reports evaluation summary
|
|
25
|
-
def test_reports_evaluation_summary
|
|
26
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:evaluation_summary, {})
|
|
27
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :evaluation_summary, {"keys" => ["my-test-key", "feature-flag.integer", "my-string-list-key", "feature-flag.integer", "feature-flag.weighted"]}, contexts: {"user" => {"tracking_id" => "92a202f2"}})
|
|
28
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :evaluation_summary, [{"key" => "my-test-key", "type" => "CONFIG", "value" => "my-test-value", "value_type" => "string", "count" => 1, "reason" => 2, "selected_value" => {"string" => "my-test-value"}, "summary" => {"config_row_index" => 0, "conditional_value_index" => 1}}, {"key" => "my-string-list-key", "type" => "CONFIG", "value" => ["a", "b", "c"], "value_type" => "string_list", "count" => 1, "reason" => 1, "selected_value" => {"stringList" => ["a", "b", "c"]}, "summary" => {"config_row_index" => 0, "conditional_value_index" => 0}}, {"key" => "feature-flag.integer", "type" => "FEATURE_FLAG", "value" => 3, "value_type" => "int", "count" => 2, "reason" => 2, "selected_value" => {"int" => 3}, "summary" => {"config_row_index" => 0, "conditional_value_index" => 1}}, {"key" => "feature-flag.weighted", "type" => "FEATURE_FLAG", "value" => 2, "value_type" => "int", "count" => 1, "reason" => 3, "selected_value" => {"int" => 2}, "summary" => {"config_row_index" => 0, "conditional_value_index" => 0, "weighted_value_index" => 2}}], endpoint: "/api/v1/telemetry")
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# reports example contexts
|
|
32
|
-
def test_reports_example_contexts
|
|
33
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:example_contexts, {})
|
|
34
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :example_contexts, {"user" => {"name" => "michael", "age" => 38, "key" => "michael:1234"}, "device" => {"mobile" => false}, "team" => {"id" => 3.5}}, contexts: {})
|
|
35
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :example_contexts, {"user" => {"name" => "michael", "age" => 38, "key" => "michael:1234"}, "device" => {"mobile" => false}, "team" => {"id" => 3.5}}, endpoint: "/api/v1/telemetry")
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# example contexts without key are not reported
|
|
39
|
-
def test_example_contexts_without_key_are_not_reported
|
|
40
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:example_contexts, {})
|
|
41
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :example_contexts, {"user" => {"name" => "michael", "age" => 38}, "device" => {"mobile" => false}, "team" => {"id" => 3.5}}, contexts: {})
|
|
42
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :example_contexts, nil, endpoint: "/api/v1/telemetry")
|
|
43
|
-
end
|
|
44
|
-
end
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
#
|
|
3
|
-
# AUTO-GENERATED from integration-test-data/tests/eval/telemetry.yaml.
|
|
4
|
-
# Regenerate with:
|
|
5
|
-
# cd integration-test-data/generators && npm run generate -- --target=ruby
|
|
6
|
-
# Source: integration-test-data/generators/src/targets/ruby.ts
|
|
7
|
-
# Do NOT edit by hand — changes will be overwritten.
|
|
8
|
-
|
|
9
|
-
require 'test_helper'
|
|
10
|
-
require 'integration/test_helpers'
|
|
11
|
-
|
|
12
|
-
class TestTelemetry < Minitest::Test
|
|
13
|
-
def setup
|
|
14
|
-
@store = IntegrationTestHelpers.build_store("telemetry")
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# reason is STATIC for config with no targeting rules
|
|
18
|
-
def test_reason_is_static_for_config_with_no_targeting_rules
|
|
19
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:evaluation_summary, {})
|
|
20
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :evaluation_summary, {"keys" => ["brand.new.string"]}, contexts: {})
|
|
21
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :evaluation_summary, [{"key" => "brand.new.string", "type" => "CONFIG", "value" => "hello.world", "value_type" => "string", "count" => 1, "reason" => 1, "selected_value" => {"string" => "hello.world"}, "summary" => {"config_row_index" => 0, "conditional_value_index" => 0}}], endpoint: "/api/v1/telemetry")
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# reason is STATIC for feature flag with only ALWAYS_TRUE rules
|
|
25
|
-
def test_reason_is_static_for_feature_flag_with_only_always_true_rules
|
|
26
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:evaluation_summary, {})
|
|
27
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :evaluation_summary, {"keys" => ["always.true"]}, contexts: {})
|
|
28
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :evaluation_summary, [{"key" => "always.true", "type" => "FEATURE_FLAG", "value" => true, "value_type" => "bool", "count" => 1, "reason" => 1, "selected_value" => {"bool" => true}, "summary" => {"config_row_index" => 0, "conditional_value_index" => 0}}], endpoint: "/api/v1/telemetry")
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# reason is TARGETING_MATCH when config has targeting rules but evaluation falls through
|
|
32
|
-
def test_reason_is_targeting_match_when_config_has_targeting_rules_but_evaluation_falls_through
|
|
33
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:evaluation_summary, {})
|
|
34
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :evaluation_summary, {"keys" => ["my-test-key"]}, contexts: {})
|
|
35
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :evaluation_summary, [{"key" => "my-test-key", "type" => "CONFIG", "value" => "my-test-value", "value_type" => "string", "count" => 1, "reason" => 2, "selected_value" => {"string" => "my-test-value"}, "summary" => {"config_row_index" => 0, "conditional_value_index" => 1}}], endpoint: "/api/v1/telemetry")
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# reason is TARGETING_MATCH when a targeting rule matches
|
|
39
|
-
def test_reason_is_targeting_match_when_a_targeting_rule_matches
|
|
40
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:evaluation_summary, {})
|
|
41
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :evaluation_summary, {"keys" => ["feature-flag.integer"]}, contexts: {"user" => {"key" => "michael"}})
|
|
42
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :evaluation_summary, [{"key" => "feature-flag.integer", "type" => "FEATURE_FLAG", "value" => 5, "value_type" => "int", "count" => 1, "reason" => 2, "selected_value" => {"int" => 5}, "summary" => {"config_row_index" => 0, "conditional_value_index" => 0}}], endpoint: "/api/v1/telemetry")
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# reason is SPLIT for weighted value evaluation
|
|
46
|
-
def test_reason_is_split_for_weighted_value_evaluation
|
|
47
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:evaluation_summary, {})
|
|
48
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :evaluation_summary, {"keys" => ["feature-flag.weighted"]}, contexts: {"user" => {"tracking_id" => "92a202f2"}})
|
|
49
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :evaluation_summary, [{"key" => "feature-flag.weighted", "type" => "FEATURE_FLAG", "value" => 2, "value_type" => "int", "count" => 1, "reason" => 3, "selected_value" => {"int" => 2}, "summary" => {"config_row_index" => 0, "conditional_value_index" => 0, "weighted_value_index" => 2}}], endpoint: "/api/v1/telemetry")
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# reason is TARGETING_MATCH for feature flag fallthrough with targeting rules
|
|
53
|
-
def test_reason_is_targeting_match_for_feature_flag_fallthrough_with_targeting_rules
|
|
54
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:evaluation_summary, {})
|
|
55
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :evaluation_summary, {"keys" => ["feature-flag.integer"]}, contexts: {})
|
|
56
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :evaluation_summary, [{"key" => "feature-flag.integer", "type" => "FEATURE_FLAG", "value" => 3, "value_type" => "int", "count" => 1, "reason" => 2, "selected_value" => {"int" => 3}, "summary" => {"config_row_index" => 0, "conditional_value_index" => 1}}], endpoint: "/api/v1/telemetry")
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# evaluation summary deduplicates identical evaluations
|
|
60
|
-
def test_evaluation_summary_deduplicates_identical_evaluations
|
|
61
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:evaluation_summary, {})
|
|
62
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :evaluation_summary, {"keys" => ["brand.new.string", "brand.new.string", "brand.new.string", "brand.new.string", "brand.new.string"]}, contexts: {})
|
|
63
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :evaluation_summary, [{"key" => "brand.new.string", "type" => "CONFIG", "value" => "hello.world", "value_type" => "string", "count" => 5, "reason" => 1, "selected_value" => {"string" => "hello.world"}, "summary" => {"config_row_index" => 0, "conditional_value_index" => 0}}], endpoint: "/api/v1/telemetry")
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# evaluation summary creates separate counters for different rules of same config
|
|
67
|
-
def test_evaluation_summary_creates_separate_counters_for_different_rules_of_same_config
|
|
68
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:evaluation_summary, {})
|
|
69
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :evaluation_summary, {"keys" => ["feature-flag.integer"], "keys_without_context" => ["feature-flag.integer"]}, contexts: {"user" => {"key" => "michael"}})
|
|
70
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :evaluation_summary, [{"key" => "feature-flag.integer", "type" => "FEATURE_FLAG", "value" => 5, "value_type" => "int", "count" => 1, "reason" => 2, "selected_value" => {"int" => 5}, "summary" => {"config_row_index" => 0, "conditional_value_index" => 0}}, {"key" => "feature-flag.integer", "type" => "FEATURE_FLAG", "value" => 3, "value_type" => "int", "count" => 1, "reason" => 2, "selected_value" => {"int" => 3}, "summary" => {"config_row_index" => 0, "conditional_value_index" => 1}}], endpoint: "/api/v1/telemetry")
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# evaluation summary groups by config key
|
|
74
|
-
def test_evaluation_summary_groups_by_config_key
|
|
75
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:evaluation_summary, {})
|
|
76
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :evaluation_summary, {"keys" => ["brand.new.string", "always.true"]}, contexts: {})
|
|
77
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :evaluation_summary, [{"key" => "brand.new.string", "type" => "CONFIG", "value" => "hello.world", "value_type" => "string", "count" => 1, "reason" => 1, "selected_value" => {"string" => "hello.world"}, "summary" => {"config_row_index" => 0, "conditional_value_index" => 0}}, {"key" => "always.true", "type" => "FEATURE_FLAG", "value" => true, "value_type" => "bool", "count" => 1, "reason" => 1, "selected_value" => {"bool" => true}, "summary" => {"config_row_index" => 0, "conditional_value_index" => 0}}], endpoint: "/api/v1/telemetry")
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# selectedValue wraps string correctly
|
|
81
|
-
def test_selectedvalue_wraps_string_correctly
|
|
82
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:evaluation_summary, {})
|
|
83
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :evaluation_summary, {"keys" => ["brand.new.string"]}, contexts: {})
|
|
84
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :evaluation_summary, [{"key" => "brand.new.string", "type" => "CONFIG", "value" => "hello.world", "value_type" => "string", "count" => 1, "reason" => 1, "selected_value" => {"string" => "hello.world"}, "summary" => {"config_row_index" => 0, "conditional_value_index" => 0}}], endpoint: "/api/v1/telemetry")
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# selectedValue wraps boolean correctly
|
|
88
|
-
def test_selectedvalue_wraps_boolean_correctly
|
|
89
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:evaluation_summary, {})
|
|
90
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :evaluation_summary, {"keys" => ["brand.new.boolean"]}, contexts: {})
|
|
91
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :evaluation_summary, [{"key" => "brand.new.boolean", "type" => "CONFIG", "value" => false, "value_type" => "bool", "count" => 1, "reason" => 1, "selected_value" => {"bool" => false}, "summary" => {"config_row_index" => 0, "conditional_value_index" => 0}}], endpoint: "/api/v1/telemetry")
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# selectedValue wraps int correctly
|
|
95
|
-
def test_selectedvalue_wraps_int_correctly
|
|
96
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:evaluation_summary, {})
|
|
97
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :evaluation_summary, {"keys" => ["brand.new.int"]}, contexts: {})
|
|
98
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :evaluation_summary, [{"key" => "brand.new.int", "type" => "CONFIG", "value" => 123, "value_type" => "int", "count" => 1, "reason" => 1, "selected_value" => {"int" => 123}, "summary" => {"config_row_index" => 0, "conditional_value_index" => 0}}], endpoint: "/api/v1/telemetry")
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# selectedValue wraps double correctly
|
|
102
|
-
def test_selectedvalue_wraps_double_correctly
|
|
103
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:evaluation_summary, {})
|
|
104
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :evaluation_summary, {"keys" => ["brand.new.double"]}, contexts: {})
|
|
105
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :evaluation_summary, [{"key" => "brand.new.double", "type" => "CONFIG", "value" => 123.99, "value_type" => "double", "count" => 1, "reason" => 1, "selected_value" => {"double" => 123.99}, "summary" => {"config_row_index" => 0, "conditional_value_index" => 0}}], endpoint: "/api/v1/telemetry")
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# selectedValue wraps string list correctly
|
|
109
|
-
def test_selectedvalue_wraps_string_list_correctly
|
|
110
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:evaluation_summary, {})
|
|
111
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :evaluation_summary, {"keys" => ["my-string-list-key"]}, contexts: {})
|
|
112
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :evaluation_summary, [{"key" => "my-string-list-key", "type" => "CONFIG", "value" => ["a", "b", "c"], "value_type" => "string_list", "count" => 1, "reason" => 1, "selected_value" => {"stringList" => ["a", "b", "c"]}, "summary" => {"config_row_index" => 0, "conditional_value_index" => 0}}], endpoint: "/api/v1/telemetry")
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# context shape merges fields across multiple records
|
|
116
|
-
def test_context_shape_merges_fields_across_multiple_records
|
|
117
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:context_shape, {})
|
|
118
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :context_shape, [{"user" => {"name" => "alice", "age" => 30}}, {"user" => {"name" => "bob", "score" => 9.5}, "team" => {"name" => "engineering"}}], contexts: {})
|
|
119
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :context_shape, [{"name" => "user", "field_types" => {"name" => 2, "age" => 1, "score" => 4}}, {"name" => "team", "field_types" => {"name" => 2}}], endpoint: "/api/v1/context-shapes")
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# example contexts deduplicates by key value
|
|
123
|
-
def test_example_contexts_deduplicates_by_key_value
|
|
124
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:example_contexts, {})
|
|
125
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :example_contexts, [{"user" => {"key" => "user-123", "name" => "alice"}}, {"user" => {"key" => "user-123", "name" => "bob"}}], contexts: {})
|
|
126
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :example_contexts, {"user" => {"key" => "user-123", "name" => "alice"}}, endpoint: "/api/v1/telemetry")
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
# telemetry disabled emits nothing
|
|
130
|
-
def test_telemetry_disabled_emits_nothing
|
|
131
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:evaluation_summary, {"collect_evaluation_summaries" => false, "context_upload_mode" => ":none"})
|
|
132
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :evaluation_summary, {"keys" => ["brand.new.string"]}, contexts: {})
|
|
133
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :evaluation_summary, nil, endpoint: "/api/v1/telemetry")
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# shapes only mode reports shapes but not examples
|
|
137
|
-
def test_shapes_only_mode_reports_shapes_but_not_examples
|
|
138
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:context_shape, {"context_upload_mode" => ":shape_only"})
|
|
139
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :context_shape, {"user" => {"name" => "alice", "key" => "alice-123"}}, contexts: {})
|
|
140
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :context_shape, [{"name" => "user", "field_types" => {"name" => 2, "key" => 2}}], endpoint: "/api/v1/context-shapes")
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# log level evaluations are excluded from telemetry
|
|
144
|
-
def test_log_level_evaluations_are_excluded_from_telemetry
|
|
145
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:evaluation_summary, {})
|
|
146
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :evaluation_summary, {"keys" => ["log-level.prefab.criteria_evaluator"]}, contexts: {})
|
|
147
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :evaluation_summary, nil, endpoint: "/api/v1/telemetry")
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# empty context produces no context telemetry
|
|
151
|
-
def test_empty_context_produces_no_context_telemetry
|
|
152
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:context_shape, {})
|
|
153
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :context_shape, {}, contexts: {})
|
|
154
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :context_shape, nil, endpoint: "/api/v1/context-shapes")
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
# confidential plain string is redacted in selectedValue
|
|
158
|
-
def test_confidential_plain_string_is_redacted_in_selectedvalue
|
|
159
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:evaluation_summary, {})
|
|
160
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :evaluation_summary, {"keys" => ["confidential.new.string"]}, contexts: {})
|
|
161
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :evaluation_summary, [{"key" => "confidential.new.string", "type" => "CONFIG", "value" => "hello.world", "value_type" => "string", "count" => 1, "reason" => 1, "selected_value" => {"string" => "*****18aa7"}, "summary" => {"config_row_index" => 0, "conditional_value_index" => 0}}], endpoint: "/api/v1/telemetry")
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
# confidential encrypted string is redacted using ciphertext hash
|
|
165
|
-
def test_confidential_encrypted_string_is_redacted_using_ciphertext_hash
|
|
166
|
-
aggregator = IntegrationTestHelpers.build_aggregator(:evaluation_summary, {})
|
|
167
|
-
IntegrationTestHelpers.feed_aggregator(aggregator, :evaluation_summary, {"keys" => ["a.secret.config"]}, contexts: {})
|
|
168
|
-
IntegrationTestHelpers.assert_aggregator_post(aggregator, :evaluation_summary, [{"key" => "a.secret.config", "type" => "CONFIG", "value" => "hello.world", "value_type" => "string", "count" => 1, "reason" => 1, "selected_value" => {"string" => "*****936c9"}, "summary" => {"config_row_index" => 0, "conditional_value_index" => 0}}], endpoint: "/api/v1/telemetry")
|
|
169
|
-
end
|
|
170
|
-
end
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module CommonHelpers
|
|
4
|
-
require 'timecop'
|
|
5
|
-
|
|
6
|
-
def setup
|
|
7
|
-
$oldstderr, $stderr = $stderr, StringIO.new
|
|
8
|
-
$logs = StringIO.new
|
|
9
|
-
|
|
10
|
-
if defined?(SemanticLogger)
|
|
11
|
-
SemanticLogger.add_appender(io: $logs)
|
|
12
|
-
SemanticLogger.sync!
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def teardown
|
|
17
|
-
if $logs && !$logs.string.empty?
|
|
18
|
-
log_lines = $logs.string.split("\n").reject do |line|
|
|
19
|
-
line.match(/Quonfig::ConfigClient -- No success loading checkpoints/)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
if log_lines.size > 0
|
|
23
|
-
$logs = nil
|
|
24
|
-
raise "Unexpected logs. Handle logs with assert_logged\n\n#{log_lines}"
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# note this skips the output check in environments like rubymine that hijack the output
|
|
29
|
-
if $stderr != $oldstderr && $stderr.respond_to?(:string) && !$stderr.string.empty?
|
|
30
|
-
if !RUBY_VERSION.start_with?('2.')
|
|
31
|
-
# Filter out ld-eventsource frozen string literal warnings in Ruby 3.4+
|
|
32
|
-
stderr_lines = $stderr.string.split("\n").reject do |line|
|
|
33
|
-
line.include?('ld-eventsource') && line.include?('literal string will be frozen in the future')
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
if !stderr_lines.empty?
|
|
37
|
-
raise "Unexpected stderr. Handle stderr with assert_stderr\n\n#{stderr_lines.join("\n")}"
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
$stderr = $oldstderr if $oldstderr
|
|
43
|
-
|
|
44
|
-
Timecop.return
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def with_env(key, value, &block)
|
|
48
|
-
old_value = ENV.fetch(key, nil)
|
|
49
|
-
|
|
50
|
-
ENV[key] = value
|
|
51
|
-
block.call
|
|
52
|
-
ensure
|
|
53
|
-
ENV[key] = old_value
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
FakeResponse = Struct.new(:status, :body)
|
|
57
|
-
|
|
58
|
-
def wait_for(condition, max_wait: 10, sleep_time: 0.01)
|
|
59
|
-
wait_time = 0
|
|
60
|
-
while !condition.call
|
|
61
|
-
wait_time += sleep_time
|
|
62
|
-
sleep sleep_time
|
|
63
|
-
|
|
64
|
-
raise "Waited #{max_wait} seconds for the condition to be true, but it never was" if wait_time > max_wait
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def context(properties)
|
|
69
|
-
Quonfig::Context.new(properties)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def assert_logged(expected)
|
|
73
|
-
# we do a uniq here because logging can happen in a separate thread so the
|
|
74
|
-
# number of times a log might happen could be slightly variable.
|
|
75
|
-
actuals = $logs.string.split("\n").uniq
|
|
76
|
-
expected.each do |expectation|
|
|
77
|
-
matched = false
|
|
78
|
-
|
|
79
|
-
actuals.each do |actual|
|
|
80
|
-
matched = true if actual.match(expectation)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
assert(matched, "expectation: #{expectation}, got: #{actuals}")
|
|
84
|
-
end
|
|
85
|
-
# mark nil to indicate we handled it
|
|
86
|
-
$logs = nil
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def assert_stderr(expected)
|
|
90
|
-
skip "Cannot verify stderr in current environment" unless $stderr.respond_to?(:string)
|
|
91
|
-
$stderr.string.split("\n").uniq.each do |line|
|
|
92
|
-
matched = false
|
|
93
|
-
|
|
94
|
-
expected.reject! do |expectation|
|
|
95
|
-
matched = true if line.include?(expectation)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
assert(matched, "expectation: #{expected}, got: #{line}")
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
assert expected.empty?, "Expected stderr to include: #{expected}, but it did not"
|
|
102
|
-
|
|
103
|
-
# restore since we've handled it
|
|
104
|
-
$stderr = $oldstderr
|
|
105
|
-
end
|
|
106
|
-
end
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Minimal stand-in for Quonfig::Client used by Evaluator/ConfigLoader tests.
|
|
4
|
-
# We deliberately keep this tiny so the tests don't depend on the live
|
|
5
|
-
# ConfigStore/Resolver/Evaluator wiring — they exercise their target in
|
|
6
|
-
# isolation.
|
|
7
|
-
class MockBaseClient
|
|
8
|
-
STAGING_ENV_ID = 1
|
|
9
|
-
PRODUCTION_ENV_ID = 2
|
|
10
|
-
TEST_ENV_ID = 3
|
|
11
|
-
|
|
12
|
-
attr_reader :options
|
|
13
|
-
|
|
14
|
-
def initialize(options = Quonfig::Options.new)
|
|
15
|
-
@options = options
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def instance_hash
|
|
19
|
-
'mock-base-client-instance-hash'
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def project_id
|
|
23
|
-
1
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def log; end
|
|
27
|
-
end
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
data/test/test_bound_client.rb
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'test_helper'
|
|
4
|
-
|
|
5
|
-
# BoundClient is a pure delegation wrapper: it forwards typed-getter calls to
|
|
6
|
-
# the underlying client with its bound context. These tests exercise that
|
|
7
|
-
# wrapper with a tiny FakeClient double so they do not depend on the rest of
|
|
8
|
-
# the eval pipeline (which is mid-JSON-migration and not fully green yet).
|
|
9
|
-
class TestBoundClient < Minitest::Test
|
|
10
|
-
# Minimal stand-in for Quonfig::Client that records the context each typed
|
|
11
|
-
# getter was called with. BoundClient only exercises the public typed-getter
|
|
12
|
-
# surface + enabled?, so that's all we need here.
|
|
13
|
-
class FakeClient
|
|
14
|
-
attr_reader :calls
|
|
15
|
-
|
|
16
|
-
def initialize
|
|
17
|
-
@calls = []
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def get_string(key, default: Quonfig::NO_DEFAULT_PROVIDED, context: Quonfig::NO_DEFAULT_PROVIDED)
|
|
21
|
-
@calls << [:get_string, key, default, context]
|
|
22
|
-
context
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def get_int(key, default: Quonfig::NO_DEFAULT_PROVIDED, context: Quonfig::NO_DEFAULT_PROVIDED)
|
|
26
|
-
@calls << [:get_int, key, default, context]
|
|
27
|
-
context
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def enabled?(feature_name, jit_context = Quonfig::NO_DEFAULT_PROVIDED)
|
|
31
|
-
@calls << [:enabled?, feature_name, jit_context]
|
|
32
|
-
jit_context
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def test_get_string_uses_bound_context
|
|
37
|
-
fake = FakeClient.new
|
|
38
|
-
bound = Quonfig::BoundClient.new(fake, user: { 'key' => '99' })
|
|
39
|
-
|
|
40
|
-
bound.get_string('my.str')
|
|
41
|
-
|
|
42
|
-
call = fake.calls.last
|
|
43
|
-
assert_equal :get_string, call[0]
|
|
44
|
-
assert_equal 'my.str', call[1]
|
|
45
|
-
assert_equal Quonfig::NO_DEFAULT_PROVIDED, call[2]
|
|
46
|
-
assert_equal({ user: { 'key' => '99' } }, call[3])
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def test_enabled_uses_bound_context
|
|
50
|
-
fake = FakeClient.new
|
|
51
|
-
bound = Quonfig::BoundClient.new(fake, user: { 'key' => '99' })
|
|
52
|
-
|
|
53
|
-
bound.enabled?('my.flag')
|
|
54
|
-
|
|
55
|
-
call = fake.calls.last
|
|
56
|
-
assert_equal :enabled?, call[0]
|
|
57
|
-
assert_equal 'my.flag', call[1]
|
|
58
|
-
assert_equal({ user: { 'key' => '99' } }, call[2])
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def test_in_context_returns_new_bound_with_merged_context
|
|
62
|
-
fake = FakeClient.new
|
|
63
|
-
bound = Quonfig::BoundClient.new(fake, user: { 'key' => '99' })
|
|
64
|
-
|
|
65
|
-
chained = bound.in_context(org: { 'id' => 'acme' })
|
|
66
|
-
|
|
67
|
-
assert_kind_of Quonfig::BoundClient, chained
|
|
68
|
-
refute_same bound, chained,
|
|
69
|
-
'in_context should return a NEW BoundClient, not self'
|
|
70
|
-
|
|
71
|
-
expected = { user: { 'key' => '99' }, org: { 'id' => 'acme' } }
|
|
72
|
-
assert_equal expected, chained.context
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def test_in_context_merged_context_is_used_on_typed_getter
|
|
76
|
-
fake = FakeClient.new
|
|
77
|
-
bound = Quonfig::BoundClient.new(fake, user: { 'key' => '99' })
|
|
78
|
-
chained = bound.in_context(org: { 'id' => 'acme' })
|
|
79
|
-
|
|
80
|
-
chained.get_string('my.str')
|
|
81
|
-
|
|
82
|
-
ctx_arg = fake.calls.last[3]
|
|
83
|
-
assert_equal({ user: { 'key' => '99' }, org: { 'id' => 'acme' } }, ctx_arg)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def test_in_context_later_keys_within_same_named_ctx_override_earlier
|
|
87
|
-
fake = FakeClient.new
|
|
88
|
-
bound = Quonfig::BoundClient.new(fake, user: { 'key' => '99', 'plan' => 'free' })
|
|
89
|
-
|
|
90
|
-
chained = bound.in_context(user: { 'plan' => 'pro' })
|
|
91
|
-
|
|
92
|
-
# 'plan' overridden; 'key' preserved from parent bound
|
|
93
|
-
assert_equal({ user: { 'key' => '99', 'plan' => 'pro' } }, chained.context)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def test_in_context_does_not_mutate_parent_bound_context
|
|
97
|
-
fake = FakeClient.new
|
|
98
|
-
bound = Quonfig::BoundClient.new(fake, user: { 'key' => '99' })
|
|
99
|
-
|
|
100
|
-
bound.in_context(org: { 'id' => 'acme' })
|
|
101
|
-
|
|
102
|
-
assert_equal({ user: { 'key' => '99' } }, bound.context)
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def test_bound_client_is_frozen
|
|
106
|
-
bound = Quonfig::BoundClient.new(FakeClient.new, user: { 'key' => '99' })
|
|
107
|
-
assert_predicate bound, :frozen?
|
|
108
|
-
end
|
|
109
|
-
end
|