Repository navigation
feat: Add flag overrides to the contract test services - #530
Merged
kinyoklion merged 14 commits intoNov 10, 2026
Merged
Conversation
kinyoklion
force-pushed
the
rlamb/overrides-python-file-source
branch
from
September 28, 2026 20:29
021778c to
2074b74
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-contract-tests
branch
from
September 28, 2026 20:29
b5ff7c2 to
0a9c386
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-file-source
branch
from
September 30, 2026 20:31
2074b74 to
1950d45
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-contract-tests
branch
from
September 30, 2026 20:31
0a9c386 to
bad30c5
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-file-source
branch
from
October 1, 2026 23:43
1950d45 to
3e47045
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-contract-tests
branch
from
October 1, 2026 23:43
bad30c5 to
a355c3d
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-file-source
branch
from
October 3, 2026 01:18
3e47045 to
cfb781a
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-contract-tests
branch
from
October 3, 2026 01:19
a355c3d to
dac6abe
Compare
Member
Author
|
bugbot review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit dac6abe. Configure here.
kinyoklion
force-pushed
the
rlamb/overrides-python-file-source
branch
from
October 13, 2026 21:02
cfb781a to
ea4dc19
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-contract-tests
branch
from
October 13, 2026 21:02
dac6abe to
3de3ec9
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-file-source
branch
from
October 13, 2026 22:40
ea4dc19 to
3de11ab
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-contract-tests
branch
2 times, most recently
from
October 15, 2026 22:25
8d2c06c to
6190743
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-file-source
branch
from
October 16, 2026 21:34
bb5a108 to
5e42ebc
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-contract-tests
branch
from
October 16, 2026 21:34
6190743 to
ac6bdd0
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-file-source
branch
from
October 28, 2026 22:17
5e42ebc to
314dea2
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-contract-tests
branch
from
October 28, 2026 22:17
ac6bdd0 to
0d759ba
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-file-source
branch
from
October 28, 2026 22:40
314dea2 to
4bace41
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-contract-tests
branch
2 times, most recently
from
October 28, 2026 22:53
1dbbf2c to
1f4ce1a
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-file-source
branch
from
October 29, 2026 16:48
99b851f to
fac9ec8
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-contract-tests
branch
from
October 29, 2026 16:48
1f4ce1a to
d1b617b
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-file-source
branch
from
October 29, 2026 17:16
fac9ec8 to
0415ec1
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-contract-tests
branch
from
October 29, 2026 17:16
d1b617b to
0d55468
Compare
kinyoklion
added a commit
to launchdarkly/sdk-test-harness
that referenced
this pull request
Nov 3, 2026
…thout the init key (#539) <!-- TERMAG_PR_BODY_BEGIN --> ## Summary Overrides only add to what the SDK reports: a flag that the override layer does not hold evaluates as it does without an override source, and the all-flags state holds every flag it holds without one, plus the overridden flags. A client that has not initialized is where SDKs broke this, and only when the store holds flags without being initialized, for example a persistent store that holds data but no init key. The flag overrides suite uses the SDK's in-memory store, which is empty until a data source provides data, so it could not tell a read of the override layer apart from a read of everything the store returns. A new `flag overrides` group in the persistent data store suite (`server_side_persistence_overrides.go`) runs when the test service has the `flag-overrides` capability. It writes flags to the store without the init key and gives the client a data source that never provides data. Each test compares a client with an override source to a client without one against the same store, so it holds whatever an SDK reports for an uninitialized store: - **a flag without an override evaluates as it does without an override source**: an override file that holds a different flag leaves the evaluation of a store flag unchanged (OVERRIDE Requirements 1.5.1 and 1.5.3). - **an empty override source leaves the all-flags state unchanged** (Requirement 1.2.2). - **overrides add to the all-flags state and make it valid**: the state holds exactly the flags of the client without a source, with override values for the overridden keys, plus the flags that exist only in the override layer, and it is valid (launchdarkly/sdk-specs#280, Requirements 1.4.1.2 and 1.4.1.3). The flag overrides suite gains **evaluate all flags returns only the override flags as a valid state** in its uninitialized client group, where the store is empty and every SDK reports only the override flags. ## Results Run with `-enable-persistence-tests` against Redis. | Test service | flag without an override | empty override source | overrides add | |---|---|---|---| | Go `v7` | fails: served from the store | fails: valid state of the store's flags | fails: the store's flags join the overrides | | Go launchdarkly/go-server-sdk#491 | passes | passes | passes | | Node `main` | passes | fails: valid state of the store's flags | passes | | Node launchdarkly/js-core#2132 first commit (`57498a9e`) | passes | passes | fails: drops a store flag the state holds without overrides | | Node launchdarkly/js-core#2132 | passes | passes | passes | | Python (top of the launchdarkly/python-server-sdk#530 stack, sync and async) | passes | passes | passes | | Ruby (top of the launchdarkly/ruby-server-sdk#454 stack) | passes | passes | passes | The Java (launchdarkly/java-core#225) and .NET (`dotnet-server-sdk-v9`) test services do not support persistent stores, so the group skips there; the new uninitialized client test passes on both. The full Redis persistent data store suite and the flag overrides suite pass against Go #491 at `30d4f216` (131 tests run). `golangci-lint` reports no issues. Go and Node fail these tests until launchdarkly/go-server-sdk#491 and launchdarkly/js-core#2132 are merged, for test services whose CI runs the persistent data store suite with this harness version. [SDK-3419](https://launchdarkly.atlassian.net/browse/SDK-3419) <!-- TERMAG_PR_BODY_END --> <!-- termag: box=i-0575d1c54853058d7 project=sdk-test-harness --> <sub>Opened from termag</sub> [SDK-3419]: https://launchdarkly.atlassian.net/browse/SDK-3419?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Overview** > Adds harness coverage for **file flag overrides** when the SDK never initializes but a **persistent store already holds feature data without the `$inited` key**—a case the existing in-memory override suite could not distinguish from “read everything from the store.” > > A new **`flag overrides`** group under the server-side persistent data store suite (`server_side_persistence_overrides.go`, gated on `flag-overrides`) seeds Redis/Consul/Dynamo with flags, uses a blocking unavailable data source, and compares clients with and without override files via `uninitializedClient`. Scenarios assert: store flags not in the override file behave like no override source; an empty override file leaves **evaluate all flags** unchanged; overrides merge into all-flags state, mark it **valid**, and apply override values while retaining other store flags. > > The standalone flag-overrides **`uninitialized client`** group gains **evaluate all flags returns only the override flags as a valid state** (empty store, overrides-only valid state). The persistent suite registers the new group from `server_side_persistence_base.go`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 09a8e02. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
kinyoklion
force-pushed
the
rlamb/overrides-python-file-source
branch
from
November 3, 2026 23:56
0415ec1 to
a8e9294
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-contract-tests
branch
from
November 3, 2026 23:56
0d55468 to
5800e36
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-file-source
branch
from
November 4, 2026 22:55
1cba56a to
d6b022b
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-contract-tests
branch
from
November 4, 2026 22:55
6172b2a to
7cf3365
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-file-source
branch
from
November 4, 2026 23:09
d6b022b to
a52ba14
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-contract-tests
branch
from
November 4, 2026 23:09
7cf3365 to
71b52ee
Compare
keelerm84
approved these changes
Nov 5, 2026
kinyoklion
force-pushed
the
rlamb/overrides-python-contract-tests
branch
from
November 6, 2026 17:53
71b52ee to
7e42fc2
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-file-source
branch
from
November 6, 2026 17:53
a52ba14 to
36ceb3f
Compare
3 of 4 tasks
Carries the override-affected marking on the evaluation event input, so the event processor keys on the marking alone and never reads the evaluation reason. An evaluation marked as override-affected produces no individual feature event and no debug event, even when the flag requests them, and it is counted in summary events like any other evaluation. The marking is part of the summary counter key, so override-affected and ordinary evaluations of the same flag, variation, and version accumulate into separate counters, and a counter that aggregates marked evaluations carries the overrideAffected marker, present only when true, like the unknown marker. The evaluator passes each prerequisite record's own marking to its event, so a marked prerequisite record produces no individual event while an unaffected prerequisite inside a marked evaluation is recorded as usual. The client passes the top-level marking to the evaluation event, keeps the marking on the reason when a migration evaluation replaces it with a wrong-type error, and presents an override-affected flag in the all-flags state with trackEvents and trackReason false and no debugEventsUntilDate, so a consumer bootstrapped from the state sends no individual events for it. The async client mirrors these changes. The OVERRIDE specification vector runner now also checks the marking each evaluation contributes to its summary counter.
…ons after the loop closes When an evaluation raises and the flag definition came from the override layer, the EXCEPTION reason carries the override indicator, the default event record is marked so no individual event is produced, and the all-flags state presents the flag with tracking off. The async data system drops a flag change notification that arrives after the event loop closed instead of raising on the reload thread.
Adds ldclient.integrations.overrides.FileOverrideSourceBuilder, the file-based override source described by the OVERRIDE specification. The source reads one or more JSON or YAML files in the file data source document format, with optional flags, flagValues, and segments members, and supplies each successful load to the SDK's override sink as a full snapshot. Files are combined in the configured order. The duplicate keys handling is fail by default, which rejects the reload and keeps the previously loaded overrides, or ignore, which keeps the first file's entry. A configured file that does not exist contributes no overrides, so a file can be created later and deleting a file removes its overrides. A file that exists but cannot be read or parsed fails that reload, the last good overrides stay in effect, the failure is logged, and the load is retried after a bounded delay and on the next detected change. Change detection is one of two modes: polling, the default, examines the files once per second by default with a one second minimum, and watching reacts to file system notifications through the watchdog package. Watching without the watchdog package and a builder with no paths are construction errors. The initial load completes during client construction. Every applied change is logged at Info level with the overrides in effect and what each configured file supplied.
…l of the file override source Change detection is set up before the initial load, so an edit made while the files are first read is picked up. Close stops the reloader even when the change detector fails to close. The builder rejects a poll interval that is not a number or not finite.
…ntries as duplicate keys A path listed more than once would make every entry of that file a duplicate of itself: under the default fail handling the load failed, and under ignore the first listing won by accident. The builder now drops the repeats, keeps the first occurrence in its place in the order, and logs a warning once at build time. This is the behavior the Node SDK has.
…t on Windows The builder dropped repeated paths by exact string match, so on Windows a path listed twice in different case survived as two paths, and every key of that file failed as a duplicate of itself under the default fail handling. Paths are now compared with os.path.normcase, which ignores case on Windows and compares exactly elsewhere. The first spelling keeps its place in the order.
… SDKs A flagValues entry becomes a flag without client-side availability, so an all-flags state for client-side SDKs leaves it out even when the flag it overrides is client-side. A full definition with clientSide is the way to override such a flag. A test pins both.
The override source keeps the overrides in effect while it finds a file empty during an in-place rewrite. A YAML file without the pyyaml package is now a configuration error when the source is built, since the file name chooses the format and such a file can never load. The builder docs say that the file name decides the format, that files must be UTF-8, that files can be rewritten in place, how to remove a file's overrides, that an empty JSON file is not a valid document, and that a YAML file is best replaced in one operation because a partly written one can still parse. End-to-end tests rewrite the files in place, slowly, in watching and polling modes, for JSON and YAML, and check that neither the source nor a client evaluating the flag ever goes without the override.
Watching follows edits to the configured files and to the targets of configured links, including a ConfigMap update, but not a link further along the path or a configured directory that is pointed elsewhere or replaced. The change detection documentation now says so and points such deployments to polling.
Both contract test services advertise the flag-overrides capability and accept the overrides object in the SDK configuration: filePaths, and the optional duplicateKeysHandling, changeDetection, and pollIntervalMs properties, which configure the file-based override source on the FDv2 data system. An overrides object without a data system configuration is rejected, because overrides are an option of the FDv2 data system only.
kinyoklion
force-pushed
the
rlamb/overrides-python-file-source
branch
from
November 10, 2026 22:24
36ceb3f to
9a582aa
Compare
kinyoklion
force-pushed
the
rlamb/overrides-python-contract-tests
branch
from
November 10, 2026 22:24
7e42fc2 to
5d19e57
Compare
Base automatically changed from
rlamb/overrides-python-file-source
to
feat/overrides
November 10, 2026 23:09
kinyoklion
marked this pull request as ready for review
November 10, 2026 23:09
kinyoklion
added a commit
that referenced
this pull request
Nov 10, 2026
<!-- TERMAG_PR_BODY_BEGIN --> **Requirements** - [x] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) - [ ] I have validated my changes against all supported platform versions - [x] Sync/async parity: matching changes made to `async_*` siblings (or N/A). N/A: `LDClient` and `AsyncLDClient` use the same `FileOverrideSourceBuilder`. **Related issues** - Bugbot finding on #529: #529 (comment) **Describe the solution you've provided** `FileOverrideSourceBuilder` accepted one path as a `str`, or a list of paths. A single `pathlib.Path`, or any other `os.PathLike`, was passed to `list()`. A path object is not iterable, so the constructor raised `TypeError: 'PosixPath' object is not iterable`. The Bugbot finding describes a different failure, a path split into its components that fails every reload, but that cannot happen. The constructor raises before a client exists. The constructor now treats a `str` or an `os.PathLike` as a single path, and converts every path with `os.fspath`. A path that does not name a string path raises `TypeError` from the constructor, with a message that names the value. That includes `bytes` (given alone, in a list, or named by a path object), an `int`, and `None`. `bytes` and `bytearray` count as single values, so a bare `bytes` path is named whole instead of being read as a sequence of integers. The type hint is `Sequence` rather than `List`, so that type checkers accept a list of path objects but not a set, whose order is not fixed. The docstring says that paths may be path objects. Tests cover: - a single path object; - a list of path objects, which already worked; - a path object whose string form differs from its path, as with `os.DirEntry`; - a source that loads a file given as a path object; - the rejection of each of the values above. Each of six broken variants of the constructor fails at least one of these tests, including the original code and a variant that converts with `str()` instead of `os.fspath`. **Describe alternatives you've considered** Leaving the builder to accept only strings, as its type hint said. `pathlib` is the usual way to build paths, a list of path objects already worked, and the error did not say what was wrong. **Additional context** This PR is stacked on #530, the top of the overrides stack, so that the approved PRs below it do not change. Retarget it as the PRs below it merge. The review of this change found three existing gaps that this PR leaves alone: - An empty path resolves to the working directory, and its reads keep failing. - In polling mode, a path that contains a NUL character makes the source fail to start, so the client runs without overrides. In watching mode, the reload fails and keeps retrying. - The file data sources treat a tuple of paths differently from a list. Testing: `mypy ldclient`, `isort --check` and `pycodestyle` pass locally. The test suite passes (2,111 tests). It was run without the Redis, DynamoDB and Consul store integration tests, which need servers that were not running. CI runs only for PRs based on `main` or `feat/**`, so it will not run on this PR until it is retargeted. <!-- TERMAG_PR_BODY_END --> <!-- termag: box=i-0575d1c54853058d7 project=Overrides --> <sub>Opened from termag</sub> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Overview** > Fixes a constructor bug where a **single** `pathlib.Path` (or other `os.PathLike`) was passed to `list()`, causing `TypeError: 'PosixPath' object is not iterable`. > > `FileOverrideSourceBuilder` now wraps a lone `str`, `bytes`/`bytearray`, or `PathLike` as one path, normalizes every entry with `os.fspath()` (so objects like `os.DirEntry` use their filesystem path, not `str()`), and stores string paths. Invalid inputs (`bytes` paths, ints, `None`, path objects that do not resolve to a `str`) fail fast with an explicit `TypeError`. Type hints use `Sequence` plus `PathLike` so callers can pass path objects while unordered collections like `set` are discouraged. > > New tests cover single/list path objects, `__fspath__` behavior, invalid types, and loading overrides from a path object. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 154b5b8. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This is the last step of the flag overrides port described by the OVERRIDE specification. It is based on the file source branch because the phases are stacked; retarget to
feat/overridesonce that branch merges.Both contract test services (the Flask service for
LDClientand the aiohttp service forAsyncLDClient) advertise theflag-overridescapability and accept theoverridesobject in the SDK configuration as the test harness service specification describes:filePaths(required, in precedence order) and the optionalduplicateKeysHandling,changeDetection, andpollIntervalMsproperties. The values configure aFileOverrideSourceBuilderon the FDv2 data system throughConfigBuilder.overrides(...)orAsyncDataSystemConfig.override_source. Anoverridesobject without adataSystemconfiguration is rejected with an error, because overrides are an option of the FDv2 data system only.Verification with the prebuilt harness from sdk-test-harness branch
rlamb/sdk-2657/flag-overrides-tests(v3.2.0-alpha.10):-run 'flag overrides'38 ran, all passed; full suite 4809 total, 15 skipped, 4794 ran, all passed.-run 'flag overrides'38 ran, all passed; full suite 4807 total, 16 skipped, 4791 ran, all passed.The 15 tests skipped by the overrides runs are the other top-level suites excluded by the filter. The tests skipped in the full runs are those for capabilities the services do not advertise (
strongly-typed,tls:verify-peer,service-endpoints,user-type,track-hooks,wrapper, and on the async servicehook-environment-id), the long-running tests, and the persistent data store tests, all of which were skipped before this change too.SDK-3250
Note
Overview
Both contract-test services (sync Flask and async aiohttp) now advertise the
flag-overridescapability and map harnessoverridesconfig into the FDv2 data system.When
dataSystemis present, an optionaloverridesblock builds aFileOverrideSourceBuilderfromfilePathsand optionalduplicateKeysHandling,changeDetection, andpollIntervalMs—wired viadatasystem.overrides(...)on the sync client andAsyncDataSystemConfig.override_sourceon the async client.overrideswithoutdataSystemraisesValueError, since overrides are FDv2-only.No production SDK behavior changes; this is harness wiring so sdk-test-harness flag-overrides suites can run against Python.
Reviewed by Cursor Bugbot for commit dac6abe. Bugbot is set up for automated code reviews on this repo. Configure here.