featurevisor 2.0.0 → 3.1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e7745fb77b8646de51fe54b58c32274829b60b046e6a47d81872e3148107eac6
4
- data.tar.gz: ef5f69f50e1af01af4b45aece70603a67dc77f3af08da98b10cf6e50dd3d77df
3
+ metadata.gz: 2cc7d95c6c897b09f4092c105b73b9776c6e4b6d1851dad7c2108ea8cced6b2b
4
+ data.tar.gz: 9b06c4ac0e499345af18967d161ef102b5636edd82dbf2013e15ae790ad7e832
5
5
  SHA512:
6
- metadata.gz: 745e81a4af47bc19e0a0bfe550badafb52e7541a067f461bdbc8b73be259399da5ca3fd519e7458ee0afa3ee6f810e0d7e28446add35ee13e970f25ecc7a0d2d
7
- data.tar.gz: 44947e427ceb1cf6b87e2e0784e7c77dddcbd8e27677c6794dd885a3a83dec65a4a2a1cec1815198abbe3e47187399700e9ec7e4342fd729428fafb21ab2dadd
6
+ metadata.gz: b2df8d3afe705ea850e4a77458c17d8090f4bf3b0ec27da761ef3447f8952ba0e2f970d0b1f75962c0c3402f01a2c8fe10b341a694eda2d17d5e004730f3aa51
7
+ data.tar.gz: ebd5698bbd941fbd2778350e6b7602375912a8455f0bd6af8ecd3034ca56d97b23c5803089c57a3a956fb638062838e65adeb3d38126bf4e72f5468512a54634
data/README.md CHANGED
@@ -19,8 +19,9 @@ This SDK is compatible with Featurevisor v3 projects and v2 datafiles.
19
19
  - [Getting variation](#getting-variation)
20
20
  - [Getting variables](#getting-variables)
21
21
  - [Type specific methods](#type-specific-methods)
22
- - [Getting all evaluations](#getting-all-evaluations)
23
- - [Sticky](#sticky)
22
+ - [Getting global variables](#getting-global-variables)
23
+ - [Getting aggregate evaluations](#getting-aggregate-evaluations)
24
+ - [Sticky features and variables](#sticky-features-and-variables)
24
25
  - [Initialize with sticky](#initialize-with-sticky)
25
26
  - [Set sticky afterwards](#set-sticky-afterwards)
26
27
  - [Setting datafile](#setting-datafile)
@@ -36,7 +37,7 @@ This SDK is compatible with Featurevisor v3 projects and v2 datafiles.
36
37
  - [Events](#events)
37
38
  - [`datafile_set`](#datafile_set)
38
39
  - [`context_set`](#context_set)
39
- - [`sticky_set`](#sticky_set)
40
+ - [`sticky_features_set` and `sticky_variables_set`](#sticky_features_set-and-sticky_variables_set)
40
41
  - [`error`](#error)
41
42
  - [Modules](#modules)
42
43
  - [Defining a module](#defining-a-module)
@@ -89,7 +90,7 @@ f = Featurevisor.create_featurevisor(
89
90
 
90
91
  Most applications only need this factory and the returned `Featurevisor::Instance`. Public extension and observability APIs include modules, diagnostics, events, and the datafile structures accepted by the factory.
91
92
 
92
- Concurrent evaluations are safe after an instance is configured. Do not mutate or close the same instance concurrently with evaluations. Serialize calls to `set_datafile`, `set_context`, `set_sticky`, `add_module`, `remove_module`, and `close`. Module, event, and diagnostic callbacks must synchronize mutable state that they capture.
93
+ Concurrent evaluations are safe after an instance is configured. Do not mutate or close the same instance concurrently with evaluations. Serialize calls to `set_datafile`, `set_context`, `set_sticky_features`, `set_sticky_variables`, `add_module`, `remove_module`, and `close`. Module, event, and diagnostic callbacks must synchronize mutable state that they capture.
93
94
 
94
95
  ## Initialization
95
96
 
@@ -295,12 +296,24 @@ f.get_variable_json(feature_key, variable_key, context = {})
295
296
 
296
297
  Type specific methods do not coerce values. `get_variable_integer` returns `nil` for the string `"1"`, and boolean getters return `nil` for non-boolean values.
297
298
 
298
- ## Getting all evaluations
299
+ ## Getting global variables
300
+
301
+ Global variables use the same methods with a single variable key:
302
+
303
+ ```ruby
304
+ message = f.get_variable_string('welcomeMessage', context)
305
+ value = f.get_variable('checkoutSettings', context)
306
+ evaluation = f.evaluate_variable('checkoutSettings', context)
307
+ ```
308
+
309
+ Global variables resolve sticky values first, then required features, then the first matching override, and finally their default value.
310
+
311
+ ## Getting aggregate evaluations
299
312
 
300
313
  You can get evaluations of all features available in the SDK instance:
301
314
 
302
315
  ```ruby
303
- all_evaluations = f.get_all_evaluations({})
316
+ all_evaluations = f.get_feature_evaluations({})
304
317
 
305
318
  puts all_evaluations
306
319
  # {
@@ -321,11 +334,17 @@ puts all_evaluations
321
334
 
322
335
  This is handy especially when you want to pass all evaluations from a backend application to the frontend.
323
336
 
324
- ## Sticky
337
+ Global variable values are available separately:
338
+
339
+ ```ruby
340
+ global_variables = f.get_variable_evaluations({})
341
+ ```
342
+
343
+ ## Sticky features and variables
325
344
 
326
345
  For the lifecycle of the SDK instance in your application, you can set some features with sticky values, meaning that they will not be evaluated against the fetched [datafile](https://featurevisor.com/docs/building-datafiles/):
327
346
 
328
- Sticky values belong to an SDK or child instance. Evaluation options do not accept sticky overrides; use `spawn(context, sticky: ...)` when a child needs its own sticky state.
347
+ Sticky values belong to an SDK or child instance. Feature and global variable sticky values are independent.
329
348
 
330
349
  ### Initialize with sticky
331
350
 
@@ -333,7 +352,7 @@ Sticky values belong to an SDK or child instance. Evaluation options do not acce
333
352
  require 'featurevisor'
334
353
 
335
354
  f = Featurevisor.create_featurevisor(
336
- sticky: {
355
+ sticky_features: {
337
356
  myFeatureKey: {
338
357
  enabled: true,
339
358
  # optional
@@ -345,6 +364,9 @@ f = Featurevisor.create_featurevisor(
345
364
  anotherFeatureKey: {
346
365
  enabled: false
347
366
  }
367
+ },
368
+ sticky_variables: {
369
+ welcomeMessage: 'Welcome back'
348
370
  }
349
371
  )
350
372
  ```
@@ -356,7 +378,7 @@ Once initialized with sticky features, the SDK will look for values there first
356
378
  You can also set sticky features after the SDK is initialized:
357
379
 
358
380
  ```ruby
359
- f.set_sticky({
381
+ f.set_sticky_features({
360
382
  myFeatureKey: {
361
383
  enabled: true,
362
384
  variation: 'treatment',
@@ -368,6 +390,8 @@ f.set_sticky({
368
390
  enabled: false
369
391
  }
370
392
  }, true) # replace existing sticky features (false by default)
393
+
394
+ f.set_sticky_variables({ welcomeMessage: 'Welcome back' }, true)
371
395
  ```
372
396
 
373
397
  ## Setting datafile
@@ -393,7 +417,7 @@ By default, `set_datafile(datafile)` merges the incoming datafile into the SDK's
393
417
  - `segments` are merged, with incoming entries overriding existing ones
394
418
  - `features` are merged, with incoming entries overriding existing ones
395
419
 
396
- This means you can call `set_datafile` more than once with different datafiles, and the SDK instance accumulates their features and segments together.
420
+ This means you can call `set_datafile` more than once with different datafiles, and the SDK instance accumulates their features, segments, and global variables together.
397
421
 
398
422
  ### Replacing
399
423
 
@@ -430,7 +454,7 @@ load_datafile(f, "checkout")
430
454
 
431
455
  ### Updating datafile
432
456
 
433
- You can set the datafile as many times as you want in your application, which will result in emitting a [`datafile_set`](#datafile_set) event that you can listen and react to accordingly.
457
+ You can set the datafile as many times as you want in your application, which will result in emitting a [`datafile_set`](#datafile-set) event that you can listen and react to accordingly.
434
458
 
435
459
  The triggers for setting the datafile again can be:
436
460
 
@@ -496,7 +520,7 @@ f = Featurevisor.create_featurevisor(
496
520
 
497
521
  Every diagnostic has `:level`, `:code`, `:message`, and an object-shaped `:details` hash. Optional `:module`, `:moduleName`, and `:originalError` fields describe provenance. Evaluation metadata belongs in `:details`.
498
522
 
499
- Diagnostic handlers are isolated from SDK behavior. An exception in a handler does not stop other handlers or evaluations.
523
+ Diagnostic handlers are isolated from SDK behaviour. An exception in a handler does not stop other handlers or evaluations.
500
524
 
501
525
 
502
526
  ## Events
@@ -545,15 +569,19 @@ unsubscribe = f.on('context_set') do |event|
545
569
  end
546
570
  ```
547
571
 
548
- ### `sticky_set`
572
+ ### `sticky_features_set` and `sticky_variables_set`
549
573
 
550
574
  ```ruby
551
- unsubscribe = f.on('sticky_set') do |event|
575
+ feature_unsubscribe = f.on('sticky_features_set') do |event|
552
576
  replaced = event[:replaced] # true if sticky features got replaced
553
577
  features = event[:features] # list of all affected feature keys
554
578
 
555
579
  puts 'Sticky features set'
556
580
  end
581
+
582
+ variable_unsubscribe = f.on('sticky_variables_set') do |event|
583
+ variables = event[:variables]
584
+ end
557
585
  ```
558
586
 
559
587
  ### `error`
@@ -583,11 +611,14 @@ evaluation = f.evaluate_variation(feature_key, context = {})
583
611
 
584
612
  # variable
585
613
  evaluation = f.evaluate_variable(feature_key, variable_key, context = {})
614
+
615
+ # global variable
616
+ global_evaluation = f.evaluate_variable(variable_key, context = {})
586
617
  ```
587
618
 
588
619
  The returned object will always contain the following properties:
589
620
 
590
- - `feature_key`: the feature key
621
+ - `feature_key`: the feature key when evaluating a feature
591
622
  - `reason`: the reason how the value was evaluated
592
623
 
593
624
  And optionally these properties depending on whether you are evaluating a feature variation or a variable:
@@ -604,7 +635,11 @@ And optionally these properties depending on whether you are evaluating a featur
604
635
 
605
636
  ## Modules
606
637
 
607
- Modules allow you to intercept the evaluation process and customize SDK behavior.
638
+ Modules allow you to intercept the evaluation process and customize SDK behaviour.
639
+
640
+ For feature evaluations, all `before` callbacks run in registration order, followed by all `before_evaluation` callbacks. After evaluation and caller defaults, all `after_evaluation` callbacks run, followed by all `after` callbacks. Global variable evaluations use only `before_evaluation` and `after_evaluation`. Required feature checks run through the complete module pipeline, and transformed defaults are preserved.
641
+
642
+ `before` and `after` remain available as deprecated feature-only compatibility callbacks. Use `before_evaluation` and `after_evaluation` for new modules so the same callbacks can handle feature and global variable evaluations.
608
643
 
609
644
  ### Defining a module
610
645
 
@@ -630,8 +665,8 @@ my_custom_module = {
630
665
  })
631
666
  },
632
667
 
633
- # before evaluation
634
- before: ->(options) {
668
+ # before feature or global variable evaluation
669
+ before_evaluation: ->(options) {
635
670
  # update context before evaluation
636
671
  options[:context] = options[:context].merge({
637
672
  someAdditionalAttribute: 'value'
@@ -639,13 +674,13 @@ my_custom_module = {
639
674
  options
640
675
  },
641
676
 
642
- # after evaluation
643
- after: ->(evaluation, options) {
677
+ # after feature or global variable evaluation
678
+ after_evaluation: ->(evaluation, options) {
644
679
  reason = evaluation[:reason]
645
680
  if reason == 'error'
646
681
  # log error
647
- return
648
682
  end
683
+ evaluation
649
684
  },
650
685
 
651
686
  # configure bucket key
@@ -720,12 +755,14 @@ Now you can pass the child instance where your individual request is being handl
720
755
  is_enabled = child_f.is_enabled('my_feature')
721
756
  variation = child_f.get_variation('my_feature')
722
757
  variable_value = child_f.get_variable('my_feature', 'my_variable')
758
+ global_value = child_f.get_variable('welcomeMessage')
723
759
  ```
724
760
 
725
761
  Similar to parent SDK, child instances also support several additional methods:
726
762
 
727
763
  - `set_context`
728
- - `set_sticky`
764
+ - `set_sticky_features`
765
+ - `set_sticky_variables`
729
766
  - `evaluate_flag`
730
767
  - `is_enabled`
731
768
  - `evaluate_variation`
@@ -739,7 +776,8 @@ Similar to parent SDK, child instances also support several additional methods:
739
776
  - `get_variable_array`
740
777
  - `get_variable_object`
741
778
  - `get_variable_json`
742
- - `get_all_evaluations`
779
+ - `get_feature_evaluations`
780
+ - `get_variable_evaluations`
743
781
  - `on`
744
782
  - `close`
745
783
 
@@ -780,7 +818,7 @@ $ bundle exec featurevisor test \
780
818
 
781
819
  The Ruby test runner builds base datafiles and Target datafiles in memory via `npx featurevisor build --json`. When an assertion contains `target`, it is evaluated against the matching Target datafile.
782
820
 
783
- All three commands accept repeatable `--target=<target>` options. `test` builds only the selected Target datafiles and runs untargeted assertions plus assertions for those targets. `benchmark` and `assess-distribution` run independently against every selected Target datafile. Without `--target`, existing project-wide behavior is preserved. Project definitions, test specs, Target discovery, and datafile generation continue to come from the Node.js CLI.
821
+ All three commands accept repeatable `--target=<target>` options. `test` builds only the selected Target datafiles and runs untargeted assertions plus assertions for those targets. `benchmark` and `assess-distribution` run independently against every selected Target datafile. Without `--target`, existing project-wide behaviour is preserved. Project definitions, test specs, Target discovery, and datafile generation continue to come from the Node.js CLI.
784
822
 
785
823
  ### Test against local monorepo's example-1
786
824
 
@@ -828,7 +866,7 @@ The provider currently requires Ruby 3.4 or newer because that is the minimum ve
828
866
  Install the provider:
829
867
 
830
868
  ```ruby
831
- gem "featurevisor-openfeature", "~> 2.0"
869
+ gem "featurevisor-openfeature", "~> 3.0"
832
870
  ```
833
871
 
834
872
  It installs the matching `featurevisor` gem and the official `openfeature-sdk` dependency. The provider and base SDK deliberately share the same version, and the provider requires that exact Featurevisor version.
@@ -852,9 +890,9 @@ enabled = client.fetch_boolean_value(
852
890
  )
853
891
  ```
854
892
 
855
- Use `checkout` for a flag, `checkout:variation` for its variation, and `checkout:title` for its `title` variable. Boolean variables use the boolean resolver. Arrays, hashes, and JSON variables use the object resolver.
893
+ Use `checkout` for a flag, `checkout:variation` for its variation, `checkout:title` for its `title` variable, and `variable:welcomeMessage` for a global variable. Boolean variables use the boolean resolver. Arrays, hashes, and JSON variables use the object resolver.
856
894
 
857
- OpenFeature's targeting key maps to `userId` by default. `targeting_key_field`, `key_separator`, and `variation_key` can customize the mapping.
895
+ OpenFeature's targeting key maps to `userId` by default. `targeting_key_field`, `key_separator`, `variation_key`, and `global_variable_prefix` can customize the mapping. The global variable prefix defaults to `variable` and cannot contain the separator.
858
896
 
859
897
  You can pass any Featurevisor initialization options directly to the provider. These options are used to create the Featurevisor instance owned by the provider:
860
898
 
@@ -908,7 +946,7 @@ The build produces `featurevisor-VERSION.gem` and `featurevisor-openfeature-VERS
908
946
  - Run `bundle install`
909
947
  - Push commit to `main` branch
910
948
  - Wait for CI to complete
911
- - Tag the release with the same version number, for example `v2.0.0`
949
+ - Tag the release with the same version number, for example `v3.1.0`
912
950
  - The workflow verifies that the tag matches the shared version
913
951
  - The workflow publishes `featurevisor` first, followed by `featurevisor-openfeature`
914
952
 
data/bin/cli.rb CHANGED
@@ -43,7 +43,7 @@ module FeaturevisorCLI
43
43
  options.environment = v
44
44
  end
45
45
 
46
- opts.on("--feature=FEATURE", "Feature key (required for benchmark)") do |v|
46
+ opts.on("--feature=FEATURE", "Feature key") do |v|
47
47
  options.feature = v
48
48
  end
49
49
 
@@ -150,6 +150,6 @@ module FeaturevisorCLI
150
150
  puts " featurevisor benchmark --feature=myFeature --environment=dev --n=10000"
151
151
  puts " featurevisor assess-distribution --feature=myFeature --n=10000"
152
152
  puts ""
153
- puts "Note: benchmark command requires --environment and --feature options"
153
+ puts "Note: benchmark requires --environment and either --feature or --variable"
154
154
  end
155
155
  end
@@ -22,8 +22,8 @@ module FeaturevisorCLI
22
22
  exit 1
23
23
  end
24
24
 
25
- unless @options.feature
26
- puts "Error: --feature is required for benchmark command"
25
+ unless @options.feature || @options.variable
26
+ puts "Error: --feature or --variable is required for benchmark command"
27
27
  exit 1
28
28
  end
29
29
 
@@ -41,7 +41,8 @@ module FeaturevisorCLI
41
41
  datafile_build_duration_ms = (datafile_build_duration * 1000).round
42
42
 
43
43
  puts "\nBenchmark Featurevisor feature"
44
- puts " Feature: #{@options.feature}"
44
+ puts " Feature: #{@options.feature}" if @options.feature
45
+ puts " Global variable: #{@options.variable}" if @options.variable && !@options.feature
45
46
  puts " Environment: #{@options.environment}"
46
47
  puts " Target: #{target}" if target
47
48
  puts " Iterations: #{@options.n}"
@@ -59,7 +60,10 @@ module FeaturevisorCLI
59
60
  puts "Against context: #{context.to_json}"
60
61
 
61
62
  # Run the appropriate benchmark
62
- if @options.variation
63
+ if @options.variable && !@options.feature
64
+ puts "Evaluating global variable \"#{@options.variable}\" #{@options.n} times..."
65
+ output = benchmark_global_variable(instance, @options.variable, context, @options.n)
66
+ elsif @options.variation
63
67
  puts "Evaluating variation #{@options.n} times..."
64
68
  output = benchmark_feature_variation(instance, @options.feature, context, @options.n)
65
69
  elsif @options.variable
@@ -223,6 +227,12 @@ module FeaturevisorCLI
223
227
  end
224
228
  end
225
229
 
230
+ def benchmark_global_variable(instance, variable_key, context, n)
231
+ benchmark_evaluation(n) do
232
+ instance.get_variable(variable_key, context)
233
+ end
234
+ end
235
+
226
236
  def format_value(value)
227
237
  if value.nil?
228
238
  "null"
data/bin/commands/test.rb CHANGED
@@ -223,22 +223,19 @@ module FeaturevisorCLI
223
223
  environment = assertion.key?(:environment) ? assertion[:environment] : false
224
224
  environment = false if environment.nil?
225
225
 
226
- target_key = assertion[:target] ? target_datafile_key(environment, assertion[:target]) : nil
227
- base_key = base_datafile_key(environment)
226
+ return datafiles_by_key[target_datafile_key(environment, assertion[:target])] if assertion[:target]
228
227
 
229
- if target_key && datafiles_by_key.key?(target_key)
230
- return datafiles_by_key[target_key]
231
- end
232
-
233
- datafiles_by_key[base_key]
228
+ datafiles_by_key[base_datafile_key(environment)]
234
229
  end
235
230
 
236
231
  def create_tester_instance(datafile, level, assertion)
237
- sticky = parse_sticky(assertion[:sticky])
232
+ sticky = parse_sticky(assertion[:stickyFeatures] || assertion[:sticky])
233
+ sticky_variables = assertion[:stickyVariables].is_a?(Hash) ? assertion[:stickyVariables] : {}
238
234
 
239
235
  Featurevisor.create_featurevisor(
240
236
  datafile: datafile,
241
- sticky: sticky,
237
+ sticky_features: sticky,
238
+ sticky_variables: sticky_variables,
242
239
  log_level: level,
243
240
  modules: [
244
241
  {
@@ -265,7 +262,7 @@ module FeaturevisorCLI
265
262
  tests.each do |test|
266
263
  test_key = test[:key]
267
264
  assertions = test[:assertions] || []
268
- if test[:feature] && !@options.targets.empty?
265
+ if (test[:feature] || test[:variable]) && !@options.targets.empty?
269
266
  assertions = assertions.select do |assertion|
270
267
  assertion[:target].nil? || @options.targets.include?(assertion[:target])
271
268
  end
@@ -299,7 +296,28 @@ module FeaturevisorCLI
299
296
  puts ""
300
297
  end
301
298
 
302
- test_result = run_test_feature(assertion, test[:feature], instance, level)
299
+ begin
300
+ test_result = run_test_feature(assertion, test[:feature], instance, level)
301
+ ensure
302
+ instance.close
303
+ end
304
+ end
305
+ elsif test[:variable]
306
+ datafile = resolve_datafile_for_assertion(assertion, datafiles_by_key)
307
+ if datafile
308
+ if @options.show_datafile
309
+ puts ""
310
+ puts JSON.pretty_generate(datafile)
311
+ puts ""
312
+ end
313
+ instance = create_tester_instance(datafile, level, assertion)
314
+ begin
315
+ test_result = run_test_variable(assertion, test[:variable], instance)
316
+ ensure
317
+ instance.close
318
+ end
319
+ else
320
+ test_result = { has_error: true, errors: " ✘ no datafile found for assertion target/environment combination\n", duration: 0 }
303
321
  end
304
322
  elsif test[:segment]
305
323
  segment_key = test[:segment]
@@ -347,6 +365,64 @@ module FeaturevisorCLI
347
365
  end
348
366
  end
349
367
 
368
+ def run_test_variable(assertion, variable_key, instance)
369
+ instance.set_context(parse_context(assertion[:context]), true)
370
+ options = {}
371
+ options[:default_variable_value] = assertion[:defaultVariableValue] if assertion.key?(:defaultVariableValue)
372
+ started = Time.now
373
+ errors = ""
374
+
375
+ errors += test_variable_expectation(assertion, variable_key, instance, options)
376
+
377
+ Array(assertion[:children]).each_with_index do |child_assertion, child_index|
378
+ child = instance.spawn(
379
+ parse_context(child_assertion[:context]),
380
+ sticky_features: parse_sticky(child_assertion[:stickyFeatures]),
381
+ sticky_variables: child_assertion[:stickyVariables].is_a?(Hash) ? child_assertion[:stickyVariables] : {}
382
+ )
383
+ begin
384
+ child_options = {}
385
+ if child_assertion.key?(:defaultVariableValue)
386
+ child_options[:default_variable_value] = child_assertion[:defaultVariableValue]
387
+ end
388
+ errors += test_variable_expectation(
389
+ child_assertion,
390
+ variable_key,
391
+ child,
392
+ child_options,
393
+ "children[#{child_index}]."
394
+ )
395
+ ensure
396
+ child.close
397
+ end
398
+ end
399
+
400
+ { has_error: !errors.empty?, errors: errors, duration: Time.now - started }
401
+ end
402
+
403
+ def test_variable_expectation(assertion, variable_key, evaluator, options, prefix = "")
404
+ evaluation = evaluator.evaluate_variable(variable_key, {}, options)
405
+ errors = ""
406
+ if assertion.key?(:expectedValue) && !compare_values(evaluation[:variable_value], assertion[:expectedValue])
407
+ errors += " ✘ #{prefix}expectedValue: expected #{format_test_value(assertion[:expectedValue])} but received #{format_test_value(evaluation[:variable_value])}\n"
408
+ end
409
+ if assertion[:expectedEvaluation].is_a?(Hash)
410
+ assertion[:expectedEvaluation].each do |key, expected|
411
+ actual = get_evaluation_value(evaluation, key)
412
+ unless compare_values(actual, expected)
413
+ errors += " ✘ #{prefix}expectedEvaluation.#{key}: expected #{format_test_value(expected)} but received #{format_test_value(actual)}\n"
414
+ end
415
+ end
416
+ end
417
+ errors
418
+ end
419
+
420
+ def format_test_value(value)
421
+ JSON.generate(value)
422
+ rescue JSON::GeneratorError
423
+ value.inspect
424
+ end
425
+
350
426
  def run_test_feature(assertion, feature_key, instance, level)
351
427
  context = parse_context(assertion[:context])
352
428
  sticky = parse_sticky(assertion[:sticky])
@@ -354,7 +430,7 @@ module FeaturevisorCLI
354
430
  # Set context and sticky for this assertion
355
431
  instance.set_context(context, false)
356
432
  if sticky && !sticky.empty?
357
- instance.set_sticky(sticky, false)
433
+ instance.set_sticky_features(sticky, false)
358
434
  end
359
435
 
360
436
  # Create override options
@@ -483,20 +559,16 @@ module FeaturevisorCLI
483
559
  child_context = parse_context(child[:context])
484
560
 
485
561
  # Create override options for child with sticky values
486
- child_override_options = create_override_options(child)
487
-
488
- # Pass sticky values to child instance
489
- child_instance = instance.spawn(child_context, child_override_options)
490
-
491
- # Set sticky values for child if they exist
492
- # Create a local copy to ensure it's never nil
493
- child_sticky = sticky || {}
494
- if !child_sticky.empty?
495
- child_instance.set_sticky(child_sticky, false)
562
+ child_instance = instance.spawn(
563
+ child_context,
564
+ sticky_features: sticky || {}
565
+ )
566
+ begin
567
+ child_result = run_test_feature_child(child, feature_key, child_instance, level)
568
+ ensure
569
+ child_instance.close
496
570
  end
497
571
 
498
- child_result = run_test_feature_child(child, feature_key, child_instance, level)
499
-
500
572
  if child_result[:has_error]
501
573
  has_error = true
502
574
  errors += child_result[:errors]
@@ -795,6 +867,8 @@ module FeaturevisorCLI
795
867
  evaluation[:force]
796
868
  when :required
797
869
  evaluation[:required]
870
+ when :requiredFeatures
871
+ evaluation[:required_features]
798
872
  when :sticky
799
873
  evaluation[:sticky]
800
874
  when :variation
@@ -809,6 +883,10 @@ module FeaturevisorCLI
809
883
  evaluation[:variable_schema]
810
884
  when :variableOverrideIndex
811
885
  evaluation[:variable_override_index]
886
+ when :variableOverrideKey
887
+ evaluation[:variable_override_key]
888
+ when :variableOverridePath
889
+ evaluation[:variable_override_path]
812
890
  else
813
891
  nil
814
892
  end