From 00de9d388e2687ffbc8ddddc330c50add22616fb Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 27 Aug 2026 17:13:04 +0100 Subject: [PATCH 01/13] 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 02/13] 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 03/13] 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 04/13] 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 05/13] 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 06/13] 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 } From 805b3537d8f68f235fa3d5450c3606d345ba1551 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 31 Aug 2026 12:43:57 +0100 Subject: [PATCH 07/13] feat: add a cycle test case where a flag defaults to enabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing cycle case has both flags disabled by default, so cutting the cycle leaves nothing for another condition to match on and any implementation passes it. Here one flag is enabled by its environment default. Cutting the cycle leaves that value in place, and an implementation that publishes it for other conditions to read will match the segment gated on it — reporting segment membership for a segment whose override was never applied. A flag resolved only by cutting a cycle has to be unreadable by another condition. Verified to fail against an implementation without that behaviour, and to be independent of the order features and segments are declared in. --- ...ic_enabled_default__should_not_match.jsonc | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 test_cases/test_flag_dependency__cyclic_enabled_default__should_not_match.jsonc diff --git a/test_cases/test_flag_dependency__cyclic_enabled_default__should_not_match.jsonc b/test_cases/test_flag_dependency__cyclic_enabled_default__should_not_match.jsonc new file mode 100644 index 0000000..27e85df --- /dev/null +++ b/test_cases/test_flag_dependency__cyclic_enabled_default__should_not_match.jsonc @@ -0,0 +1,104 @@ +{ + // Given: Two features whose dependencies form a cycle, where one of them + // (`b`) is enabled by its environment default + // When: The context is evaluated + // Then: Neither override is applied and neither segment matches + // + // NOTE: This is the cycle that distinguishes implementations. Cutting the + // cycle leaves `b` at its default, which is *enabled* - and if that + // value is then published for other conditions to read, the segment + // `a_requires_b` matches on it. The result is a context reported as + // belonging to a segment whose override was never applied. A flag + // resolved only by cutting a cycle must not be readable by another + // condition. + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/heads/test/flag-dependencies/schema.json", + "context": { + "environment": { + "key": "key", + "name": "Environment" + }, + "features": { + "a": { + "key": "1", + "name": "a", + "enabled": false, + "value": "a_default" + }, + "b": { + "key": "2", + "name": "b", + "enabled": true, + "value": "b_default" + } + }, + "segments": { + "a_on_b": { + "key": "a_on_b", + "name": "a_requires_b", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.b.enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "key": "1", + "name": "a", + "enabled": true, + "value": "a_override" + } + ] + }, + "b_on_a": { + "key": "b_on_a", + "name": "b_requires_a", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "EQUAL", + "property": "$.flags.a.enabled", + "value": "true" + } + ] + } + ], + "overrides": [ + { + "key": "2", + "name": "b", + "enabled": true, + "value": "b_override" + } + ] + } + } + }, + "result": { + "flags": { + "a": { + "enabled": false, + "name": "a", + "reason": "ERROR; code=CIRCULAR_DEPENDENCY", + "value": "a_default", + "variant": null + }, + "b": { + "enabled": true, + "name": "b", + "reason": "ERROR; code=CIRCULAR_DEPENDENCY", + "value": "b_default", + "variant": null + } + }, + "segments": [] + } +} From 3f10cd6852590ce34eeafce3ae360c3f6fe25729 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 31 Aug 2026 13:44:02 +0100 Subject: [PATCH 08/13] chore: point schemas back at main Reverts the temporary pointers added while the schema was in review. Flagsmith/flagsmith#8396 is merged, so `EvaluationContext.flags` is on `main` and the test cases no longer need to reference an in-flight branch to validate. `schema.json` goes back to the context schema on `refs/heads/main`, and the flag dependency cases to the `refs/tags/v2.0.0` schema every other case in the corpus uses. --- schema.json | 2 +- ...g_dependency__absent_prerequisite__should_not_override.jsonc | 2 +- ...pendency__bracket_rooted_property__should_not_override.jsonc | 2 +- ...test_flag_dependency__bracketed_field__should_override.jsonc | 2 +- ..._dependency__competing_overrides__lowest_priority_wins.jsonc | 2 +- ..._dependency__context_supplied_flags__should_be_ignored.jsonc | 2 +- .../test_flag_dependency__cyclic__should_not_override.jsonc | 2 +- ...g_dependency__cyclic_enabled_default__should_not_match.jsonc | 2 +- ...endency__multiple_overrides__each_feature_gets_its_own.jsonc | 2 +- .../test_flag_dependency__nested_rule__should_override.jsonc | 2 +- ..._flag_dependency__no_overrides__segment_still_reported.jsonc | 2 +- ...dependency__prerequisite_disabled__should_not_override.jsonc | 2 +- ...flag_dependency__prerequisite_enabled__should_override.jsonc | 2 +- ..._flag_dependency__quoted_feature_name__should_override.jsonc | 2 +- ...dependency__segment_metadata__reported_with_dependency.jsonc | 2 +- ..._dependency__shared_segment__resolved_once_per_feature.jsonc | 2 +- .../test_flag_dependency__transitive__should_cascade.jsonc | 2 +- ..._flag_dependency__transitive_unmet__should_not_cascade.jsonc | 2 +- test_cases/test_flag_dependency__value__should_override.jsonc | 2 +- test_cases/test_flag_dependency__variant__should_override.jsonc | 2 +- ...lag_dependency__wildcard_property__should_not_override.jsonc | 2 +- 21 files changed, 21 insertions(+), 21 deletions(-) diff --git a/schema.json b/schema.json index f83e2ca..2a01f0e 100644 --- a/schema.json +++ b/schema.json @@ -3,7 +3,7 @@ "type": "object", "properties": { "context": { - "$ref": "https://raw.githubusercontent.com/Flagsmith/flagsmith/refs/heads/feat/dependent-flags-evaluation-context/sdk/evaluation-context.json" + "$ref": "https://raw.githubusercontent.com/Flagsmith/flagsmith/refs/heads/main/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 index a55891f..bbf8da2 100644 --- a/test_cases/test_flag_dependency__absent_prerequisite__should_not_override.jsonc +++ b/test_cases/test_flag_dependency__absent_prerequisite__should_not_override.jsonc @@ -5,7 +5,7 @@ // // 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", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", "context": { "environment": { "key": "key", 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 index e4185aa..33c718d 100644 --- 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 @@ -10,7 +10,7 @@ // 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", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", "context": { "environment": { "key": "key", diff --git a/test_cases/test_flag_dependency__bracketed_field__should_override.jsonc b/test_cases/test_flag_dependency__bracketed_field__should_override.jsonc index f7334f5..191e0de 100644 --- a/test_cases/test_flag_dependency__bracketed_field__should_override.jsonc +++ b/test_cases/test_flag_dependency__bracketed_field__should_override.jsonc @@ -8,7 +8,7 @@ // `$.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", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", "context": { "environment": { "key": "key", 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 index 7f00707..0cf2f87 100644 --- a/test_cases/test_flag_dependency__competing_overrides__lowest_priority_wins.jsonc +++ b/test_cases/test_flag_dependency__competing_overrides__lowest_priority_wins.jsonc @@ -6,7 +6,7 @@ // // 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", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", "context": { "environment": { "key": "key", 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 67d31c2..54552b1 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/heads/test/flag-dependencies/schema.json", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/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 c397065..6ed494b 100644 --- a/test_cases/test_flag_dependency__cyclic__should_not_override.jsonc +++ b/test_cases/test_flag_dependency__cyclic__should_not_override.jsonc @@ -13,7 +13,7 @@ // 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", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", "context": { "environment": { "key": "key", diff --git a/test_cases/test_flag_dependency__cyclic_enabled_default__should_not_match.jsonc b/test_cases/test_flag_dependency__cyclic_enabled_default__should_not_match.jsonc index 27e85df..46453ad 100644 --- a/test_cases/test_flag_dependency__cyclic_enabled_default__should_not_match.jsonc +++ b/test_cases/test_flag_dependency__cyclic_enabled_default__should_not_match.jsonc @@ -11,7 +11,7 @@ // belonging to a segment whose override was never applied. A flag // resolved only by cutting a cycle must not be readable by another // condition. - "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/heads/test/flag-dependencies/schema.json", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", "context": { "environment": { "key": "key", 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 index 450e161..b4a2622 100644 --- 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 @@ -6,7 +6,7 @@ // // 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", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", "context": { "environment": { "key": "key", diff --git a/test_cases/test_flag_dependency__nested_rule__should_override.jsonc b/test_cases/test_flag_dependency__nested_rule__should_override.jsonc index 08597e6..f0cef68 100644 --- a/test_cases/test_flag_dependency__nested_rule__should_override.jsonc +++ b/test_cases/test_flag_dependency__nested_rule__should_override.jsonc @@ -6,7 +6,7 @@ // // 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", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", "context": { "environment": { "key": "key", 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 index 81bd4b1..94850ca 100644 --- a/test_cases/test_flag_dependency__no_overrides__segment_still_reported.jsonc +++ b/test_cases/test_flag_dependency__no_overrides__segment_still_reported.jsonc @@ -6,7 +6,7 @@ // 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", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/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 b2cf00f..2b4ec34 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/heads/test/flag-dependencies/schema.json", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/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 05e0d9b..b8e68c6 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/heads/test/flag-dependencies/schema.json", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", "context": { "environment": { "key": "key", 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 index ae1cfb7..7c26f84 100644 --- a/test_cases/test_flag_dependency__quoted_feature_name__should_override.jsonc +++ b/test_cases/test_flag_dependency__quoted_feature_name__should_override.jsonc @@ -6,7 +6,7 @@ // // 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", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", "context": { "environment": { "key": "key", 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 index 5671620..576fadb 100644 --- a/test_cases/test_flag_dependency__segment_metadata__reported_with_dependency.jsonc +++ b/test_cases/test_flag_dependency__segment_metadata__reported_with_dependency.jsonc @@ -5,7 +5,7 @@ // // 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", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", "context": { "environment": { "key": "key", 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 index 1cc99e7..338e2d4 100644 --- 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 @@ -9,7 +9,7 @@ // 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", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/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 07e5f1b..1afa893 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/heads/test/flag-dependencies/schema.json", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", "context": { "environment": { "key": "key", 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 index 5f592ee..759797f 100644 --- a/test_cases/test_flag_dependency__transitive_unmet__should_not_cascade.jsonc +++ b/test_cases/test_flag_dependency__transitive_unmet__should_not_cascade.jsonc @@ -6,7 +6,7 @@ // 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", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/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 b004628..be00319 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/heads/test/flag-dependencies/schema.json", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", "context": { "environment": { "key": "key", diff --git a/test_cases/test_flag_dependency__variant__should_override.jsonc b/test_cases/test_flag_dependency__variant__should_override.jsonc index d0ef745..73b8172 100644 --- a/test_cases/test_flag_dependency__variant__should_override.jsonc +++ b/test_cases/test_flag_dependency__variant__should_override.jsonc @@ -8,7 +8,7 @@ // 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", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", "context": { "environment": { "key": "key", 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 index 8d815b6..ffed0a1 100644 --- a/test_cases/test_flag_dependency__wildcard_property__should_not_override.jsonc +++ b/test_cases/test_flag_dependency__wildcard_property__should_not_override.jsonc @@ -10,7 +10,7 @@ // 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", + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", "context": { "environment": { "key": "key", From 2a918b22bf1a6f6e661a19590f7391777c20e7cc Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 1 Sep 2026 12:09:25 +0100 Subject: [PATCH 09/13] docs: say what the enabled-default cycle case adds The case explained its own mechanism but never named the case it exists alongside, so it reads as a duplicate of `cyclic__should_not_override`. Both flags there are disabled by default, so cutting the cycle leaves nothing truthy to match on and any implementation passes it; that is the distinction worth stating. --- ...yclic_enabled_default__should_not_match.jsonc | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test_cases/test_flag_dependency__cyclic_enabled_default__should_not_match.jsonc b/test_cases/test_flag_dependency__cyclic_enabled_default__should_not_match.jsonc index 46453ad..b29b0fa 100644 --- a/test_cases/test_flag_dependency__cyclic_enabled_default__should_not_match.jsonc +++ b/test_cases/test_flag_dependency__cyclic_enabled_default__should_not_match.jsonc @@ -4,13 +4,15 @@ // When: The context is evaluated // Then: Neither override is applied and neither segment matches // - // NOTE: This is the cycle that distinguishes implementations. Cutting the - // cycle leaves `b` at its default, which is *enabled* - and if that - // value is then published for other conditions to read, the segment - // `a_requires_b` matches on it. The result is a context reported as - // belonging to a segment whose override was never applied. A flag - // resolved only by cutting a cycle must not be readable by another - // condition. + // NOTE: This is the cycle that distinguishes implementations. In + // `cyclic__should_not_override` both flags are disabled by default, so + // cutting the cycle leaves nothing truthy for the other condition to + // match on and any implementation passes it. Here `b` is enabled by + // default: cutting the cycle leaves that value in place, and an + // implementation that publishes it for other conditions to read will + // match `a_requires_b` on it — reporting a context as belonging to a + // segment whose override was never applied. A flag resolved only by + // cutting a cycle must not be readable by another condition. "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", "context": { "environment": { From 72f65f2ee5f7ef58a9518e31321c9a906109093e Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 1 Sep 2026 12:09:25 +0100 Subject: [PATCH 10/13] test: drop the multiple overrides case `shared_segment__resolved_once_per_feature` also carries a segment with overrides for two features, so it already exercises selecting the override that names the feature being resolved, and additionally covers reusing that segment's verdict for the second feature. --- ...overrides__each_feature_gets_its_own.jsonc | 98 ------------------- 1 file changed, 98 deletions(-) delete mode 100644 test_cases/test_flag_dependency__multiple_overrides__each_feature_gets_its_own.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 deleted file mode 100644 index b4a2622..0000000 --- a/test_cases/test_flag_dependency__multiple_overrides__each_feature_gets_its_own.jsonc +++ /dev/null @@ -1,98 +0,0 @@ -{ - // 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/tags/v2.0.0/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" - } - ] - } -} From 4299e542c5bac70540649071903dc6487efdfc07 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 1 Sep 2026 12:09:25 +0100 Subject: [PATCH 11/13] test: drop the dependency segment metadata case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reporting a segment's metadata does not depend on how the segment was matched, and `segment__with_metadata__match__metadata_in_result` already covers it. The case was added when resolution reported matched segments through a separate pass, where metadata could be dropped on that path alone. That pass no longer exists — segments are reported by the same code whether or not a dependency was involved — so a dependency-specific metadata bug is no longer expressible. --- ...t_metadata__reported_with_dependency.jsonc | 86 ------------------- 1 file changed, 86 deletions(-) delete mode 100644 test_cases/test_flag_dependency__segment_metadata__reported_with_dependency.jsonc 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 deleted file mode 100644 index 576fadb..0000000 --- a/test_cases/test_flag_dependency__segment_metadata__reported_with_dependency.jsonc +++ /dev/null @@ -1,86 +0,0 @@ -{ - // 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/tags/v2.0.0/schema.json", - "context": { - "environment": { - "key": "key", - "name": "Environment" - }, - "features": { - "prerequisite": { - "key": "1", - "name": "prerequisite", - "enabled": true, - "value": null - }, - "dependent": { - "key": "2", - "name": "dependent", - "enabled": false, - "value": "off" - } - }, - "segments": { - "1": { - "key": "1", - "name": "dependency_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" - } - } - ] - } -} From d75acfa658a02a67f444de197b25216696645bc5 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 1 Sep 2026 12:19:31 +0100 Subject: [PATCH 12/13] test: rename the enabled-prerequisite cycle case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cyclic_enabled_default` was ambiguous — it reads as though the cycle is enabled by default, and collides with the `DEFAULT` reason — and `should_not_match` did not say what must not match, which is the whole distinction from its sibling. Named for the two things that matter: the prerequisite in the cycle is enabled, and the segment gated on it must not match. Alongside the sibling the pair now reads as a contrast rather than a near-duplicate: cyclic__should_not_override the flag is not overridden cyclic_enabled_prerequisite__segment_should_not_match the segment is not matched --- ...__cyclic_enabled_prerequisite__segment_should_not_match.jsonc} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test_cases/{test_flag_dependency__cyclic_enabled_default__should_not_match.jsonc => test_flag_dependency__cyclic_enabled_prerequisite__segment_should_not_match.jsonc} (100%) diff --git a/test_cases/test_flag_dependency__cyclic_enabled_default__should_not_match.jsonc b/test_cases/test_flag_dependency__cyclic_enabled_prerequisite__segment_should_not_match.jsonc similarity index 100% rename from test_cases/test_flag_dependency__cyclic_enabled_default__should_not_match.jsonc rename to test_cases/test_flag_dependency__cyclic_enabled_prerequisite__segment_should_not_match.jsonc From c7a9173a56874dcf53359b5ef8bee1e863520b94 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 1 Sep 2026 12:40:12 +0100 Subject: [PATCH 13/13] test: add a cycle case where the condition matches on absence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `IS_NOT_SET` is the one operator that matches on a flag which could not be resolved — every other operator fails against no value. So a segment overriding the very flag it tests for absence has its condition made true by the cycle, and the override applies because the cycle was cut. A segment whose evaluation cut a cycle read a flag that has no value, so neither its match nor its failure to match is sound, and it must not be applied either way. --- ...is_not_set__segment_should_not_match.jsonc | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 test_cases/test_flag_dependency__cyclic_is_not_set__segment_should_not_match.jsonc diff --git a/test_cases/test_flag_dependency__cyclic_is_not_set__segment_should_not_match.jsonc b/test_cases/test_flag_dependency__cyclic_is_not_set__segment_should_not_match.jsonc new file mode 100644 index 0000000..ee69a3b --- /dev/null +++ b/test_cases/test_flag_dependency__cyclic_is_not_set__segment_should_not_match.jsonc @@ -0,0 +1,66 @@ +{ + // Given: A segment overriding `a`, matching when `a` is not set — a cycle, + // since resolving `a` requires evaluating this segment + // When: The context is evaluated + // Then: The override is not applied and the segment does not match + // + // NOTE: `IS_NOT_SET` is the one operator that matches on a flag which could + // not be resolved: every other operator fails against no value. So a + // cycle can make a condition true, and the override it guards would + // apply *because* the cycle was cut. A segment whose evaluation cut a + // cycle read a flag that has no value, so neither its match nor its + // failure to match is sound, and it must not be applied either way. + "$schema": "https://raw.githubusercontent.com/Flagsmith/engine-test-data/refs/tags/v2.0.0/schema.json", + "context": { + "environment": { + "key": "key", + "name": "Environment" + }, + "features": { + "a": { + "key": "1", + "name": "a", + "enabled": false, + "value": "a_default" + } + }, + "segments": { + "1": { + "key": "1", + "name": "a_requires_a_unset", + "rules": [ + { + "type": "ALL", + "conditions": [ + { + "operator": "IS_NOT_SET", + "property": "$.flags.a.enabled", + "value": "" + } + ] + } + ], + "overrides": [ + { + "key": "1", + "name": "a", + "enabled": true, + "value": "a_override" + } + ] + } + } + }, + "result": { + "flags": { + "a": { + "enabled": false, + "name": "a", + "reason": "ERROR; code=CIRCULAR_DEPENDENCY", + "value": "a_default", + "variant": null + } + }, + "segments": [] + } +}