diff --git a/test_cases/test_flag_dependency__absent_prerequisite__should_not_override.jsonc b/test_cases/test_flag_dependency__absent_prerequisite__should_not_override.jsonc new file mode 100644 index 0000000..bbf8da2 --- /dev/null +++ b/test_cases/test_flag_dependency__absent_prerequisite__should_not_override.jsonc @@ -0,0 +1,61 @@ +{ + // Given: A segment conditioned on a feature that is not in the context + // When: The context is evaluated + // Then: The override is not applied, and evaluation does not fail + // + // NOTE: A dependency on an absent feature resolves to no value, as an + // unset property would. It is not an error. + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", + "context": { + "environment": { + "key": "key", + "name": "Environment" + }, + "features": { + "dependent": { + "key": "2", + "name": "dependent", + "enabled": false, + "value": "off" + } + }, + "segments": { + "1": { + "key": "1", + "name": "dependency_on_absent", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.nonexistent.enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "key": "2", + "name": "dependent", + "enabled": true, + "value": "on" + } + ] + } + } + }, + "result": { + "flags": { + "dependent": { + "enabled": false, + "name": "dependent", + "reason": "DEFAULT", + "value": "off", + "variant": null + } + }, + "segments": [] + } +} diff --git a/test_cases/test_flag_dependency__bracket_rooted_property__should_not_override.jsonc b/test_cases/test_flag_dependency__bracket_rooted_property__should_not_override.jsonc new file mode 100644 index 0000000..33c718d --- /dev/null +++ b/test_cases/test_flag_dependency__bracket_rooted_property__should_not_override.jsonc @@ -0,0 +1,79 @@ +{ + // Given: A segment overriding `dependent`, conditioned on a property that + // is a valid JSONPath query but is rooted with a bracket + // When: The context is evaluated with `prerequisite` enabled + // Then: No override is applied, and the segment does not match + // + // NOTE: Only a property prefixed `$.` is treated as a JSONPath query. A + // bracket-rooted query such as `$['flags']['prerequisite']` is a + // trait key, and as no such trait is set it resolves to no value. + // This is deliberate, and pinned here so that an implementation + // using a full JSONPath parser for the prefix test does not start + // honouring it and diverge. + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", + "context": { + "environment": { + "key": "key", + "name": "Environment" + }, + "features": { + "prerequisite": { + "key": "1", + "name": "prerequisite", + "enabled": true, + "value": null + }, + "dependent": { + "key": "2", + "name": "dependent", + "enabled": false, + "value": "off" + } + }, + "segments": { + "1": { + "key": "1", + "name": "dependency_on_prerequisite", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$['flags']['prerequisite'].enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "key": "2", + "name": "dependent", + "enabled": true, + "value": "on" + } + ] + } + } + }, + "result": { + "flags": { + "prerequisite": { + "enabled": true, + "name": "prerequisite", + "reason": "DEFAULT", + "value": null, + "variant": null + }, + "dependent": { + "enabled": false, + "name": "dependent", + "reason": "DEFAULT", + "value": "off", + "variant": null + } + }, + "segments": [] + } +} diff --git a/test_cases/test_flag_dependency__bracketed_field__should_override.jsonc b/test_cases/test_flag_dependency__bracketed_field__should_override.jsonc new file mode 100644 index 0000000..191e0de --- /dev/null +++ b/test_cases/test_flag_dependency__bracketed_field__should_override.jsonc @@ -0,0 +1,81 @@ +{ + // Given: A segment overriding `dependent`, conditioned on `prerequisite` + // with the *field* selected by a bracketed name rather than a dot + // When: The context is evaluated with `prerequisite` enabled + // Then: The dependency is satisfied, so `dependent` takes the override + // + // NOTE: `$.flags.prerequisite['enabled']` selects the same node as + // `$.flags.prerequisite.enabled`. An implementation that matches the + // property as text, rather than parsing it, is liable to miss this + // spelling and leave the dependency unresolved. + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", + "context": { + "environment": { + "key": "key", + "name": "Environment" + }, + "features": { + "prerequisite": { + "key": "1", + "name": "prerequisite", + "enabled": true, + "value": null + }, + "dependent": { + "key": "2", + "name": "dependent", + "enabled": false, + "value": "off" + } + }, + "segments": { + "1": { + "key": "1", + "name": "dependency_on_prerequisite", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.prerequisite['enabled']", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "key": "2", + "name": "dependent", + "enabled": true, + "value": "on" + } + ] + } + } + }, + "result": { + "flags": { + "prerequisite": { + "enabled": true, + "name": "prerequisite", + "reason": "DEFAULT", + "value": null, + "variant": null + }, + "dependent": { + "enabled": true, + "name": "dependent", + "reason": "TARGETING_MATCH; segment=dependency_on_prerequisite", + "value": "on", + "variant": null + } + }, + "segments": [ + { + "name": "dependency_on_prerequisite" + } + ] + } +} diff --git a/test_cases/test_flag_dependency__competing_overrides__lowest_priority_wins.jsonc b/test_cases/test_flag_dependency__competing_overrides__lowest_priority_wins.jsonc new file mode 100644 index 0000000..0cf2f87 --- /dev/null +++ b/test_cases/test_flag_dependency__competing_overrides__lowest_priority_wins.jsonc @@ -0,0 +1,108 @@ +{ + // Given: Two matching segments overriding `dependent`, both conditioned + // on `prerequisite`, with different override priorities + // When: The context is evaluated with the dependency satisfied + // Then: The lower priority number wins, as for any segment override + // + // NOTE: Resolving dependencies must not disturb override precedence, and + // in particular must not make it depend on resolution order. + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", + "context": { + "environment": { + "key": "key", + "name": "Environment" + }, + "features": { + "prerequisite": { + "key": "1", + "name": "prerequisite", + "enabled": true, + "value": null + }, + "dependent": { + "key": "2", + "name": "dependent", + "enabled": false, + "value": "off" + } + }, + "segments": { + "weak": { + "key": "weak", + "name": "weaker_dependency", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.prerequisite.enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "key": "2", + "name": "dependent", + "enabled": true, + "value": "weaker", + "priority": 10 + } + ] + }, + "strong": { + "key": "strong", + "name": "stronger_dependency", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.prerequisite.enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "key": "2", + "name": "dependent", + "enabled": true, + "value": "stronger", + "priority": 1 + } + ] + } + } + }, + "result": { + "flags": { + "prerequisite": { + "enabled": true, + "name": "prerequisite", + "reason": "DEFAULT", + "value": null, + "variant": null + }, + "dependent": { + "enabled": true, + "name": "dependent", + "reason": "TARGETING_MATCH; segment=stronger_dependency", + "value": "stronger", + "variant": null + } + }, + "segments": [ + { + "name": "weaker_dependency" + }, + { + "name": "stronger_dependency" + } + ] + } +} diff --git a/test_cases/test_flag_dependency__context_supplied_flags__should_be_ignored.jsonc b/test_cases/test_flag_dependency__context_supplied_flags__should_be_ignored.jsonc new file mode 100644 index 0000000..54552b1 --- /dev/null +++ b/test_cases/test_flag_dependency__context_supplied_flags__should_be_ignored.jsonc @@ -0,0 +1,87 @@ +{ + // Given: A context arriving with `$.flags` already populated, claiming that + // `prerequisite` is enabled, while the feature itself is disabled + // When: The context is evaluated + // Then: The supplied flags are discarded and `prerequisite` is evaluated from + // its feature context, so the dependency is not satisfied + // + // NOTE: `$.flags` is marked read-only in the context schema: it is populated + // by the engine as evaluation progresses. In remote and edge evaluation + // the context comes from the client, so an implementation that trusts a + // supplied `$.flags` would let a client satisfy its own dependencies. + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", + "context": { + "environment": { + "key": "key", + "name": "Environment" + }, + "features": { + "prerequisite": { + "enabled": false, + "key": "1", + "name": "prerequisite", + "value": null + }, + "dependent": { + "enabled": false, + "key": "2", + "name": "dependent", + "value": "off" + } + }, + "flags": { + "prerequisite": { + "enabled": true, + "name": "prerequisite", + "reason": "SUPPLIED_BY_CLIENT", + "value": null, + "variant": null + } + }, + "segments": { + "1": { + "key": "1", + "name": "dependency_on_prerequisite", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.prerequisite.enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "enabled": true, + "key": "2", + "name": "dependent", + "value": "on" + } + ] + } + } + }, + "result": { + "flags": { + "prerequisite": { + "enabled": false, + "name": "prerequisite", + "reason": "DEFAULT", + "value": null, + "variant": null + }, + "dependent": { + "enabled": false, + "name": "dependent", + "reason": "DEFAULT", + "value": "off", + "variant": null + } + }, + "segments": [] + } +} diff --git a/test_cases/test_flag_dependency__cyclic__should_not_override.jsonc b/test_cases/test_flag_dependency__cyclic__should_not_override.jsonc new file mode 100644 index 0000000..6ed494b --- /dev/null +++ b/test_cases/test_flag_dependency__cyclic__should_not_override.jsonc @@ -0,0 +1,106 @@ +{ + // Given: Two features whose dependencies form a cycle — the segment + // overriding `a` is conditioned on `b`, and the segment overriding + // `b` is conditioned on `a` + // When: The context is evaluated + // Then: Neither override is applied, and evaluation terminates normally + // + // NOTE: Cycles are expected to be rejected where dependencies are written, + // so this should be unreachable in practice. It is pinned here because + // a naive resolver recurses until it exhausts the stack. Both flags + // serve their environment default, reported as an error rather than as + // `DEFAULT` so that a flag which could not be resolved is + // distinguishable from one that was never gated. Only flags in the + // cycle are reported this way; a flag merely depending on one is not. + // The result does not depend on which flag is resolved first. + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", + "context": { + "environment": { + "key": "key", + "name": "Environment" + }, + "features": { + "a": { + "enabled": false, + "key": "1", + "name": "a", + "value": null + }, + "b": { + "enabled": false, + "key": "2", + "name": "b", + "value": null + } + }, + "segments": { + "a_on_b": { + "key": "a_on_b", + "name": "a_depends_on_b", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.b.enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "enabled": true, + "key": "1", + "name": "a", + "value": null + } + ] + }, + "b_on_a": { + "key": "b_on_a", + "name": "b_depends_on_a", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.a.enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "enabled": true, + "key": "2", + "name": "b", + "value": null + } + ] + } + } + }, + "result": { + "flags": { + "a": { + "enabled": false, + "name": "a", + "reason": "ERROR; code=CIRCULAR_DEPENDENCY", + "value": null, + "variant": null + }, + "b": { + "enabled": false, + "name": "b", + "reason": "ERROR; code=CIRCULAR_DEPENDENCY", + "value": null, + "variant": null + } + }, + "segments": [] + } +} diff --git a/test_cases/test_flag_dependency__cyclic_enabled_prerequisite__segment_should_not_match.jsonc b/test_cases/test_flag_dependency__cyclic_enabled_prerequisite__segment_should_not_match.jsonc new file mode 100644 index 0000000..b29b0fa --- /dev/null +++ b/test_cases/test_flag_dependency__cyclic_enabled_prerequisite__segment_should_not_match.jsonc @@ -0,0 +1,106 @@ +{ + // Given: Two features whose dependencies form a cycle, where one of them + // (`b`) is enabled by its environment default + // When: The context is evaluated + // Then: Neither override is applied and neither segment matches + // + // NOTE: This is the cycle that distinguishes implementations. In + // `cyclic__should_not_override` both flags are disabled by default, so + // cutting the cycle leaves nothing truthy for the other condition to + // match on and any implementation passes it. Here `b` is enabled by + // default: cutting the cycle leaves that value in place, and an + // implementation that publishes it for other conditions to read will + // match `a_requires_b` on it — reporting a context as belonging to a + // segment whose override was never applied. A flag resolved only by + // cutting a cycle must not be readable by another condition. + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", + "context": { + "environment": { + "key": "key", + "name": "Environment" + }, + "features": { + "a": { + "key": "1", + "name": "a", + "enabled": false, + "value": "a_default" + }, + "b": { + "key": "2", + "name": "b", + "enabled": true, + "value": "b_default" + } + }, + "segments": { + "a_on_b": { + "key": "a_on_b", + "name": "a_requires_b", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.b.enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "key": "1", + "name": "a", + "enabled": true, + "value": "a_override" + } + ] + }, + "b_on_a": { + "key": "b_on_a", + "name": "b_requires_a", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.a.enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "key": "2", + "name": "b", + "enabled": true, + "value": "b_override" + } + ] + } + } + }, + "result": { + "flags": { + "a": { + "enabled": false, + "name": "a", + "reason": "ERROR; code=CIRCULAR_DEPENDENCY", + "value": "a_default", + "variant": null + }, + "b": { + "enabled": true, + "name": "b", + "reason": "ERROR; code=CIRCULAR_DEPENDENCY", + "value": "b_default", + "variant": null + } + }, + "segments": [] + } +} diff --git a/test_cases/test_flag_dependency__cyclic_is_not_set__segment_should_not_match.jsonc b/test_cases/test_flag_dependency__cyclic_is_not_set__segment_should_not_match.jsonc new file mode 100644 index 0000000..ee69a3b --- /dev/null +++ b/test_cases/test_flag_dependency__cyclic_is_not_set__segment_should_not_match.jsonc @@ -0,0 +1,66 @@ +{ + // Given: A segment overriding `a`, matching when `a` is not set — a cycle, + // since resolving `a` requires evaluating this segment + // When: The context is evaluated + // Then: The override is not applied and the segment does not match + // + // NOTE: `IS_NOT_SET` is the one operator that matches on a flag which could + // not be resolved: every other operator fails against no value. So a + // cycle can make a condition true, and the override it guards would + // apply *because* the cycle was cut. A segment whose evaluation cut a + // cycle read a flag that has no value, so neither its match nor its + // failure to match is sound, and it must not be applied either way. + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", + "context": { + "environment": { + "key": "key", + "name": "Environment" + }, + "features": { + "a": { + "key": "1", + "name": "a", + "enabled": false, + "value": "a_default" + } + }, + "segments": { + "1": { + "key": "1", + "name": "a_requires_a_unset", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "IS_NOT_SET", + "property": "$.flags.a.enabled", + "value": "" + } + ] + } + ], + "overrides": [ + { + "key": "1", + "name": "a", + "enabled": true, + "value": "a_override" + } + ] + } + } + }, + "result": { + "flags": { + "a": { + "enabled": false, + "name": "a", + "reason": "ERROR; code=CIRCULAR_DEPENDENCY", + "value": "a_default", + "variant": null + } + }, + "segments": [] + } +} diff --git a/test_cases/test_flag_dependency__nested_rule__should_override.jsonc b/test_cases/test_flag_dependency__nested_rule__should_override.jsonc new file mode 100644 index 0000000..f0cef68 --- /dev/null +++ b/test_cases/test_flag_dependency__nested_rule__should_override.jsonc @@ -0,0 +1,98 @@ +{ + // Given: A segment whose flag condition sits in a nested rule group, + // alongside a trait condition in the top-level group + // When: The context is evaluated with both satisfied + // Then: The dependency is satisfied, so `dependent` takes the override + // + // NOTE: Dependencies have to be found in nested rules, not just top-level + // conditions, or the flag is read before it has been resolved. + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", + "context": { + "environment": { + "key": "key", + "name": "Environment" + }, + "identity": { + "identifier": "nested_user", + "key": "key_nested_user", + "traits": { + "tier": "gold" + } + }, + "features": { + "prerequisite": { + "key": "1", + "name": "prerequisite", + "enabled": true, + "value": null + }, + "dependent": { + "key": "2", + "name": "dependent", + "enabled": false, + "value": "off" + } + }, + "segments": { + "1": { + "key": "1", + "name": "dependency_in_nested_rule", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "tier", + "value": "gold" + } + ], + "rules": [ + { + "type": "ANY", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.prerequisite.enabled", + "value": "true" + } + ] + } + ] + } + ], + "overrides": [ + { + "key": "2", + "name": "dependent", + "enabled": true, + "value": "on" + } + ] + } + } + }, + "result": { + "flags": { + "prerequisite": { + "enabled": true, + "name": "prerequisite", + "reason": "DEFAULT", + "value": null, + "variant": null + }, + "dependent": { + "enabled": true, + "name": "dependent", + "reason": "TARGETING_MATCH; segment=dependency_in_nested_rule", + "value": "on", + "variant": null + } + }, + "segments": [ + { + "name": "dependency_in_nested_rule" + } + ] + } +} diff --git a/test_cases/test_flag_dependency__no_overrides__segment_still_reported.jsonc b/test_cases/test_flag_dependency__no_overrides__segment_still_reported.jsonc new file mode 100644 index 0000000..94850ca --- /dev/null +++ b/test_cases/test_flag_dependency__no_overrides__segment_still_reported.jsonc @@ -0,0 +1,59 @@ +{ + // Given: A segment conditioned on a flag but overriding nothing + // When: The context is evaluated with the dependency satisfied + // Then: The segment is reported as matched + // + // NOTE: Such a segment still has to be evaluated. An implementation that + // only resolves dependencies for segments carrying overrides would + // report no segment membership here. + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", + "context": { + "environment": { + "key": "key", + "name": "Environment" + }, + "features": { + "prerequisite": { + "key": "1", + "name": "prerequisite", + "enabled": true, + "value": null + } + }, + "segments": { + "1": { + "key": "1", + "name": "dependency_without_overrides", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.prerequisite.enabled", + "value": "true" + } + ] + } + ], + "overrides": [] + } + } + }, + "result": { + "flags": { + "prerequisite": { + "enabled": true, + "name": "prerequisite", + "reason": "DEFAULT", + "value": null, + "variant": null + } + }, + "segments": [ + { + "name": "dependency_without_overrides" + } + ] + } +} diff --git a/test_cases/test_flag_dependency__prerequisite_disabled__should_not_override.jsonc b/test_cases/test_flag_dependency__prerequisite_disabled__should_not_override.jsonc new file mode 100644 index 0000000..2b4ec34 --- /dev/null +++ b/test_cases/test_flag_dependency__prerequisite_disabled__should_not_override.jsonc @@ -0,0 +1,73 @@ +{ + // Given: A feature `dependent`, and a segment overriding it whose only + // condition is on the result of another feature, `prerequisite` + // When: A context is evaluated in which `prerequisite` is disabled + // Then: The dependency is not satisfied, so `dependent` keeps its + // environment default and the segment does not match + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", + "context": { + "environment": { + "key": "key", + "name": "Environment" + }, + "features": { + "prerequisite": { + "enabled": false, + "key": "1", + "name": "prerequisite", + "value": null + }, + "dependent": { + "enabled": false, + "key": "2", + "name": "dependent", + "value": "off" + } + }, + "segments": { + "1": { + "key": "1", + "name": "dependency_on_prerequisite", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.prerequisite.enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "enabled": true, + "key": "2", + "name": "dependent", + "value": "on" + } + ] + } + } + }, + "result": { + "flags": { + "prerequisite": { + "enabled": false, + "name": "prerequisite", + "reason": "DEFAULT", + "value": null, + "variant": null + }, + "dependent": { + "enabled": false, + "name": "dependent", + "reason": "DEFAULT", + "value": "off", + "variant": null + } + }, + "segments": [] + } +} diff --git a/test_cases/test_flag_dependency__prerequisite_enabled__should_override.jsonc b/test_cases/test_flag_dependency__prerequisite_enabled__should_override.jsonc new file mode 100644 index 0000000..b8e68c6 --- /dev/null +++ b/test_cases/test_flag_dependency__prerequisite_enabled__should_override.jsonc @@ -0,0 +1,81 @@ +{ + // Given: A feature `dependent`, and a segment overriding it whose only + // condition is on the result of another feature, `prerequisite`, + // by way of the `$.flags..enabled` property + // When: A context is evaluated in which `prerequisite` is enabled + // Then: The dependency is satisfied, so `dependent` takes the override + // + // NOTE: This requires resolving `prerequisite` before evaluating the segment. + // Implementations that evaluate all segments before any flag will not + // see a value at `$.flags.prerequisite.enabled` and so will fail here. + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", + "context": { + "environment": { + "key": "key", + "name": "Environment" + }, + "features": { + "prerequisite": { + "enabled": true, + "key": "1", + "name": "prerequisite", + "value": null + }, + "dependent": { + "enabled": false, + "key": "2", + "name": "dependent", + "value": "off" + } + }, + "segments": { + "1": { + "key": "1", + "name": "dependency_on_prerequisite", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.prerequisite.enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "enabled": true, + "key": "2", + "name": "dependent", + "value": "on" + } + ] + } + } + }, + "result": { + "flags": { + "prerequisite": { + "enabled": true, + "name": "prerequisite", + "reason": "DEFAULT", + "value": null, + "variant": null + }, + "dependent": { + "enabled": true, + "name": "dependent", + "reason": "TARGETING_MATCH; segment=dependency_on_prerequisite", + "value": "on", + "variant": null + } + }, + "segments": [ + { + "name": "dependency_on_prerequisite" + } + ] + } +} diff --git a/test_cases/test_flag_dependency__quoted_feature_name__should_override.jsonc b/test_cases/test_flag_dependency__quoted_feature_name__should_override.jsonc new file mode 100644 index 0000000..7c26f84 --- /dev/null +++ b/test_cases/test_flag_dependency__quoted_feature_name__should_override.jsonc @@ -0,0 +1,79 @@ +{ + // Given: A feature whose name is not a bare JSONPath identifier, and a + // segment conditioned on it using a quoted name selector + // When: The context is evaluated with that feature enabled + // Then: The dependency is satisfied, so `dependent` takes the override + // + // NOTE: Feature names may contain spaces, so the quoted form has to be + // supported; a dotted-only implementation cannot express this. + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", + "context": { + "environment": { + "key": "key", + "name": "Environment" + }, + "features": { + "my feature": { + "key": "1", + "name": "my feature", + "enabled": true, + "value": null + }, + "dependent": { + "key": "2", + "name": "dependent", + "enabled": false, + "value": "off" + } + }, + "segments": { + "1": { + "key": "1", + "name": "dependency_on_my_feature", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags['my feature'].enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "key": "2", + "name": "dependent", + "enabled": true, + "value": "on" + } + ] + } + } + }, + "result": { + "flags": { + "my feature": { + "enabled": true, + "name": "my feature", + "reason": "DEFAULT", + "value": null, + "variant": null + }, + "dependent": { + "enabled": true, + "name": "dependent", + "reason": "TARGETING_MATCH; segment=dependency_on_my_feature", + "value": "on", + "variant": null + } + }, + "segments": [ + { + "name": "dependency_on_my_feature" + } + ] + } +} diff --git a/test_cases/test_flag_dependency__shared_segment__resolved_once_per_feature.jsonc b/test_cases/test_flag_dependency__shared_segment__resolved_once_per_feature.jsonc new file mode 100644 index 0000000..338e2d4 --- /dev/null +++ b/test_cases/test_flag_dependency__shared_segment__resolved_once_per_feature.jsonc @@ -0,0 +1,181 @@ +{ + // Given: One segment gating two features on the same flag, and two further + // segments each depending on one of those two features + // When: The context is evaluated with the root flag enabled + // Then: Both gated features and both leaves take their overrides + // + // NOTE: The shared segment is reached twice while resolving, once per + // feature depending on it, and each time has to select the override + // naming the feature being resolved rather than its first. A diamond + // rather than a chain: `root` fans out and the two branches rejoin + // only in the shared segment's verdict. + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", + "context": { + "environment": { + "key": "key", + "name": "Environment" + }, + "features": { + "root": { + "key": "1", + "name": "root", + "enabled": true, + "value": null + }, + "shared_a": { + "key": "2", + "name": "shared_a", + "enabled": false, + "value": null + }, + "shared_b": { + "key": "3", + "name": "shared_b", + "enabled": false, + "value": null + }, + "leaf_of_a": { + "key": "4", + "name": "leaf_of_a", + "enabled": false, + "value": null + }, + "leaf_of_b": { + "key": "5", + "name": "leaf_of_b", + "enabled": false, + "value": null + } + }, + "segments": { + "shared": { + "key": "shared", + "name": "shared_dependency", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.root.enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "key": "2", + "name": "shared_a", + "enabled": true, + "value": "a_on" + }, + { + "key": "3", + "name": "shared_b", + "enabled": true, + "value": "b_on" + } + ] + }, + "on_a": { + "key": "on_a", + "name": "depends_on_shared_a", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.shared_a.enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "key": "4", + "name": "leaf_of_a", + "enabled": true, + "value": "leaf_a_on" + } + ] + }, + "on_b": { + "key": "on_b", + "name": "depends_on_shared_b", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.shared_b.enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "key": "5", + "name": "leaf_of_b", + "enabled": true, + "value": "leaf_b_on" + } + ] + } + } + }, + "result": { + "flags": { + "root": { + "enabled": true, + "name": "root", + "reason": "DEFAULT", + "value": null, + "variant": null + }, + "shared_a": { + "enabled": true, + "name": "shared_a", + "reason": "TARGETING_MATCH; segment=shared_dependency", + "value": "a_on", + "variant": null + }, + "shared_b": { + "enabled": true, + "name": "shared_b", + "reason": "TARGETING_MATCH; segment=shared_dependency", + "value": "b_on", + "variant": null + }, + "leaf_of_a": { + "enabled": true, + "name": "leaf_of_a", + "reason": "TARGETING_MATCH; segment=depends_on_shared_a", + "value": "leaf_a_on", + "variant": null + }, + "leaf_of_b": { + "enabled": true, + "name": "leaf_of_b", + "reason": "TARGETING_MATCH; segment=depends_on_shared_b", + "value": "leaf_b_on", + "variant": null + } + }, + "segments": [ + { + "name": "shared_dependency" + }, + { + "name": "depends_on_shared_a" + }, + { + "name": "depends_on_shared_b" + } + ] + } +} diff --git a/test_cases/test_flag_dependency__transitive__should_cascade.jsonc b/test_cases/test_flag_dependency__transitive__should_cascade.jsonc new file mode 100644 index 0000000..1afa893 --- /dev/null +++ b/test_cases/test_flag_dependency__transitive__should_cascade.jsonc @@ -0,0 +1,120 @@ +{ + // Given: Three features, where `c` depends on `b` and `b` depends on `a`, + // declared in the context in reverse dependency order (c, b, a) + // When: A context is evaluated in which `a` is enabled + // Then: Enabling `a` cascades, so both `b` and `c` take their overrides + // + // NOTE: The declaration order is deliberately the reverse of the resolution + // order. Implementations that resolve flags in context order without + // following dependencies first will fail to enable `c`. + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", + "context": { + "environment": { + "key": "key", + "name": "Environment" + }, + "features": { + "c": { + "enabled": false, + "key": "3", + "name": "c", + "value": null + }, + "b": { + "enabled": false, + "key": "2", + "name": "b", + "value": null + }, + "a": { + "enabled": true, + "key": "1", + "name": "a", + "value": null + } + }, + "segments": { + "b_on_a": { + "key": "b_on_a", + "name": "b_depends_on_a", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.a.enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "enabled": true, + "key": "2", + "name": "b", + "value": "b_on" + } + ] + }, + "c_on_b": { + "key": "c_on_b", + "name": "c_depends_on_b", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.b.enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "enabled": true, + "key": "3", + "name": "c", + "value": "c_on" + } + ] + } + } + }, + "result": { + "flags": { + "c": { + "enabled": true, + "name": "c", + "reason": "TARGETING_MATCH; segment=c_depends_on_b", + "value": "c_on", + "variant": null + }, + "b": { + "enabled": true, + "name": "b", + "reason": "TARGETING_MATCH; segment=b_depends_on_a", + "value": "b_on", + "variant": null + }, + "a": { + "enabled": true, + "name": "a", + "reason": "DEFAULT", + "value": null, + "variant": null + } + }, + "segments": [ + { + "name": "b_depends_on_a" + }, + { + "name": "c_depends_on_b" + } + ] + } +} diff --git a/test_cases/test_flag_dependency__transitive_unmet__should_not_cascade.jsonc b/test_cases/test_flag_dependency__transitive_unmet__should_not_cascade.jsonc new file mode 100644 index 0000000..759797f --- /dev/null +++ b/test_cases/test_flag_dependency__transitive_unmet__should_not_cascade.jsonc @@ -0,0 +1,112 @@ +{ + // Given: Three features, where `c` depends on `b` and `b` depends on `a` + // When: The context is evaluated with `a` disabled + // Then: Neither `b` nor `c` takes its override + // + // NOTE: The counterpart to the cascading case. An implementation that + // applies an override on a merely *present* dependency, rather than + // a satisfied one, passes that case and fails this one. + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", + "context": { + "environment": { + "key": "key", + "name": "Environment" + }, + "features": { + "c": { + "key": "3", + "name": "c", + "enabled": false, + "value": null + }, + "b": { + "key": "2", + "name": "b", + "enabled": false, + "value": null + }, + "a": { + "key": "1", + "name": "a", + "enabled": false, + "value": null + } + }, + "segments": { + "b_on_a": { + "key": "b_on_a", + "name": "b_depends_on_a", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.a.enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "key": "2", + "name": "b", + "enabled": true, + "value": "b_on" + } + ] + }, + "c_on_b": { + "key": "c_on_b", + "name": "c_depends_on_b", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.b.enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "key": "3", + "name": "c", + "enabled": true, + "value": "c_on" + } + ] + } + } + }, + "result": { + "flags": { + "a": { + "enabled": false, + "name": "a", + "reason": "DEFAULT", + "value": null, + "variant": null + }, + "b": { + "enabled": false, + "name": "b", + "reason": "DEFAULT", + "value": null, + "variant": null + }, + "c": { + "enabled": false, + "name": "c", + "reason": "DEFAULT", + "value": null, + "variant": null + } + }, + "segments": [] + } +} diff --git a/test_cases/test_flag_dependency__value__should_override.jsonc b/test_cases/test_flag_dependency__value__should_override.jsonc new file mode 100644 index 0000000..be00319 --- /dev/null +++ b/test_cases/test_flag_dependency__value__should_override.jsonc @@ -0,0 +1,76 @@ +{ + // Given: A segment overriding `dependent`, conditioned on the *value* of + // another feature rather than on whether it is enabled + // When: A context is evaluated in which `prerequisite` has the value "blue" + // Then: The dependency is satisfied, so `dependent` takes the override + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", + "context": { + "environment": { + "key": "key", + "name": "Environment" + }, + "features": { + "prerequisite": { + "enabled": true, + "key": "1", + "name": "prerequisite", + "value": "blue" + }, + "dependent": { + "enabled": false, + "key": "2", + "name": "dependent", + "value": null + } + }, + "segments": { + "1": { + "key": "1", + "name": "dependency_on_value", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.prerequisite.value", + "value": "blue" + } + ] + } + ], + "overrides": [ + { + "enabled": true, + "key": "2", + "name": "dependent", + "value": null + } + ] + } + } + }, + "result": { + "flags": { + "prerequisite": { + "enabled": true, + "name": "prerequisite", + "reason": "DEFAULT", + "value": "blue", + "variant": null + }, + "dependent": { + "enabled": true, + "name": "dependent", + "reason": "TARGETING_MATCH; segment=dependency_on_value", + "value": null, + "variant": null + } + }, + "segments": [ + { + "name": "dependency_on_value" + } + ] + } +} diff --git a/test_cases/test_flag_dependency__variant__should_override.jsonc b/test_cases/test_flag_dependency__variant__should_override.jsonc new file mode 100644 index 0000000..73b8172 --- /dev/null +++ b/test_cases/test_flag_dependency__variant__should_override.jsonc @@ -0,0 +1,93 @@ +{ + // Given: A multivariate `prerequisite` with a single full-weight keyed + // variant, and a segment conditioned on the variant selected + // When: An identity context is evaluated + // Then: `prerequisite` resolves to that variant, satisfying the + // dependency, so `dependent` takes the override + // + // NOTE: A dependency may be conditioned on any field of a flag result, + // not only `enabled`. The single 100% weight makes the bucketing + // deterministic, so no assumption is made about the hashing. + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", + "context": { + "environment": { + "key": "key", + "name": "Environment" + }, + "identity": { + "identifier": "variant_user", + "key": "key_variant_user" + }, + "features": { + "prerequisite": { + "key": "1", + "name": "prerequisite", + "enabled": true, + "value": null, + "variants": [ + { + "key": "treatment", + "value": "on", + "weight": 100, + "priority": 1 + } + ] + }, + "dependent": { + "key": "2", + "name": "dependent", + "enabled": false, + "value": "off" + } + }, + "segments": { + "1": { + "key": "1", + "name": "dependency_on_variant", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.prerequisite.variant", + "value": "treatment" + } + ] + } + ], + "overrides": [ + { + "key": "2", + "name": "dependent", + "enabled": true, + "value": "on" + } + ] + } + } + }, + "result": { + "flags": { + "prerequisite": { + "enabled": true, + "name": "prerequisite", + "reason": "SPLIT; weight=100", + "value": "on", + "variant": "treatment" + }, + "dependent": { + "enabled": true, + "name": "dependent", + "reason": "TARGETING_MATCH; segment=dependency_on_variant", + "value": "on", + "variant": null + } + }, + "segments": [ + { + "name": "dependency_on_variant" + } + ] + } +} diff --git a/test_cases/test_flag_dependency__wildcard_property__should_not_override.jsonc b/test_cases/test_flag_dependency__wildcard_property__should_not_override.jsonc new file mode 100644 index 0000000..ffed0a1 --- /dev/null +++ b/test_cases/test_flag_dependency__wildcard_property__should_not_override.jsonc @@ -0,0 +1,79 @@ +{ + // Given: A segment overriding `dependent`, conditioned on a wildcard + // query over the flags mapping rather than on a single flag + // When: The context is evaluated with every flag disabled + // Then: The override is not applied + // + // NOTE: Conditions resolve to a scalar, so a query that may select more + // than one node is unsupported: it yields whichever node is found + // first. Every flag here is disabled, so the condition fails + // whichever is selected, and whether or not an implementation + // treats such a query as a dependency at all. What such a query + // resolves to when flags differ is deliberately not pinned. + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", + "context": { + "environment": { + "key": "key", + "name": "Environment" + }, + "features": { + "prerequisite": { + "key": "1", + "name": "prerequisite", + "enabled": false, + "value": null + }, + "dependent": { + "key": "2", + "name": "dependent", + "enabled": false, + "value": "off" + } + }, + "segments": { + "1": { + "key": "1", + "name": "wildcard_dependency", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.*.enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "key": "2", + "name": "dependent", + "enabled": true, + "value": "on" + } + ] + } + } + }, + "result": { + "flags": { + "prerequisite": { + "enabled": false, + "name": "prerequisite", + "reason": "DEFAULT", + "value": null, + "variant": null + }, + "dependent": { + "enabled": false, + "name": "dependent", + "reason": "DEFAULT", + "value": "off", + "variant": null + } + }, + "segments": [] + } +}