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,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
@@ -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