From 00de9d388e2687ffbc8ddddc330c50add22616fb Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 27 Aug 2026 17:13:04 +0100 Subject: [PATCH 1/6] feat: add flag dependency test cases Covers segments conditioned on another flag's result via a `$.flags.` condition property, as used by dependent flags: - a dependency satisfied, and the same dependency unsatisfied - a dependency on a flag's value rather than on whether it is enabled - a transitive chain, declared in reverse dependency order so that resolving flags in context order is not enough to pass - a cycle, which must terminate rather than recurse. Both flags resolve to their defaults whichever is resolved first, so the expectation does not depend on iteration order - a context arriving with `$.flags` already populated, which must be discarded rather than allowed to satisfy its own dependency --- ...xt_supplied_flags__should_be_ignored.jsonc | 87 +++++++++++++ ...endency__cyclic__should_not_override.jsonc | 103 +++++++++++++++ ...uisite_disabled__should_not_override.jsonc | 73 +++++++++++ ...rerequisite_enabled__should_override.jsonc | 81 ++++++++++++ ...pendency__transitive__should_cascade.jsonc | 120 ++++++++++++++++++ ...g_dependency__value__should_override.jsonc | 76 +++++++++++ 6 files changed, 540 insertions(+) create mode 100644 test_cases/test_flag_dependency__context_supplied_flags__should_be_ignored.jsonc create mode 100644 test_cases/test_flag_dependency__cyclic__should_not_override.jsonc create mode 100644 test_cases/test_flag_dependency__prerequisite_disabled__should_not_override.jsonc create mode 100644 test_cases/test_flag_dependency__prerequisite_enabled__should_override.jsonc create mode 100644 test_cases/test_flag_dependency__transitive__should_cascade.jsonc create mode 100644 test_cases/test_flag_dependency__value__should_override.jsonc 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..a19d539 --- /dev/null +++ b/test_cases/test_flag_dependency__cyclic__should_not_override.jsonc @@ -0,0 +1,103 @@ +{ + // 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 + // resolve to their defaults regardless of which is resolved first, so + // the expected result does not depend on iteration order. + "$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": "DEFAULT", + "value": null, + "variant": null + }, + "b": { + "enabled": false, + "name": "b", + "reason": "DEFAULT", + "value": null, + "variant": null + } + }, + "segments": [] + } +} 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__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__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" + } + ] + } +} From e8e1f686d581a7d403b0bb1931c0cb73fcf72751 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 27 Aug 2026 18:10:45 +0100 Subject: [PATCH 2/6] chore: point schemas at the in-flight dependent flags branches REVERT BEFORE MERGE. `EvaluationContext.flags` only exists on the schema branch of Flagsmith/flagsmith#8396, so validating the new test cases against `refs/heads/main` silently proves nothing: the key is simply unknown to the schema, and `EvaluationContext` doesn't set `additionalProperties` to false, so anything at all passes. Points `schema.json` at the context schema on that branch, and the new test cases at `schema.json` on this one, so that validation is meaningful while both are in review. With this, `check-jsonschema` rejects e.g. a non-boolean `$.context.flags..enabled`, which it accepted before. Once #8396 is merged, both refs should go back to `main` (and the test cases to a tag, in line with the rest of the corpus). --- schema.json | 2 +- ..._dependency__context_supplied_flags__should_be_ignored.jsonc | 2 +- .../test_flag_dependency__cyclic__should_not_override.jsonc | 2 +- ...dependency__prerequisite_disabled__should_not_override.jsonc | 2 +- ...flag_dependency__prerequisite_enabled__should_override.jsonc | 2 +- .../test_flag_dependency__transitive__should_cascade.jsonc | 2 +- test_cases/test_flag_dependency__value__should_override.jsonc | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) 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__context_supplied_flags__should_be_ignored.jsonc b/test_cases/test_flag_dependency__context_supplied_flags__should_be_ignored.jsonc index 54552b1..67d31c2 100644 --- 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 @@ -9,7 +9,7 @@ // 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", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/heads/test/flag-dependencies/schema.json", "context": { "environment": { "key": "key", diff --git a/test_cases/test_flag_dependency__cyclic__should_not_override.jsonc b/test_cases/test_flag_dependency__cyclic__should_not_override.jsonc index a19d539..0aed65f 100644 --- a/test_cases/test_flag_dependency__cyclic__should_not_override.jsonc +++ b/test_cases/test_flag_dependency__cyclic__should_not_override.jsonc @@ -10,7 +10,7 @@ // a naive resolver recurses until it exhausts the stack. Both flags // resolve to their defaults regardless of which is resolved first, so // the expected result does not depend on iteration order. - "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/heads/test/flag-dependencies/schema.json", "context": { "environment": { "key": "key", 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 index 2b4ec34..b2cf00f 100644 --- a/test_cases/test_flag_dependency__prerequisite_disabled__should_not_override.jsonc +++ b/test_cases/test_flag_dependency__prerequisite_disabled__should_not_override.jsonc @@ -4,7 +4,7 @@ // 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", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/heads/test/flag-dependencies/schema.json", "context": { "environment": { "key": "key", diff --git a/test_cases/test_flag_dependency__prerequisite_enabled__should_override.jsonc b/test_cases/test_flag_dependency__prerequisite_enabled__should_override.jsonc index b8e68c6..05e0d9b 100644 --- a/test_cases/test_flag_dependency__prerequisite_enabled__should_override.jsonc +++ b/test_cases/test_flag_dependency__prerequisite_enabled__should_override.jsonc @@ -8,7 +8,7 @@ // 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", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/heads/test/flag-dependencies/schema.json", "context": { "environment": { "key": "key", diff --git a/test_cases/test_flag_dependency__transitive__should_cascade.jsonc b/test_cases/test_flag_dependency__transitive__should_cascade.jsonc index 1afa893..07e5f1b 100644 --- a/test_cases/test_flag_dependency__transitive__should_cascade.jsonc +++ b/test_cases/test_flag_dependency__transitive__should_cascade.jsonc @@ -7,7 +7,7 @@ // 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", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/heads/test/flag-dependencies/schema.json", "context": { "environment": { "key": "key", diff --git a/test_cases/test_flag_dependency__value__should_override.jsonc b/test_cases/test_flag_dependency__value__should_override.jsonc index be00319..b004628 100644 --- a/test_cases/test_flag_dependency__value__should_override.jsonc +++ b/test_cases/test_flag_dependency__value__should_override.jsonc @@ -3,7 +3,7 @@ // 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", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/heads/test/flag-dependencies/schema.json", "context": { "environment": { "key": "key", From f9b1abc5f4148b414b11f1e7702a0b1d6fa7a7e5 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 27 Aug 2026 18:49:09 +0100 Subject: [PATCH 3/6] feat: add flag dependency test cases for spellings, nesting and priority Extends the dependent flags coverage with cases that were previously carried as unit tests in flagsmith-engine, and so proved nothing about any other implementation: - `$.flags.a['enabled']` and `$.flags['my feature'].enabled`, two spellings an implementation matching the property as text rather than parsing it is liable to miss - `$['flags']['a'].enabled`, which is deliberately *not* a dependency: only a `$.`-prefixed property is a JSONPath query, and a bracket-rooted one is a trait key - a dependency in a nested rule group rather than a top-level condition - a dependency on `variant` rather than `enabled` - a transitive chain whose root is disabled, the counterpart to the cascading case - a dependency on a feature absent from the context - competing overrides, to pin that resolving dependencies doesn't disturb override precedence - a segment conditioned on a flag but overriding nothing, which still has to be evaluated for its membership to be reported Expected results were generated by running the engine, as in #57. --- ...nt_prerequisite__should_not_override.jsonc | 61 ++++++++++ ...rooted_property__should_not_override.jsonc | 79 ++++++++++++ ...cy__bracketed_field__should_override.jsonc | 81 +++++++++++++ ...ting_overrides__lowest_priority_wins.jsonc | 108 +++++++++++++++++ ...ndency__nested_rule__should_override.jsonc | 98 +++++++++++++++ ...no_overrides__segment_still_reported.jsonc | 59 +++++++++ ...quoted_feature_name__should_override.jsonc | 79 ++++++++++++ ...transitive_unmet__should_not_cascade.jsonc | 112 ++++++++++++++++++ ...dependency__variant__should_override.jsonc | 93 +++++++++++++++ 9 files changed, 770 insertions(+) create mode 100644 test_cases/test_flag_dependency__absent_prerequisite__should_not_override.jsonc create mode 100644 test_cases/test_flag_dependency__bracket_rooted_property__should_not_override.jsonc create mode 100644 test_cases/test_flag_dependency__bracketed_field__should_override.jsonc create mode 100644 test_cases/test_flag_dependency__competing_overrides__lowest_priority_wins.jsonc create mode 100644 test_cases/test_flag_dependency__nested_rule__should_override.jsonc create mode 100644 test_cases/test_flag_dependency__no_overrides__segment_still_reported.jsonc create mode 100644 test_cases/test_flag_dependency__quoted_feature_name__should_override.jsonc create mode 100644 test_cases/test_flag_dependency__transitive_unmet__should_not_cascade.jsonc create mode 100644 test_cases/test_flag_dependency__variant__should_override.jsonc 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__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__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__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__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" + } + ] + } +} From 2db3b504f61a10827449f68d12971bd16baac11b Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 27 Aug 2026 19:04:10 +0100 Subject: [PATCH 4/6] feat: add flag dependency cases for metadata, multiple overrides and wildcards Three further cases, each covering a path the existing ones leave untested in a reference implementation: - a dependency segment carrying metadata, which has to survive resolution and be reported as any segment's metadata is - one segment carrying overrides for two features, so that resolving one feature selects the override naming it rather than the segment's first - a wildcard query over the flags mapping, which is unsupported because conditions resolve to a scalar. Every flag in that case is disabled, so the condition fails whichever node the query happens to select, and the expectation holds whether or not an implementation treats such a query as a dependency. What it resolves to when flags differ is deliberately left unpinned. --- ...overrides__each_feature_gets_its_own.jsonc | 98 +++++++++++++++++++ ...t_metadata__reported_with_dependency.jsonc | 86 ++++++++++++++++ ...ldcard_property__should_not_override.jsonc | 79 +++++++++++++++ 3 files changed, 263 insertions(+) create mode 100644 test_cases/test_flag_dependency__multiple_overrides__each_feature_gets_its_own.jsonc create mode 100644 test_cases/test_flag_dependency__segment_metadata__reported_with_dependency.jsonc create mode 100644 test_cases/test_flag_dependency__wildcard_property__should_not_override.jsonc 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__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__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": [] + } +} From 9d17cc57944cde403a970e202cdf37f747647bba Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 27 Aug 2026 19:30:38 +0100 Subject: [PATCH 5/6] feat: add a shared dependency segment test case One segment gating two features on the same flag, with two further segments each depending on one of those features. A diamond rather than a chain, so the shared segment is reached twice while resolving, once per feature depending on it. Guards two things a chain doesn't reach: reusing a segment's verdict for the second feature, and selecting the override naming the feature being resolved rather than the segment's first override. --- ...d_segment__resolved_once_per_feature.jsonc | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 test_cases/test_flag_dependency__shared_segment__resolved_once_per_feature.jsonc 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" + } + ] + } +} From 30e30f2b81b144c7e188dfa03228e3c574383476 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Fri, 28 Aug 2026 18:15:32 +0100 Subject: [PATCH 6/6] feat: report a circular dependency as an error reason A flag whose dependencies form a cycle serves its environment default, which was previously indistinguishable from a flag that was never gated at all. Report `ERROR; code=CIRCULAR_DEPENDENCY` instead, so the condition that could not be evaluated is visible to whoever is looking at the result. Only flags in the cycle are reported this way. A flag merely depending on one resolves normally against whatever the cycle settled on. --- ...flag_dependency__cyclic__should_not_override.jsonc | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test_cases/test_flag_dependency__cyclic__should_not_override.jsonc b/test_cases/test_flag_dependency__cyclic__should_not_override.jsonc index 0aed65f..c397065 100644 --- a/test_cases/test_flag_dependency__cyclic__should_not_override.jsonc +++ b/test_cases/test_flag_dependency__cyclic__should_not_override.jsonc @@ -8,8 +8,11 @@ // 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 - // resolve to their defaults regardless of which is resolved first, so - // the expected result does not depend on iteration order. + // 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": { @@ -86,14 +89,14 @@ "a": { "enabled": false, "name": "a", - "reason": "DEFAULT", + "reason": "ERROR; code=CIRCULAR_DEPENDENCY", "value": null, "variant": null }, "b": { "enabled": false, "name": "b", - "reason": "DEFAULT", + "reason": "ERROR; code=CIRCULAR_DEPENDENCY", "value": null, "variant": null }