From c94dd148c6b7869d12a8faa41c448d3d590fdf9f Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Wed, 18 Mar 2026 10:58:59 +0100 Subject: [PATCH] feat(evaluator): port new test scenarios from PRs #341-345 - semver edge cases: v-prefix, partial version, build metadata (@semver-edge-cases) - operator error null-return fallback (@operator-errors): semver invalid version/operator, fractional null bucket key - nested $ref resolution via is_privileged (@evaluator-ref-edge-cases) - nested fractional expressions: if/var as bucket variant names (@fractional-nested) - fractional hash edge cases (@fractional-v2) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Simon Schrottner --- evaluator/flags/testkit-flags.json | 92 ++++++++++++++++++++++++ evaluator/gherkin/evaluator-refs.feature | 16 +++++ evaluator/gherkin/fractional.feature | 39 ++++++++++ evaluator/gherkin/semver.feature | 53 ++++++++++++++ 4 files changed, 200 insertions(+) diff --git a/evaluator/flags/testkit-flags.json b/evaluator/flags/testkit-flags.json index 7a0efc8..5a32a21 100644 --- a/evaluator/flags/testkit-flags.json +++ b/evaluator/flags/testkit-flags.json @@ -386,6 +386,92 @@ null ] } + }, + "fractional-hash-edge-flag": { + "state": "ENABLED", + "variants": { "lower": "lower", "upper": "upper" }, + "defaultVariant": "lower", + "targeting": { + "fractional": [ + { "var": "targetingKey" }, + [ "lower", 50 ], + [ "upper", 50 ] + ] + } + }, + "semver-v-prefix-flag": { + "state": "ENABLED", + "variants": { "match": "match", "no-match": "no-match" }, + "defaultVariant": "no-match", + "targeting": { "if": [{"sem_ver": [{"var": "version"}, "=", "v1.0.0"]}, "match", "no-match"] } + }, + "semver-partial-version-flag": { + "state": "ENABLED", + "variants": { "match": "match", "no-match": "no-match" }, + "defaultVariant": "no-match", + "targeting": { "if": [{"sem_ver": [{"var": "version"}, "^", "1"]}, "match", "no-match"] } + }, + "semver-build-metadata-flag": { + "state": "ENABLED", + "variants": { "match": "match", "no-match": "no-match" }, + "defaultVariant": "no-match", + "targeting": { "if": [{"sem_ver": [{"var": "version"}, "=", "1.0.0+build"]}, "match", "no-match"] } + }, + "semver-invalid-version-flag": { + "state": "ENABLED", + "variants": { "match": "match", "no-match": "no-match", "fallback": "fallback" }, + "defaultVariant": "fallback", + "targeting": { "sem_ver": [{"var": "version"}, "=", "1.0.0"] } + }, + "semver-invalid-operator-flag": { + "state": "ENABLED", + "variants": { "match": "match", "no-match": "no-match", "fallback": "fallback" }, + "defaultVariant": "fallback", + "targeting": { "sem_ver": [{"var": "version"}, "===", "1.0.0"] } + }, + "fractional-null-bucket-key-flag": { + "state": "ENABLED", + "variants": { "one": "one", "two": "two", "fallback": "fallback" }, + "defaultVariant": "fallback", + "targeting": { + "fractional": [ + {"var": "missing_key"}, + ["one", 50], + ["two", 50] + ] + } + }, + "nested-ref-targeted-flag": { + "state": "ENABLED", + "variants": { "privileged": "privileged", "standard": "standard", "none": "none" }, + "defaultVariant": "none", + "targeting": { + "if": [{"$ref": "is_privileged"}, "privileged", "standard"] + } + }, + "fractional-nested-if-flag": { + "state": "ENABLED", + "variants": { "premium": "premium", "standard": "standard", "fallback": "fallback" }, + "defaultVariant": "fallback", + "targeting": { + "fractional": [ + { "var": "targetingKey" }, + [{"if": [{"==": [{"var": "tier"}, "premium"]}, "premium", "standard"]}, 50], + [ "standard", 50 ] + ] + } + }, + "fractional-nested-var-flag": { + "state": "ENABLED", + "variants": { "red": "red", "green": "green", "blue": "blue", "fallback": "fallback" }, + "defaultVariant": "fallback", + "targeting": { + "fractional": [ + { "var": "targetingKey" }, + [ { "var": "color" }, 50 ], + [ "blue", 50 ] + ] + } } }, "$evaluators": { @@ -394,6 +480,12 @@ "ballmer@macrosoft.com", {"var": ["email"]} ] + }, + "is_privileged": { + "or": [ + {"$ref": "is_ballmer"}, + {"==": ["admin", {"var": ["role"]}]} + ] } } } diff --git a/evaluator/gherkin/evaluator-refs.feature b/evaluator/gherkin/evaluator-refs.feature index 0d8467d..bf54d5e 100644 --- a/evaluator/gherkin/evaluator-refs.feature +++ b/evaluator/gherkin/evaluator-refs.feature @@ -13,3 +13,19 @@ Feature: Evaluator evaluator refs | key | value | | some-email-targeted-flag | hi | | some-other-email-targeted-flag | yes | + + # Nested $ref resolution — is_privileged references is_ballmer. + # Use -t "not @evaluator-ref-edge-cases" to exclude during transition. + @evaluator-ref-edge-cases + Scenario Outline: Nested evaluator ref resolution + Given an evaluator + And a String-flag with key "nested-ref-targeted-flag" and a fallback value "fallback" + And a context containing a key "", with type "String" and with value "" + When the flag was evaluated with details + Then the resolved details value should be "" + + Examples: + | context_key | context_value | value | + | email | ballmer@macrosoft.com | privileged | + | role | admin | privileged | + | email | other@example.com | standard | diff --git a/evaluator/gherkin/fractional.feature b/evaluator/gherkin/fractional.feature index c71b7fd..cbb4760 100644 --- a/evaluator/gherkin/fractional.feature +++ b/evaluator/gherkin/fractional.feature @@ -110,3 +110,42 @@ Feature: Evaluator fractional operator | SI7p- | lower | | 6LvT0 | upper | | ceQdGm | upper | + + # Nested JSON Logic expressions as bucket variant names. + # Use -t "not @fractional-nested" to exclude during transition. + @fractional-nested + Scenario Outline: Fractional operator with nested if expression as variant name + Given a String-flag with key "fractional-nested-if-flag" and a fallback value "fallback" + And a context containing a targeting key with value "" + And a context containing a key "tier", with type "String" and with value "" + When the flag was evaluated with details + Then the resolved details value should be "" + + Examples: + | targetingKey | tier | value | + | jon@company.com | premium | premium | + | jon@company.com | basic | standard | + | user1 | premium | standard | + | user1 | basic | standard | + + @fractional-nested + Scenario Outline: Fractional operator with nested var expression as variant name + Given a String-flag with key "fractional-nested-var-flag" and a fallback value "fallback" + And a context containing a targeting key with value "" + And a context containing a key "color", with type "String" and with value "" + When the flag was evaluated with details + Then the resolved details value should be "" + + Examples: + | targetingKey | color | value | + | jon@company.com | red | red | + | jon@company.com | green | green | + | user1 | red | blue | + | jon@company.com | yellow | fallback | + | jon@company.com | | fallback | + + @operator-errors + Scenario: Fractional operator with missing bucket key falls back to default variant + Given a String-flag with key "fractional-null-bucket-key-flag" and a fallback value "wrong" + When the flag was evaluated with details + Then the resolved details value should be "fallback" diff --git a/evaluator/gherkin/semver.feature b/evaluator/gherkin/semver.feature index dbdce52..f525bb1 100644 --- a/evaluator/gherkin/semver.feature +++ b/evaluator/gherkin/semver.feature @@ -31,3 +31,56 @@ Feature: Evaluator semantic version operator | 3.0.1 | minor | | 3.1.0 | major | | 4.0.0 | none | + + # Edge cases: v-prefix, partial versions, build metadata. + # Use -t "not @semver-edge-cases" to exclude during transition. + @semver-edge-cases + Scenario Outline: sem_ver v-prefix handling + Given a String-flag with key "semver-v-prefix-flag" and a fallback value "fallback" + And a context containing a key "version", with type "String" and with value "" + When the flag was evaluated with details + Then the resolved details value should be "" + + Examples: + | version | value | + | 1.0.0 | match | + | v1.0.0 | match | + | 2.0.0 | no-match | + + @semver-edge-cases + Scenario Outline: sem_ver partial version handling + Given a String-flag with key "semver-partial-version-flag" and a fallback value "fallback" + And a context containing a key "version", with type "String" and with value "" + When the flag was evaluated with details + Then the resolved details value should be "" + + Examples: + | version | value | + | 1.5.0 | match | + | 1.0.0 | match | + | 2.0.0 | no-match | + + @semver-edge-cases + Scenario Outline: sem_ver build metadata ignored + Given a String-flag with key "semver-build-metadata-flag" and a fallback value "fallback" + And a context containing a key "version", with type "String" and with value "" + When the flag was evaluated with details + Then the resolved details value should be "" + + Examples: + | version | value | + | 1.0.0 | match | + | 1.0.0+other | match | + | 2.0.0 | no-match | + + @operator-errors + Scenario Outline: sem_ver operator errors return null and fall back to default variant + Given a String-flag with key "" and a fallback value "wrong" + And a context containing a key "version", with type "String" and with value "" + When the flag was evaluated with details + Then the resolved details value should be "" + + Examples: + | key | context_value | value | + | semver-invalid-version-flag | not-a-version | fallback | + | semver-invalid-operator-flag | 1.0.0 | fallback |