Conversation
A flag value can survive a round trip through the wrong numeric type and come back subtly wrong, and nothing here noticed. 2147483647 routed through a 32-bit float returns 2147483648. 9007199254740991 through anything narrower than a double is rounded. A float that happens to be integral, 10.0, can arrive as the integer 10 and look correct until something divides by it. So three flags, and scenarios in both harnesses that would catch each case: large-integer-flag 2^31 - 1, the largest 32-bit signed integer huge-integer-flag 2^53 - 1, the largest integer a double holds exactly integral-float-flag 10.0, a float whose value is integral The flags file is picked up by the default configuration automatically, since that configuration is every file in flags/ combined. huge-integer-flag is tagged @large-integers separately from the other two, because a language whose integer type is 32 bits cannot ask for that value at all -- excluding the tag is the honest answer there, rather than a failure. The rest are tagged @precision so a provider that cannot pass them yet can exclude them and migrate, which is what the tagging scheme in the README is for. The variant names are max-int32, max-safe and ten because those are the names the OpenFeature provider conformance suite asserts (open-feature/spec#423). The same reasoning applies as for the falsy flags: this harness and that suite describe the same backend, and two vocabularies for one flag set is how they drift apart. A backend serving this configuration now satisfies that suite's precision scenarios without transcribing anything. No coercion scenarios here. Whether a provider may return a float through an integer accessor is unsettled in the specification (open-feature/spec#430) and is capability-gated where it is tested, so it does not belong in a harness every SDK runs by default. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (4)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe change adds precision-focused flag fixtures and Gherkin scenarios. Tests cover 32-bit integers, safe integers up to ChangesNumeric precision validation
Priority: ⬇️ Low Estimated code review effort: 2 (Simple) | ~10 minutes Change: Other Suggested reviewers: Merge Risk: ⚪ Minimal · up to The precision coverage additions are internally consistent and introduce no actionable merge risk. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
|
Sibling: #393 adds type-mismatch scenarios to both harnesses. The two are independent — different flags, different features, no shared lines — so either can merge first. Both came out of the same exercise: running the cross-language provider conformance suite (open-feature/spec#417) against this testbed for the first time. |
…rest @lifecycle was withheld, and that was the expensive mistake in this adoption rather than a cautious one. The provider implements openfeature.StateHandler, Init builds the client and blocks until the event stream is up or the deadline expires, and Init can and does fail -- so the lifecycle scenarios assert something real here. Withholding the tag made Go blind to six scenarios Java was running, which is why Java ran 36 of the 40 and Go ran 29. Declaring it costs nothing now. Measured, per resolver: flagd-rpc 40 scenarios, 6 gated skips, 34 executed, 33 passed, 1 failed flagd-in-process 40 scenarios, 5 gated skips, 35 executed, 34 passed, 1 failed The one failure in each is "A large integer resolves without loss of precision", which fails with FLAG_NOT_FOUND because large-integer-flag is absent from flagd-testbed. open-feature/flagd-testbed#392 adds it. It says nothing about the provider and gets no deviation entry, because the gap is in the fixture and an entry there would attribute it to the provider. Two capabilities are withheld without a deviation entry, and the reason in both cases is that the specification permits the behaviour rather than requiring it. This is the correction this commit is really about: @reinitialization is undeclared. "A provider that was shut down can be initialized again" fails in both resolvers -- Shutdown clears the initialised flag, the second Init proceeds, and what it waits for never arrives, so it returns "provider initialization deadline exceeded". Requirement 2.5.2 says a provider SHOULD revert to its uninitialized state and its supporting text says "some providers MAY allow reinitialization from this state". Permitted, not required. The scenario is gated as of spec fc99d5ac and is now reported as skipped. An earlier draft of this file recorded it as a known deviation against tck.Lifecycle, which would have published a defect claim against a provider exercising a choice the specification offers it. @Stale stays undeclared on RPC, and that too is now a design choice rather than a defect. Requirement 5.1.1's supporting text offers both behaviours in one breath: a provider unable to evaluate flags "can" signal that with PROVIDER_ERROR, and one that caches rule-sets or evaluations "can" signal PROVIDER_STALE. The RPC resolver takes the first -- losing the stream sends of.ProviderError directly, and the SDK then short-circuits to the code default rather than serving cached values. Confirmed by running rather than by reading: declaring @Stale on RPC fails with "timed out after 15s waiting for a PROVIDER_STALE event". The in-process resolver does emit it and passes the scenario in 12.2s, so @Stale is declared there. That two resolvers of one provider report an outage differently is worth knowing and is written down in the file; it is not a conformance gap. JS and Java declare @Stale for both resolvers. On this evidence that is a vacuous declaration for RPC, which is a reason to leave it withheld here rather than to copy them. One capability keeps its deviation entry, and it is worth saying why it survives the same check: @numeric-coercion's rule is not in the specification at all (open-feature/spec#430), but flagd accepted it for itself in an ADR and has open-feature/flagd#1996 open to implement it. The summary now says so, so a consumer does not read the entry as a specification violation. Also fixed: Config.KnownDeviations was never populated. The per-suite knownDeviations field was collected and then dropped, because the call site passed Capabilities and not KnownDeviations -- the quietest possible way for a report to lose the one field that tells a defect apart from a design choice, and it read as wired because the struct field was filled in. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
Running this suite at spec fc99d5ac produces 12 or 13 failures, and a different set each time, so anyone reading a result needs to know which of them say anything about the provider. Two do not, and the rest are not even about the provider. Stable, and gaps in the fixture: integral-float-flag and large-integer-flag are absent from flagd-testbed, so the scenarios that ask for them fail with FLAG_NOT_FOUND against any provider at all. number-zero-flag and huge-integer-flag are missing too, though those are gated or unasked here. open-feature/flagd-testbed#392 adds them. No deviation entry, because the gap is in the fixture and an entry there would attribute it to the provider. Everything else moves between runs. Two consecutive runs gave 13 then 12 failures with almost disjoint failing sets, every one FLAG_NOT_FOUND on a flag the testbed certainly has -- boolean-flag, string-zero-flag, object-flag. The cause is the control API rather than this file: the per-scenario reset calls POST /start, which stops flagd, deletes the combined flag file, regenerates it, restarts flagd and polls :8014/readyz -- and flagd answers readyz before its file source has loaded the flags. A provider with a lifecycle never notices, because its own Init blocks until the stream is up or the sync completes and the flags have arrived by then; both flagd suites are stable against the same backend at the same revision, which is the control. A stateless provider evaluates the instant POST /start returns and races the load. So POST /start returning before the backend serves flags makes the reset unusable by precisely the providers that have no way to wait for it. Adding a sleep or a retry here would hide that from every other language's adoption, so it is written down and left alone. @numeric-coercion is kept, and now deliberately rather than by inertia. The comment claiming the lossless direction had no scenario is stale -- integral-float-flag added two, so the tag is a stronger claim than when it was first declared. One of the two cannot be answered by this backend, but the other two scenarios pass, and withholding the tag would also skip the lossy one. That is the scenario worth keeping: narrowing 0.5 to 0 with no error code is the failure mode flagd has and this provider does not. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
…s not a deviation Three of the flags the suite's assets added are absent from flagd-testbed v3.8.0: large-integer-flag, huge-integer-flag and integral-float-flag. Only the first is reached by a scenario that runs here -- huge-integer-flag is asked for solely under @large-integers, which is not applicable in Java, and integral-float-flag solely under @numeric-coercion, which this provider withholds -- so exactly one untagged scenario, the 32-bit precision one, fails FLAG_NOT_FOUND in both modes. open-feature/flagd-testbed#392 is open for it; the Compose tag gets bumped when it lands, which is why the note lives next to the tag as well as in the class. A missing flag is a gap in the stack, not in the provider, so it is documented rather than declared as a KnownDeviation. A deviation says the provider is wrong, and the provider was never given the flag to get wrong. The three falsy flags used to fail the same way and no longer do, which is worth writing down because the failure looked identical. The testbed's zero-flags.json already served boolean-zero-flag, integer-zero-flag and string-zero-flag with zero/non-zero variants, while the canonical set called them false-flag, zero-flag and empty-string-flag; the base moved the canonical names onto the testbed's rather than the other way round, so those three scenarios now resolve against flags that were always there. Also says why capabilities() calls declarableExcept rather than EnumSet.complementOf, which now matters more than it did: the complement would claim @large-integers as well as the two reserved tags, and the suite refuses that declaration at startup. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
…s not a deviation Three of the flags the suite's assets added are absent from flagd-testbed v3.8.0: large-integer-flag, huge-integer-flag and integral-float-flag. Only the first is reached by a scenario that runs here -- huge-integer-flag is asked for solely under @large-integers, which is not applicable in Java, and integral-float-flag solely under @numeric-coercion, which this provider withholds -- so exactly one untagged scenario, the 32-bit precision one, fails FLAG_NOT_FOUND in both modes. open-feature/flagd-testbed#392 is open for it; the Compose tag gets bumped when it lands, which is why the note lives next to the tag as well as in the class. A missing flag is a gap in the stack, not in the provider, so it is documented rather than declared as a KnownDeviation. A deviation says the provider is wrong, and the provider was never given the flag to get wrong. The three falsy flags used to fail the same way and no longer do, which is worth writing down because the failure looked identical. The testbed's zero-flags.json already served boolean-zero-flag, integer-zero-flag and string-zero-flag with zero/non-zero variants, while the canonical set called them false-flag, zero-flag and empty-string-flag; the base moved the canonical names onto the testbed's rather than the other way round, so those three scenarios now resolve against flags that were always there. Also says why capabilities() calls declarableExcept rather than EnumSet.complementOf, which now matters more than it did: the complement would claim @large-integers as well as the two reserved tags, and the suite refuses that declaration at startup. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
…rest @lifecycle was withheld, and that was the expensive mistake in this adoption rather than a cautious one. The provider implements openfeature.StateHandler, Init builds the client and blocks until the event stream is up or the deadline expires, and Init can and does fail -- so the lifecycle scenarios assert something real here. Withholding the tag made Go blind to six scenarios Java was running, which is why Java ran 36 of the 40 and Go ran 29. Declaring it costs nothing now. Measured, per resolver: flagd-rpc 40 scenarios, 6 gated skips, 34 executed, 33 passed, 1 failed flagd-in-process 40 scenarios, 5 gated skips, 35 executed, 34 passed, 1 failed The one failure in each is "A large integer resolves without loss of precision", which fails with FLAG_NOT_FOUND because large-integer-flag is absent from flagd-testbed. open-feature/flagd-testbed#392 adds it. It says nothing about the provider and gets no deviation entry, because the gap is in the fixture and an entry there would attribute it to the provider. Two capabilities are withheld without a deviation entry, and the reason in both cases is that the specification permits the behaviour rather than requiring it. This is the correction this commit is really about: @reinitialization is undeclared. "A provider that was shut down can be initialized again" fails in both resolvers -- Shutdown clears the initialised flag, the second Init proceeds, and what it waits for never arrives, so it returns "provider initialization deadline exceeded". Requirement 2.5.2 says a provider SHOULD revert to its uninitialized state and its supporting text says "some providers MAY allow reinitialization from this state". Permitted, not required. The scenario is gated as of spec fc99d5ac and is now reported as skipped. An earlier draft of this file recorded it as a known deviation against tck.Lifecycle, which would have published a defect claim against a provider exercising a choice the specification offers it. @Stale stays undeclared on RPC, and that too is now a design choice rather than a defect. Requirement 5.1.1's supporting text offers both behaviours in one breath: a provider unable to evaluate flags "can" signal that with PROVIDER_ERROR, and one that caches rule-sets or evaluations "can" signal PROVIDER_STALE. The RPC resolver takes the first -- losing the stream sends of.ProviderError directly, and the SDK then short-circuits to the code default rather than serving cached values. Confirmed by running rather than by reading: declaring @Stale on RPC fails with "timed out after 15s waiting for a PROVIDER_STALE event". The in-process resolver does emit it and passes the scenario in 12.2s, so @Stale is declared there. That two resolvers of one provider report an outage differently is worth knowing and is written down in the file; it is not a conformance gap. JS and Java declare @Stale for both resolvers. On this evidence that is a vacuous declaration for RPC, which is a reason to leave it withheld here rather than to copy them. One capability keeps its deviation entry, and it is worth saying why it survives the same check: @numeric-coercion's rule is not in the specification at all (open-feature/spec#430), but flagd accepted it for itself in an ADR and has open-feature/flagd#1996 open to implement it. The summary now says so, so a consumer does not read the entry as a specification violation. Also fixed: Config.KnownDeviations was never populated. The per-suite knownDeviations field was collected and then dropped, because the call site passed Capabilities and not KnownDeviations -- the quietest possible way for a report to lose the one field that tells a defect apart from a design choice, and it read as wired because the struct field was filled in. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
Running this suite at spec fc99d5ac produces 12 or 13 failures, and a different set each time, so anyone reading a result needs to know which of them say anything about the provider. Two do not, and the rest are not even about the provider. Stable, and gaps in the fixture: integral-float-flag and large-integer-flag are absent from flagd-testbed, so the scenarios that ask for them fail with FLAG_NOT_FOUND against any provider at all. number-zero-flag and huge-integer-flag are missing too, though those are gated or unasked here. open-feature/flagd-testbed#392 adds them. No deviation entry, because the gap is in the fixture and an entry there would attribute it to the provider. Everything else moves between runs. Two consecutive runs gave 13 then 12 failures with almost disjoint failing sets, every one FLAG_NOT_FOUND on a flag the testbed certainly has -- boolean-flag, string-zero-flag, object-flag. The cause is the control API rather than this file: the per-scenario reset calls POST /start, which stops flagd, deletes the combined flag file, regenerates it, restarts flagd and polls :8014/readyz -- and flagd answers readyz before its file source has loaded the flags. A provider with a lifecycle never notices, because its own Init blocks until the stream is up or the sync completes and the flags have arrived by then; both flagd suites are stable against the same backend at the same revision, which is the control. A stateless provider evaluates the instant POST /start returns and races the load. So POST /start returning before the backend serves flags makes the reset unusable by precisely the providers that have no way to wait for it. Adding a sleep or a retry here would hide that from every other language's adoption, so it is written down and left alone. @numeric-coercion is kept, and now deliberately rather than by inertia. The comment claiming the lossless direction had no scenario is stale -- integral-float-flag added two, so the tag is a stronger claim than when it was first declared. One of the two cannot be answered by this backend, but the other two scenarios pass, and withholding the tag would also skip the lossy one. That is the scenario worth keeping: narrowing 0.5 to 0 with no error code is the failure mode flagd has and this provider does not. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
…s not a deviation Three of the flags the suite's assets added are absent from flagd-testbed v3.8.0: large-integer-flag, huge-integer-flag and integral-float-flag. Only the first is reached by a scenario that runs here -- huge-integer-flag is asked for solely under @large-integers, which is not applicable in Java, and integral-float-flag solely under @numeric-coercion, which this provider withholds -- so exactly one untagged scenario, the 32-bit precision one, fails FLAG_NOT_FOUND in both modes. open-feature/flagd-testbed#392 is open for it; the Compose tag gets bumped when it lands, which is why the note lives next to the tag as well as in the class. A missing flag is a gap in the stack, not in the provider, so it is documented rather than declared as a KnownDeviation. A deviation says the provider is wrong, and the provider was never given the flag to get wrong. The three falsy flags used to fail the same way and no longer do, which is worth writing down because the failure looked identical. The testbed's zero-flags.json already served boolean-zero-flag, integer-zero-flag and string-zero-flag with zero/non-zero variants, while the canonical set called them false-flag, zero-flag and empty-string-flag; the base moved the canonical names onto the testbed's rather than the other way round, so those three scenarios now resolve against flags that were always there. Also says why capabilities() calls declarableExcept rather than EnumSet.complementOf, which now matters more than it did: the complement would claim @large-integers as well as the two reserved tags, and the suite refuses that declaration at startup. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
…rest @lifecycle was withheld, and that was the expensive mistake in this adoption rather than a cautious one. The provider implements openfeature.StateHandler, Init builds the client and blocks until the event stream is up or the deadline expires, and Init can and does fail -- so the lifecycle scenarios assert something real here. Withholding the tag made Go blind to six scenarios Java was running, which is why Java ran 36 of the 40 and Go ran 29. Declaring it costs nothing now. Measured, per resolver: flagd-rpc 40 scenarios, 6 gated skips, 34 executed, 33 passed, 1 failed flagd-in-process 40 scenarios, 5 gated skips, 35 executed, 34 passed, 1 failed The one failure in each is "A large integer resolves without loss of precision", which fails with FLAG_NOT_FOUND because large-integer-flag is absent from flagd-testbed. open-feature/flagd-testbed#392 adds it. It says nothing about the provider and gets no deviation entry, because the gap is in the fixture and an entry there would attribute it to the provider. Two capabilities are withheld without a deviation entry, and the reason in both cases is that the specification permits the behaviour rather than requiring it. This is the correction this commit is really about: @reinitialization is undeclared. "A provider that was shut down can be initialized again" fails in both resolvers -- Shutdown clears the initialised flag, the second Init proceeds, and what it waits for never arrives, so it returns "provider initialization deadline exceeded". Requirement 2.5.2 says a provider SHOULD revert to its uninitialized state and its supporting text says "some providers MAY allow reinitialization from this state". Permitted, not required. The scenario is gated as of spec fc99d5ac and is now reported as skipped. An earlier draft of this file recorded it as a known deviation against tck.Lifecycle, which would have published a defect claim against a provider exercising a choice the specification offers it. @Stale stays undeclared on RPC, and that too is now a design choice rather than a defect. Requirement 5.1.1's supporting text offers both behaviours in one breath: a provider unable to evaluate flags "can" signal that with PROVIDER_ERROR, and one that caches rule-sets or evaluations "can" signal PROVIDER_STALE. The RPC resolver takes the first -- losing the stream sends of.ProviderError directly, and the SDK then short-circuits to the code default rather than serving cached values. Confirmed by running rather than by reading: declaring @Stale on RPC fails with "timed out after 15s waiting for a PROVIDER_STALE event". The in-process resolver does emit it and passes the scenario in 12.2s, so @Stale is declared there. That two resolvers of one provider report an outage differently is worth knowing and is written down in the file; it is not a conformance gap. JS and Java declare @Stale for both resolvers. On this evidence that is a vacuous declaration for RPC, which is a reason to leave it withheld here rather than to copy them. One capability keeps its deviation entry, and it is worth saying why it survives the same check: @numeric-coercion's rule is not in the specification at all (open-feature/spec#430), but flagd accepted it for itself in an ADR and has open-feature/flagd#1996 open to implement it. The summary now says so, so a consumer does not read the entry as a specification violation. Also fixed: Config.KnownDeviations was never populated. The per-suite knownDeviations field was collected and then dropped, because the call site passed Capabilities and not KnownDeviations -- the quietest possible way for a report to lose the one field that tells a defect apart from a design choice, and it read as wired because the struct field was filled in. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
Running this suite at spec fc99d5ac produces 12 or 13 failures, and a different set each time, so anyone reading a result needs to know which of them say anything about the provider. Two do not, and the rest are not even about the provider. Stable, and gaps in the fixture: integral-float-flag and large-integer-flag are absent from flagd-testbed, so the scenarios that ask for them fail with FLAG_NOT_FOUND against any provider at all. number-zero-flag and huge-integer-flag are missing too, though those are gated or unasked here. open-feature/flagd-testbed#392 adds them. No deviation entry, because the gap is in the fixture and an entry there would attribute it to the provider. Everything else moves between runs. Two consecutive runs gave 13 then 12 failures with almost disjoint failing sets, every one FLAG_NOT_FOUND on a flag the testbed certainly has -- boolean-flag, string-zero-flag, object-flag. The cause is the control API rather than this file: the per-scenario reset calls POST /start, which stops flagd, deletes the combined flag file, regenerates it, restarts flagd and polls :8014/readyz -- and flagd answers readyz before its file source has loaded the flags. A provider with a lifecycle never notices, because its own Init blocks until the stream is up or the sync completes and the flags have arrived by then; both flagd suites are stable against the same backend at the same revision, which is the control. A stateless provider evaluates the instant POST /start returns and races the load. So POST /start returning before the backend serves flags makes the reset unusable by precisely the providers that have no way to wait for it. Adding a sleep or a retry here would hide that from every other language's adoption, so it is written down and left alone. @numeric-coercion is kept, and now deliberately rather than by inertia. The comment claiming the lossless direction had no scenario is stale -- integral-float-flag added two, so the tag is a stronger claim than when it was first declared. One of the two cannot be answered by this backend, but the other two scenarios pass, and withholding the tag would also skip the lossy one. That is the scenario worth keeping: narrowing 0.5 to 0 with no error code is the failure mode flagd has and this provider does not. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
…rest @lifecycle was withheld, and that was the expensive mistake in this adoption rather than a cautious one. The provider implements openfeature.StateHandler, Init builds the client and blocks until the event stream is up or the deadline expires, and Init can and does fail -- so the lifecycle scenarios assert something real here. Withholding the tag made Go blind to six scenarios Java was running, which is why Java ran 36 of the 40 and Go ran 29. Declaring it costs nothing now. Measured, per resolver: flagd-rpc 40 scenarios, 6 gated skips, 34 executed, 33 passed, 1 failed flagd-in-process 40 scenarios, 5 gated skips, 35 executed, 34 passed, 1 failed The one failure in each is "A large integer resolves without loss of precision", which fails with FLAG_NOT_FOUND because large-integer-flag is absent from flagd-testbed. open-feature/flagd-testbed#392 adds it. It says nothing about the provider and gets no deviation entry, because the gap is in the fixture and an entry there would attribute it to the provider. Two capabilities are withheld without a deviation entry, and the reason in both cases is that the specification permits the behaviour rather than requiring it. This is the correction this commit is really about: @reinitialization is undeclared. "A provider that was shut down can be initialized again" fails in both resolvers -- Shutdown clears the initialised flag, the second Init proceeds, and what it waits for never arrives, so it returns "provider initialization deadline exceeded". Requirement 2.5.2 says a provider SHOULD revert to its uninitialized state and its supporting text says "some providers MAY allow reinitialization from this state". Permitted, not required. The scenario is gated as of spec fc99d5ac and is now reported as skipped. An earlier draft of this file recorded it as a known deviation against tck.Lifecycle, which would have published a defect claim against a provider exercising a choice the specification offers it. @Stale stays undeclared on RPC, and that too is now a design choice rather than a defect. Requirement 5.1.1's supporting text offers both behaviours in one breath: a provider unable to evaluate flags "can" signal that with PROVIDER_ERROR, and one that caches rule-sets or evaluations "can" signal PROVIDER_STALE. The RPC resolver takes the first -- losing the stream sends of.ProviderError directly, and the SDK then short-circuits to the code default rather than serving cached values. Confirmed by running rather than by reading: declaring @Stale on RPC fails with "timed out after 15s waiting for a PROVIDER_STALE event". The in-process resolver does emit it and passes the scenario in 12.2s, so @Stale is declared there. That two resolvers of one provider report an outage differently is worth knowing and is written down in the file; it is not a conformance gap. JS and Java declare @Stale for both resolvers. On this evidence that is a vacuous declaration for RPC, which is a reason to leave it withheld here rather than to copy them. One capability keeps its deviation entry, and it is worth saying why it survives the same check: @numeric-coercion's rule is not in the specification at all (open-feature/spec#430), but flagd accepted it for itself in an ADR and has open-feature/flagd#1996 open to implement it. The summary now says so, so a consumer does not read the entry as a specification violation. Also fixed: Config.KnownDeviations was never populated. The per-suite knownDeviations field was collected and then dropped, because the call site passed Capabilities and not KnownDeviations -- the quietest possible way for a report to lose the one field that tells a defect apart from a design choice, and it read as wired because the struct field was filled in. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
Running this suite at spec fc99d5ac produces 12 or 13 failures, and a different set each time, so anyone reading a result needs to know which of them say anything about the provider. Two do not, and the rest are not even about the provider. Stable, and gaps in the fixture: integral-float-flag and large-integer-flag are absent from flagd-testbed, so the scenarios that ask for them fail with FLAG_NOT_FOUND against any provider at all. number-zero-flag and huge-integer-flag are missing too, though those are gated or unasked here. open-feature/flagd-testbed#392 adds them. No deviation entry, because the gap is in the fixture and an entry there would attribute it to the provider. Everything else moves between runs. Two consecutive runs gave 13 then 12 failures with almost disjoint failing sets, every one FLAG_NOT_FOUND on a flag the testbed certainly has -- boolean-flag, string-zero-flag, object-flag. The cause is the control API rather than this file: the per-scenario reset calls POST /start, which stops flagd, deletes the combined flag file, regenerates it, restarts flagd and polls :8014/readyz -- and flagd answers readyz before its file source has loaded the flags. A provider with a lifecycle never notices, because its own Init blocks until the stream is up or the sync completes and the flags have arrived by then; both flagd suites are stable against the same backend at the same revision, which is the control. A stateless provider evaluates the instant POST /start returns and races the load. So POST /start returning before the backend serves flags makes the reset unusable by precisely the providers that have no way to wait for it. Adding a sleep or a retry here would hide that from every other language's adoption, so it is written down and left alone. @numeric-coercion is kept, and now deliberately rather than by inertia. The comment claiming the lossless direction had no scenario is stale -- integral-float-flag added two, so the tag is a stronger claim than when it was first declared. One of the two cannot be answered by this backend, but the other two scenarios pass, and withholding the tag would also skip the lossy one. That is the scenario worth keeping: narrowing 0.5 to 0 with no error code is the failure mode flagd has and this provider does not. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
…rest @lifecycle was withheld, and that was the expensive mistake in this adoption rather than a cautious one. The provider implements openfeature.StateHandler, Init builds the client and blocks until the event stream is up or the deadline expires, and Init can and does fail -- so the lifecycle scenarios assert something real here. Withholding the tag made Go blind to six scenarios Java was running, which is why Java ran 36 of the 40 and Go ran 29. Declaring it costs nothing now. Measured, per resolver: flagd-rpc 40 scenarios, 6 gated skips, 34 executed, 33 passed, 1 failed flagd-in-process 40 scenarios, 5 gated skips, 35 executed, 34 passed, 1 failed The one failure in each is "A large integer resolves without loss of precision", which fails with FLAG_NOT_FOUND because large-integer-flag is absent from flagd-testbed. open-feature/flagd-testbed#392 adds it. It says nothing about the provider and gets no deviation entry, because the gap is in the fixture and an entry there would attribute it to the provider. Two capabilities are withheld without a deviation entry, and the reason in both cases is that the specification permits the behaviour rather than requiring it. This is the correction this commit is really about: @reinitialization is undeclared. "A provider that was shut down can be initialized again" fails in both resolvers -- Shutdown clears the initialised flag, the second Init proceeds, and what it waits for never arrives, so it returns "provider initialization deadline exceeded". Requirement 2.5.2 says a provider SHOULD revert to its uninitialized state and its supporting text says "some providers MAY allow reinitialization from this state". Permitted, not required. The scenario is gated as of spec fc99d5ac and is now reported as skipped. An earlier draft of this file recorded it as a known deviation against tck.Lifecycle, which would have published a defect claim against a provider exercising a choice the specification offers it. @Stale stays undeclared on RPC, and that too is now a design choice rather than a defect. Requirement 5.1.1's supporting text offers both behaviours in one breath: a provider unable to evaluate flags "can" signal that with PROVIDER_ERROR, and one that caches rule-sets or evaluations "can" signal PROVIDER_STALE. The RPC resolver takes the first -- losing the stream sends of.ProviderError directly, and the SDK then short-circuits to the code default rather than serving cached values. Confirmed by running rather than by reading: declaring @Stale on RPC fails with "timed out after 15s waiting for a PROVIDER_STALE event". The in-process resolver does emit it and passes the scenario in 12.2s, so @Stale is declared there. That two resolvers of one provider report an outage differently is worth knowing and is written down in the file; it is not a conformance gap. JS and Java declare @Stale for both resolvers. On this evidence that is a vacuous declaration for RPC, which is a reason to leave it withheld here rather than to copy them. One capability keeps its deviation entry, and it is worth saying why it survives the same check: @numeric-coercion's rule is not in the specification at all (open-feature/spec#430), but flagd accepted it for itself in an ADR and has open-feature/flagd#1996 open to implement it. The summary now says so, so a consumer does not read the entry as a specification violation. Also fixed: Config.KnownDeviations was never populated. The per-suite knownDeviations field was collected and then dropped, because the call site passed Capabilities and not KnownDeviations -- the quietest possible way for a report to lose the one field that tells a defect apart from a design choice, and it read as wired because the struct field was filled in. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
… run Follows the TCK's move to spec 26362f85. The e2e module reaches the TCK through a replace, and a replace does not bring the replacement's requirements with it, so this module's own pin is what selects the Gherkin the suite actually runs -- leaving it at fc99d5ac would have run the previous revision's assets against the new vocabulary and reported a spec revision this module does not use. Both capabilities are declared because the scenarios passed, not because flagd plausibly supports them. The run is the whole justification, so the numbers: each resolver goes from 40 canonical scenarios to 52, and each reports 50 passed and 2 failed. @Variants passes seven of its eight rows in both resolvers. The variant name survives the trip from the ruleset through the wire format into ResolutionDetail for booleans, strings, integers, floats and all three falsy flags. The eighth row asks large-integer-flag for its max-int32 variant and receives "", because that flag is absent from flagd-testbed -- the same fixture gap that already fails the untagged large-integer scenario, and the reason tck.LargeIntegers stays undeclared. Withholding the tag over it would skip seven working rows to hide one missing flag, so the comment on the capability list now says the suite carries two fixture failures rather than one. open-feature/flagd-testbed#392 adds the flags and both go away together; neither gets a knownDeviations entry, because the gap is in the fixture and an entry there would attribute it to the provider. @targeting was reserved rather than declarable until this spec revision, on the reasoning that asserting anything about the evaluation context needed an echo endpoint on the control API. It does not: targeting-key-flag carries one JsonLogic rule on the targeting key, so a matching context resolves to a different value than a non-matching one or none at all, and a provider that dropped the context is caught by the resolved value itself. All three scenarios pass in both resolvers, and running both was not a formality -- in-process evaluates the rule itself while RPC has flagd evaluate it, so agreement between them is evidence rather than duplication. The flag has been in flagd-testbed since flagd-testbed#103, released in v0.5.1 in February 2024, so unlike tck.LargeIntegers this needs no image bump. The new mandatory scenario that supplies an evaluation context to an untargeted flag passes in both resolvers too, which is worth stating because it is the half of requirement 2.2.1 that nothing here was testing: every resolve method takes a context, no scenario supplied one, and a provider that threw on any context or serialised it into a malformed request would have passed the whole suite. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
Running this suite at spec fc99d5ac produces 12 or 13 failures, and a different set each time, so anyone reading a result needs to know which of them say anything about the provider. Two do not, and the rest are not even about the provider. Stable, and gaps in the fixture: integral-float-flag and large-integer-flag are absent from flagd-testbed, so the scenarios that ask for them fail with FLAG_NOT_FOUND against any provider at all. number-zero-flag and huge-integer-flag are missing too, though those are gated or unasked here. open-feature/flagd-testbed#392 adds them. No deviation entry, because the gap is in the fixture and an entry there would attribute it to the provider. Everything else moves between runs. Two consecutive runs gave 13 then 12 failures with almost disjoint failing sets, every one FLAG_NOT_FOUND on a flag the testbed certainly has -- boolean-flag, string-zero-flag, object-flag. The cause is the control API rather than this file: the per-scenario reset calls POST /start, which stops flagd, deletes the combined flag file, regenerates it, restarts flagd and polls :8014/readyz -- and flagd answers readyz before its file source has loaded the flags. A provider with a lifecycle never notices, because its own Init blocks until the stream is up or the sync completes and the flags have arrived by then; both flagd suites are stable against the same backend at the same revision, which is the control. A stateless provider evaluates the instant POST /start returns and races the load. So POST /start returning before the backend serves flags makes the reset unusable by precisely the providers that have no way to wait for it. Adding a sleep or a retry here would hide that from every other language's adoption, so it is written down and left alone. @numeric-coercion is kept, and now deliberately rather than by inertia. The comment claiming the lossless direction had no scenario is stale -- integral-float-flag added two, so the tag is a stronger claim than when it was first declared. One of the two cannot be answered by this backend, but the other two scenarios pass, and withholding the tag would also skip the lossy one. That is the scenario worth keeping: narrowing 0.5 to 0 with no error code is the failure mode flagd has and this provider does not. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
This module reaches the TCK through a replace, and a replace does not bring the replacement's requirements with it, so its own requirement is what selects the Gherkin the suite runs. Left at fc99d5ac it would have run the previous revision's assets while reporting conformance against a revision it does not use -- the drift the flagd e2e module has the same one-line guard against. Both capabilities are declared on the run rather than on the code. The suite is documented above as non-deterministic, because POST /start on the launchpad returns before flagd serves flags and a stateless provider races the load, so one green run would not be evidence of anything. Three consecutive runs gave the identical result: 52 scenarios, 9 skipped as undeclared, 43 run, 40 passed, 3 failed, with the same three failures every time. @Variants passes seven of its eight rows. OFREP's evaluation response carries a variant field and this provider passes it straight into ResolutionDetail, for booleans, strings, integers, floats and all three falsy flags. The eighth row asks large-integer-flag for its max-int32 variant and receives "", because that flag is absent from flagd-testbed -- the same absence that already fails the untagged large-integer scenario, now counted twice rather than a new defect. The third stable failure is integral-float-flag, absent for the same reason. All three are fixture gaps, all three go green with open-feature/flagd-testbed#392, and none gets a knownDeviations entry, because an entry there would attribute the fixture's gap to the provider. @targeting was reserved rather than declarable until this spec revision. For a JSON-over-HTTP provider it is the cheapest capability here to get right, because the evaluation context IS the request body and there is no separate passthrough path to get wrong. All three scenarios pass -- targeting-key-flag resolves to "hit" for the matching targeting key and "miss" for a non-matching one or none at all -- and so does the new untagged scenario that supplies a context to an untargeted flag. Worth having precisely because of how this provider is built: until this revision no scenario supplied a context at all, so one that serialised it into a malformed body would have passed the whole suite. The README's capability table gains rows for both, and its counts are corrected while they are being touched: it claimed 24 of 29 scenarios and that @events gated lifecycle.feature, when @lifecycle now does and @lifecycle and @large-integers were missing from the table entirely. Its "Known asymmetry" section claimed no scenario covers integer-flag requested as a float; one does now, it is declared under @numeric-coercion, and it passes -- so the asymmetry is deliberate rather than untested. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
… have The tag was declared and two of its three scenarios failed. errors.feature says a declarer must satisfy all three, and says why: the two lossless rows exist to catch the shortcut of rejecting every float. This provider takes that shortcut. json.loads yields `int` for 10 and `float` for 0.5, and the check at ofrep/__init__.py:249-256 admits a value only on an exact isinstance against one of them. Nothing in that path widens or narrows a number. So the lossy row passes -- float-flag asked for as an Integer is a TYPE_MISMATCH rather than a silent 0 -- and integer-flag asked for as a Float fails, because 10 is not an instance of float. That is the same architecture as the Java OFREP adoption, which withholds the tag for the same reason. The Go adoption declares it, and the difference is the JSON decoder rather than a decision either author made: encoding/json makes every JSON number a float64, so integer-ness never survives the wire and ResolveInt has to round-trip through int64 -- which yields lossless coercion, and TYPE_MISMATCH on loss, for free. Over OFREP this capability follows the language's JSON library. No knownDeviation accompanies the withdrawal. A deviation records a gap in behaviour a provider is required to have, and numeric coercion is a declared capability rather than a requirement. The honest record is the undeclared tag and the three skips it produces. Measured before and after, 52 scenarios both times: 4 failed / 38 passed / 9 skipped / 1 xfailed becomes 2 failed / 37 passed / 12 skipped / 1 xfailed. The two remaining failures are both large-integer-flag, which the pinned testbed does not serve (open-feature/flagd-testbed#392). The header also claimed every declared capability had been seen to pass, which was untrue while this tag was declared, and the closing note claimed the withheld set matched Go's as well as Java's. Both corrected. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
Running this suite at spec fc99d5ac produces 12 or 13 failures, and a different set each time, so anyone reading a result needs to know which of them say anything about the provider. Two do not, and the rest are not even about the provider. Stable, and gaps in the fixture: integral-float-flag and large-integer-flag are absent from flagd-testbed, so the scenarios that ask for them fail with FLAG_NOT_FOUND against any provider at all. number-zero-flag and huge-integer-flag are missing too, though those are gated or unasked here. open-feature/flagd-testbed#392 adds them. No deviation entry, because the gap is in the fixture and an entry there would attribute it to the provider. Everything else moves between runs. Two consecutive runs gave 13 then 12 failures with almost disjoint failing sets, every one FLAG_NOT_FOUND on a flag the testbed certainly has -- boolean-flag, string-zero-flag, object-flag. The cause is the control API rather than this file: the per-scenario reset calls POST /start, which stops flagd, deletes the combined flag file, regenerates it, restarts flagd and polls :8014/readyz -- and flagd answers readyz before its file source has loaded the flags. A provider with a lifecycle never notices, because its own Init blocks until the stream is up or the sync completes and the flags have arrived by then; both flagd suites are stable against the same backend at the same revision, which is the control. A stateless provider evaluates the instant POST /start returns and races the load. So POST /start returning before the backend serves flags makes the reset unusable by precisely the providers that have no way to wait for it. Adding a sleep or a retry here would hide that from every other language's adoption, so it is written down and left alone. @numeric-coercion is kept, and now deliberately rather than by inertia. The comment claiming the lossless direction had no scenario is stale -- integral-float-flag added two, so the tag is a stronger claim than when it was first declared. One of the two cannot be answered by this backend, but the other two scenarios pass, and withholding the tag would also skip the lossy one. That is the scenario worth keeping: narrowing 0.5 to 0 with no error code is the failure mode flagd has and this provider does not. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
This module reaches the TCK through a replace, and a replace does not bring the replacement's requirements with it, so its own requirement is what selects the Gherkin the suite runs. Left at fc99d5ac it would have run the previous revision's assets while reporting conformance against a revision it does not use -- the drift the flagd e2e module has the same one-line guard against. Both capabilities are declared on the run rather than on the code. The suite is documented above as non-deterministic, because POST /start on the launchpad returns before flagd serves flags and a stateless provider races the load, so one green run would not be evidence of anything. Three consecutive runs gave the identical result: 52 scenarios, 9 skipped as undeclared, 43 run, 40 passed, 3 failed, with the same three failures every time. @Variants passes seven of its eight rows. OFREP's evaluation response carries a variant field and this provider passes it straight into ResolutionDetail, for booleans, strings, integers, floats and all three falsy flags. The eighth row asks large-integer-flag for its max-int32 variant and receives "", because that flag is absent from flagd-testbed -- the same absence that already fails the untagged large-integer scenario, now counted twice rather than a new defect. The third stable failure is integral-float-flag, absent for the same reason. All three are fixture gaps, all three go green with open-feature/flagd-testbed#392, and none gets a knownDeviations entry, because an entry there would attribute the fixture's gap to the provider. @targeting was reserved rather than declarable until this spec revision. For a JSON-over-HTTP provider it is the cheapest capability here to get right, because the evaluation context IS the request body and there is no separate passthrough path to get wrong. All three scenarios pass -- targeting-key-flag resolves to "hit" for the matching targeting key and "miss" for a non-matching one or none at all -- and so does the new untagged scenario that supplies a context to an untargeted flag. Worth having precisely because of how this provider is built: until this revision no scenario supplied a context at all, so one that serialised it into a malformed body would have passed the whole suite. The README's capability table gains rows for both, and its counts are corrected while they are being touched: it claimed 24 of 29 scenarios and that @events gated lifecycle.feature, when @lifecycle now does and @lifecycle and @large-integers were missing from the table entirely. Its "Known asymmetry" section claimed no scenario covers integer-flag requested as a float; one does now, it is declared under @numeric-coercion, and it passes -- so the asymmetry is deliberate rather than untested. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
…s not a deviation Three of the flags the suite's assets added are absent from flagd-testbed v3.8.0: large-integer-flag, huge-integer-flag and integral-float-flag. Only the first is reached by a scenario that runs here -- huge-integer-flag is asked for solely under @large-integers, which is not applicable in Java, and integral-float-flag solely under @numeric-coercion, which this provider withholds -- so exactly one untagged scenario, the 32-bit precision one, fails FLAG_NOT_FOUND in both modes. open-feature/flagd-testbed#392 is open for it; the Compose tag gets bumped when it lands, which is why the note lives next to the tag as well as in the class. A missing flag is a gap in the stack, not in the provider, so it is documented rather than declared as a KnownDeviation. A deviation says the provider is wrong, and the provider was never given the flag to get wrong. The three falsy flags used to fail the same way and no longer do, which is worth writing down because the failure looked identical. The testbed's zero-flags.json already served boolean-zero-flag, integer-zero-flag and string-zero-flag with zero/non-zero variants, while the canonical set called them false-flag, zero-flag and empty-string-flag; the base moved the canonical names onto the testbed's rather than the other way round, so those three scenarios now resolve against flags that were always there. Also says why capabilities() calls declarableExcept rather than EnumSet.complementOf, which now matters more than it did: the complement would claim @large-integers as well as the two reserved tags, and the suite refuses that declaration at startup. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
… have The tag was declared and two of its three scenarios failed. errors.feature says a declarer must satisfy all three, and says why: the two lossless rows exist to catch the shortcut of rejecting every float. This provider takes that shortcut. json.loads yields `int` for 10 and `float` for 0.5, and the check at ofrep/__init__.py:249-256 admits a value only on an exact isinstance against one of them. Nothing in that path widens or narrows a number. So the lossy row passes -- float-flag asked for as an Integer is a TYPE_MISMATCH rather than a silent 0 -- and integer-flag asked for as a Float fails, because 10 is not an instance of float. That is the same architecture as the Java OFREP adoption, which withholds the tag for the same reason. The Go adoption declares it, and the difference is the JSON decoder rather than a decision either author made: encoding/json makes every JSON number a float64, so integer-ness never survives the wire and ResolveInt has to round-trip through int64 -- which yields lossless coercion, and TYPE_MISMATCH on loss, for free. Over OFREP this capability follows the language's JSON library. No knownDeviation accompanies the withdrawal. A deviation records a gap in behaviour a provider is required to have, and numeric coercion is a declared capability rather than a requirement. The honest record is the undeclared tag and the three skips it produces. Measured before and after, 52 scenarios both times: 4 failed / 38 passed / 9 skipped / 1 xfailed becomes 2 failed / 37 passed / 12 skipped / 1 xfailed. The two remaining failures are both large-integer-flag, which the pinned testbed does not serve (open-feature/flagd-testbed#392). The header also claimed every declared capability had been seen to pass, which was untrue while this tag was declared, and the closing note claimed the withheld set matched Go's as well as Java's. Both corrected. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
Running this suite at spec fc99d5ac produces 12 or 13 failures, and a different set each time, so anyone reading a result needs to know which of them say anything about the provider. Two do not, and the rest are not even about the provider. Stable, and gaps in the fixture: integral-float-flag and large-integer-flag are absent from flagd-testbed, so the scenarios that ask for them fail with FLAG_NOT_FOUND against any provider at all. number-zero-flag and huge-integer-flag are missing too, though those are gated or unasked here. open-feature/flagd-testbed#392 adds them. No deviation entry, because the gap is in the fixture and an entry there would attribute it to the provider. Everything else moves between runs. Two consecutive runs gave 13 then 12 failures with almost disjoint failing sets, every one FLAG_NOT_FOUND on a flag the testbed certainly has -- boolean-flag, string-zero-flag, object-flag. The cause is the control API rather than this file: the per-scenario reset calls POST /start, which stops flagd, deletes the combined flag file, regenerates it, restarts flagd and polls :8014/readyz -- and flagd answers readyz before its file source has loaded the flags. A provider with a lifecycle never notices, because its own Init blocks until the stream is up or the sync completes and the flags have arrived by then; both flagd suites are stable against the same backend at the same revision, which is the control. A stateless provider evaluates the instant POST /start returns and races the load. So POST /start returning before the backend serves flags makes the reset unusable by precisely the providers that have no way to wait for it. Adding a sleep or a retry here would hide that from every other language's adoption, so it is written down and left alone. @numeric-coercion is kept, and now deliberately rather than by inertia. The comment claiming the lossless direction had no scenario is stale -- integral-float-flag added two, so the tag is a stronger claim than when it was first declared. One of the two cannot be answered by this backend, but the other two scenarios pass, and withholding the tag would also skip the lossy one. That is the scenario worth keeping: narrowing 0.5 to 0 with no error code is the failure mode flagd has and this provider does not. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
This module reaches the TCK through a replace, and a replace does not bring the replacement's requirements with it, so its own requirement is what selects the Gherkin the suite runs. Left at fc99d5ac it would have run the previous revision's assets while reporting conformance against a revision it does not use -- the drift the flagd e2e module has the same one-line guard against. Both capabilities are declared on the run rather than on the code. The suite is documented above as non-deterministic, because POST /start on the launchpad returns before flagd serves flags and a stateless provider races the load, so one green run would not be evidence of anything. Three consecutive runs gave the identical result: 52 scenarios, 9 skipped as undeclared, 43 run, 40 passed, 3 failed, with the same three failures every time. @Variants passes seven of its eight rows. OFREP's evaluation response carries a variant field and this provider passes it straight into ResolutionDetail, for booleans, strings, integers, floats and all three falsy flags. The eighth row asks large-integer-flag for its max-int32 variant and receives "", because that flag is absent from flagd-testbed -- the same absence that already fails the untagged large-integer scenario, now counted twice rather than a new defect. The third stable failure is integral-float-flag, absent for the same reason. All three are fixture gaps, all three go green with open-feature/flagd-testbed#392, and none gets a knownDeviations entry, because an entry there would attribute the fixture's gap to the provider. @targeting was reserved rather than declarable until this spec revision. For a JSON-over-HTTP provider it is the cheapest capability here to get right, because the evaluation context IS the request body and there is no separate passthrough path to get wrong. All three scenarios pass -- targeting-key-flag resolves to "hit" for the matching targeting key and "miss" for a non-matching one or none at all -- and so does the new untagged scenario that supplies a context to an untargeted flag. Worth having precisely because of how this provider is built: until this revision no scenario supplied a context at all, so one that serialised it into a malformed body would have passed the whole suite. The README's capability table gains rows for both, and its counts are corrected while they are being touched: it claimed 24 of 29 scenarios and that @events gated lifecycle.feature, when @lifecycle now does and @lifecycle and @large-integers were missing from the table entirely. Its "Known asymmetry" section claimed no scenario covers integer-flag requested as a float; one does now, it is declared under @numeric-coercion, and it passes -- so the asymmetry is deliberate rather than untested. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
… have The tag was declared and two of its three scenarios failed. errors.feature says a declarer must satisfy all three, and says why: the two lossless rows exist to catch the shortcut of rejecting every float. This provider takes that shortcut. json.loads yields `int` for 10 and `float` for 0.5, and the check at ofrep/__init__.py:249-256 admits a value only on an exact isinstance against one of them. Nothing in that path widens or narrows a number. So the lossy row passes -- float-flag asked for as an Integer is a TYPE_MISMATCH rather than a silent 0 -- and integer-flag asked for as a Float fails, because 10 is not an instance of float. That is the same architecture as the Java OFREP adoption, which withholds the tag for the same reason. The Go adoption declares it, and the difference is the JSON decoder rather than a decision either author made: encoding/json makes every JSON number a float64, so integer-ness never survives the wire and ResolveInt has to round-trip through int64 -- which yields lossless coercion, and TYPE_MISMATCH on loss, for free. Over OFREP this capability follows the language's JSON library. No knownDeviation accompanies the withdrawal. A deviation records a gap in behaviour a provider is required to have, and numeric coercion is a declared capability rather than a requirement. The honest record is the undeclared tag and the three skips it produces. Measured before and after, 52 scenarios both times: 4 failed / 38 passed / 9 skipped / 1 xfailed becomes 2 failed / 37 passed / 12 skipped / 1 xfailed. The two remaining failures are both large-integer-flag, which the pinned testbed does not serve (open-feature/flagd-testbed#392). The header also claimed every declared capability had been seen to pass, which was untrue while this tag was declared, and the closing note claimed the withheld set matched Go's as well as Java's. Both corrected. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
This was the last of the eight adoptions across four languages still driving
containers by hand, which made it the one adoption that did not answer the
question the compose work exists to answer: whether the harness is sufficient.
It is. There is no container code in tck_test.go any more, and the module no
longer requires testcontainers-go directly -- it arrives as an indirect
dependency of tools/tck.
Deleted: startTestbed, which called compose.NewDockerCompose itself, created a
temporary flags directory and passed it in as FLAGS_DIR because the testbed
submodule's compose file bind-mounts ${FLAGS_DIR} and an unset value defaults to
the submodule's own directory, registered two t.Cleanups, declared its own wait
strategy, looked up two mapped ports by string, built the tck.HTTPControl
itself, hard-coded localhost as the host and slept two seconds for the
launchpad. What replaces it is three options -- WithComposeFile, WithBackendPorts
and WithProviderFromEndpoint -- and a Compose file of six non-comment lines.
95 lines of adopter-written code become 43 plus 6 of YAML. endpoint.Host() is
also correct where the hard-coded localhost was only usually correct: with a
remote Docker daemon, Docker Desktop on some platforms or a rootless setup the
host is not localhost.
The Compose file is this package's own rather than the testbed submodule's, for
the same reasons the flagd adoption's is: no ${FLAGS_DIR} bind mount to work
around, no envoy sidecar, and a service called "backend", which is the TCK's
default and the name both the flagd adoption here and Java's use. It publishes
8016 and 8080 only, since nothing here speaks flagd's gRPC protocols. A welcome
consequence is that this suite no longer needs the submodule checked out at all.
Nothing the harness could not express. The one thing worth reporting is what it
made unnecessary: the FLAGS_DIR plumbing and the two-second launchpad sleep both
went away rather than needing a new option, the former because a purpose-built
compose file has nothing to mount and the latter because AwaitReady is a real
readiness check against the control API.
Verified by running, not by compiling. Five runs of the harness version produced
41, 12, 11, 33 and 5 failures of 47 executed scenarios; three runs of the
hand-rolled version it replaces, on the same machine and the same image,
produced 19, 21 and 40. So the harness is no worse, and the flapping is the
backend's: the launchpad answers 404 to /reset, so every scenario is isolated
with /start, which returns before flagd's file source has loaded the
regenerated flags. Three failures are the floor and all three are the fixture
gap in open-feature/flagd-testbed#392. The README now carries those numbers
instead of a single good run's.
Also here: TCK_RUN in place of PROVIDER_TCK_RUN, the assets pin following the
base to v0.0.0-20260912211427-ccdb88790bb4, the CI reasoning handed to Appendix
F's "Running the suite in CI", and an Adopters row in the harness README so both
Go adoptions are visible as having taken the Compose path.
Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
The assets pin brings reason.feature, gated as a whole on @standard-reasons -- a provider's claim that it uses the standard resolution vocabulary with the standard meanings. For OFREP it is the thinnest of the six capabilities this suite declares: the provider passes the server's reason string straight into ResolutionDetail, so what runs here verifies flagd's OFREP endpoint plus this provider's refusal to rewrite what it sends. Worth verifying anyway -- a provider that mapped reasons onto an enumeration and dropped the ones it did not recognise would fail exactly here, which is python-sdk-contrib#418 in another language's OFREP provider. Declared on evidence from three runs rather than from reading the source. Two of them pass all six scenarios. The third failed two rows of the STATIC outline with reason ERROR, which is the launchpad race this README already documents -- the same FLAG_NOT_FOUND as every other flapping failure, surfacing in the reason field instead of the value -- and not a vocabulary disagreement. The README now says to judge the capability on consistent failures rather than on a red run containing one. Counts move with the pin. 56 canonical scenarios become 65 and the nine skips are the same nine, because @standard-reasons is declared and the @targeting and @disabled-flags capabilities its scenarios compose with are declared too, so 47 executed becomes 56. The three-failure floor is unchanged and is still the fixture gap, open-feature/flagd-testbed#392. The failure distribution recorded in the README gains this pass's three runs: 20, 4 and 3 of 65. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
… have The tag was declared and two of its three scenarios failed. errors.feature says a declarer must satisfy all three, and says why: the two lossless rows exist to catch the shortcut of rejecting every float. This provider takes that shortcut. json.loads yields `int` for 10 and `float` for 0.5, and the check at ofrep/__init__.py:249-256 admits a value only on an exact isinstance against one of them. Nothing in that path widens or narrows a number. So the lossy row passes -- float-flag asked for as an Integer is a TYPE_MISMATCH rather than a silent 0 -- and integer-flag asked for as a Float fails, because 10 is not an instance of float. That is the same architecture as the Java OFREP adoption, which withholds the tag for the same reason. The Go adoption declares it, and the difference is the JSON decoder rather than a decision either author made: encoding/json makes every JSON number a float64, so integer-ness never survives the wire and ResolveInt has to round-trip through int64 -- which yields lossless coercion, and TYPE_MISMATCH on loss, for free. Over OFREP this capability follows the language's JSON library. No knownDeviation accompanies the withdrawal. A deviation records a gap in behaviour a provider is required to have, and numeric coercion is a declared capability rather than a requirement. The honest record is the undeclared tag and the three skips it produces. Measured before and after, 52 scenarios both times: 4 failed / 38 passed / 9 skipped / 1 xfailed becomes 2 failed / 37 passed / 12 skipped / 1 xfailed. The two remaining failures are both large-integer-flag, which the pinned testbed does not serve (open-feature/flagd-testbed#392). The header also claimed every declared capability had been seen to pass, which was untrue while this tag was declared, and the closing note claimed the withheld set matched Go's as well as Java's. Both corrected. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
…s not a deviation Three of the flags the suite's assets added are absent from flagd-testbed v3.8.0: large-integer-flag, huge-integer-flag and integral-float-flag. Only the first is reached by a scenario that runs here -- huge-integer-flag is asked for solely under @large-integers, which is not applicable in Java, and integral-float-flag solely under @numeric-coercion, which this provider withholds -- so exactly one untagged scenario, the 32-bit precision one, fails FLAG_NOT_FOUND in both modes. open-feature/flagd-testbed#392 is open for it; the Compose tag gets bumped when it lands, which is why the note lives next to the tag as well as in the class. A missing flag is a gap in the stack, not in the provider, so it is documented rather than declared as a KnownDeviation. A deviation says the provider is wrong, and the provider was never given the flag to get wrong. The three falsy flags used to fail the same way and no longer do, which is worth writing down because the failure looked identical. The testbed's zero-flags.json already served boolean-zero-flag, integer-zero-flag and string-zero-flag with zero/non-zero variants, while the canonical set called them false-flag, zero-flag and empty-string-flag; the base moved the canonical names onto the testbed's rather than the other way round, so those three scenarios now resolve against flags that were always there. Also says why capabilities() calls declarableExcept rather than EnumSet.complementOf, which now matters more than it did: the complement would claim @large-integers as well as the two reserved tags, and the suite refuses that declaration at startup. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
…s not a deviation Three of the flags the suite's assets added are absent from flagd-testbed v3.8.0: large-integer-flag, huge-integer-flag and integral-float-flag. Only the first is reached by a scenario that runs here -- huge-integer-flag is asked for solely under @large-integers, which is not applicable in Java, and integral-float-flag solely under @numeric-coercion, which this provider withholds -- so exactly one untagged scenario, the 32-bit precision one, fails FLAG_NOT_FOUND in both modes. open-feature/flagd-testbed#392 is open for it; the Compose tag gets bumped when it lands, which is why the note lives next to the tag as well as in the class. A missing flag is a gap in the stack, not in the provider, so it is documented rather than declared as a KnownDeviation. A deviation says the provider is wrong, and the provider was never given the flag to get wrong. The three falsy flags used to fail the same way and no longer do, which is worth writing down because the failure looked identical. The testbed's zero-flags.json already served boolean-zero-flag, integer-zero-flag and string-zero-flag with zero/non-zero variants, while the canonical set called them false-flag, zero-flag and empty-string-flag; the base moved the canonical names onto the testbed's rather than the other way round, so those three scenarios now resolve against flags that were always there. Also says why capabilities() calls declarableExcept rather than EnumSet.complementOf, which now matters more than it did: the complement would claim @large-integers as well as the two reserved tags, and the suite refuses that declaration at startup. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
The adoption hand-rolled its container lifecycle: `FlagdComposeContainer.build()`
at module scope, a `beforeAll` to start it, a guarded `afterAll` to stop it, a
port lookup per resolver, and an `HttpControl` built from a thunk over the
launchpad's mapped port. Every one of those is now the suite's job, so all of it
goes: `runContainerizedProviderTck` is handed a Compose file, the two ports the
two resolvers connect to, and a factory that reads the endpoint it discovers.
The Compose file is this adoption's own, at `src/e2e/tck/docker-compose.yaml`,
and it is deliberately not `test-harness/docker-compose.yaml` next door. Three
differences, each of which is the point:
- **No `/flags` bind mount.** The launchpad writes the configuration it is
asked for into the container's own `/flags` from the rawflags baked into the
testbed image, so the mount buys nothing and costs isolation: the
neighbouring e2e suites share one host directory through it, and with two
containers up each rewrites the other's flags. That is why this branch
previously grew a per-instance temp directory -- and why that commit is gone
rather than kept. Removing the shared resource beats scheduling around it,
and a conformance suite whose failures read as provider defects
(`error-code was 'FLAG_NOT_FOUND', expected 'TYPE_MISMATCH'` on a flag that
exists) is the worst possible thing to leave to a scheduling guarantee.
`flagdComposeContainer.ts` is therefore untouched by this branch again: the
pre-existing e2e suites still import it, so it cannot be deleted, but the
conformance suites no longer go near it.
- **No environment variables.** The image tag is pinned in the file rather
than read from `test-harness/version.txt`. Every capability the two suites
declare rests on a run against v3.10.1 specifically, so a bump is a
deliberate act with a result to record, not something a dependency update
does quietly.
- **No host port bindings.** The suite requires dynamically mapped ports and
discovers them after startup.
One Compose file for both suites, as in Java: 8013 is RPC evaluation and 8015
the in-process sync stream, both served by the one testbed service, so each
suite declares only the port its resolver uses.
`retryGracePeriod` stops being a per-suite knob. Both resolvers passed 30, and
they have to: the grace period is emitted from the shared provider layer rather
than from either transport, which is the same reason both declare `@stale`.
Measured rather than asserted, on v3.10.1, under node 24:
- flagd-rpc: 49 passed, 5 skipped, 2 failed.
- flagd-in-process: 49 passed, 5 skipped, 2 failed.
Identical, which is the finding the two suites exist to produce. Both failures
are the two already recorded against the testbed image rather than against the
provider -- the mandatory 32-bit precision scenario and the `max-int32` row of
the `@variants` outline, both wanting a `large-integer-flag` that v3.10.1 does
not serve (open-feature/flagd-testbed#392). The `@stale`, `@lifecycle` and
`@unavailable` scenarios all pass, which is what establishes that the suite's
await-ready on the control API is sufficient without the post-command settle
delay that was dropped.
Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
…rest @lifecycle was withheld, and that was the expensive mistake in this adoption rather than a cautious one. The provider implements openfeature.StateHandler, Init builds the client and blocks until the event stream is up or the deadline expires, and Init can and does fail -- so the lifecycle scenarios assert something real here. Withholding the tag made Go blind to six scenarios Java was running, which is why Java ran 36 of the 40 and Go ran 29. Declaring it costs nothing now. Measured, per resolver: flagd-rpc 40 scenarios, 6 gated skips, 34 executed, 33 passed, 1 failed flagd-in-process 40 scenarios, 5 gated skips, 35 executed, 34 passed, 1 failed The one failure in each is "A large integer resolves without loss of precision", which fails with FLAG_NOT_FOUND because large-integer-flag is absent from flagd-testbed. open-feature/flagd-testbed#392 adds it. It says nothing about the provider and gets no deviation entry, because the gap is in the fixture and an entry there would attribute it to the provider. Two capabilities are withheld without a deviation entry, and the reason in both cases is that the specification permits the behaviour rather than requiring it. This is the correction this commit is really about: @reinitialization is undeclared. "A provider that was shut down can be initialized again" fails in both resolvers -- Shutdown clears the initialised flag, the second Init proceeds, and what it waits for never arrives, so it returns "provider initialization deadline exceeded". Requirement 2.5.2 says a provider SHOULD revert to its uninitialized state and its supporting text says "some providers MAY allow reinitialization from this state". Permitted, not required. The scenario is gated as of spec fc99d5ac and is now reported as skipped. An earlier draft of this file recorded it as a known deviation against tck.Lifecycle, which would have published a defect claim against a provider exercising a choice the specification offers it. @Stale stays undeclared on RPC, and that too is now a design choice rather than a defect. Requirement 5.1.1's supporting text offers both behaviours in one breath: a provider unable to evaluate flags "can" signal that with PROVIDER_ERROR, and one that caches rule-sets or evaluations "can" signal PROVIDER_STALE. The RPC resolver takes the first -- losing the stream sends of.ProviderError directly, and the SDK then short-circuits to the code default rather than serving cached values. Confirmed by running rather than by reading: declaring @Stale on RPC fails with "timed out after 15s waiting for a PROVIDER_STALE event". The in-process resolver does emit it and passes the scenario in 12.2s, so @Stale is declared there. That two resolvers of one provider report an outage differently is worth knowing and is written down in the file; it is not a conformance gap. JS and Java declare @Stale for both resolvers. On this evidence that is a vacuous declaration for RPC, which is a reason to leave it withheld here rather than to copy them. One capability keeps its deviation entry, and it is worth saying why it survives the same check: @numeric-coercion's rule is not in the specification at all (open-feature/spec#430), but flagd accepted it for itself in an ADR and has open-feature/flagd#1996 open to implement it. The summary now says so, so a consumer does not read the entry as a specification violation. Also fixed: Config.KnownDeviations was never populated. The per-suite knownDeviations field was collected and then dropped, because the call site passed Capabilities and not KnownDeviations -- the quietest possible way for a report to lose the one field that tells a defect apart from a design choice, and it read as wired because the struct field was filled in. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
… run Follows the TCK's move to spec 26362f85. The e2e module reaches the TCK through a replace, and a replace does not bring the replacement's requirements with it, so this module's own pin is what selects the Gherkin the suite actually runs -- leaving it at fc99d5ac would have run the previous revision's assets against the new vocabulary and reported a spec revision this module does not use. Both capabilities are declared because the scenarios passed, not because flagd plausibly supports them. The run is the whole justification, so the numbers: each resolver goes from 40 canonical scenarios to 52, and each reports 50 passed and 2 failed. @Variants passes seven of its eight rows in both resolvers. The variant name survives the trip from the ruleset through the wire format into ResolutionDetail for booleans, strings, integers, floats and all three falsy flags. The eighth row asks large-integer-flag for its max-int32 variant and receives "", because that flag is absent from flagd-testbed -- the same fixture gap that already fails the untagged large-integer scenario, and the reason tck.LargeIntegers stays undeclared. Withholding the tag over it would skip seven working rows to hide one missing flag, so the comment on the capability list now says the suite carries two fixture failures rather than one. open-feature/flagd-testbed#392 adds the flags and both go away together; neither gets a knownDeviations entry, because the gap is in the fixture and an entry there would attribute it to the provider. @targeting was reserved rather than declarable until this spec revision, on the reasoning that asserting anything about the evaluation context needed an echo endpoint on the control API. It does not: targeting-key-flag carries one JsonLogic rule on the targeting key, so a matching context resolves to a different value than a non-matching one or none at all, and a provider that dropped the context is caught by the resolved value itself. All three scenarios pass in both resolvers, and running both was not a formality -- in-process evaluates the rule itself while RPC has flagd evaluate it, so agreement between them is evidence rather than duplication. The flag has been in flagd-testbed since flagd-testbed#103, released in v0.5.1 in February 2024, so unlike tck.LargeIntegers this needs no image bump. The new mandatory scenario that supplies an evaluation context to an untargeted flag passes in both resolvers too, which is worth stating because it is the half of requirement 2.2.1 that nothing here was testing: every resolve method takes a context, no scenario supplied one, and a provider that threw on any context or serialised it into a malformed request would have passed the whole suite. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
Follows the assets pin to spec 009afe06 and declares the capability it adds. Anything that moves the TCK's pin has to move this module's go.mod too, because the e2e module carries the assets as an indirect dependency and a suite reading one revision of the Gherkin against another revision's flag set is the exact drift the module arrangement exists to prevent. Bumped by hand rather than with `go get`, which also wanted to take the go directive to 1.26.0 and flagd/core to v0.17.0 -- both unrelated to this change. @disabled-flags was expected to split the two resolvers and does not. The capability is gated because what a disabled flag resolves to depends on where the caller's default is substituted, which put the RPC resolver on the wrong side of the line by construction: it asks flagd to resolve every flag, and the caller's default never leaves the process. Both resolvers pass all four rows. Running it is what showed why, and it is not the reason the expectation assumed. flagd answers with reason DISABLED, an empty variant and a zero value, and isDefaultOrDisabledFallback in pkg/service/rpc/service.go recognises that pair and keeps defaultValue rather than taking the response's. The zero value is not what carries it: only the boolean row's default (false) coincides with its zero, and "bye" against "", 1 against 0 and 0.1 against 0.0 all fail if the response value is taken. So the line is "does the response distinguish a disabled flag from a resolved one" rather than "does the server see the caller's default" -- and an OFREP response does not, which is what keeps the tag worth having. The in-process resolver reads the state out of the ruleset it synced and arrives at the same answer by a different route, so running both was evidence rather than duplication. Both suites now run 56 scenarios: 54 pass and 2 fail. The two failures are the pre-existing pair in both resolvers and neither is about the provider -- large-integer-flag is absent from flagd-testbed, so the large-integer scenario fails with FLAG_NOT_FOUND and the last @Variants row has no variant to name. open-feature/flagd-testbed#392 adds the flag and both go green together. Neither gets a knownDeviations entry, because the gap is in the fixture. One caution is written down because it cost a wrong conclusion here: the first verification pass failed this outline with FLAG_NOT_FOUND and failed the object scenario with reason ERROR in the same run, and three subsequent runs of the RPC suite and a fourth of both showed neither. That is the launchpad reset race the OFREP suite documents -- POST /start returns before flagd's file source has loaded the regenerated flag file -- and not a property of these scenarios. Any single red run against this backend has to be re-run before it means anything. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
The assets pin brings reason.feature, whose six scenarios are gated as a whole on @standard-reasons -- a claim that the provider uses the standard resolution vocabulary with the standard meanings, not an exemption from anything. Declared for both resolvers, on evidence from running rather than from reading the provider: all nine executed rows pass in both. flagd reports STATIC for a rule-less flag, TARGETING_MATCH for a matching rule, DEFAULT for a rule that exists and did not match, DISABLED for a disabled flag and ERROR for a failed evaluation, which is Appendix F's mapping exactly. Both suites already declare @targeting and @disabled-flags, so all six scenarios run here rather than three of them skipping for a composed capability. Withholding it would have cost nothing in coverage of MUSTs -- values, variants and error codes are asserted elsewhere, ungated -- which is what makes declaring it a claim worth making rather than a default worth taking. Counts move with the pin: 56 scenarios to 65, and both resolvers still produce the identical result, 63 passed and 2 failed. The two failures are unchanged and are still the fixture gap, open-feature/flagd-testbed#392: large-integer-flag is absent from flagd-testbed:v3.8.0. This was an unusually quiet run of a suite whose launchpad race has produced 2 to 20 failures; the README's instruction to re-run a red result before reading anything into it still stands. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
Running this suite at spec fc99d5ac produces 12 or 13 failures, and a different set each time, so anyone reading a result needs to know which of them say anything about the provider. Two do not, and the rest are not even about the provider. Stable, and gaps in the fixture: integral-float-flag and large-integer-flag are absent from flagd-testbed, so the scenarios that ask for them fail with FLAG_NOT_FOUND against any provider at all. number-zero-flag and huge-integer-flag are missing too, though those are gated or unasked here. open-feature/flagd-testbed#392 adds them. No deviation entry, because the gap is in the fixture and an entry there would attribute it to the provider. Everything else moves between runs. Two consecutive runs gave 13 then 12 failures with almost disjoint failing sets, every one FLAG_NOT_FOUND on a flag the testbed certainly has -- boolean-flag, string-zero-flag, object-flag. The cause is the control API rather than this file: the per-scenario reset calls POST /start, which stops flagd, deletes the combined flag file, regenerates it, restarts flagd and polls :8014/readyz -- and flagd answers readyz before its file source has loaded the flags. A provider with a lifecycle never notices, because its own Init blocks until the stream is up or the sync completes and the flags have arrived by then; both flagd suites are stable against the same backend at the same revision, which is the control. A stateless provider evaluates the instant POST /start returns and races the load. So POST /start returning before the backend serves flags makes the reset unusable by precisely the providers that have no way to wait for it. Adding a sleep or a retry here would hide that from every other language's adoption, so it is written down and left alone. @numeric-coercion is kept, and now deliberately rather than by inertia. The comment claiming the lossless direction had no scenario is stale -- integral-float-flag added two, so the tag is a stronger claim than when it was first declared. One of the two cannot be answered by this backend, but the other two scenarios pass, and withholding the tag would also skip the lossy one. That is the scenario worth keeping: narrowing 0.5 to 0 with no error code is the failure mode flagd has and this provider does not. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
This module reaches the TCK through a replace, and a replace does not bring the replacement's requirements with it, so its own requirement is what selects the Gherkin the suite runs. Left at fc99d5ac it would have run the previous revision's assets while reporting conformance against a revision it does not use -- the drift the flagd e2e module has the same one-line guard against. Both capabilities are declared on the run rather than on the code. The suite is documented above as non-deterministic, because POST /start on the launchpad returns before flagd serves flags and a stateless provider races the load, so one green run would not be evidence of anything. Three consecutive runs gave the identical result: 52 scenarios, 9 skipped as undeclared, 43 run, 40 passed, 3 failed, with the same three failures every time. @Variants passes seven of its eight rows. OFREP's evaluation response carries a variant field and this provider passes it straight into ResolutionDetail, for booleans, strings, integers, floats and all three falsy flags. The eighth row asks large-integer-flag for its max-int32 variant and receives "", because that flag is absent from flagd-testbed -- the same absence that already fails the untagged large-integer scenario, now counted twice rather than a new defect. The third stable failure is integral-float-flag, absent for the same reason. All three are fixture gaps, all three go green with open-feature/flagd-testbed#392, and none gets a knownDeviations entry, because an entry there would attribute the fixture's gap to the provider. @targeting was reserved rather than declarable until this spec revision. For a JSON-over-HTTP provider it is the cheapest capability here to get right, because the evaluation context IS the request body and there is no separate passthrough path to get wrong. All three scenarios pass -- targeting-key-flag resolves to "hit" for the matching targeting key and "miss" for a non-matching one or none at all -- and so does the new untagged scenario that supplies a context to an untargeted flag. Worth having precisely because of how this provider is built: until this revision no scenario supplied a context at all, so one that serialised it into a malformed body would have passed the whole suite. The README's capability table gains rows for both, and its counts are corrected while they are being touched: it claimed 24 of 29 scenarios and that @events gated lifecycle.feature, when @lifecycle now does and @lifecycle and @large-integers were missing from the table entirely. Its "Known asymmetry" section claimed no scenario covers integer-flag requested as a float; one does now, it is declared under @numeric-coercion, and it passes -- so the asymmetry is deliberate rather than untested. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
This was the last of the eight adoptions across four languages still driving
containers by hand, which made it the one adoption that did not answer the
question the compose work exists to answer: whether the harness is sufficient.
It is. There is no container code in tck_test.go any more, and the module no
longer requires testcontainers-go directly -- it arrives as an indirect
dependency of tools/tck.
Deleted: startTestbed, which called compose.NewDockerCompose itself, created a
temporary flags directory and passed it in as FLAGS_DIR because the testbed
submodule's compose file bind-mounts ${FLAGS_DIR} and an unset value defaults to
the submodule's own directory, registered two t.Cleanups, declared its own wait
strategy, looked up two mapped ports by string, built the tck.HTTPControl
itself, hard-coded localhost as the host and slept two seconds for the
launchpad. What replaces it is three options -- WithComposeFile, WithBackendPorts
and WithProviderFromEndpoint -- and a Compose file of six non-comment lines.
95 lines of adopter-written code become 43 plus 6 of YAML. endpoint.Host() is
also correct where the hard-coded localhost was only usually correct: with a
remote Docker daemon, Docker Desktop on some platforms or a rootless setup the
host is not localhost.
The Compose file is this package's own rather than the testbed submodule's, for
the same reasons the flagd adoption's is: no ${FLAGS_DIR} bind mount to work
around, no envoy sidecar, and a service called "backend", which is the TCK's
default and the name both the flagd adoption here and Java's use. It publishes
8016 and 8080 only, since nothing here speaks flagd's gRPC protocols. A welcome
consequence is that this suite no longer needs the submodule checked out at all.
Nothing the harness could not express. The one thing worth reporting is what it
made unnecessary: the FLAGS_DIR plumbing and the two-second launchpad sleep both
went away rather than needing a new option, the former because a purpose-built
compose file has nothing to mount and the latter because AwaitReady is a real
readiness check against the control API.
Verified by running, not by compiling. Five runs of the harness version produced
41, 12, 11, 33 and 5 failures of 47 executed scenarios; three runs of the
hand-rolled version it replaces, on the same machine and the same image,
produced 19, 21 and 40. So the harness is no worse, and the flapping is the
backend's: the launchpad answers 404 to /reset, so every scenario is isolated
with /start, which returns before flagd's file source has loaded the
regenerated flags. Three failures are the floor and all three are the fixture
gap in open-feature/flagd-testbed#392. The README now carries those numbers
instead of a single good run's.
Also here: TCK_RUN in place of PROVIDER_TCK_RUN, the assets pin following the
base to v0.0.0-20260912211427-ccdb88790bb4, the CI reasoning handed to Appendix
F's "Running the suite in CI", and an Adopters row in the harness README so both
Go adoptions are visible as having taken the Compose path.
Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
The assets pin brings reason.feature, gated as a whole on @standard-reasons -- a provider's claim that it uses the standard resolution vocabulary with the standard meanings. For OFREP it is the thinnest of the six capabilities this suite declares: the provider passes the server's reason string straight into ResolutionDetail, so what runs here verifies flagd's OFREP endpoint plus this provider's refusal to rewrite what it sends. Worth verifying anyway -- a provider that mapped reasons onto an enumeration and dropped the ones it did not recognise would fail exactly here, which is python-sdk-contrib#418 in another language's OFREP provider. Declared on evidence from three runs rather than from reading the source. Two of them pass all six scenarios. The third failed two rows of the STATIC outline with reason ERROR, which is the launchpad race this README already documents -- the same FLAG_NOT_FOUND as every other flapping failure, surfacing in the reason field instead of the value -- and not a vocabulary disagreement. The README now says to judge the capability on consistent failures rather than on a red run containing one. Counts move with the pin. 56 canonical scenarios become 65 and the nine skips are the same nine, because @standard-reasons is declared and the @targeting and @disabled-flags capabilities its scenarios compose with are declared too, so 47 executed becomes 56. The three-failure floor is unchanged and is still the fixture gap, open-feature/flagd-testbed#392. The failure distribution recorded in the README gains this pass's three runs: 20, 4 and 3 of 65. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
The adoption hand-rolled its container lifecycle: `FlagdComposeContainer.build()`
at module scope, a `beforeAll` to start it, a guarded `afterAll` to stop it, a
port lookup per resolver, and an `HttpControl` built from a thunk over the
launchpad's mapped port. Every one of those is now the suite's job, so all of it
goes: `runContainerizedProviderTck` is handed a Compose file, the two ports the
two resolvers connect to, and a factory that reads the endpoint it discovers.
The Compose file is this adoption's own, at `src/e2e/tck/docker-compose.yaml`,
and it is deliberately not `test-harness/docker-compose.yaml` next door. Three
differences, each of which is the point:
- **No `/flags` bind mount.** The launchpad writes the configuration it is
asked for into the container's own `/flags` from the rawflags baked into the
testbed image, so the mount buys nothing and costs isolation: the
neighbouring e2e suites share one host directory through it, and with two
containers up each rewrites the other's flags. That is why this branch
previously grew a per-instance temp directory -- and why that commit is gone
rather than kept. Removing the shared resource beats scheduling around it,
and a conformance suite whose failures read as provider defects
(`error-code was 'FLAG_NOT_FOUND', expected 'TYPE_MISMATCH'` on a flag that
exists) is the worst possible thing to leave to a scheduling guarantee.
`flagdComposeContainer.ts` is therefore untouched by this branch again: the
pre-existing e2e suites still import it, so it cannot be deleted, but the
conformance suites no longer go near it.
- **No environment variables.** The image tag is pinned in the file rather
than read from `test-harness/version.txt`. Every capability the two suites
declare rests on a run against v3.10.1 specifically, so a bump is a
deliberate act with a result to record, not something a dependency update
does quietly.
- **No host port bindings.** The suite requires dynamically mapped ports and
discovers them after startup.
One Compose file for both suites, as in Java: 8013 is RPC evaluation and 8015
the in-process sync stream, both served by the one testbed service, so each
suite declares only the port its resolver uses.
`retryGracePeriod` stops being a per-suite knob. Both resolvers passed 30, and
they have to: the grace period is emitted from the shared provider layer rather
than from either transport, which is the same reason both declare `@stale`.
Measured rather than asserted, on v3.10.1, under node 24:
- flagd-rpc: 49 passed, 5 skipped, 2 failed.
- flagd-in-process: 49 passed, 5 skipped, 2 failed.
Identical, which is the finding the two suites exist to produce. Both failures
are the two already recorded against the testbed image rather than against the
provider -- the mandatory 32-bit precision scenario and the `max-int32` row of
the `@variants` outline, both wanting a `large-integer-flag` that v3.10.1 does
not serve (open-feature/flagd-testbed#392). The `@stale`, `@lifecycle` and
`@unavailable` scenarios all pass, which is what establishes that the suite's
await-ready on the control API is sufficient without the post-command settle
delay that was dropped.
Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
… have The tag was declared and two of its three scenarios failed. errors.feature says a declarer must satisfy all three, and says why: the two lossless rows exist to catch the shortcut of rejecting every float. This provider takes that shortcut. json.loads yields `int` for 10 and `float` for 0.5, and the check at ofrep/__init__.py:249-256 admits a value only on an exact isinstance against one of them. Nothing in that path widens or narrows a number. So the lossy row passes -- float-flag asked for as an Integer is a TYPE_MISMATCH rather than a silent 0 -- and integer-flag asked for as a Float fails, because 10 is not an instance of float. That is the same architecture as the Java OFREP adoption, which withholds the tag for the same reason. The Go adoption declares it, and the difference is the JSON decoder rather than a decision either author made: encoding/json makes every JSON number a float64, so integer-ness never survives the wire and ResolveInt has to round-trip through int64 -- which yields lossless coercion, and TYPE_MISMATCH on loss, for free. Over OFREP this capability follows the language's JSON library. No knownDeviation accompanies the withdrawal. A deviation records a gap in behaviour a provider is required to have, and numeric coercion is a declared capability rather than a requirement. The honest record is the undeclared tag and the three skips it produces. Measured before and after, 52 scenarios both times: 4 failed / 38 passed / 9 skipped / 1 xfailed becomes 2 failed / 37 passed / 12 skipped / 1 xfailed. The two remaining failures are both large-integer-flag, which the pinned testbed does not serve (open-feature/flagd-testbed#392). The header also claimed every declared capability had been seen to pass, which was untrue while this tag was declared, and the closing note claimed the withheld set matched Go's as well as Java's. Both corrected. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
… have The tag was declared and two of its three scenarios failed. errors.feature says a declarer must satisfy all three, and says why: the two lossless rows exist to catch the shortcut of rejecting every float. This provider takes that shortcut. json.loads yields `int` for 10 and `float` for 0.5, and the check at ofrep/__init__.py:249-256 admits a value only on an exact isinstance against one of them. Nothing in that path widens or narrows a number. So the lossy row passes -- float-flag asked for as an Integer is a TYPE_MISMATCH rather than a silent 0 -- and integer-flag asked for as a Float fails, because 10 is not an instance of float. That is the same architecture as the Java OFREP adoption, which withholds the tag for the same reason. The Go adoption declares it, and the difference is the JSON decoder rather than a decision either author made: encoding/json makes every JSON number a float64, so integer-ness never survives the wire and ResolveInt has to round-trip through int64 -- which yields lossless coercion, and TYPE_MISMATCH on loss, for free. Over OFREP this capability follows the language's JSON library. No knownDeviation accompanies the withdrawal. A deviation records a gap in behaviour a provider is required to have, and numeric coercion is a declared capability rather than a requirement. The honest record is the undeclared tag and the three skips it produces. Measured before and after, 52 scenarios both times: 4 failed / 38 passed / 9 skipped / 1 xfailed becomes 2 failed / 37 passed / 12 skipped / 1 xfailed. The two remaining failures are both large-integer-flag, which the pinned testbed does not serve (open-feature/flagd-testbed#392). The header also claimed every declared capability had been seen to pass, which was untrue while this tag was declared, and the closing note claimed the withheld set matched Go's as well as Java's. Both corrected. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
…s not a deviation Three of the flags the suite's assets added are absent from flagd-testbed v3.8.0: large-integer-flag, huge-integer-flag and integral-float-flag. Only the first is reached by a scenario that runs here -- huge-integer-flag is asked for solely under @large-integers, which is not applicable in Java, and integral-float-flag solely under @numeric-coercion, which this provider withholds -- so exactly one untagged scenario, the 32-bit precision one, fails FLAG_NOT_FOUND in both modes. open-feature/flagd-testbed#392 is open for it; the Compose tag gets bumped when it lands, which is why the note lives next to the tag as well as in the class. A missing flag is a gap in the stack, not in the provider, so it is documented rather than declared as a KnownDeviation. A deviation says the provider is wrong, and the provider was never given the flag to get wrong. The three falsy flags used to fail the same way and no longer do, which is worth writing down because the failure looked identical. The testbed's zero-flags.json already served boolean-zero-flag, integer-zero-flag and string-zero-flag with zero/non-zero variants, while the canonical set called them false-flag, zero-flag and empty-string-flag; the base moved the canonical names onto the testbed's rather than the other way round, so those three scenarios now resolve against flags that were always there. Also says why capabilities() calls declarableExcept rather than EnumSet.complementOf, which now matters more than it did: the complement would claim @large-integers as well as the two reserved tags, and the suite refuses that declaration at startup. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
…viate
The adoption withheld @numeric-coercion and recorded a tracked deviation for
it at the same time. That is the one combination Appendix F's known-deviation
guidance singles out to avoid: a withheld capability plus a deviation asserts
that the provider is broken at something the suite never put to it, and the
three skips it produces cannot say which of "does not coerce" and "coerces,
and loses information one way round" is true.
Measured over three full runs, both resolvers, identically:
- "An integer requested as a float is widened without loss" PASSES.
integer-flag (10) through GetFloatDetails returns 10, reason STATIC, no
error code. So this provider does coerce, and the second shape does not
apply: shape 2 is legitimate only when the provider cannot attempt the
behaviour at all.
- "A float flag is not silently narrowed to an integer" FAILS. float-flag
(0.5) through GetIntDetails returns 0 with no error code, not
TYPE_MISMATCH with the code default. This is the deviation, and it stands
unchanged -- flagd measured against its own accepted numeric-coercion ADR,
tracked as open-feature/flagd#1996.
- "An integral float requested as an integer is coerced without loss" FAILS
with FLAG_NOT_FOUND, because flagd-testbed v3.8.0 does not serve
integral-float-flag. That is the backend's gap, not flagd's.
So both suites now declare the tag, keep the deviation, and run 65 scenarios
with 61 passing and 4 failing instead of 63 and 2. One of the four is the
provider's; three are the fixture's, and open-feature/flagd-testbed#392
retires all three together.
The second failure is the price of declaring and it is paid rather than
dodged: one red scenario belonging to flagd-testbed is a smaller loss than
three skips that misdescribe the provider. It is named in the deviation
summary so a consumer reading the report does not count it against flagd.
Also corrects a claim in the comment that was stated more strongly than the
evidence supports. It said the Python flagd provider gets this right "in
either resolver". It splits: the in-process resolver refuses 0.5 correctly,
and the RPC resolver narrows it to 0 exactly as this one does.
Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
Running this suite at spec fc99d5ac produces 12 or 13 failures, and a different set each time, so anyone reading a result needs to know which of them say anything about the provider. Two do not, and the rest are not even about the provider. Stable, and gaps in the fixture: integral-float-flag and large-integer-flag are absent from flagd-testbed, so the scenarios that ask for them fail with FLAG_NOT_FOUND against any provider at all. number-zero-flag and huge-integer-flag are missing too, though those are gated or unasked here. open-feature/flagd-testbed#392 adds them. No deviation entry, because the gap is in the fixture and an entry there would attribute it to the provider. Everything else moves between runs. Two consecutive runs gave 13 then 12 failures with almost disjoint failing sets, every one FLAG_NOT_FOUND on a flag the testbed certainly has -- boolean-flag, string-zero-flag, object-flag. The cause is the control API rather than this file: the per-scenario reset calls POST /start, which stops flagd, deletes the combined flag file, regenerates it, restarts flagd and polls :8014/readyz -- and flagd answers readyz before its file source has loaded the flags. A provider with a lifecycle never notices, because its own Init blocks until the stream is up or the sync completes and the flags have arrived by then; both flagd suites are stable against the same backend at the same revision, which is the control. A stateless provider evaluates the instant POST /start returns and races the load. So POST /start returning before the backend serves flags makes the reset unusable by precisely the providers that have no way to wait for it. Adding a sleep or a retry here would hide that from every other language's adoption, so it is written down and left alone. @numeric-coercion is kept, and now deliberately rather than by inertia. The comment claiming the lossless direction had no scenario is stale -- integral-float-flag added two, so the tag is a stronger claim than when it was first declared. One of the two cannot be answered by this backend, but the other two scenarios pass, and withholding the tag would also skip the lossy one. That is the scenario worth keeping: narrowing 0.5 to 0 with no error code is the failure mode flagd has and this provider does not. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
This module reaches the TCK through a replace, and a replace does not bring the replacement's requirements with it, so its own requirement is what selects the Gherkin the suite runs. Left at fc99d5ac it would have run the previous revision's assets while reporting conformance against a revision it does not use -- the drift the flagd e2e module has the same one-line guard against. Both capabilities are declared on the run rather than on the code. The suite is documented above as non-deterministic, because POST /start on the launchpad returns before flagd serves flags and a stateless provider races the load, so one green run would not be evidence of anything. Three consecutive runs gave the identical result: 52 scenarios, 9 skipped as undeclared, 43 run, 40 passed, 3 failed, with the same three failures every time. @Variants passes seven of its eight rows. OFREP's evaluation response carries a variant field and this provider passes it straight into ResolutionDetail, for booleans, strings, integers, floats and all three falsy flags. The eighth row asks large-integer-flag for its max-int32 variant and receives "", because that flag is absent from flagd-testbed -- the same absence that already fails the untagged large-integer scenario, now counted twice rather than a new defect. The third stable failure is integral-float-flag, absent for the same reason. All three are fixture gaps, all three go green with open-feature/flagd-testbed#392, and none gets a knownDeviations entry, because an entry there would attribute the fixture's gap to the provider. @targeting was reserved rather than declarable until this spec revision. For a JSON-over-HTTP provider it is the cheapest capability here to get right, because the evaluation context IS the request body and there is no separate passthrough path to get wrong. All three scenarios pass -- targeting-key-flag resolves to "hit" for the matching targeting key and "miss" for a non-matching one or none at all -- and so does the new untagged scenario that supplies a context to an untargeted flag. Worth having precisely because of how this provider is built: until this revision no scenario supplied a context at all, so one that serialised it into a malformed body would have passed the whole suite. The README's capability table gains rows for both, and its counts are corrected while they are being touched: it claimed 24 of 29 scenarios and that @events gated lifecycle.feature, when @lifecycle now does and @lifecycle and @large-integers were missing from the table entirely. Its "Known asymmetry" section claimed no scenario covers integer-flag requested as a float; one does now, it is declared under @numeric-coercion, and it passes -- so the asymmetry is deliberate rather than untested. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
A flag value can survive a round trip through the wrong numeric type and come back subtly wrong, and nothing in this repository notices today:
2147483647(2^31 − 1) routed through a 32-bit float returns2147483648.9007199254740991(2^53 − 1) through anything narrower than a double is rounded.10.0, can arrive as the integer10and look correct until something divides by it.This adds three flags and scenarios in both harnesses that would catch each case.
large-integer-flaghuge-integer-flagintegral-float-flag10.0, a float whose value is integralflags/precision-flags.json— the provider harness. Picked up by thedefaultconfiguration automatically, since that configuration is every file inflags/combined, so the launchpad needs no change.gherkin/evaluation.feature— a@precisionoutline for the first two cases, and a separate scenario for 2^53 − 1.evaluator/flags/testkit-flags.jsonand a newevaluator/gherkin/precision.feature— the same coverage for the evaluator, followingzero-values.feature.Related
max-int32/max-safe/tenvariant names come fromHow this was found: running the Go flagd provider against this testbed through the new conformance suite failed four scenarios with
FLAG_NOT_FOUND. Three were a naming divergence, now fixed on the specification side by adopting this repository's names. The fourth,large-integer-flag, was genuinely missing — hence this PR.