diff --git a/evaluator/flags/testkit-flags.json b/evaluator/flags/testkit-flags.json index fc696d4..1db5f14 100644 --- a/evaluator/flags/testkit-flags.json +++ b/evaluator/flags/testkit-flags.json @@ -462,29 +462,38 @@ }, "semver-invalid-version-flag": { "state": "ENABLED", - "variants": { "match": "match", "no-match": "no-match", "fallback": "fallback" }, + "variants": { "true": "true", "false": "false", "fallback": "fallback" }, "defaultVariant": "fallback", "targeting": { - "sem_ver": [{"var": "version"}, "=", "1.0.0"] + "if": [ + {"sem_ver": [{"var": "version"}, "=", "1.0.0"]}, + "true", "false" + ] } }, "semver-invalid-operator-flag": { "state": "ENABLED", - "variants": { "match": "match", "no-match": "no-match", "fallback": "fallback" }, + "variants": { "true": "true", "false": "false", "fallback": "fallback" }, "defaultVariant": "fallback", "targeting": { - "sem_ver": [{"var": "version"}, "===", "1.0.0"] + "if": [ + {"sem_ver": [{"var": "version"}, "===", "1.0.0"]}, + "true", "false" + ] } }, "fractional-null-bucket-key-flag": { "state": "ENABLED", - "variants": { "one": "one", "two": "two", "fallback": "fallback" }, + "variants": { "true": "true", "false": "false", "fallback": "fallback" }, "defaultVariant": "fallback", "targeting": { - "fractional": [ - {"var": "missing_key"}, - ["one", 50], - ["two", 50] + "if": [ + {"fractional": [ + {"var": "missing_key"}, + ["one", 50], + ["two", 50] + ]}, + "true", "false" ] } }, @@ -583,6 +592,88 @@ "miss" ] } + }, + "starts-with-non-string-flag": { + "state": "ENABLED", + "variants": { "true": "true", "false": "false", "fallback": "fallback" }, + "defaultVariant": "fallback", + "targeting": { + "if": [ + {"starts_with": [{"var": "num"}, "abc"]}, + "true", "false" + ] + } + }, + "ends-with-non-string-flag": { + "state": "ENABLED", + "variants": { "true": "true", "false": "false", "fallback": "fallback" }, + "defaultVariant": "fallback", + "targeting": { + "if": [ + {"ends_with": [{"var": "num"}, "xyz"]}, + "true", "false" + ] + } + }, + "starts-with-wrong-args-flag": { + "state": "ENABLED", + "variants": { "true": "true", "false": "false", "fallback": "fallback" }, + "defaultVariant": "fallback", + "targeting": { + "if": [ + {"starts_with": ["abc"]}, + "true", "false" + ] + } + }, + "ends-with-wrong-args-flag": { + "state": "ENABLED", + "variants": { "true": "true", "false": "false", "fallback": "fallback" }, + "defaultVariant": "fallback", + "targeting": { + "if": [ + {"ends_with": ["xyz"]}, + "true", "false" + ] + } + }, + "fractional-zero-weights-flag": { + "state": "ENABLED", + "variants": { "true": "true", "false": "false", "fallback": "fallback" }, + "defaultVariant": "fallback", + "targeting": { + "if": [ + {"fractional": [ + {"var": "targetingKey"}, + ["one", 0], + ["two", 0] + ]}, + "true", "false" + ] + } + }, + "fractional-negative-weight-flag": { + "state": "ENABLED", + "variants": { "one": "one", "two": "two", "fallback": "fallback" }, + "defaultVariant": "fallback", + "targeting": { + "fractional": [ + {"var": "targetingKey"}, + ["one", -50], + ["two", 100] + ] + } + }, + "semver-wrong-args-flag": { + "state": "ENABLED", + "variants": { "true": "true", "false": "false", "fallback": "fallback" }, + "defaultVariant": "fallback", + "targeting": { + "if": [ + {"sem_ver": [{"var": "version"}, "="]}, + "true", "false" + ] + } } }, "$evaluators": { diff --git a/evaluator/gherkin/fractional.feature b/evaluator/gherkin/fractional.feature index ee24e7d..a58fc37 100644 --- a/evaluator/gherkin/fractional.feature +++ b/evaluator/gherkin/fractional.feature @@ -195,3 +195,24 @@ Feature: Evaluator fractional operator And 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" + And the reason should be "DEFAULT" + + # Follow-up error scenarios from https://github.com/open-feature/flagd/issues/1874 + + @operator-errors + Scenario: fractional with all-zero bucket weights falls back to default variant + Given an evaluator + And a String-flag with key "fractional-zero-weights-flag" and a fallback value "wrong" + And a context containing a targeting key with value "any-user" + When the flag was evaluated with details + Then the resolved details value should be "fallback" + And the reason should be "DEFAULT" + + @operator-errors + Scenario: fractional negative bucket weight is clamped to zero + # ["one", -50] is treated as ["one", 0]; "two" gets 100% of the weight + Given an evaluator + And a String-flag with key "fractional-negative-weight-flag" and a fallback value "wrong" + And a context containing a targeting key with value "any-user" + When the flag was evaluated with details + Then the resolved details value should be "two" diff --git a/evaluator/gherkin/semver.feature b/evaluator/gherkin/semver.feature index 8004bee..18c5130 100644 --- a/evaluator/gherkin/semver.feature +++ b/evaluator/gherkin/semver.feature @@ -39,6 +39,7 @@ Feature: Evaluator semantic version operator 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 "fallback" + And the reason should be "DEFAULT" Examples: | key | context_value | | semver-invalid-version-flag | not-a-version | @@ -86,3 +87,14 @@ Feature: Evaluator semantic version operator | 1.0.0 | match | | 1.0.0+other | match | | 2.0.0 | no-match | + + # Follow-up error scenarios from https://github.com/open-feature/flagd/issues/1874 + + @operator-errors + Scenario: sem_ver returns null for wrong argument count + Given an evaluator + And a String-flag with key "semver-wrong-args-flag" and a fallback value "wrong" + And a context containing a key "version", with type "String" and with value "1.0.0" + When the flag was evaluated with details + Then the resolved details value should be "fallback" + And the reason should be "DEFAULT" diff --git a/evaluator/gherkin/string.feature b/evaluator/gherkin/string.feature index 14e995f..7e77176 100644 --- a/evaluator/gherkin/string.feature +++ b/evaluator/gherkin/string.feature @@ -16,3 +16,32 @@ Feature: Evaluator string comparison operator | uvwxyz | postfix | | abcxyz | prefix | | lmnopq | none | + + # Follow-up error scenarios from https://github.com/open-feature/flagd/issues/1874 + # starts_with and ends_with must return null (not false) on error so that the + # default variant is selected, rather than looking up a non-existent "false" variant. + + @operator-errors + Scenario Outline: starts_with and ends_with return null for non-string input + Given an evaluator + And a String-flag with key "" and a fallback value "wrong" + And a context containing a key "num", with type "Integer" and with value "123" + When the flag was evaluated with details + Then the resolved details value should be "fallback" + And the reason should be "DEFAULT" + Examples: + | key | + | starts-with-non-string-flag | + | ends-with-non-string-flag | + + @operator-errors + Scenario Outline: starts_with and ends_with return null for wrong argument count + Given an evaluator + And a String-flag with key "" and a fallback value "wrong" + When the flag was evaluated with details + Then the resolved details value should be "fallback" + And the reason should be "DEFAULT" + Examples: + | key | + | starts-with-wrong-args-flag | + | ends-with-wrong-args-flag | diff --git a/flags/edge-case-flags.json b/flags/edge-case-flags.json index f7fcf30..42d6eac 100644 --- a/flags/edge-case-flags.json +++ b/flags/edge-case-flags.json @@ -56,40 +56,155 @@ "semver-invalid-version-flag": { "state": "ENABLED", "variants": { - "match": "match", - "no-match": "no-match", + "true": "true", + "false": "false", "fallback": "fallback" }, "defaultVariant": "fallback", "targeting": { - "sem_ver": [{"var": "version"}, "=", "1.0.0"] + "if": [ + {"sem_ver": [{"var": "version"}, "=", "1.0.0"]}, + "true", "false" + ] } }, "semver-invalid-operator-flag": { "state": "ENABLED", "variants": { - "match": "match", - "no-match": "no-match", + "true": "true", + "false": "false", "fallback": "fallback" }, "defaultVariant": "fallback", "targeting": { - "sem_ver": [{"var": "version"}, "===", "1.0.0"] + "if": [ + {"sem_ver": [{"var": "version"}, "===", "1.0.0"]}, + "true", "false" + ] } }, "fractional-null-bucket-key-flag": { "state": "ENABLED", "variants": { - "one": "one", - "two": "two", + "true": "true", + "false": "false", + "fallback": "fallback" + }, + "defaultVariant": "fallback", + "targeting": { + "if": [ + {"fractional": [ + {"var": "missing_key"}, + ["one", 50], + ["two", 50] + ]}, + "true", "false" + ] + } + }, + "starts-with-non-string-flag": { + "state": "ENABLED", + "variants": { + "true": "true", + "false": "false", + "fallback": "fallback" + }, + "defaultVariant": "fallback", + "targeting": { + "if": [ + {"starts_with": [{"var": "num"}, "abc"]}, + "true", "false" + ] + } + }, + "ends-with-non-string-flag": { + "state": "ENABLED", + "variants": { + "true": "true", + "false": "false", + "fallback": "fallback" + }, + "defaultVariant": "fallback", + "targeting": { + "if": [ + {"ends_with": [{"var": "num"}, "xyz"]}, + "true", "false" + ] + } + }, + "starts-with-wrong-args-flag": { + "state": "ENABLED", + "variants": { + "true": "true", + "false": "false", + "fallback": "fallback" + }, + "defaultVariant": "fallback", + "targeting": { + "if": [ + {"starts_with": ["abc"]}, + "true", "false" + ] + } + }, + "ends-with-wrong-args-flag": { + "state": "ENABLED", + "variants": { + "true": "true", + "false": "false", + "fallback": "fallback" + }, + "defaultVariant": "fallback", + "targeting": { + "if": [ + {"ends_with": ["xyz"]}, + "true", "false" + ] + } + }, + "fractional-zero-weights-flag": { + "state": "ENABLED", + "variants": { + "true": "true", + "false": "false", "fallback": "fallback" }, "defaultVariant": "fallback", + "targeting": { + "if": [ + {"fractional": [ + {"var": "targetingKey"}, + ["one", 0], + ["two", 0] + ]}, + "true", "false" + ] + } + }, + "fractional-negative-weight-flag": { + "state": "ENABLED", + "variants": { "one": "one", "two": "two", "fallback": "fallback" }, + "defaultVariant": "fallback", "targeting": { "fractional": [ - {"var": "missing_key"}, - ["one", 50], - ["two", 50] + {"var": "targetingKey"}, + ["one", -50], + ["two", 100] + ] + } + }, + "semver-wrong-args-flag": { + "state": "ENABLED", + "variants": { + "true": "true", + "false": "false", + "fallback": "fallback" + }, + "defaultVariant": "fallback", + "targeting": { + "if": [ + {"sem_ver": [{"var": "version"}, "="]}, + "true", "false" ] } } diff --git a/gherkin/targeting.feature b/gherkin/targeting.feature index 8a193b9..ec90364 100644 --- a/gherkin/targeting.feature +++ b/gherkin/targeting.feature @@ -349,6 +349,7 @@ Feature: Targeting rules 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 "fallback" + And the reason should be "DEFAULT" Examples: | key | context_value | | semver-invalid-version-flag | not-a-version | @@ -359,3 +360,54 @@ Feature: Targeting rules Given a String-flag with key "fractional-null-bucket-key-flag" and a default value "wrong" When the flag was evaluated with details Then the resolved details value should be "fallback" + And the reason should be "DEFAULT" + + # Follow-up error scenarios from https://github.com/open-feature/flagd/issues/1874 + # Operators must return null (not false) on error so the default variant is selected. + + @operator-errors @string + Scenario Outline: starts_with and ends_with return null for non-string input + Given a String-flag with key "" and a default value "wrong" + And a context containing a key "num", with type "Integer" and with value "123" + When the flag was evaluated with details + Then the resolved details value should be "fallback" + And the reason should be "DEFAULT" + Examples: + | key | + | starts-with-non-string-flag | + | ends-with-non-string-flag | + + @operator-errors @string + Scenario Outline: starts_with and ends_with return null for wrong argument count + Given a String-flag with key "" and a default value "wrong" + When the flag was evaluated with details + Then the resolved details value should be "fallback" + And the reason should be "DEFAULT" + Examples: + | key | + | starts-with-wrong-args-flag | + | ends-with-wrong-args-flag | + + @operator-errors @semver + Scenario: sem_ver returns null for wrong argument count + Given a String-flag with key "semver-wrong-args-flag" and a default value "wrong" + And a context containing a key "version", with type "String" and with value "1.0.0" + When the flag was evaluated with details + Then the resolved details value should be "fallback" + And the reason should be "DEFAULT" + + @operator-errors @fractional + Scenario: fractional with all-zero bucket weights falls back to default variant + Given a String-flag with key "fractional-zero-weights-flag" and a default value "wrong" + And a context containing a targeting key with value "any-user" + When the flag was evaluated with details + Then the resolved details value should be "fallback" + And the reason should be "DEFAULT" + + @operator-errors @fractional + Scenario: fractional negative bucket weight is clamped to zero + # ["one", -50] is treated as ["one", 0]; "two" gets 100% of the weight + Given a String-flag with key "fractional-negative-weight-flag" and a default value "wrong" + And a context containing a targeting key with value "any-user" + When the flag was evaluated with details + Then the resolved details value should be "two"