Testkit
Table of contents
- Overview
- Installation
- Choosing a Layer
- Basic Usage
- Managing Flags
- Tracking Evaluations
- Provider Status
- Behavior Controls
- Testing Patterns
- Test Isolation
- Behavior-Matrix Testing with zio-bdd
- Typed fixtures with FlagDef
- Law-checking a custom FlagType
- Best Practices
Overview
The testkit module provides TestFeatureProvider, an in-memory OpenFeature provider designed for testing. It allows you to:
- Pre-configure flag values
- Dynamically update flags during tests
- Track which flags were evaluated
- Verify evaluation counts and contexts
The TestFeatureProvider implements the OpenFeature FeatureProvider interface, so it works seamlessly with the ZIO OpenFeature layer system.
Installation
libraryDependencies += "io.github.etacassiopeia" %% "zio-openfeature-testkit" % "<version>" % Test
Choosing a Layer
| Layer | Provider starts as | Use when |
|---|---|---|
layer(flags) | Ready | Most tests — flags work immediately |
scopedLayer(flags) | Ready | Same, self-contained scope |
asyncLayer(flags) | NotReady | Testing startup/initialization behavior — requires manual setStatus |
asyncReadyLayer(flags, delay) | NotReady → Ready | Simulating real async init without manual status management |
Rule of thumb: Use layer unless you specifically need to test how your code handles a provider that isn’t ready yet.
Basic Usage
Creating a Test Layer
The simplest way to use the testkit is with TestFeatureProvider.layer:
import zio.*
import zio.test.*
import zio.openfeature.*
import zio.openfeature.testkit.*
// Create layer with initial flags
val testLayer = TestFeatureProvider.layer(Map(
"feature-a" -> true,
"feature-b" -> "variant-1",
"max-items" -> 100
))
// Use in tests
val test = for
result <- FeatureFlags.boolean("feature-a", false)
yield assertTrue(result == true)
test.provide(Scope.default >>> testLayer)
Creating a Provider Directly
For more control, create the provider directly:
for
provider <- TestFeatureProvider.make(Map(
"feature" -> true,
"variant" -> "control"
))
// Use provider methods directly
_ <- provider.setFlag("new-flag", "value")
yield ()
Naming a Provider
Every provider reports a metadata name, and two things key providers by it — so two test providers that share a name cannot be told apart. makeNamed gives each one its own:
for
primary <- TestFeatureProvider.makeNamed("primary")
fallback <- TestFeatureProvider.makeNamed("fallback", Map("flag" -> true))
yield (primary, fallback)
You need it in exactly two situations:
- Chaining two test providers. The Java SDK keys a
MultiProvider’s providers by metadata name and keeps only the last of a given name (logging the collision at INFO), so a chain of two default-named test providers is really a chain of one — and a test of fall-through or precedence between them passes or fails for the wrong reason. See Absent keys and provider chains for what makes a chain fall through at all. - Swapping providers.
FeatureFlags.setProvidercompares the old and new provider’s names to decide which provider an incoming event came from, so a swap between two same-named providers is invisible to that guard.
There are deliberately no named twins of layer / scopedLayer / asyncLayer: a chain is built from raw providers, and a single named provider becomes a FeatureFlags layer through layerFrom(provider), which preserves its name. Every provider-creating factory other than makeNamed reports TestFeatureProvider.DefaultName.
Managing Flags
Setting Flags
for
provider <- TestFeatureProvider.make
_ <- provider.setFlag("new-flag", true)
_ <- provider.setFlag("count", 42)
_ <- provider.setFlag("name", "test")
yield ()
Setting Multiple Flags
setFlags merges into the existing flags — flags seeded via make(Map(...)) / layer(Map(...)) or set earlier are kept, and a key present in the new map overwrites its previous value:
provider.setFlags(Map(
"flag-1" -> true,
"flag-2" -> "value"
))
// Existing flags are kept; flag-1 / flag-2 are added or overwritten
Use replaceFlags to discard every existing flag first:
provider.replaceFlags(Map(
"flag-1" -> true
))
// All previous flags are removed; only flag-1 remains
Removing Flags
// Remove single flag
provider.removeFlag("flag-to-remove")
// Clear all flags
provider.clearFlags
Absent keys and provider chains
A key that has not been set (never set, removed, or cleared) is reported as FLAG_NOT_FOUND — what every real provider shipped here reports for an absent key. How that surfaces depends on the tier you evaluate through:
- the typed tier (
boolean/booleanDetails/value/ …) fails withFeatureFlagError.FlagNotFound(key)— the tier you reach for precisely because a default would be wrong does not hand one back silently; - the total tier (
booleanOrDefault/resolveOrDefault/ …) serves your default, and the resolution carriesreason = ErrorwitherrorCode = FlagNotFound; - hooks see the
errorstage rather thanaftereither way — soHook.logging()reports an unset key at error level, the spec-correct stage for an error-coded resolution.
for
typed <- FeatureFlags.boolean("never-set", default = false).either
resolution <- FeatureFlags.resolveOrDefault[Boolean]("never-set", default = false)
yield assertTrue(
typed == Left(FeatureFlagError.FlagNotFound("never-set")),
resolution.value == false, // your default
resolution.reason == ResolutionReason.Error,
resolution.errorCode.contains(ErrorCode.FlagNotFound)
)
So a test that means “this flag is off” should set it to false rather than leave it unset — an unset key is “this provider has no such flag”, which is a different state.
Reporting not-found is what lets the test provider take part in a chain:
// The test provider has no "checkout.v2", so the chain moves on to the next provider.
val chain = FeatureFlags.multiProvider(List(testProvider, realProvider))
MultiProviderStrategy.firstMatch advances to the next provider only when a provider reports FLAG_NOT_FOUND; a result carrying reason = DEFAULT is treated as an answer and ends the chain. One caveat when chaining two test providers: the Java SDK keys a chain’s providers by metadata name and silently keeps only the last of two same-named instances — give each one its own name with makeNamed.
Tracking Evaluations
Check If Flag Was Evaluated
for
provider <- TestFeatureProvider.make(Map("feature" -> true))
layer = TestFeatureProvider.layerFrom(provider)
_ <- FeatureFlags.boolean("feature", false).provide(Scope.default >>> layer)
was <- provider.wasEvaluated("feature")
wasNot <- provider.wasEvaluated("other-flag")
yield assertTrue(was) && assertTrue(!wasNot)
Count Evaluations
for
provider <- TestFeatureProvider.make(Map("feature" -> true))
layer = TestFeatureProvider.layerFrom(provider)
_ <- FeatureFlags.boolean("feature", false).provide(Scope.default >>> layer)
_ <- FeatureFlags.boolean("feature", false).provide(Scope.default >>> layer)
_ <- FeatureFlags.boolean("feature", false).provide(Scope.default >>> layer)
count <- provider.evaluationCount("feature")
yield assertTrue(count == 3)
Get All Evaluations
for
provider <- TestFeatureProvider.make(Map("flag-a" -> true, "flag-b" -> "value"))
layer = TestFeatureProvider.layerFrom(provider)
_ <- FeatureFlags.boolean("flag-a", false, EvaluationContext("user-1"))
.provide(Scope.default >>> layer)
_ <- FeatureFlags.string("flag-b", "", EvaluationContext("user-2"))
.provide(Scope.default >>> layer)
evals <- provider.getEvaluations
yield
// evals is List[(String, zio.openfeature.EvaluationContext)] — the library's own context type,
// so you can assert on evals.head._2.targetingKey / .getString(...) directly.
// Need the raw dev.openfeature.sdk.EvaluationContext instead? Use provider.getRawEvaluations.
assertTrue(evals.length == 2)
Clear Evaluation History
provider.clearEvaluations
Provider Status
Managing Status
When using TestFeatureProvider.layer, the provider starts in Ready status. You can change the status for testing different scenarios:
for
provider <- ZIO.service[TestFeatureProvider]
initial <- provider.status // Ready (after layer creation)
_ <- provider.setStatus(ProviderStatus.Error)
error <- provider.status
_ <- provider.setStatus(ProviderStatus.Stale)
stale <- provider.status
yield
assertTrue(initial == ProviderStatus.Ready) &&
assertTrue(error == ProviderStatus.Error) &&
assertTrue(stale == ProviderStatus.Stale)
The setStatus method updates both the ZIO status and the underlying OpenFeature provider state.
Emitting Events
// Simple event
provider.emitEvent(ProviderEvent.ConfigurationChanged(
Set("flag-1", "flag-2"),
provider.metadata
))
// Event with metadata
provider.emitEvent(ProviderEvent.ConfigurationChanged(
Set("flag-1"),
provider.metadata,
FlagMetadata.fromStrings("source" -> "webhook")
))
Behavior Controls
Simulate real-world failure modes like slow responses, intermittent failures, and specific error types. Useful for testing timeouts, circuit breakers, and fallback logic.
Imperative API
for
tp <- ZIO.service[TestFeatureProvider]
// Simulate network latency
_ <- tp.setDelay(200.millis)
// Make all evaluations fail
_ <- tp.setFailing(true)
// Simulate specific error types
_ <- tp.setErrorMode(TestFeatureProvider.ErrorMode.FlagNotFound)
// Simulate flaky service (30% failure rate)
_ <- tp.setFailureProbability(0.3)
// Reset everything
_ <- tp.clearBehavior
yield ()
Available error modes: FlagNotFound, ParseError, TypeMismatch, ProviderNotReady, General.
An error mode applies to every evaluation while it is set. ErrorMode.FlagNotFound behaves exactly like a genuinely absent key (see Absent keys and provider chains) but for all keys at once; to model “this key is absent, that one is present”, just leave the key unset.
Provider exceptions are caught by the Java SDK and come back as an error code on the resolution. The typed tier turns that code into the matching typed failure; the total tier serves your default and keeps the code on the resolution. Pick the one your test wants to assert:
tp.setErrorMode(TestFeatureProvider.ErrorMode.FlagNotFound)
typed <- FeatureFlags.booleanDetails("flag", default = false).either
// typed == Left(FeatureFlagError.FlagNotFound("flag"))
resolution <- FeatureFlags.resolveOrDefault[Boolean]("flag", default = false)
// resolution.errorCode == Some(ErrorCode.FlagNotFound)
// resolution.value == false (the default)
TestAspect API
For cleaner test setup/teardown, use ZIO test aspects. Behavior is set before the test and cleaned up after:
test("handles slow provider") {
for
result <- FeatureFlags.boolean("flag", false).timeout(100.millis)
yield assertTrue(result.isEmpty)
} @@ TestFeatureProvider.withDelay(500.millis)
test("handles provider failures") {
for
typed <- FeatureFlags.booleanDetails("flag", default = false).either
resolution <- FeatureFlags.resolveOrDefault[Boolean]("flag", default = false)
yield assertTrue(typed.isLeft, resolution.errorCode.isDefined)
} @@ TestFeatureProvider.withFailures
Available aspects:
| Aspect | Effect |
|---|---|
TestFeatureProvider.withDelay(d) | Adds delay before each evaluation |
TestFeatureProvider.withFailures | All evaluations fail with a general error |
TestFeatureProvider.withErrorMode(mode) | All evaluations fail with a specific error |
TestFeatureProvider.withFailureProbability(p) | Evaluations fail randomly (0.0 to 1.0) |
Aspects require TestFeatureProvider in the environment. Apply .provide(testLayer) at the suite level when using aspects on individual tests.
Testing Patterns
Simple Flag Testing
import zio.test.*
import zio.openfeature.*
import zio.openfeature.testkit.*
object MyServiceSpec extends ZIOSpecDefault:
def spec = suite("MyService")(
test("shows premium content for premium users") {
val testLayer = TestFeatureProvider.layer(Map(
"premium-content" -> true
))
for
result <- MyService.getContent("user-123")
yield assertTrue(result.hasPremiumContent)
}.provide(
MyService.live,
Scope.default >>> testLayer
)
)
Testing Multiple Scenarios
def testWithFlags[R, E, A](flags: Map[String, Any])(
test: ZIO[R & FeatureFlags, E, A]
): ZIO[R, E, A] =
test.provide(Scope.default >>> TestFeatureProvider.layer(flags))
suite("Feature variations")(
test("enabled") {
testWithFlags(Map("feature" -> true)) {
for result <- myLogic yield assertTrue(result.featureEnabled)
}
},
test("disabled") {
testWithFlags(Map("feature" -> false)) {
for result <- myLogic yield assertTrue(!result.featureEnabled)
}
}
)
Verifying Flag Usage
test("service evaluates expected flags") {
for
provider <- TestFeatureProvider.make(Map(
"feature-a" -> true,
"feature-b" -> "variant"
))
layer = TestFeatureProvider.layerFrom(provider)
_ <- MyService.doSomething.provide(Scope.default >>> layer)
wasA <- provider.wasEvaluated("feature-a")
wasB <- provider.wasEvaluated("feature-b")
wasC <- provider.wasEvaluated("feature-c")
yield
assertTrue(wasA) &&
assertTrue(wasB) &&
assertTrue(!wasC) // Should not evaluate feature-c
}
Testing Context Propagation
The getEvaluations method returns each captured context as the library’s own EvaluationContext, so you can assert on context propagation with the Scala API — no Java SDK types required:
test("context is passed to provider") {
val ctx = EvaluationContext("user-123")
.withAttribute("plan", "premium")
for
provider <- TestFeatureProvider.make(Map("feature" -> true))
layer = TestFeatureProvider.layerFrom(provider)
_ <- FeatureFlags.boolean("feature", false, ctx)
.provide(Scope.default >>> layer)
evals <- provider.getEvaluations
(_, captured) = evals.head
yield
// captured is zio.openfeature.EvaluationContext (the library type)
assertTrue(captured.targetingKey.contains("user-123")) &&
assertTrue(captured.getString("plan").contains("premium"))
}
If you specifically need the raw dev.openfeature.sdk.EvaluationContext (e.g. to assert on the SDK type), use getRawEvaluations instead, which returns List[(String, dev.openfeature.sdk.EvaluationContext)].
Using Transactions for Override Testing
Combine testkit with transactions for fine-grained control:
test("feature logic with overrides") {
val baseLayer = TestFeatureProvider.layer(Map(
"feature-a" -> true,
"feature-b" -> false
))
// Test with base values
val baseTest = for
a <- FeatureFlags.boolean("feature-a", false)
b <- FeatureFlags.boolean("feature-b", false)
yield assertTrue(a == true) && assertTrue(b == false)
// Test with overrides
val overrideTest = FeatureFlags.transaction(Map("feature-b" -> true)) {
for
a <- FeatureFlags.boolean("feature-a", false)
b <- FeatureFlags.boolean("feature-b", false)
yield assertTrue(a == true) && assertTrue(b == true)
}
(baseTest *> overrideTest.map(_.result)).provide(Scope.default >>> baseLayer)
}
Testing Async Initialization
Use TestFeatureProvider.asyncLayer to test how your code handles a provider that isn’t ready yet:
test("service handles provider not ready") {
for
result <- MyService.getFeature.either
yield assertTrue(result.isLeft) // Fails with ProviderNotReady
}.provide(Scope.default >>> TestFeatureProvider.asyncLayer(Map("feature" -> true)))
test("service works after provider becomes ready") {
for
tp <- ZIO.service[TestFeatureProvider]
_ <- tp.setStatus(ProviderStatus.Ready)
result <- MyService.getFeature
yield assertTrue(result == true)
}.provide(Scope.default >>> TestFeatureProvider.asyncLayer(Map("feature" -> true)))
The asyncLayer creates a provider that starts in NotReady state. Call setStatus(ProviderStatus.Ready) to simulate the provider becoming ready. This is useful for testing graceful degradation and startup behavior.
Simulating Real Async Init
If you don’t need to test the NotReady state directly, use asyncReadyLayer which auto-transitions to Ready after a configurable delay:
test("service works with async provider") {
for
_ <- ZIO.sleep(200.millis) // Wait for auto-init
result <- MyService.getFeature
yield assertTrue(result == true)
}.provide(Scope.default >>> TestFeatureProvider.asyncReadyLayer(
Map("feature" -> true),
initDelay = 100.millis
))
This simulates a real provider (e.g., Optimizely connecting to its server) without requiring manual setStatus calls in every test.
Test Isolation
Automatic Isolation
TestFeatureProvider.layer, asyncLayer, and layerFrom each create an isolated OpenFeatureAPI instance with its own provider repository and event support. This means tests using these layers can run in parallel without cross-test contamination — no extra configuration needed.
// These tests run in parallel safely — each gets its own isolated API instance
test("test 1") {
for result <- FeatureFlags.boolean("flag", false)
yield assertTrue(result == true)
}.provide(Scope.default >>> TestFeatureProvider.layer(Map("flag" -> true)))
test("test 2") {
for result <- FeatureFlags.boolean("flag", false)
yield assertTrue(result == false)
}.provide(Scope.default >>> TestFeatureProvider.layer(Map("flag" -> false)))
If you need to access both the provider and the FeatureFlags service (e.g. to track evaluations or emit events), use layerFrom:
test("tracks evaluations") {
for
provider <- TestFeatureProvider.make(Map("flag" -> true))
layer = TestFeatureProvider.layerFrom(provider)
_ <- FeatureFlags.boolean("flag", false).provide(Scope.default >>> layer)
was <- provider.wasEvaluated("flag")
yield assertTrue(was)
}
Note: The public factory methods (
FeatureFlags.fromProvider,fromProvider(provider, config), etc.) use the globalOpenFeatureAPIsingleton and are not isolated. If you test with these directly, use@@ TestAspect.sequentialto prevent conflicts.
Behavior-Matrix Testing with zio-bdd
This pattern works with any FeatureFlags layer — TestFeatureProvider, OptimizelyProvider, OFREP, or your own provider. It’s a feature of the zio-bdd test framework’s layer-injection hooks, not something specific to this testkit module, so it’s documented here rather than on a provider-specific page.
The layer-injection hook tiers
A zio-bdd suite (object MySpec extends ZIOSteps[R, S]) builds its environment through four overridable hooks, each one tier more specific than the last. Override exactly one — the others delegate down to it by default:
| Hook | Called | Default |
|---|---|---|
applicationLayer | Once per test run | — |
featureLayer(meta) | Once per .feature file | delegates to applicationLayer |
scenarioLayer(meta) | Once per scenario | delegates to featureLayer |
flagLayer(meta, flags) | Once per @flags(...) tag occurrence on a scenario | delegates to scenarioLayer |
override def scenarioLayer(meta: ScenarioMetadata): ZLayer[Any, Throwable, R] =
if (meta.tags.contains("use-mock")) mockHttpLayer else realHttpLayer
@flags(...) tags
flagLayer is the hook to override when a scenario needs different flag values per run. Tag a scenario with @flags(key=value, ...), and the framework parses the tag into a Map[String, String] before calling flagLayer(meta, flags):
override def flagLayer(meta: ScenarioMetadata, flags: Map[String, String]): ZLayer[Any, Throwable, FeatureFlags] =
environment >>> FlagConfig.layer(flags)
Two things to know about how tags expand into runs:
- One tag, multiple keys → one run.
@flags(datafile=X, plan=Y)parses to a singleMap("datafile" -> "X", "plan" -> "Y")and callsflagLayeronce, with both keys present together. - Multiple tags → multiple runs. Two separate tag occurrences on the same scenario — written on consecutive lines:
@flags(datafile=X) @flags(datafile=Y) Scenario: ...expand into two independent runs, each calling
flagLayeronce with only that tag’s own map (not merged) — the scenario body executes twice, against two separately-built environments.
A blank line between or around @flags(...) tags is fine. A blank line inside the free-text description directly under Feature: is not — it silently drops the whole feature instead of raising a parse error; see zio-bdd#87.
See Optimizely → Testing your app for a worked example applying this to a real provider, with datafile fixtures driving the @flags(datafile=...) values.
Typed fixtures with FlagDef
setFlag("user.tier", "premum") compiles. The typo ships, the test passes, and production fails with TYPE_MISMATCH. Building the fixture from a FlagDef instead makes the value type-checked against the flag’s declared type:
import zio.openfeature.testkit.FlagOverride.Ops // brings `:=` into scope
val UserPlan = FlagDef[Tier]("user.tier", Tier.Free)
val layer = TestFeatureProvider.layer(UserPlan := Tier.Premium) // compiles
// TestFeatureProvider.layer(UserPlan := "premium") // does not
The value is stored as flagType.encode(value) — the wire form a real provider would carry — so the test reads it back through the same decode path production uses. := additionally checks that the encoding round-trips back through decode, and fails at fixture-construction if it cannot: a codec that cannot read its own output would otherwise hand you a fixture your test believes in and production cannot read.
Every flag-seeding factory has a typed twin, so switching factories does not send you back to Map[String, Any]:
TestFeatureProvider.make(UserPlan := Tier.Premium)
TestFeatureProvider.layer(UserPlan := Tier.Premium)
TestFeatureProvider.scopedLayer(UserPlan := Tier.Premium)
TestFeatureProvider.asyncLayer(UserPlan := Tier.Premium)
TestFeatureProvider.asyncReadyLayer(100.millis, UserPlan := Tier.Premium)
asyncReadyLayer takes its delay explicitly here, because its untyped form defaults both parameters and a plain varargs overload would make a bare asyncReadyLayer() ambiguous.
The same helpers accept a FlagDef wherever they took a key — setFlag, setFlags, replaceFlags, removeFlag, wasEvaluated, evaluationCount.
The key-based API is unchanged and is still the right tool for the cases a
FlagDefcannot express: an undeclared key, a key belonging to another system, or a negative case such asFLAG_NOT_FOUND.
One inference trap worth knowing: FlagDef("k", Tier.Free) where Tier.Free is a case object infers the type as Tier.Free.type and then cannot find a FlagType for it. Name the type — FlagDef[Tier]("k", Tier.Free). A Scala 3 enum’s parameterless cases are typed as the enum itself and are unaffected.
Law-checking a custom FlagType
If you hand-write a FlagType[A], FlagTypeLaws holds it to the same contract the library’s own instances meet. Pass a Gen for your type:
import zio.openfeature.testkit.FlagTypeLaws
object CelsiusCodecSpec extends ZIOSpecDefault {
def spec = suite("Celsius")(
FlagTypeLaws.all(Gen.int(-100, 100).map(Celsius(_)))
)
}
There are two laws, and the difference between them is the point:
| Law | Checks | Catches |
|---|---|---|
roundTrip | decode(encode(a)) == Right(a) in memory | an encoder and decoder that disagree |
throughValueBridge | the same, but after crossing the OpenFeature Value conversion | a lossy encoding |
roundTrip is the law as FlagType states it, and it passes trivially for every built-in instance because their encode is the identity. throughValueBridge is the one that finds real problems, because that conversion is lossy in two ways worth knowing about:
- every number comes back as a
Double, so aLongbeyond 2^53 does not survive an object-path round trip; - a structure member the bridge cannot represent is dropped, so its key reads back as absent (which is exactly how an
Optionfield decodes toNone).
all runs both. Reach for throughValueBridge in particular when your type is object-backed — when its wireType is not one of the scalars — since that is the path it will actually be evaluated on.
The laws live in the shared source tree, so they are available on Scala 2.13 as well as 3. (Only FlagType.derived is Scala 3 only.)
Best Practices
1. Use Descriptive Flag Names
val testLayer = TestFeatureProvider.layer(Map(
"premium-feature-enabled" -> true,
"max-upload-size-mb" -> 100,
"checkout-variant" -> "new"
))
2. Create Test Fixtures
object TestFixtures:
val premiumUser = TestFeatureProvider.layer(Map(
"premium" -> true,
"max-items" -> 1000
))
val freeUser = TestFeatureProvider.layer(Map(
"premium" -> false,
"max-items" -> 10
))
// Usage
test("premium user behavior") {
myTest.provide(Scope.default >>> TestFixtures.premiumUser)
}
3. Verify Expected Evaluations
Use wasEvaluated for cleaner flag usage assertions:
test("service only evaluates necessary flags") {
for
provider <- TestFeatureProvider.make(Map(
"needed-flag" -> true,
"unneeded-flag" -> true
))
layer = TestFeatureProvider.layerFrom(provider)
_ <- myService.provide(Scope.default >>> layer)
wasNeeded <- provider.wasEvaluated("needed-flag")
wasUnneeded <- provider.wasEvaluated("unneeded-flag")
yield
assertTrue(wasNeeded) &&
assertTrue(!wasUnneeded)
}
4. Test Edge Cases
suite("edge cases")(
test("handles missing flag") {
val layer = TestFeatureProvider.layer
// A missing key is FLAG_NOT_FOUND: the typed tier fails, the total tier serves the default.
FeatureFlags.booleanOrDefault("missing", false)
.map(result => assertTrue(result == false))
.provide(Scope.default >>> layer)
},
test("handles type mismatch") {
val layer = TestFeatureProvider.layer(Map("flag" -> "string"))
FeatureFlags.boolean("flag", false)
.map(result => assertTrue(result == false)) // Uses default
.provide(Scope.default >>> layer)
}
)