diff --git a/schema.json b/schema.json index 2a01f0e..f83e2ca 100644 --- a/schema.json +++ b/schema.json @@ -3,7 +3,7 @@ "type": "object", "properties": { "context": { - "$ref": "https://raw.githubusercontent.com/Flagsmith/flagsmith/refs/heads/main/sdk/evaluation-context.json" + "$ref": "https://raw.githubusercontent.com/Flagsmith/flagsmith/refs/heads/feat/dependent-flags-evaluation-context/sdk/evaluation-context.json" }, "result": { "$ref": "https://raw.githubusercontent.com/Flagsmith/flagsmith/refs/heads/main/sdk/evaluation-result.json" 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..a55891f --- /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/heads/test/flag-dependencies/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..e4185aa --- /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/heads/test/flag-dependencies/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..f7334f5 --- /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/heads/test/flag-dependencies/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..7f00707 --- /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/heads/test/flag-dependencies/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..67d31c2 --- /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/heads/test/flag-dependencies/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..c397065 --- /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/heads/test/flag-dependencies/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__multiple_overrides__each_feature_gets_its_own.jsonc b/test_cases/test_flag_dependency__multiple_overrides__each_feature_gets_its_own.jsonc new file mode 100644 index 0000000..450e161 --- /dev/null +++ b/test_cases/test_flag_dependency__multiple_overrides__each_feature_gets_its_own.jsonc @@ -0,0 +1,98 @@ +{ + // Given: One segment, conditioned on `prerequisite`, carrying overrides + // for two different features + // When: The context is evaluated with the dependency satisfied + // Then: Each feature takes its own override, and neither takes the other's + // + // NOTE: Resolving one feature has to select the override naming that + // feature, not merely the first the segment carries. + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/heads/test/flag-dependencies/schema.json", + "context": { + "environment": { + "key": "key", + "name": "Environment" + }, + "features": { + "prerequisite": { + "key": "1", + "name": "prerequisite", + "enabled": true, + "value": null + }, + "dependent_a": { + "key": "2", + "name": "dependent_a", + "enabled": false, + "value": "a_off" + }, + "dependent_b": { + "key": "3", + "name": "dependent_b", + "enabled": false, + "value": "b_off" + } + }, + "segments": { + "1": { + "key": "1", + "name": "dependency_with_two_overrides", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.prerequisite.enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "key": "2", + "name": "dependent_a", + "enabled": true, + "value": "a_on" + }, + { + "key": "3", + "name": "dependent_b", + "enabled": true, + "value": "b_on" + } + ] + } + } + }, + "result": { + "flags": { + "prerequisite": { + "enabled": true, + "name": "prerequisite", + "reason": "DEFAULT", + "value": null, + "variant": null + }, + "dependent_a": { + "enabled": true, + "name": "dependent_a", + "reason": "TARGETING_MATCH; segment=dependency_with_two_overrides", + "value": "a_on", + "variant": null + }, + "dependent_b": { + "enabled": true, + "name": "dependent_b", + "reason": "TARGETING_MATCH; segment=dependency_with_two_overrides", + "value": "b_on", + "variant": null + } + }, + "segments": [ + { + "name": "dependency_with_two_overrides" + } + ] + } +} 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..08597e6 --- /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/heads/test/flag-dependencies/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..81bd4b1 --- /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/heads/test/flag-dependencies/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..b2cf00f --- /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/heads/test/flag-dependencies/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..05e0d9b --- /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/heads/test/flag-dependencies/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..ae1cfb7 --- /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/heads/test/flag-dependencies/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__segment_metadata__reported_with_dependency.jsonc b/test_cases/test_flag_dependency__segment_metadata__reported_with_dependency.jsonc new file mode 100644 index 0000000..5671620 --- /dev/null +++ b/test_cases/test_flag_dependency__segment_metadata__reported_with_dependency.jsonc @@ -0,0 +1,86 @@ +{ + // Given: A segment carrying metadata, conditioned on another flag + // When: The context is evaluated with the dependency satisfied + // Then: The segment is reported with its metadata, as any segment is + // + // NOTE: Resolving dependencies must not drop the segment's metadata on + // the way to reporting membership. + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/heads/test/flag-dependencies/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_with_metadata", + "metadata": { + "id": 77, + "source": "api" + }, + "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_with_metadata", + "value": "on", + "variant": null + } + }, + "segments": [ + { + "name": "dependency_with_metadata", + "metadata": { + "id": 77, + "source": "api" + } + } + ] + } +} 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..1cc99e7 --- /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/heads/test/flag-dependencies/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..07e5f1b --- /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/heads/test/flag-dependencies/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..5f592ee --- /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/heads/test/flag-dependencies/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..b004628 --- /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/heads/test/flag-dependencies/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..d0ef745 --- /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/heads/test/flag-dependencies/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..8d815b6 --- /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/heads/test/flag-dependencies/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": [] + } +}