From 2bdef866d78b8e0abf44049d757334125b084cbc Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Thu, 29 Oct 2026 15:34:32 -0300 Subject: [PATCH 1/2] cover env-level flag dependency --- README.md | 3 ++ features/local__get_environment_flags.feature | 36 +++++++++++++++++++ runner/pyproject.toml | 1 + runner/sdk_spec/flagsmith_admin.py | 11 ++++++ runner/tests/conftest.py | 34 ++++++++++++++---- 5 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 features/local__get_environment_flags.feature diff --git a/README.md b/README.md index 68163f7..f511df1 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,9 @@ Each release also pins the Flagsmith version in `compose.yml` that it was verifi - `Create the SDK client with events enabled` receives the same two, then the base URL of Flagsmith's events API, and returns a client that sends events there. Only `@events` scenarios use it, so an SDK without event tracking can leave it out. + - `Create the SDK client with local evaluation` receives the same two as `Create the SDK client`, + and returns a client that evaluates flags locally, with its environment already loaded. Only + `@local-evaluation` scenarios use it, so an SDK without local evaluation can leave it out. - A `When` step receives the client, then the step's placeholders in order, then its data table. A ` | value` table arrives as a map, e.g. `{"plan": "premium", "age": 30}`. - Values are JSON values. Pass flag and trait values through with their types intact. diff --git a/features/local__get_environment_flags.feature b/features/local__get_environment_flags.feature new file mode 100644 index 0000000..07aa703 --- /dev/null +++ b/features/local__get_environment_flags.feature @@ -0,0 +1,36 @@ +@local-evaluation +Feature: Get environment flags with local evaluation + A server-side SDK evaluates the flags for its environment against the environment document, + serving the same flags Flagsmith would. + + Background: + Given a Flagsmith environment + And the SDK evaluates flags locally + + Scenario: Environment flags are disabled while a flag they depend on is disabled + Given the environment has the following features: + | feature | enabled | value | + | payments | false | null | + | checkout | true | "v2" | + | search | true | null | + | autocomplete | true | 5 | + And feature "checkout" depends on feature "payments" + And feature "autocomplete" depends on feature "search" + When the application gets the environment flags + Then the application receives the following flags: + | feature | enabled | value | + | payments | false | null | + | checkout | false | "v2" | + | search | true | null | + | autocomplete | true | 5 | + + Scenario: Environment flags ignore segments that need an identity + Given the environment has the following features: + | feature | enabled | value | + | banner | true | "hello" | + And a segment "premium_users" where trait "plan" equals "premium" + And segment "premium_users" overrides feature "banner" with enabled false and value "hello premium user" + When the application gets the environment flags + Then the application receives the following flags: + | feature | enabled | value | + | banner | true | "hello" | diff --git a/runner/pyproject.toml b/runner/pyproject.toml index 4dc0f2a..1eebb52 100644 --- a/runner/pyproject.toml +++ b/runner/pyproject.toml @@ -18,4 +18,5 @@ addopts = "--strict-markers -p no:cacheprovider" markers = [ "core: Behaviour every SDK must implement", "events: Tracking and exposure events, powering experimentation", + "local-evaluation: Evaluating flags in the SDK, against the environment document", ] diff --git a/runner/sdk_spec/flagsmith_admin.py b/runner/sdk_spec/flagsmith_admin.py index 303ab6e..c5fc855 100644 --- a/runner/sdk_spec/flagsmith_admin.py +++ b/runner/sdk_spec/flagsmith_admin.py @@ -188,6 +188,17 @@ def create_segment_override( }, ) + def create_flag_dependency( + self, environment: Environment, feature: str, prerequisite: str + ) -> None: + """Disables the feature in the environment unless the prerequisite is enabled.""" + self._request( + "POST", + f"/environments/{environment.api_key}/features/" + f"{environment.features[feature].id}/dependencies/" + f"{environment.features[prerequisite].id}/", + ) + def get_identity_traits(self, environment: Environment, identifier: str) -> dict[str, Any]: identities = self._request( "GET", f"/environments/{environment.api_key}/identities/?q={identifier}" diff --git a/runner/tests/conftest.py b/runner/tests/conftest.py index ede0aba..ed3c9c0 100644 --- a/runner/tests/conftest.py +++ b/runner/tests/conftest.py @@ -67,6 +67,7 @@ class ScenarioState: environment: Environment | None = None client: str | None = None events: bool = False + local_evaluation: bool = False result: Any = None @@ -96,7 +97,12 @@ def sdk(pytestconfig: pytest.Config) -> Iterator[SDK]: if not SDK_COMMAND: pytest.exit("Set SDK_COMMAND to the command that starts the SDK harness", returncode=4) sdk = SDK(SDK_COMMAND) - required = [CREATE_CLIENT, CREATE_CLIENT_WITH_EVENTS, *SDK_STEPS] + required = [ + CREATE_CLIENT, + CREATE_CLIENT_WITH_EVENTS, + CREATE_CLIENT_WITH_LOCAL_EVALUATION, + *SDK_STEPS, + ] implemented = [step for step in required if step in sdk.implemented_steps] # Write through the terminal reporter, so the line is shown even with output captured. terminal = pytestconfig.pluginmanager.get_plugin("terminalreporter") @@ -154,6 +160,8 @@ def require_environment(state: ScenarioState) -> Environment: # A second client, for scenarios that are about the events an SDK sends. Events are off by # default in every SDK, and the events API is not derived from the Flagsmith API URL. CREATE_CLIENT_WITH_EVENTS = "Create the SDK client with events enabled" +# A client evaluating flags against the environment document, for server-side SDKs only. +CREATE_CLIENT_WITH_LOCAL_EVALUATION = "Create the SDK client with local evaluation" SDK_STEPS: list[str] = [] PLACEHOLDER = re.compile(r'("?)\{(\w+)\}\1') @@ -166,11 +174,12 @@ def sdk_client(state: ScenarioState, sdk: SDK) -> str: args = [environment.server_side_key, f"{FLAGSMITH_SDK_URL.rstrip('/')}/api/v1/"] if state.events: args.append(f"{EVENTS_SDK_URL.rstrip('/')}/") - state.client = sdk.step( - CREATE_CLIENT_WITH_EVENTS if state.events else CREATE_CLIENT, - args, - returns_client=True, - ) + step = CREATE_CLIENT_WITH_EVENTS + elif state.local_evaluation: + step = CREATE_CLIENT_WITH_LOCAL_EVALUATION + else: + step = CREATE_CLIENT + state.client = sdk.step(step, args, returns_client=True) return state.client @@ -237,6 +246,12 @@ def sdk_sends_events(state: ScenarioState, flagsmith: FlagsmithAdmin) -> None: state.events = True +@given("the SDK evaluates flags locally") +def sdk_evaluates_flags_locally(state: ScenarioState) -> None: + """Asks for a client with local evaluation, which the runner creates for the first SDK step.""" + state.local_evaluation = True + + @given("the environment has the following features:") def environment_features( state: ScenarioState, flagsmith: FlagsmithAdmin, datatable: list[list[str]] @@ -291,6 +306,13 @@ def segment_override( ) +@given(parsers.parse('feature "{feature}" depends on feature "{prerequisite}"')) +def flag_dependency( + state: ScenarioState, flagsmith: FlagsmithAdmin, feature: str, prerequisite: str +) -> None: + flagsmith.create_flag_dependency(require_environment(state), feature, prerequisite) + + # Then: the runner checks the outcome From f8d537e3fed59115f19603252721de63bb8688a0 Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Thu, 29 Oct 2026 16:13:17 -0300 Subject: [PATCH 2/2] clear capabilities --- .github/workflows/compatibility.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index 55ac4b4..fe5e8bd 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -25,67 +25,67 @@ jobs: repository: Flagsmith/flagsmith-python-client ref: main language: python - tags: '' + tags: core or events or local-evaluation - name: nodejs repository: Flagsmith/flagsmith-nodejs-client ref: main language: nodejs - tags: '' + tags: core or events or local-evaluation - name: java repository: Flagsmith/flagsmith-java-client ref: main language: java - tags: '' + tags: core or events or local-evaluation - name: dotnet repository: Flagsmith/flagsmith-dotnet-client ref: main language: dotnet - tags: '' + tags: core or events or local-evaluation - name: go repository: Flagsmith/flagsmith-go-client ref: main language: go - tags: '' + tags: core or events or local-evaluation - name: ruby repository: Flagsmith/flagsmith-ruby-client ref: main language: ruby - tags: '' + tags: core or events or local-evaluation - name: php repository: Flagsmith/flagsmith-php-client ref: main language: php - tags: '' + tags: core or events or local-evaluation - name: rust repository: Flagsmith/flagsmith-rust-client ref: main language: rust - tags: '' + tags: core or events or local-evaluation - name: elixir repository: Flagsmith/flagsmith-elixir-client ref: main language: elixir - tags: '' + tags: core or events or local-evaluation - name: js-client repository: Flagsmith/flagsmith-js-client ref: main language: nodejs - tags: '' + tags: core or events - name: ios repository: Flagsmith/flagsmith-ios-client ref: main language: swift - tags: '' + tags: core or events - name: android repository: Flagsmith/flagsmith-kotlin-android-client ref: main language: kotlin - tags: '' + tags: core or events - name: flutter repository: Flagsmith/flagsmith-flutter-client ref: main language: dart - tags: '' + tags: core or events uses: ./.github/workflows/run-spec.yml with: spec-repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}