Conversation
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueComment |
aepfli
added a commit
to open-feature/js-sdk-contrib
that referenced
this pull request
Aug 24, 2026
…ng untested ones
Three defects in the capability rollup, all of them ways the report could report
a result the suite never obtained.
A failed capability was emitted as {"state": "failed"} with no reason. The schema
requires a reason for any outcome other than passed, so that entry does not
validate -- and it appears only when a provider is actually failing, which is
precisely when the report matters. It now says how many of how many scenarios
carrying the tag failed, and points at the per-scenario results for which and why.
No test caught it because every self-test suite passes, so nothing that runs end
to end reaches that branch. The new test drives the report builder directly with
synthetic records, which is the only way to exercise a failure without breaking a
provider on purpose.
A declared capability that no scenario carries was reported as passed. @targeting
is reserved -- it is in the vocabulary and nothing tests it, because asserting
that an evaluation context reached the backend needs an echo operation the control
API does not have -- so a provider declaring it got a green result for a claim
nothing had examined.
A declared capability whose every scenario was skipped for a *different* one was
likewise reported as passed, because the rollup counted a capability as exercised
by tag presence rather than by execution. Both scenarios in events.feature carry
@events as well as @Stale or @configuration-change, so the in-memory suite
reported @events as passed while neither scenario ran.
Both are the vacuous pass the capability vocabulary was introduced to eliminate,
arriving through the report rather than through the suite. A scenario now counts
towards a capability only if it actually ran, and a declared capability with
nothing to show is omitted. The suite asked no question of it, so it has no answer
to report, and a consumer sees the tag is absent rather than a pass it cannot rely
on. Omitting is preferred to inventing a fifth outcome: the four in the schema are
about what the provider did, and "this run demonstrated nothing" is a fact about
the run.
An undeclared capability is still reported as not-declared with its reason, and
not-applicable is unaffected -- @strict-numeric-typing being unanswerable in a
language with no integer type is a different statement from a capability nothing
exercised, and both need saying.
Matches the same fix in Go, open-feature/go-sdk-contrib#944.
Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli
force-pushed
the
feat/provider-tck-report
branch
from
September 11, 2026 07:37
e037280 to
cb69ccf
Compare
aepfli
added a commit
to open-feature/js-sdk-contrib
that referenced
this pull request
Sep 11, 2026
…ng untested ones
Three defects in the capability rollup, all of them ways the report could report
a result the suite never obtained.
A failed capability was emitted as {"state": "failed"} with no reason. The schema
requires a reason for any outcome other than passed, so that entry does not
validate -- and it appears only when a provider is actually failing, which is
precisely when the report matters. It now says how many of how many scenarios
carrying the tag failed, and points at the per-scenario results for which and why.
No test caught it because every self-test suite passes, so nothing that runs end
to end reaches that branch. The new test drives the report builder directly with
synthetic records, which is the only way to exercise a failure without breaking a
provider on purpose.
A declared capability that no scenario carries was reported as passed. @targeting
is reserved -- it is in the vocabulary and nothing tests it, because asserting
that an evaluation context reached the backend needs an echo operation the control
API does not have -- so a provider declaring it got a green result for a claim
nothing had examined.
A declared capability whose every scenario was skipped for a *different* one was
likewise reported as passed, because the rollup counted a capability as exercised
by tag presence rather than by execution. Both scenarios in events.feature carry
@events as well as @Stale or @configuration-change, so the in-memory suite
reported @events as passed while neither scenario ran.
Both are the vacuous pass the capability vocabulary was introduced to eliminate,
arriving through the report rather than through the suite. A scenario now counts
towards a capability only if it actually ran, and a declared capability with
nothing to show is omitted. The suite asked no question of it, so it has no answer
to report, and a consumer sees the tag is absent rather than a pass it cannot rely
on. Omitting is preferred to inventing a fifth outcome: the four in the schema are
about what the provider did, and "this run demonstrated nothing" is a fact about
the run.
An undeclared capability is still reported as not-declared with its reason, and
not-applicable is unaffected -- @strict-numeric-typing being unanswerable in a
language with no integer type is a different statement from a capability nothing
exercised, and both need saying.
Matches the same fix in Go, open-feature/go-sdk-contrib#944.
Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli
force-pushed
the
feat/provider-tck-report
branch
5 times, most recently
from
September 11, 2026 11:09
6bc4c55 to
e794277
Compare
aepfli
added a commit
to open-feature/js-sdk-contrib
that referenced
this pull request
Sep 11, 2026
…ng untested ones
Three defects in the capability rollup, all of them ways the report could report
a result the suite never obtained.
A failed capability was emitted as {"state": "failed"} with no reason. The schema
requires a reason for any outcome other than passed, so that entry does not
validate -- and it appears only when a provider is actually failing, which is
precisely when the report matters. It now says how many of how many scenarios
carrying the tag failed, and points at the per-scenario results for which and why.
No test caught it because every self-test suite passes, so nothing that runs end
to end reaches that branch. The new test drives the report builder directly with
synthetic records, which is the only way to exercise a failure without breaking a
provider on purpose.
A declared capability that no scenario carries was reported as passed. @targeting
is reserved -- it is in the vocabulary and nothing tests it, because asserting
that an evaluation context reached the backend needs an echo operation the control
API does not have -- so a provider declaring it got a green result for a claim
nothing had examined.
A declared capability whose every scenario was skipped for a *different* one was
likewise reported as passed, because the rollup counted a capability as exercised
by tag presence rather than by execution. Both scenarios in events.feature carry
@events as well as @Stale or @configuration-change, so the in-memory suite
reported @events as passed while neither scenario ran.
Both are the vacuous pass the capability vocabulary was introduced to eliminate,
arriving through the report rather than through the suite. A scenario now counts
towards a capability only if it actually ran, and a declared capability with
nothing to show is omitted. The suite asked no question of it, so it has no answer
to report, and a consumer sees the tag is absent rather than a pass it cannot rely
on. Omitting is preferred to inventing a fifth outcome: the four in the schema are
about what the provider did, and "this run demonstrated nothing" is a fact about
the run.
An undeclared capability is still reported as not-declared with its reason, and
not-applicable is unaffected -- @strict-numeric-typing being unanswerable in a
language with no integer type is a different statement from a capability nothing
exercised, and both need saying.
Matches the same fix in Go, open-feature/go-sdk-contrib#944.
Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli
force-pushed
the
feat/provider-tck
branch
from
September 11, 2026 13:34
44013f5 to
06a1d5c
Compare
aepfli
force-pushed
the
feat/provider-tck-report
branch
from
September 11, 2026 13:34
e794277 to
9a8f61c
Compare
aepfli
added a commit
to open-feature/js-sdk-contrib
that referenced
this pull request
Sep 11, 2026
…ng untested ones
Three defects in the capability rollup, all of them ways the report could report
a result the suite never obtained.
A failed capability was emitted as {"state": "failed"} with no reason. The schema
requires a reason for any outcome other than passed, so that entry does not
validate -- and it appears only when a provider is actually failing, which is
precisely when the report matters. It now says how many of how many scenarios
carrying the tag failed, and points at the per-scenario results for which and why.
No test caught it because every self-test suite passes, so nothing that runs end
to end reaches that branch. The new test drives the report builder directly with
synthetic records, which is the only way to exercise a failure without breaking a
provider on purpose.
A declared capability that no scenario carries was reported as passed. @targeting
is reserved -- it is in the vocabulary and nothing tests it, because asserting
that an evaluation context reached the backend needs an echo operation the control
API does not have -- so a provider declaring it got a green result for a claim
nothing had examined.
A declared capability whose every scenario was skipped for a *different* one was
likewise reported as passed, because the rollup counted a capability as exercised
by tag presence rather than by execution. Both scenarios in events.feature carry
@events as well as @Stale or @configuration-change, so the in-memory suite
reported @events as passed while neither scenario ran.
Both are the vacuous pass the capability vocabulary was introduced to eliminate,
arriving through the report rather than through the suite. A scenario now counts
towards a capability only if it actually ran, and a declared capability with
nothing to show is omitted. The suite asked no question of it, so it has no answer
to report, and a consumer sees the tag is absent rather than a pass it cannot rely
on. Omitting is preferred to inventing a fifth outcome: the four in the schema are
about what the provider did, and "this run demonstrated nothing" is a fact about
the run.
An undeclared capability is still reported as not-declared with its reason, and
not-applicable is unaffected -- @strict-numeric-typing being unanswerable in a
language with no integer type is a different statement from a capability nothing
exercised, and both need saying.
Matches the same fix in Go, open-feature/go-sdk-contrib#944.
Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli
force-pushed
the
feat/provider-tck-report
branch
from
September 11, 2026 16:31
81856b1 to
da5bdbd
Compare
aepfli
added a commit
to open-feature/js-sdk-contrib
that referenced
this pull request
Sep 11, 2026
…ng untested ones
Three defects in the capability rollup, all of them ways the report could report
a result the suite never obtained.
A failed capability was emitted as {"state": "failed"} with no reason. The schema
requires a reason for any outcome other than passed, so that entry does not
validate -- and it appears only when a provider is actually failing, which is
precisely when the report matters. It now says how many of how many scenarios
carrying the tag failed, and points at the per-scenario results for which and why.
No test caught it because every self-test suite passes, so nothing that runs end
to end reaches that branch. The new test drives the report builder directly with
synthetic records, which is the only way to exercise a failure without breaking a
provider on purpose.
A declared capability that no scenario carries was reported as passed. @targeting
is reserved -- it is in the vocabulary and nothing tests it, because asserting
that an evaluation context reached the backend needs an echo operation the control
API does not have -- so a provider declaring it got a green result for a claim
nothing had examined.
A declared capability whose every scenario was skipped for a *different* one was
likewise reported as passed, because the rollup counted a capability as exercised
by tag presence rather than by execution. Both scenarios in events.feature carry
@events as well as @Stale or @configuration-change, so the in-memory suite
reported @events as passed while neither scenario ran.
Both are the vacuous pass the capability vocabulary was introduced to eliminate,
arriving through the report rather than through the suite. A scenario now counts
towards a capability only if it actually ran, and a declared capability with
nothing to show is omitted. The suite asked no question of it, so it has no answer
to report, and a consumer sees the tag is absent rather than a pass it cannot rely
on. Omitting is preferred to inventing a fifth outcome: the four in the schema are
about what the provider did, and "this run demonstrated nothing" is a fact about
the run.
An undeclared capability is still reported as not-declared with its reason, and
not-applicable is unaffected -- @strict-numeric-typing being unanswerable in a
language with no integer type is a different statement from a capability nothing
exercised, and both need saying.
Matches the same fix in Go, open-feature/go-sdk-contrib#944.
Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli
added a commit
to open-feature/js-sdk-contrib
that referenced
this pull request
Sep 11, 2026
…ng untested ones
Three defects in the capability rollup, all of them ways the report could report
a result the suite never obtained.
A failed capability was emitted as {"state": "failed"} with no reason. The schema
requires a reason for any outcome other than passed, so that entry does not
validate -- and it appears only when a provider is actually failing, which is
precisely when the report matters. It now says how many of how many scenarios
carrying the tag failed, and points at the per-scenario results for which and why.
No test caught it because every self-test suite passes, so nothing that runs end
to end reaches that branch. The new test drives the report builder directly with
synthetic records, which is the only way to exercise a failure without breaking a
provider on purpose.
A declared capability that no scenario carries was reported as passed. @targeting
is reserved -- it is in the vocabulary and nothing tests it, because asserting
that an evaluation context reached the backend needs an echo operation the control
API does not have -- so a provider declaring it got a green result for a claim
nothing had examined.
A declared capability whose every scenario was skipped for a *different* one was
likewise reported as passed, because the rollup counted a capability as exercised
by tag presence rather than by execution. Both scenarios in events.feature carry
@events as well as @Stale or @configuration-change, so the in-memory suite
reported @events as passed while neither scenario ran.
Both are the vacuous pass the capability vocabulary was introduced to eliminate,
arriving through the report rather than through the suite. A scenario now counts
towards a capability only if it actually ran, and a declared capability with
nothing to show is omitted. The suite asked no question of it, so it has no answer
to report, and a consumer sees the tag is absent rather than a pass it cannot rely
on. Omitting is preferred to inventing a fifth outcome: the four in the schema are
about what the provider did, and "this run demonstrated nothing" is a fact about
the run.
An undeclared capability is still reported as not-declared with its reason, and
not-applicable is unaffected -- @strict-numeric-typing being unanswerable in a
language with no integer type is a different statement from a capability nothing
exercised, and both need saying.
Matches the same fix in Go, open-feature/go-sdk-contrib#944.
Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli
force-pushed
the
feat/provider-tck-report
branch
from
September 11, 2026 18:01
da5bdbd to
6d7c0cc
Compare
aepfli
force-pushed
the
feat/provider-tck
branch
from
September 11, 2026 18:12
8e436f6 to
7ad62c7
Compare
aepfli
force-pushed
the
feat/provider-tck-report
branch
2 times, most recently
from
September 12, 2026 06:33
fe9fda9 to
a71c35b
Compare
aepfli
added a commit
to open-feature/js-sdk-contrib
that referenced
this pull request
Sep 12, 2026
…ng untested ones
Three defects in the capability rollup, all of them ways the report could report
a result the suite never obtained.
A failed capability was emitted as {"state": "failed"} with no reason. The schema
requires a reason for any outcome other than passed, so that entry does not
validate -- and it appears only when a provider is actually failing, which is
precisely when the report matters. It now says how many of how many scenarios
carrying the tag failed, and points at the per-scenario results for which and why.
No test caught it because every self-test suite passes, so nothing that runs end
to end reaches that branch. The new test drives the report builder directly with
synthetic records, which is the only way to exercise a failure without breaking a
provider on purpose.
A declared capability that no scenario carries was reported as passed. @targeting
is reserved -- it is in the vocabulary and nothing tests it, because asserting
that an evaluation context reached the backend needs an echo operation the control
API does not have -- so a provider declaring it got a green result for a claim
nothing had examined.
A declared capability whose every scenario was skipped for a *different* one was
likewise reported as passed, because the rollup counted a capability as exercised
by tag presence rather than by execution. Both scenarios in events.feature carry
@events as well as @Stale or @configuration-change, so the in-memory suite
reported @events as passed while neither scenario ran.
Both are the vacuous pass the capability vocabulary was introduced to eliminate,
arriving through the report rather than through the suite. A scenario now counts
towards a capability only if it actually ran, and a declared capability with
nothing to show is omitted. The suite asked no question of it, so it has no answer
to report, and a consumer sees the tag is absent rather than a pass it cannot rely
on. Omitting is preferred to inventing a fifth outcome: the four in the schema are
about what the provider did, and "this run demonstrated nothing" is a fact about
the run.
An undeclared capability is still reported as not-declared with its reason, and
not-applicable is unaffected -- @strict-numeric-typing being unanswerable in a
language with no integer type is a different statement from a capability nothing
exercised, and both need saying.
Matches the same fix in Go, open-feature/go-sdk-contrib#944.
Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli
force-pushed
the
feat/provider-tck-report
branch
from
September 12, 2026 10:43
a71c35b to
9cf29bb
Compare
aepfli
added a commit
to open-feature/js-sdk-contrib
that referenced
this pull request
Sep 12, 2026
…ng untested ones
Three defects in the capability rollup, all of them ways the report could report
a result the suite never obtained.
A failed capability was emitted as {"state": "failed"} with no reason. The schema
requires a reason for any outcome other than passed, so that entry does not
validate -- and it appears only when a provider is actually failing, which is
precisely when the report matters. It now says how many of how many scenarios
carrying the tag failed, and points at the per-scenario results for which and why.
No test caught it because every self-test suite passes, so nothing that runs end
to end reaches that branch. The new test drives the report builder directly with
synthetic records, which is the only way to exercise a failure without breaking a
provider on purpose.
A declared capability that no scenario carries was reported as passed. @targeting
is reserved -- it is in the vocabulary and nothing tests it, because asserting
that an evaluation context reached the backend needs an echo operation the control
API does not have -- so a provider declaring it got a green result for a claim
nothing had examined.
A declared capability whose every scenario was skipped for a *different* one was
likewise reported as passed, because the rollup counted a capability as exercised
by tag presence rather than by execution. Both scenarios in events.feature carry
@events as well as @Stale or @configuration-change, so the in-memory suite
reported @events as passed while neither scenario ran.
Both are the vacuous pass the capability vocabulary was introduced to eliminate,
arriving through the report rather than through the suite. A scenario now counts
towards a capability only if it actually ran, and a declared capability with
nothing to show is omitted. The suite asked no question of it, so it has no answer
to report, and a consumer sees the tag is absent rather than a pass it cannot rely
on. Omitting is preferred to inventing a fifth outcome: the four in the schema are
about what the provider did, and "this run demonstrated nothing" is a fact about
the run.
An undeclared capability is still reported as not-declared with its reason, and
not-applicable is unaffected -- @strict-numeric-typing being unanswerable in a
language with no integer type is a different statement from a capability nothing
exercised, and both need saying.
Matches the same fix in Go, open-feature/go-sdk-contrib#944.
Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli
force-pushed
the
feat/provider-tck-report
branch
from
September 12, 2026 12:35
9cf29bb to
e8ea5d1
Compare
aepfli
added a commit
to open-feature/js-sdk-contrib
that referenced
this pull request
Sep 12, 2026
…ed ones
Three defects in the capability rollup, all of them ways the report could report
a result the suite never obtained.
A failed capability was emitted as {"state": "failed"} with no reason. The schema
requires a reason for any outcome other than passed, so that entry does not
validate -- and it appears only when a provider is actually failing, which is
precisely when the report matters. It now says how many of how many scenarios
carrying the tag failed, and points at the per-scenario results for which and why.
No test caught it because every self-test suite passes, so nothing that runs end
to end reaches that branch. The new test drives the report builder directly with
synthetic records, which is the only way to exercise a failure without breaking a
provider on purpose.
A declared capability that no scenario carries was reported as passed. @targeting
is reserved -- it is in the vocabulary and nothing tests it, because asserting
that an evaluation context reached the backend needs an echo operation the control
API does not have -- so a provider declaring it got a green result for a claim
nothing had examined.
A declared capability whose every scenario was skipped for a *different* one was
likewise reported as passed, because the rollup counted a capability as exercised
by tag presence rather than by execution. Both scenarios in events.feature carry
@events as well as @Stale or @configuration-change, so the in-memory suite
reported @events as passed while neither scenario ran.
Both are the vacuous pass the capability vocabulary was introduced to eliminate,
arriving through the report rather than through the suite. A scenario now counts
towards a capability only if it actually ran, and a declared capability with
nothing to show is omitted. The suite asked no question of it, so it has no answer
to report, and a consumer sees the tag is absent rather than a pass it cannot rely
on. Omitting is preferred to inventing a fifth outcome: the four in the schema are
about what the provider did, and "this run demonstrated nothing" is a fact about
the run.
An undeclared capability is still reported as not-declared with its reason, and
not-applicable is unaffected -- @strict-numeric-typing being unanswerable in a
language with no integer type is a different statement from a capability nothing
exercised, and both need saying.
Matches the same fix in Go, open-feature/go-sdk-contrib#944.
Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
Setting PROVIDER_TCK_REPORT_DIR makes each suite write its run to <dir>/<name>.json against the report schema in the specification repository (open-feature/spec#425, part of open-feature/spec#424). Go is the language that needs this first. godog counts a capability-gated skip in its passed tally, so a run that skipped five of twenty-nine scenarios prints "29 scenarios (29 passed)". Appendix F is unambiguous that a scenario skipped for an undeclared capability is reported as skipped with the reason and never as passed, and the harness does say so in a separate log line -- but the headline number still says something false, and the number is what gets read. pytest and jest-cucumber report skips correctly, so this is the runner's property rather than the suite's design. The report does not fix godog's summary. It makes the summary stop mattering, by recording every scenario's outcome individually so a consumer can check the rule instead of trusting the runner to have applied it. The same run now reports twenty-four passed and five not-declared, each with its reason. Identity comes from revision.go, generated by sync_assets.go beside the embedded artifacts. Generating both in one command is what keeps them honest: the CI check regenerates and fails on any difference, so a revision disagreeing with the artifacts beside it cannot be committed. The check is widened to cover the generated file, which it would otherwise have missed. Both the commit and the tree hash are recorded, the tree because it identifies the artifacts alone -- unchanged by unrelated edits elsewhere in the specification, so two runs of identical artifacts agree even when pinned to different commits, and checkable because `git rev-parse <commit>:specification/assets/provider-tck` reproduces it. Two smaller decisions. The provider is identified by the name it reports through its own metadata, with Config.Name recorded as the configuration, because Config.Name is chosen to read well in a failure message -- "flagd-rpc" -- and a provider with two materially different modes produces two reports that are not interchangeable. And how the backend was driven is read through an optional interface rather than a new BackendControl method, so that adding it breaks no existing implementation and a control that does not implement it simply omits the field. Emission is opt-in through the environment rather than through Config so that producing a report is a property of the run and not of the code: CI sets it, a local run does not, and no adopter changes a line to publish one. The tests assert the property that motivated the work -- that no scenario the capability gate stopped is ever reported as passed, and that every scenario is accounted for exactly once, since a report that silently omitted what it skipped would satisfy the first half while still misleading a reader. That test earned its place immediately: it caught this emitter recording every skipped scenario twice, the second time as passed, because godog does not deliver the before hook's ErrSkip to the after hook. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
…ed ones
Two defects in the capability rollup, both found by the Java implementation
reviewing this one.
A failed capability was emitted as {"state": "failed"} with no reason. The schema
now requires a reason for any outcome other than passed, so that entry does not
validate -- and it would have appeared only when a provider was actually failing,
which is precisely when the report matters. It now says how many of how many
scenarios carrying the tag failed, and points at the per-scenario results for
which and why.
No test caught it because every self-test suite passes, so nothing that runs end
to end ever reaches that branch. The new internal test drives the report builder
directly with synthetic records, which is the only way to exercise a failure
without breaking a provider on purpose.
A declared capability that no scenario carries was reported as passed. @targeting
is reserved -- it exists in the vocabulary but nothing tests it, because asserting
that an evaluation context reached the backend needs an echo operation the control
API does not have -- so a provider declaring it got a green result for a claim
nothing had examined. That is the vacuous pass the capability vocabulary was
introduced to eliminate, arriving through the report rather than through the
suite.
Such a capability is now omitted. The suite asked no question, so it has no answer
to report, and a consumer sees the tag is absent rather than a pass it cannot
rely on. Omitting is preferred to inventing a fifth outcome: the four in the
schema are about what the provider did, and "the suite does not test this" is a
fact about the suite.
Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
A scenario entry in the conformance report was identified by feature and name. Every row of a Scenario Outline shares one name, so the type-mismatch matrix in errors.feature produced eleven entries that differed only in durationMs. If one row failed and ten passed, the report could not say which failed, and a consumer keying on feature and name kept whichever row it read last. Per the report schema, a scenarioResult now carries `example`: the Examples row it came from, keyed by column header, with the cells verbatim as strings. Gherkin has no types, so "1" stays "1" rather than becoming 1 -- the report says what the table said. It is present only for outline rows and omitted otherwise. godog hands a hook an already-expanded pickle, whose step text has the parameters substituted in and whose row is otherwise gone. What survives is AstNodeIds, whose last entry is the id of the Examples TableRow. The row is therefore recovered by parsing the embedded feature files a second time and indexing every TableRow by that id. Those ids come from a counter godog creates once per run and shares across the files it parses, so reproducing them means reproducing godog's parse -- same files, same order, pickle compilation in between. That coupling is not left to be trusted: a pickle that came from an outline and did not resolve fails the run, because quietly returning to the ambiguity this field exists to remove is worse than a build failure. The capability gate records its outcome before a scenario starts, so it fills the field in too. Four skipped rows of the @object outline are as ambiguous as four failed ones. The gate's own bookkeeping is keyed by pickle id rather than by scenario name for the same reason. Gherkin allows an Examples block to carry its own tags, so two rows of one outline can differ in whether the gate stops them; keyed by name, gating one row suppressed the after hook for every row and the rows that did run would have vanished from the report. gherkin/go/v26 moves from an indirect requirement to a direct one. It is the same module and version godog already builds against, so no dependency is added and no go.sum entry changes. Verified against the schema on open-feature/spec#425 with a Draft 2020-12 validator: every report the self-tests emit validates, the eleven matrix rows carry eleven distinct examples matching the feature file, and (feature, name, example) is unique across every scenario in a report. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
A capability whose scenarios were all skipped still reported as passed. The in-memory report claimed "@events": "passed" while both scenarios in events.feature had been skipped. Those scenarios carry @events alongside @Stale and @configuration-change, so withholding either one skipped them -- and the rollup counted a skipped scenario's remaining tags as exercising their capabilities. @events was declared, so it missed the not-declared branch, and fell through to passed on the strength of two scenarios that never executed. It is the same vacuous pass as the reserved-capability case fixed alongside it, reached by a different route: the rollup was counting tag presence rather than execution. A scenario now contributes to its capabilities only when its outcome is passed or failed. A declared capability whose every scenario was skipped falls to zero exercised and is omitted, which is the honest answer -- the suite has the question but never got to put it to this provider. Found by the Python implementation, which reached the same rollup semantics and then noticed what they produced. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
The report emitted its own per-scenario result list, with a four-value outcome enum and a field identifying which Scenario Outline row an entry came from. All three already exist in Cucumber Messages, which is maintained, cross-language and schema'd. The report is now an envelope plus a referenced Messages stream: one run writes <dir>/<name>.json and <dir>/<name>.ndjson, with the envelope naming and digesting the latter. godog 0.15.1 has no Messages formatter -- it registers cucumber (the legacy relishapp JSON), events, junit, pretty and progress -- so pkg/tck/messages.go is one, registered through the public godog.Format plugin interface. Its JUnit output was not a usable fallback: a capability-gated skip comes out as skipped="0" on the suite, in a non-standard <error type="skipped"> element, with the step text where the skip reason should be. The formatter events are the right source, and not incidentally so. For the first step of a gated scenario godog inserts a FAILED step result into its internal storage and also calls Skipped on the formatter. The built-in formatters read storage, which is where the malformed JUnit comes from; a formatter built on the events sees one SKIPPED. Verified end to end: a run godog summarises as "29 scenarios (29 passed)" produces a stream reporting 24 PASSED and 5 SKIPPED, each skip naming the capability that gated it. Two things the formatter interface does not give us. There is no scenario-finished event, so the stream is accumulated and written at Summary rather than streamed, which also lets the envelopes be written in the conventional order. And Skipped(pickle, step, definition) carries no error, so the capability that gated a scenario is unrecoverable from the events; the reason comes from the gate itself and lands in TestStepResult.message. The example field is gone. Messages identifies a row exactly: a pickle's astNodeIds end with the id of the Examples TableRow, and the stream carries the gherkinDocument those ids belong to. That also removes a coupling. Recovering the row from a *godog.Scenario meant reparsing the embedded feature files and reproducing the node ids godog assigns, which come from a counter godog shares across the files it parses -- so reproducing them meant reproducing godog's whole parse, and pkg/tck/examples.go existed to do that. A formatter is handed the same gherkinDocument godog compiled the pickles from, so the ids agree by construction. assetsTree is gone for the same reason. It was a git tree hash over the artifact directory, carried so a consumer could tell which questions a report answers without trusting the recorded commit. The stream carries the executed feature text, which answers that with the source rather than with a hash of it, and unlike the hash cannot be asserted wrongly because it is the input godog parsed. What stays OpenFeature-specific. The declaration -- which capabilities the provider claims -- is an input to reading the results rather than a summary of them: a SKIPPED scenario says the question was not put to this provider, and only the declaration says whether that is because the provider declines the capability. And the tested subject, since no standard results format has a slot for it; Messages records the runner, the runtime and the machine. Verified: the three envelopes validate against the reshaped schema with a Draft 2020-12 validator, their digests match, and each 543-message stream validates against the published Cucumber Messages v21.0.1 JSON schemas with no errors. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
The envelope named the results format but not its version, and Messages is versioned. This implementation builds against messages/go/v21 while the Python TCK emits 34.2.0 and cucumber-jvm ships a different one again, so a consumer receiving two reports cannot assume one schema validates both. Guessing is worse than not validating. A later schema accepts messages this producer could not have emitted, and an earlier one rejects messages that are perfectly valid, so a check against the wrong version reports a result that has nothing to do with the stream. During this work a schema copy of a later release was left in a shared temporary directory, and validating a v21 stream against it would have done exactly that. It reuses the protocol version already computed for the stream's own Meta message, rather than adding a second source: the envelope and the stream now cannot disagree about which release produced it. That helper reads the version from the build and falls back only when the build carries none, which happens for a test binary built from the module itself. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
Two reports of the same kind of provider disagreed about whether they described an in-process backend: the JavaScript in-memory suite said so and the Go one stayed silent. Not because the backends differ, but because Go's self-test controls never implemented the optional interface that reports it. The field is optional in the report and the interface is optional here, both so that introducing it broke no existing control. Together they make omission invisible: the suite passes, the report validates, and the field is simply absent. It surfaced only when four languages' reports were compared side by side. The two self-test controls now report in-process, which is what they are -- they manipulate a provider in this process and there is no backend to drive. More usefully, a control that reports nothing now says so in the run output. The silence was the actual defect: every control either drives a real backend over HTTP or manipulates an in-process one, so there is no third case an absent value legitimately describes, and an adopter had no way to discover their report had a hole in it. Logged rather than failed, because a missing optional field is not a conformance problem -- it is a gap in what the report can say about the run. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
The tag was named for a stricter rule than the specification wants. flagd is implementing an accepted numeric coercion ADR (open-feature/flagd#1996) whose rule is that coercion is permitted when lossless and must fail with TYPE_MISMATCH only when information would be lost: 10.0 requested as an integer succeeds, 0.5 does not. Appendix F said "does not coerce between integer and float", which forbids the case the ADR requires to work, and flagd's own testbed is gaining @numeric-coercion scenarios -- two vocabularies for one property is the drift a shared vocabulary exists to prevent. So tck.StrictNumericTyping becomes tck.NumericCoercion, its tag becomes @numeric-coercion, and its doc comment states the corrected rule rather than "does not coerce". The spec submodule moves to dc4d7ae8 and the embedded assets are re-synced from it, which also picks up the renamed lifecycle readiness scenario and the POST /start requirement on the control API. No scenario is added: the existing one asks about 0.5 and remains the lossy half of the contract. Two gaps are recorded rather than closed, in the capability's doc comment and in the README. The lossless half has no scenario, because the canonical flag set contains no integral float to ask it of and adding one changes the flag set for every language at once, so a provider that wrongly rejects 10.0 as an integer still passes. And accessor width is not modelled at all: the ADR distinguishes a 64-bit integer accessor from a 32-bit one, and this suite is silent about the difference. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
@targeting and @caching are reserved: they are part of the vocabulary but no scenario carries either tag. Appendix F now states plainly that such a tag must not be declared and must not appear in a conformance report's declaration -- nothing carries it, so declaring it cannot be verified, cannot even produce a skip, and tells a reader of the report only that something was claimed and nothing examined. That was a live defect here rather than a hypothetical one. A Java conformance report asserts both tags as declared, not by anyone's decision but because that adoption declares "every capability except X" and collects every reserved tag on the way past. AllCapabilities had the same shape, and it is the default when Config.Capabilities is nil. So the reserved set is named once, beside the constants, and exposed as Capability.IsReserved. AllCapabilities omits it, which also fixes the default. An adopter who names a reserved capability outright is failed rather than warned: it is rejected by newCapabilitySet, which Config.validate calls, so the run stops before any scenario with the same message shape as any other configuration problem. Warning would leave the claim in a published report, which is the outcome the rule exists to prevent, and the fix is to delete one line. Putting the check in newCapabilitySet rather than only in Config.validate is deliberate: the capability set is the only thing declaration.declared is built from, so a reserved tag now has no path into a report at all, and there is no second route that could drift from the rule. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
A conformance report is a claim that a provider was asked the canonical questions, and nothing so far established that it was asked all of them. `go test -run` matching one scenario name produced a green suite and a well-formed report describing one scenario out of twenty-nine; a mis-wired extension filesystem would do the same. There is no field in the envelope a consumer could read to notice, and the schema is closed, so failing the test is the only lever there is. The expectation is the embedded assets compiled by godog's own parser rather than by a second one. A parser of this package's own would disagree with godog about exactly the cases that matter -- an Examples block carrying its own tags, scenarios inside a Rule -- and the expectation has to be what a full run would actually have produced. Two shapes of gap are distinguished, because they have different causes. A scenario absent from the results was never parsed. A scenario present with no outcome was announced and then never executed: godog reports a pickle before handing the scenario to the subtest, so a `-run` selector leaves the test case behind with no step result in it. The outcome comes from the Messages formatter for the same reason -- the After hook does not run for a scenario that never started, so nothing else in the suite sees it. A capability-gated scenario is not a gap. It ran the gate and appears as SKIPPED with its reason, so the question was put and declined; treating that as a gap would force every provider to declare every capability. Extension scenarios are counted and reported but can never close a gap. An adopter's feature is an addition to the canonical set, and a rule where supplying enough scenarios of your own made the canonical ones optional would defeat the check. The duplicate status ordering in report_internal_test.go is folded into the one the formatter now uses, since a scenario's outcome is derived in one place rather than two. This is the guard on its own. Its other half, the extension point in Config.ExtensionFeatures and Config.ExtensionSteps, reads no report and went to the base branch instead. The guard sits here rather than on the base because its input is the report's own stream: r.executed has exactly one producer, messagesSink.executed in messages.go, and what the guard protects is the published report -- its failure message says so. Giving the base a second producer for r.executed would mean duplicating the Messages plumbing there to no end. selftest_extensions_test.go arrives with the guard rather than with the extension point for the same reason: it establishes what actually ran by reading the conformance report, so it is a report-side test of a base-side feature and does not compile without report.go. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
The assets now arrive as a Go module, so the generator that wrote revision.go is gone and the constant it wrote has no source. Point it at the pin in go.mod, which is where the revision is actually recorded now, and drop the "Code generated -- DO NOT EDIT" header that is no longer true. The obvious replacement does not exist. runtime/debug.ReadBuildInfo reports no dependencies at all from a library package's test binary -- not in workspace mode, not in plain module mode, not under a replace, across go1.22, go1.25 and go1.26 -- and the TCK only ever runs inside one, both its own self-tests and an adopter's TestConformance. So the value is written by hand and duplicated from go.mod on purpose. A hand-written constant can go stale in silence, which is the one failure mode that leaves a report structurally valid and semantically wrong: naming a revision the run did not use. So TestSpecRevisionIsRecorded reads ../../go.mod and fails when the two disagree. That file is inside the module, present in every clone and in every module zip, so moving the pin without updating the constant breaks this suite's own tests rather than a consumer's report. The pin is a pseudo-version today, which still names the commit -- the trailing twelve characters are its prefix. Once the assets are released under a tag it reads as that tag, which is the form worth putting in a report. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
Reading the assets out of the module instead of a vendored copy shortened every canonical feature uri: gherkin/errors.feature, where it used to be assets/gherkin/errors.feature. The reporting side keys on that prefix to tell a canonical scenario from an extension, so three extension self-tests classified every canonical scenario as "neither canonical nor an extension" and failed. The short form is the right one to land on, not merely the one that fell out: Appendix F says canonical features keep the path they have in the spec repository, and gherkin/errors.feature is that path. assets/ was an artefact of where the copies were vendored to. The synthetic uris in report_internal_test.go move with it. They are fixtures and nothing resolves them, but a fixture that no longer matches the prefix the code partitions on is a test that agrees with itself and with nothing else. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
…re lacks The canonical assets gained two lossless numeric-coercion scenarios -- an integral float requested as an integer, and an integer requested as a float. The extension self-tests failed both. They failed honestly. Their fixture backend is the Go SDK's memprovider.InMemoryProvider, which does not coerce: it type-asserts, so it refuses integral-float-flag as an integer and integer-flag as a float and reports TYPE_MISMATCH for each. TestInMemoryProvider had already recorded that finding and withdrawn NumericCoercion. vendorConfig restated the capability list instead of sharing it, so it kept declaring a capability the provider never had -- true only while @numeric-coercion carried the lossy scenario alone, which memprovider passes by refusing every float. So the declaration was corrected rather than the assertion, and the list now lives in one place: inMemoryCapabilities, shared by every self-test that drives this backend. The extension self-tests ask whether ExtensionFeatures and ExtensionSteps work; what the fixture provider is capable of is not their question, and answering it separately is how the two lists came to disagree. The canonical-set guard needed nothing -- it parses the embedded assets through godog itself, so it counted all 40 scenarios without being told the number. Also points SpecRevision at the assets module version the base branch now pins, which TestSpecRevisionIsRecorded compares against go.mod. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
The base moved the spec assets pin to the revision that gates the
re-initialisation scenario on @reinitialization. SpecRevision is a
hand-written copy of that pin -- nothing can generate it, because a
library package's test binary carries no module build information at
all -- so it has to be moved by hand too.
TestSpecRevisionIsRecorded is the reason that is safe rather than
hopeful. It reads ../../go.mod and compares, and it failed here exactly
as designed:
SpecRevision = "v0.0.0-20260911113217-ba002ce8e807" but go.mod pins
github.com/open-feature/spec/specification/assets/provider-tck at
"v0.0.0-20260911124500-fc99d5ace4da"; move the pin with `go get` and
update SpecRevision to match
A report whose specRevision names a different revision of the
conformance definition than the suite actually ran is worse than one
with no revision at all, since a consumer has no way to notice. That
matters more than usual for this particular move: the set of mandatory
scenarios changed, so two runs a fortnight apart answer different
questions, and specRevision is the only field that says which.
Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
The declaration says which capabilities were claimed and the results say which scenarios ran. Neither says why a capability was not claimed, and the two reasons are not alike: declining @configuration-change for want of a streaming transport is a decision, declining @numeric-coercion because 0.5 narrows to 0 with no error code is a defect. A consumer reading the report sees the same absence for both. Config.KnownDeviations is where the adopter states the difference, and this carries it into the envelope so it reaches whoever reads the report rather than only whoever reads the adoption. Appendix F expects the field under this name, and the Java TCK already emits it, so a consumer comparing two languages' reports reads one shape. Verbatim, and deliberately so. A summary is prose written by the provider author for someone comparing providers; an emitter that paraphrased or normalised it would be substituting its own account of the defect for theirs. Top-level, beside the declaration rather than inside it, matching Java. An entry is not a statement about the declared set: it may concern a capability that WAS declared -- the capability holds, one scenario it gates does not -- or a mandatory scenario belonging to no capability at all. What it qualifies is the declaration as a whole. Omitted when empty, which is the opposite of the rule for declared capabilities, and for a reason. An empty declared list is a claim: this provider declares nothing. An empty deviation list would read as "this provider has no known defects", which is not what silence means -- only that none were recorded. Two tests pin the two halves, along with the untracked-and-uncapability-ed shape, which is the one most likely to be dropped or defaulted on the way through. No adopter-facing API file changes here: the type and both constructors live on the base, because they are something an adopter writes. This branch only reads them. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
The field existed only so that a consumer unmarshalling ReportDeclaration saw the whole schema; its own comment said so, and that nothing populated it because Config has no field for it. The schema has since dropped it: not-declared and not-applicable are both skips, the skip carries its reason, and a capability that cannot hold in a language at all is a property of the SDK recorded in Appendix F rather than restated in every report. So there is no schema left for it to mirror, and nothing to populate it with. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
SpecRevision is duplicated from go.mod by hand, for the reason its own comment gives, and TestSpecRevisionIsRecorded is what stops the two drifting. The pin moved with the @Variants and @targeting work on the parent branch, so this is the other half of that move: a report that named the previous revision would tell a consumer the run answered questions it did not ask. The shadowing fixture follows the same rename. It takes its scenario name from the canonical evaluation.feature on purpose -- being hostile is the point, and a name that no longer collides with anything tests nothing. "Resolve values with variant and reason" became "Resolve values with reason" when the variant assertions moved into their own gated outline, so the fixture and the constant the test compares against both move with it. The sample envelope in the README is refreshed from a real run for the same reason: it showed a bare commit id where specRevision now carries the module pseudo-version, and a declaration the in-memory suite has not made for two revisions. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
Two findings golangci-lint v2.13.2 reports against this branch and has done since the report machinery landed, so `make lint` is red on it today. Neither is a behaviour change. The Fprintf writes a bare newline into a Messages stream whose encoding has already failed, which is a best-effort marker in something already broken -- the one error here with genuinely nowhere to go. Discarding it explicitly says so; leaving it unchecked only looked like it did. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
SpecRevision is duplicated from go.mod on purpose -- nothing can generate it, because a library package's test binary carries no module build information -- and TestSpecRevisionIsRecorded exists so that moving the pin without updating the constant breaks this suite's own tests rather than a consumer's report. spec 009afe06 moved the pin, so this is that test doing its job. The README's illustrative numbers move with it. Every one of them was measured from a run of the in-memory suite rather than adjusted by arithmetic: the Messages stream now tallies 270 PASSED and 125 SKIPPED steps, godog's headline reads "56 scenarios (56 passed)" of which eighteen did not run, and a `-run` selector matching a single scenario name now leaves 27 canonical scenario names with no outcome rather than 12. The sample envelope's specRevision follows the pin; its digest deliberately does not, because a Messages stream carries timestamps and so hashes differently on every run -- that field is opaque by nature and no value printed here could be the right one. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
…inery The base branch moved the package to tools/tck and replaced tck.Config with functional options. The reporting machinery and its tests are the part of this branch the base could not carry along, so they are brought over here. Mechanical, with two things worth noting. vendorConfig now returns []tck.Option rather than a tck.Config, and the shadowing test appends tck.WithFeatures instead of assigning a field. That is the ordering guarantee doing real work: a later option wins over an earlier one, which is how one shared adoption can be pointed at a different extension filesystem without a second copy of it. The revision guard read ../../go.mod, which was the module root back when the package sat under pkg/tck. The package is the module root now, so it reads go.mod beside it. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
…erface replaces
A net deletion. BackendControl now requires ControlAPI() ControlAPI on the base
branch, so the optional interface this branch used to sniff for, the
empty-string fallback behind it, and the runner log that told an adopter their
control had not said, all stop having a case to cover.
What goes:
- controlAPIReporter, the unexported optional interface.
- controlAPIOf, whose own doc comment had already reached the conclusion this
implements -- "silence here is a small lie by omission: every control is
either driving a real backend over HTTP or manipulating an in-process one,
so there is no third case the empty value legitimately covers".
- runner.reportControlAPIGap, a log line that existed only because the field
could be absent. A required method is a compile error instead, which is
where that belongs.
- InProcessControl.ControlAPI() string and the self-test control's, both
superseded by the typed ones on the base.
Report.Backend stops being a pointer and loses omitempty, and
ReportBackend.ControlAPI loses its own: the schema now requires the backend
block at the top level and controlApi within it. The old shape made the one
value most worth knowing -- in-process, the case the enum exists for -- the one
value that could never legally appear.
No report test asserted an absent backend block or an absent controlApi, so
none had to change. An emitted report was validated against the updated schema
by hand: conformance-report.schema.json at spec bcd005f1 accepts
reports/in-memory.json unchanged, including the newly required members.
Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
SpecRevision is written by hand and TestSpecRevisionIsRecorded fails when it disagrees with go.mod, which is what it is for: the base branch moved the pin to the appendix tip 93eb1a58 and this is the constant that says so in every report. The report sample in the README follows, and gains two members it should already have had: controlApi, now required, and results.formatVersion, which the emitter has always written. Two stale ./pkg/tck/ links from the flattening go with them. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
PROVIDER_TCK_REPORT_DIR becomes TCK_REPORT_DIR, following the package rename to `tck` and TCK_RUN on the base. The two move together on purpose: a partial rename would leave TCK_RUN beside PROVIDER_TCK_REPORT_DIR in one repository, which is worse than either consistent answer. Nothing is published and nothing is scripted against either name, so this is free now and expensive later. The recorded SpecRevision follows the pin the base moved, to v0.0.0-20260912211427-ccdb88790bb4, in the two places that carry it by hand: revision.go, which nothing can generate because a library package's test binary has no module build information, and the README's sample report. TestSpecRevisionIsRecorded is what makes the pair safe -- it reads go.mod and fails when the constant disagrees, which is how the report branch's copy of the pin was found in the first place. Verified rather than assumed: emitting with TCK_REPORT_DIR set writes both files and the envelope reads "specRevision": "v0.0.0-20260912211427-ccdb88790bb4". Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
SpecRevision is written by hand and TestSpecRevisionIsRecorded is what keeps it honest against go.mod, so moving the pin moves it too: spec c342461a, pseudo- version v0.0.0-20260913074034-c342461aa95d. Two things downstream of the pin move with it. The shadowing fixture impersonates a canonical scenario by name, and the name it copied was "Resolve values with reason" -- an outline that lost its reason column in this revision and is now "Resolve values". A fixture that no longer collides tests nothing: TestAnExtensionCannotShadowACanonicalScenario would have gone green because there was nothing left to shadow. It now copies the new name, and the canonical-rows assertion is what caught the drift. The README's sample report gains "@standard-reasons" in declaration.declared and a regenerated results.digest, both taken from an emitted report rather than edited by hand. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
…ip reason into the report SpecRevision and the README's sample envelope move to v0.0.0-20260913091110-89b1519a08d8 with the base. TestSpecRevisionIsRecorded is what caught the second of those, which is the only reason the pair stays in step. The sample envelope's results.digest is deliberately not touched. It is not a function of the pin: the Messages stream carries per-run durations and ids, so three consecutive runs of the same suite against the same assets produce three different digests. The field in the README is illustrative like the version numbers beside it, and rewriting it each pass would suggest a reproducibility it does not have. The report side of the inexpressibility refusal is the half a consumer actually reads. A capability-gated skip carries its reason into the Messages stream as the step result's message, so the two refusals have to differ there and not only in the log -- "this provider does not declare it" describes a choice the provider made, and a capability the Go SDK cannot express was never the provider's to choose. The outcome is deliberately not a new value: the report schema is shared across four languages and the distinction belongs in the reason, which the schema already carries. Go has no inexpressible capability, so the new test installs one and drives the real gate. Mutating the branch away reds it and the base-branch test together. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
SpecRevision is duplicated from go.mod on purpose and TestSpecRevisionIsRecorded fails when the two disagree, so the re-pin to spec 4cab0320 has to be followed here by hand. The sample envelope in the README carries the same string and moves with it. results.digest in that envelope is deliberately left alone: the Cucumber Messages stream carries per-run durations and ids, so the digest is not a function of the pin and rewriting it each pass would imply a reproducibility it does not have. Nothing else moves. The assets are byte-identical across the two pins, so the report's fixtures and counts are unchanged. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
The base branch now runs the conformance suites from `make tck` and has removed the environment variable that used to decide whether they ran at all. This one is a different variable and stays, but with the other one gone the distinction is worth stating rather than leaving to be inferred: TCK_REPORT_DIR selects an output, and `make tck` without it runs exactly the same scenarios and writes no report. The example gains the adoption form beside the self-test one, so the single documented command appears here too. Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
The base README was rewritten as a guide to this binding, and the
reporting section is rebased onto it. Two things were carried over
rather than kept:
- the "What identifies a scenario" section still described the
"example" field, which was removed when the results moved to
Cucumber Messages -- Messages carries the Examples row identity in a
pickle's astNodeIds, so the field had nothing left to do. The
surviving two sentences are in "Why Cucumber Messages".
- the rationale for Messages, the godog formatter and the skip tally
is compressed. What is kept is the part that is Go's: godog counts a
capability-gated skip as passed, has no Messages formatter, and
reports a gated scenario correctly through its formatter events
while holding a FAILED result for the same step in its internal
storage.
83478 bytes to 33778, no behaviour change.
Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
aepfli
force-pushed
the
feat/provider-tck-report
branch
from
September 14, 2026 06:26
14391ce to
1720e69
Compare
aepfli
added a commit
to open-feature/js-sdk-contrib
that referenced
this pull request
Sep 14, 2026
…ed ones
Three defects in the capability rollup, all of them ways the report could report
a result the suite never obtained.
A failed capability was emitted as {"state": "failed"} with no reason. The schema
requires a reason for any outcome other than passed, so that entry does not
validate -- and it appears only when a provider is actually failing, which is
precisely when the report matters. It now says how many of how many scenarios
carrying the tag failed, and points at the per-scenario results for which and why.
No test caught it because every self-test suite passes, so nothing that runs end
to end reaches that branch. The new test drives the report builder directly with
synthetic records, which is the only way to exercise a failure without breaking a
provider on purpose.
A declared capability that no scenario carries was reported as passed. @targeting
is reserved -- it is in the vocabulary and nothing tests it, because asserting
that an evaluation context reached the backend needs an echo operation the control
API does not have -- so a provider declaring it got a green result for a claim
nothing had examined.
A declared capability whose every scenario was skipped for a *different* one was
likewise reported as passed, because the rollup counted a capability as exercised
by tag presence rather than by execution. Both scenarios in events.feature carry
@events as well as @Stale or @configuration-change, so the in-memory suite
reported @events as passed while neither scenario ran.
Both are the vacuous pass the capability vocabulary was introduced to eliminate,
arriving through the report rather than through the suite. A scenario now counts
towards a capability only if it actually ran, and a declared capability with
nothing to show is omitted. The suite asked no question of it, so it has no answer
to report, and a consumer sees the tag is absent rather than a pass it cannot rely
on. Omitting is preferred to inventing a fifth outcome: the four in the schema are
about what the provider did, and "this run demonstrated nothing" is a fact about
the run.
An undeclared capability is still reported as not-declared with its reason, and
not-applicable is unaffected -- @strict-numeric-typing being unanswerable in a
language with no integer type is a different statement from a capability nothing
exercised, and both need saying.
Matches the same fix in Go, open-feature/go-sdk-contrib#944.
Signed-off-by: Simon Schrottner <simon.schrottner@flagsmith.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Emits a machine-readable conformance report for each suite run, when
TCK_REPORT_DIRis set. Unset means no report, which is the default and is notan error. It gates an output, not the run: the suite runs either way, and
which target ran it is
make tck's business rather than an environmentvariable's.
Tracking: open-feature/spec#417. Appendix F: open-feature/spec#423. Report
format: open-feature/spec#424. Schema: open-feature/spec#425.
What a run writes
Two files. An envelope at
<dir>/<name>.json, conforming to the schema inopen-feature/spec#425, and the results it references at
<dir>/<name>.ndjson,which is a Cucumber Messages stream.
{ "schemaVersion": "1", "provider": { "name": "InMemoryProvider", "language": "go", "configuration": "in-memory" }, "sdk": { "name": "github.com/open-feature/go-sdk", "version": "v1.18.0" }, "tck": { "implementation": "go-sdk-contrib/tools/tck", "version": "v0.1.0", "specRevision": "4cab032043f9b2335a023310b1010b20d7a7d0d9" }, "backend": { "description": "the Go SDK's memprovider.InMemoryProvider, rebuilt per scenario", "controlApi": "in-process" }, "declaration": { "declared": ["@events", "@numeric-coercion", "@object"] }, "results": { "format": "cucumber-messages", "location": "in-memory.ndjson", "digest": "sha256:4754d458ac5a1a137080082b7947f8f1eafc5df9f6d54551440da9f8ac6a0dce" } }The envelope identifies what was tested and what the provider claims. It does
not contain the results. A Messages stream carries the feature sources and so is
far larger than the envelope, and a consumer deciding whether it cares about a
report should not have to fetch a whole run to find out.
results.digestcoversthe payload byte for byte.
Why the results are a standard format
This PR previously defined its own: a per-scenario list, a four-value outcome
enum, and a field naming the Examples row an entry came from. All three are
gone. The per-scenario outcome, the tags, the executed feature source and the
identity of a Scenario Outline row are already specified by Cucumber Messages,
which is maintained, cross-language, schema'd, and emitted natively by
cucumber-jvm. Restating them here meant a second format to version, and two
places for the same fact to disagree.
Reading the results needs no bespoke tooling: every scenario is a
pickle,every result is a
testCasewith onetestStepFinishedper step, and ascenario's outcome is the most severe of its step results, which is how Cucumber
itself derives it.
exampleis removed because Messages already carries row identity. Apickle's
astNodeIdsend with the id of the ExamplesTableRowit was expandedfrom, and the stream carries the
gherkinDocumentthose ids belong to, so therow's cells are recoverable from the stream alone. Four implementations had each
reinvented that field independently, one of them by reverse-engineering how its
runner maps a pickle back to a table row.
Removing it removed a coupling too. Recovering the row from a
*godog.Scenariomeant reparsing the embedded feature files and reproducing the AST node ids
godog assigns, which come from a counter godog shares across the files it
parses, so reproducing them meant reproducing godog's whole parse. That is what
pkg/tck/examples.godid, and it is deleted. A formatter is handed the samegherkinDocumentgodog compiled the pickles from, so the ids agree byconstruction rather than by imitation.
assetsTreeis removed because Messages carries the executed source. Thetree hash existed so a consumer could tell which questions a report answers
without trusting the recorded commit. The stream's
sourcemessages answer thatwith the feature text rather than with a hash of it, and unlike the hash cannot
be asserted wrongly, because it is the input godog parsed.
What stays OpenFeature-specific, and why
The declaration.
declaration.declaredis an input to reading the resultsrather than a summary of them, which is why it cannot be derived from the
payload. A
SKIPPEDscenario says the question was not put to this provider;only the declaration says whether that is because the provider declines the
capability. Given the declaration and a scenario's tags, which the stream
carries, the reason for a skip follows without being transported per scenario.
The tested subject. No standard results format has a slot for the provider
under test: Messages records the runner, the runtime and the OS, not the
subject. So
provider,sdk,tckandbackendstay in the envelope.@strict-numeric-typingis now@numeric-coercion, and the rule is correctedtck.StrictNumericTypingbecomestck.NumericCoercionand its tag becomes@numeric-coercion. The spec submodule moved todc4d7ae8for this change and the embedded assetswere re-synced from it, so
SpecRevisionchanged too. It has since advanced to4cab0320, which iswhat the envelope above records; the assets are byte-identical across that range.
The old name was chosen for a stricter rule than the specification wants. flagd
is implementing an accepted numeric coercion ADR
(open-feature/flagd#1996) whose rule is that coercion is permitted when
lossless and must fail with
TYPE_MISMATCHonly when information would belost:
10.0requested as an integer succeeds,0.5does not. Appendix Fpreviously said "does not coerce between integer and float", which forbids the
case the ADR requires to work. flagd's own testbed is gaining
@numeric-coercionscenarios as part of that work, so keeping a second name forone property was drift, not caution.
When this was written the tag had one scenario, asking about
0.5— the lossyhalf — and this section recorded "the lossless case has no scenario" as an open
gap. That gap is closed. The canonical set gained
integral-float-flag, anderrors.featurenow carries three@numeric-coercionscenarios: the lossy one,10.0requested as an integer, and10requested as a float. A providerdeclaring the tag must satisfy all three, which is what stops the shortcut of
rejecting every float and calling it strictness — the shortcut two of the four
OFREP adoptions turned out to be taking.
One gap does remain, and Appendix F records it rather than closing it here:
Accessor width is modelled, and what remains open is narrower than it looks. The ADR
distinguishes a 64-bit integer accessor from a 32-bit one, and flagd's testbed tags the 32-bit-only
scenarios
@int32-bounded. Appendix F draws the same boundary from the other side: every language'saccessor can ask for 2^31 − 1, so that precision scenario is untagged and mandatory; only some
can ask for 2^53 − 1, so that one carries
@large-integers, which a provider on a 32-bitaccessor leaves undeclared. Mandatory-versus-gated instead of two tags, and no tag needed for the
common case.
What is genuinely open is the negative half: nothing asserts what a 32-bit-accessor provider owes
when asked for a value it cannot represent. The scenario simply skips, so a provider that silently
truncates passes exactly like one that returns
TYPE_MISMATCH. That is deliberate rather thanoverlooked — spec#430 has not settled what a
provider owes a value that does not fit the requested accessor, and asserting it here would be this
suite inventing a rule the specification does not have.
The capability's doc comment says so.
A reserved capability can no longer be declared
@cachingis reserved: part of the vocabulary, carried by no scenario. It wasnot the only one when this was written —
@targetingwas reserved too, andstopped being so at spec
26362f85, when scenarios started carrying it. That isthe shape a reserved tag is meant to have: a placeholder that either grows
scenarios or is removed, never a claim a report can carry indefinitely. Appendix F now states plainly that such a tag must not be declared and
must not appear in a report's declaration — nothing carries it, so declaring it
cannot be verified, cannot even produce a skip, and tells a reader of the report
only that something was claimed and nothing examined.
This was a live defect rather than a hypothetical one. A Java conformance report
asserts both tags as declared, not by anyone's decision but because that
adoption declares "every capability except X" and collects every reserved tag on
the way past.
AllCapabilities()had the same shape, and it is the default whenConfig.Capabilitiesis nil.Capability.IsReserved.AllCapabilities()omits it, which also fixes the nil default. Its doccomment says what it now returns and why.
The check lives in
newCapabilitySet, whichConfig.validatecalls, so therun stops before any scenario with the same message shape as any other
configuration problem. Warning would leave the claim in a published report,
which is the outcome the rule exists to prevent, and the fix is to delete one
line.
Config.validateis deliberate:the capability set is the only thing
declaration.declaredis built from, soa reserved tag now has no path into a report at all, and there is no second
route that could drift from the rule.
The Messages formatter
godog 0.15.1 has no Messages formatter. It registers
cucumber(the legacyrelishapp JSON),
events,junit,prettyandprogress. Sopkg/tck/messages.gois one, registered through the publicgodog.Formatplugin interface and consuming the formatter event stream. Nothing in it
implements the Messages specification: the types come from
github.com/cucumber/messages/go/v21, which this module already depended on. Itis a candidate for contributing upstream to godog.
godog's JUnit output was not a usable fallback: a capability-gated skip comes
out as
skipped="0"on the suite, in a non-standard<error type="skipped">element, with the step text where the skip reason should be.
That is not a coincidence, and it is the reason the events are the right source.
For the first step of a gated scenario godog inserts a
FAILEDstep result intoits internal storage and calls
Skippedon the formatter. The built-informatters read storage, which is where the malformed JUnit comes from; a
formatter built on the events sees one
SKIPPED. External formatters cannotread storage anyway, since
SetStoragetakes an internal type, which here is afeature.
Two limits of the formatter interface, both worked around and both documented in
the file:
TestRunStarted,Feature,Pickle, the per-step results andSummary, and nothing that fires when ascenario ends. The stream is therefore accumulated and written at
Summaryrather than streamed, which also lets the envelopes be emitted in the
conventional order. It assumes serial execution, which this suite already
enforces for unrelated reasons.
Skipped(pickle, step, definition)carries noerror, so the capability that gated a scenario is unrecoverable from the
events. The reason comes from the capability gate itself and lands in
TestStepResult.message. This is the one part of the formatter that isspecific to this suite rather than general.
The skip is truthful
This is the whole reason the report exists. godog counts a capability-gated skip
in its passed tally:
Five of those twenty-nine did not run. Appendix F is unambiguous that such a
scenario is reported as skipped with the reason and never as passed.
The stream for that same run reports 24 PASSED and 5 SKIPPED, accounts for
all 29 scenarios exactly once, and gives every skip a reason naming the
capability that caused it. At step level, 182 PASSED and 26 SKIPPED.
Three schema changes since this was opened
All three are in spec#425 and
this branch emits against them.
backend.controlApiis required. A provider with no backend still had itsflag state manipulated somehow, and which of the two ways that was is the
single most important thing a reader needs to know what the results are worth.
The old shape made the whole
backendblock omissible, which made the onevalue most worth knowing —
in-process, the case the enum exists for — the onethat could never appear.
ReportBackend.ControlAPIis never empty: the controlstates it, nothing infers it.
summaryis required, and its issue link is not. Adeviation with no prose is a line of tags that tells a reader nothing; a
deviation with no filed issue is an honest state to be in, and requiring one
encourages filing a placeholder.
skips that come from an undeclared capability. It does not explain a skip the
implementation refuses —
@large-integersin Java,@numeric-coercioninJavaScript — which is a property of the SDK rather than of the provider, and a
reader who conflated the two would read a language's limit as a provider's
decision.
Verification
gofmt,go build ./...,go vet ./...clean; package tests pass, includingunder
-race.schema with a Draft 2020-12 validator, and every
results.digestmatches itspayload.
meta, 4source, 4gherkinDocument, 29pickle, 29 each oftestCase/testCaseStarted/testCaseFinished, 208 eachof
testStepStarted/testStepFinished,testRunStarted,testRunFinished), validates against the published Cucumber Messagesv21.0.1 JSON schemas with zero errors — and also against 24.1.0, a
newer major than the types that produced it, so the stream is readable by a
current cucumber toolchain and not only by the version it was emitted with.
errors.featureresolve to eleven distinctastNodeIdsand eleven distinctExamples rows, recovered from the stream's own
gherkinDocument.For the two changes above:
make provider-tck-assets-checkpasses after the submodule bump. The syncedassets contain
@numeric-coercionand no occurrence of@strict-numeric-typing.controllable-in-memory26 PASSED / 3 SKIPPED,in-memory24 / 5,multi-provider25 / 4. The renamed scenario, "A float flag is not silentlynarrowed to an integer", is reported PASSED under
@numeric-coercionin allthree, and every gated skip is still SKIPPED with a reason naming its
capability.
declaration.declaredin all three envelopes contains neither@targetingnor
@caching.AllCapabilities()excludes exactly thereserved set and nothing else, the nil default excludes it,
Config.validaterejects each reserved capability by name, and the declaration built by
buildReportfrom the default set carries no reserved tag. Removing theexclusion from
AllCapabilities()fails three of them.The submodule bump also picks up two unrelated spec changes between
dfa16586and
dc4d7ae8: the lifecycle readiness scenario is renamed to "A provider thatsuccessfully initializes becomes ready", and the control API's
POST /startgains the requirement that it not return until the seeded flag state is being
served. Nothing in this module referenced the old scenario name, and no Go code
implements
/start.Tests were rewritten accordingly: the assertions about
scenarios[], the fouroutcomes and
exampleare gone, replaced by assertions over the Messagesstream. The two properties that matter are kept and asserted directly, that no
gated scenario is ever reported as passed and that every scenario is accounted
for exactly once. The internal tests drive the formatter through godog's event
sequence to cover the
FAILEDpath no passing suite reaches.