Skip to content

test(flagsmith): run the provider conformance suite against the testbed - #1849

Draft
aepfli wants to merge 11 commits into
feat/provider-tckfrom
feat/provider-tck-flagsmith
Draft

aepfli wants to merge 11 commits into
feat/provider-tckfrom
feat/provider-tck-flagsmith

Conversation

@aepfli

@aepfli aepfli commented Sep 12, 2026 •

Copy link
Copy Markdown
Member

Runs the OpenFeature Provider Conformance Suite against the Flagsmith Java provider.

Stacked on feat/provider-tck. Companion to go-sdk-contrib#959 and js-sdk-contrib#1623 — same container, same scenarios.

Result: 35 pass, 11 fail, 19 skipped of 65

Go Python Java JS
passed 45 29 35 31
failed 2 11 11 13
skipped 18 25 19 21
of 65 65 65 65

@standard-reasons is what changed this run. It was 24 pass / 12 fail before that capability existed; eight of those twelve failures were this provider not populating the resolution reason, and they are now skips carrying a recorded reason rather than bare failures.

@standard-reasons — declared, and failing

This provider never populates the resolution reason. Every evaluation returns null, so not one of the reasons the capability claims is reported. The resolved values are correct throughout; only the reason is missing.

An earlier pass withheld this capability, which turned eight failures into skips — the exact move KnownDeviation exists to discourage, and the base has since corrected the worked example that taught it. The provider does build a resolution and simply leaves the reason out, so running those scenarios establishes something and the failures belong in the results with the deviation attached.

What makes it a defect rather than a permitted absence is that the Go Flagsmith provider reports STATIC, DISABLED and TARGETING_MATCH against the identical backend.

The 5 remaining failures

  • Two are unreadable floats. Flagsmith has no float type — feature_state_value is natively boolean, integer or string — so every float is stored as a string. Go's provider parses it back ("Because We store floats as string"); this one type-checks and falls through to the caller default, so getFloatValue never works against Flagsmith. Python has the same gap; the @numeric-coercion deviation now records this as measured rather than predicted.
  • Two are the shared type-system pair. float-flag and object-flag requested as a String succeed rather than reporting TYPE_MISMATCH, because on this backend both genuinely are strings. Not a provider bug in any of the four languages.
  • One is object-flag not resolving as a structure.

@large-integers — refused by the TCK, and correctly

The base now refuses this capability in Java rather than leaving it to the adopter: Java's integer accessor is a 32-bit Integer, so 2^53−1 cannot be asked for at all. That is a property of the SDK, not of the provider, and it is the same reason Java withholds it for flagd.

Worth knowing alongside it: Value.asInteger() silently truncates a Long to its low 32 bits — 4294967301 becomes 5, 2^53−1 becomes −1 — with no error. That is a defect in the Java SDK rather than anything this PR touches, but it is why the refusal matters: declaring the capability would not have errored, it would have returned a plausible wrong number.

Other capabilities

Declared: @object, @targeting, @disabled-flags, @string-typing.

@string-typing declared, @fully-typed-values withheld — and this adoption is why the suite
split them. Flagsmith's feature_state_value is natively boolean, integer or string, so this
provider correctly reports TYPE_MISMATCH for a boolean and an integer asked for as strings, while
floats and structures are stored as text and cannot be asked the question at all. One tag over all
four made this suite withhold everything and give up two passes it had earned; splitting them
recovered exactly those two, 33 → 35 passing and 21 → 19 skipped, with no change to the provider.
This file's own note ended "revisit if the capability is ever split by flag type" — this is that
revisit.

@disabled-flags holds because Flagsmith models a feature state as enabled plus a value, so the canonical set's four disabled-* flags map straight onto it, and this provider returns the caller default with reason DISABLED. Go agrees; Python and JS both raise GENERAL and withhold it.

@variants is withheld: Flagsmith has no variant concept for a plain feature, and the evaluation response carries no variant key. Permitted rather than defective — 2.2.4 makes it a SHOULD — so no deviation.

The lifecycle and event capabilities are withheld because this provider has no observable initialisation for the suite to assert against.

A finding about the TCK, not the provider

tools/tck exports jackson-databind 2.22.1, while flagsmith-java-client 7.4.3 pins jackson-annotations and jackson-core at 2.15.2. Maven's nearest-wins assembles databind 2.22 against annotations 2.15, and the Flagsmith client's ObjectMapper dies with ClassNotFoundException: com.fasterxml.jackson.annotation.JsonSerializeAs.

Verified this is the TCK's doing: with the TCK dependency removed the provider's own 35 tests pass; with it added, 22 error. Re-tested after the TCK moved testcontainers to provided/optional — still present, so it wants its own fix upstream: shade Jackson, or stop exporting a hard databind version.

The pin here is the workaround. It is compile scope and has to be, because the provider's own main source imports com.fasterxml.jackson.databind. It cannot be aligned upward — jackson-annotations has no 2.22.1 release, it tracks its own version line — so that route needs the Jackson BOM.

The backend

aepfli/flagsmith-tck-testbed 0.1.0, pinned rather than :latest because four adoptions pull it and a mutable tag lets a push change four PRs' results with no diff to explain it.

Why this is a draft

  • The testbed image lives in a personal namespace.
  • The Jackson conflict wants a decision upstream rather than a per-provider pin.

@coderabbitai

coderabbitai Bot commented Sep 12, 2026 •

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Comment @coderabbitai help to get the list of available commands.

@aepfli
aepfli force-pushed the feat/provider-tck-flagsmith branch 8 times, most recently from 4ded5e1 to 894a27a Compare September 14, 2026 09:44
@aepfli
aepfli force-pushed the feat/provider-tck-flagsmith branch 2 times, most recently from 306466d to d44f20f Compare September 15, 2026 20:34
@aepfli
aepfli force-pushed the feat/provider-tck-flagsmith branch from d44f20f to c00a3b8 Compare September 16, 2026 07:46
@aepfli
aepfli force-pushed the feat/provider-tck-flagsmith branch 2 times, most recently from 5302fdc to e2baaf6 Compare September 21, 2026 11:00
aepfli added 8 commits October 1, 2026 10:10
Experimental adoption. 52 scenarios: 20 pass, 12 fail, 20 skipped.

Eight of the twelve failures are 'expected STATIC but was null' -- this
provider never populates the resolution reason. The values are all
correct; only the reason is missing. 2.2.5 makes it a SHOULD, so null is
arguably permitted, but the Go and Python Flagsmith providers both
populate it against the identical backend.

Two more are float resolution. Flagsmith stores floats as strings,
because feature_state_value is natively boolean, integer or string only.
Go's provider parses the string back; this one type-checks and falls
back to the code default, so GetFloatValue never works against Flagsmith.

The last two are shared with every other language: float-flag and
object-flag requested as a String succeed, because on this backend both
really are strings.

@large-integers is withheld deliberately: Java's accessor is a 32-bit
Integer, so 2^53-1 cannot be asked for. Same reason Java withholds it
for flagd, and it accounts for the extra skip against Go's 19.

The Jackson pin is a workaround for a TCK-introduced conflict, not a
provider defect: provider-tck exports jackson-databind 2.22.1 while
flagsmith-java-client pins jackson-annotations 2.15.2, and the client's
ObjectMapper then dies on JsonSerializeAs. Verified by removing the TCK
dependency, after which the provider's own 35 tests pass.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
Flagsmith's native model is `enabled` plus a value, so the canonical
set's four disabled-* flags map straight onto it. The testbed grew the
flags in the same pass.

56 scenarios: 24 pass, 12 fail, 20 skip. The four new scenarios pass; the
twelve failures are unchanged.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
The tool moved from tools/provider-tck to tools/tck and its package from
dev.openfeature.contrib.tools.providertck to
dev.openfeature.contrib.tools.tck. Dependency and imports follow.

The explicit testcontainers dependency is now load-bearing rather than
redundant: the TCK made its own provided and optional, so an adopter
declares it.

The Jackson pin stays, and was re-tested rather than assumed -- removing
it still fails with ClassNotFoundException on JsonSerializeAs, so the
TCK still exports databind 2.22.1 against the client's annotations
2.15.2. The comment is corrected: it is compile scope and has to be,
because the provider's own main source imports
com.fasterxml.jackson.databind. Saying 'test scope only' was wrong.

56 scenarios: 24 pass, 12 fail, 20 skip. Unchanged by the rebase.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
…onvention

The compose file moves to src/test/resources/tck/docker-compose.yaml,
the path the TCK javadoc uses, and the image is pinned to 0.1.0 rather
than :latest.

Pinning is the substantive half. Four language adoptions pull this image,
and a mutable tag lets a push to the testbed change four pull requests'
results with no diff anywhere to explain it. The tag stays overridable
through FLAGSMITH_TESTBED_IMAGE.

56 scenarios: 24 pass, 12 fail, 20 skip. Unchanged.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
The base added @standard-reasons, and it is the capability that turned
eight of this provider's twelve failures into skips: it never populates
the resolution reason, so every evaluation returns null and not one of
the reasons the capability claims is reported. The resolved values were
correct throughout -- only the reason was missing.

Withheld with a deviation rather than simply skipped, because 2.2.5
makes the reason a SHOULD and null is arguably permitted. What makes it
a defect worth recording is that the Go Flagsmith provider reports
STATIC, DISABLED and TARGETING_MATCH against the identical backend, so
this is a gap rather than a considered choice.

The numeric-coercion deviation is now measured rather than predicted:
this provider behaves like Python, not like Go. float-flag resolves to
the caller default, so reading a float back as a float does not work
against Flagsmith at all.

65 scenarios: 31 pass, 5 fail, 29 skip -- up from 24/12/20. The five
remaining are two unreadable floats, the shared type-system pair, and
object-flag not resolving as a structure.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
The base corrected the worked example that taught withhold-plus-deviate,
and this adoption was doing exactly what the old example illustrated.
KnownDeviation now states the rule plainly: withholding a capability in
order to turn a failing scenario into a skip is the failure mode the
field exists to prevent.

@standard-reasons moves from withheld to declared. The previous pass
withheld it precisely to turn eight failures into skips, which is the
move the rule names. This provider does build a resolution and simply
leaves the reason out, so running those scenarios establishes something
real, and the failures belong in the results with the deviation attached.

@numeric-coercion moves the same way, for the same reason: the provider
has the accessors and gets the answer wrong rather than declining to
answer.

Adds an ungated deviation, with a null capability, for the two mandatory
rows that fail because Flagsmith stores floats and objects as strings.
That form exists for a gap against a scenario belonging to no tag.

65 scenarios: 35 pass, 13 fail, 17 skip -- from 31/5/29. Twelve more
scenarios run; four of them pass and eight fail visibly, which is the
point of the change.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
Three things were written here that are documented elsewhere: the rule
for when a deviation is the right shape (tck.KnownDeviation owns it), the
control API's inability to hand connection parameters to a provider, and
the backend's design and cross-language results (the testbed's README and
FINDINGS own those).

Each is now a sentence and a link. What stays is what only this adoption
knows: which capabilities this provider has, which it gets wrong, and
why. The deviation summaries stay verbose on purpose -- they travel into
a conformance report read across languages, so they have to stand alone.

No behaviour change; the suite reports the same numbers.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
The deviation said the provider never populates the reason. Reading the
source rather than the run output: it returns DISABLED for a disabled
flag and leaves the reason null on every successful resolution, so STATIC
and TARGETING_MATCH are the ones never reported. DISABLED is one of the
reasons the capability claims, and saying otherwise understated what the
provider does.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli added 3 commits October 1, 2026 10:10
…eplaces

This is the backend the capability was created for, and the run says so rather
than the prediction. With the tag declared: two of its four scenarios fail and
two pass. float-flag through the String accessor resolves to "0.5" and
object-flag to its raw JSON text, while boolean-flag and integer-flag report
TYPE_MISMATCH correctly. The split is exactly Flagsmith's type system --
feature_state_value is natively boolean, integer or string, so a boolean really
is a boolean and an integer really is an integer, while a float and a structure
have no native type and are stored as strings. Asked for as strings, they are
returned, and that is the resolved flag value.

Withheld rather than declared-and-failing, which is the opposite call from
@numeric-coercion beside it. Appendix F's scenario-level rule is subordinate to
a prior question -- whether an answer is owed -- and here none is: TYPE_MISMATCH
is obliged by no requirement, and the only normative statement about value type
is Requirement 1.3.4, a SHOULD on the client. Where the specification permits
declining, withholding is the honest report however askable the scenarios are.
@numeric-coercion stays declared because flagd's ADR is a rule this suite binds
providers to; nothing binds Flagsmith to report a type its backend lacks.

The deviation that recorded these same two scenarios goes with it. It existed
because they were mandatory and failing, and it called the question open; the
pin answers it. Keeping it would assert a defect the specification says is not
one, which is precisely what a knownDeviations entry must not do.

Measured: 65 scenarios, 33 passing, 21 skipped, 11 failing -- against 35
passing, 17 skipped and 13 failing with the tag declared. The four scenarios
now skip naming @string-typing, two of which this provider had been getting
right; that cost is recorded in the javadoc rather than hidden, since the tag
is the unit of declaration and this backend's typing is not uniform across the
four flags.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
The previous pass withheld one tag over all four scenarios and wrote down
what that cost: two of the four were skipped despite this provider getting
them right, because the tag was the unit of declaration and Flagsmith's
typing is not uniform across the four flags. That note ended "revisit if the
capability is ever split by flag type". Specification revision bda599f1
split it, citing this measurement, so this is the revisit.

feature_state_value is natively boolean, integer or string only. A boolean
flag really is a boolean and an integer really is an integer, so
@string-typing is declared and its two rows are held to. A float and a
structure have no native type and are stored as strings, so asked for as
strings they are returned -- there is no mismatch to report, and
@fully-typed-values is withheld. Withheld rather than declared-and-failing
for the reason the file already argued: TYPE_MISMATCH is obliged by no
requirement, so no answer is owed and a deviation would assert a defect that
does not exist.

Measured, both sides of the re-pin: 65 scenarios either way, 33 passing / 11
failing / 21 skipped before, 35 / 11 / 19 after. The two scenarios that
started running are the boolean and integer rows and both pass; the failure
count does not move. The skips now attribute as VARIANTS 8, LIFECYCLE 6,
FULLY_TYPED_VALUES 2, EVENTS 2 and LARGE_INTEGERS 1 -- where the withheld
capability was answering for four scenarios it now answers for exactly the
two whose question this backend cannot be asked.

Also fixes a contradiction this file carried: the numeric-coercion deviation
said the capability was "withheld pending the run" while capabilities()
declared it, and its indentation had drifted out of line. The run has since
happened, so it now records what was measured -- Java resembles Python
rather than Go and does not parse Flagsmith's string-stored float, which
fails both lossless coercion scenarios and the untagged float scenario with
the caller's default. Declared and left failing, because the provider does
attempt the coercion.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
173 lines to 132. The string-typing note argued the case for the
capability split across five paragraphs; the split has landed, so one
paragraph saying which half this backend answers is enough. The
deviation summaries lose their narrative and keep their claim.

No behaviour change.

Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
@aepfli
aepfli force-pushed the feat/provider-tck-flagsmith branch from e2baaf6 to f91d8eb Compare October 1, 2026 08:11

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants