Skip to content

Add @evryg/effect-bdd: a runner-agnostic, inspectable BDD DSL for Effect#107

Open
jbmusso wants to merge 11 commits into
mainfrom
effect-bdd-dsl
Open

Add @evryg/effect-bdd: a runner-agnostic, inspectable BDD DSL for Effect#107
jbmusso wants to merge 11 commits into
mainfrom
effect-bdd-dsl

Conversation

@jbmusso

@jbmusso jbmusso commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds @evryg/effect-bdd — a runner-agnostic, inspectable Behaviour-Driven-Development DSL for Effect.

A scenario is reified data built with a type-safe, phase-typed Given / When / Then builder, so the same value can be:

  • run under any test runner — assertion failures surface as a typed ScenarioError, never via a runner's expect;
  • rendered to Gherkin, listed as steps, or projected to a serializable document;
  • grouped & filtered into features by tag.

Two layers

core/ — the generic DSL

  • Step (Given/When/Assertion combinator values), Scenario (term + phase-typed builder), Run (the run interpreter + ScenarioError), Gherkin (render / steps / tags / toDocument), Feature (group + filterByTags).
  • The phase machine accumulates a named-key context, enforces step dependencies, and feeds each outcome (then / thenFails / thenDies) the correct payload type.

harness/ — an optional layer, a pure composition over the (untouched) core

  • Command — reified, re-interpretable actions via Schema.TaggedUnion + a dispatcher (the tag is managed by Effect; no _tag written by hand).
  • Probe — service spies (Layer.mock + a call-log) that capture indirect outputs.
  • makeHarness — a per-domain preset: when takes a bare command, then receives the probes' recorded calls as an injected observation object, and run auto-provides the probe layers.

Examples

examples/core/{Cart,Counter}.ts (combinator mode) and examples/harness/{Cart,Counter}.ts (preset mode) — the same two domains expressed both ways.

Verification

  • pnpm check (typecheck) — clean
  • pnpm test — 283 tests pass (incl. *.test-d.ts type-level acceptance tests)
  • pnpm circular, pnpm lint — clean
  • pnpm docgen — succeeds
  • Changeset included (@evryg/effect-bdd: minor)

@jbmusso
jbmusso force-pushed the effect-bdd-dsl branch 3 times, most recently from ce9c933 to 15aa147 Compare June 23, 2026 07:29
jbmusso added 11 commits June 23, 2026 10:44
Mirror the packages/neo4j layout (tsconfig set, vitest/docgen config) and wire
the package into the root tsconfig references, path aliases and the vitest
shared alias map. Placeholder src/index.ts compiles cleanly via tsc -b.
Add Given/When/Assertion combinator values (Step.ts) and the reified Scenario
term with its phase-typed builder (Scenario.ts): given/and accumulate a
named-key context and enforce combinator-declared dependencies, when introduces
the action's A/E, and then/thenFails/thenDies select the outcome and feed the
correct payload to each assertion. Authoring yields data, not an Effect.

Unit tests cover the reified shape from both the sugar and combinator forms;
Scenario.test-d.ts locks the four compile-time guarantees (phase ordering,
context accumulation, dependency enforcement, outcome-correct payloads).
run folds the givens into a context, performs the action, classifies its Exit
(success / typed failure / defect) and checks each assertion against the chosen
outcome. Mismatches, failed preconditions and broken assertions surface as a
typed ScenarioError — assertion throws/failed Effects are captured via
Effect.exit, never a runner's expect, so a scenario runs under any runner.

Unit tests cover success, thenFails, thenDies, assertion failure, outcome
mismatch and service threading; Run.test-d.ts asserts run demands exactly the
accumulated R.
Treat a Scenario as inspectable data: steps lists its steps with Gherkin
keywords, tags returns its tags, toGherkin renders the scenario as Gherkin text,
and toDocument projects the describable skeleton onto a serializable
ScenarioDocument schema (for .feature export). None of these execute the
scenario. Unit tests cover keywords, recorded outcomes, rendering and a
Schema round-trip.
feature groups scenarios under a name; filterByTags keeps the scenarios carrying
any of the given tags (order-preserving), and selectByTags narrows a feature.
Because a Scenario is a value, a suite is just data to filter and reorder. Unit
tests cover grouping, suite filtering and feature narrowing.
Add README, a changeset (minor), and two PascalCase examples (CartDsl, CounterDsl)
showing higher-level domain vocabularies built from the combinators — constrained
sub-builders and specialised type safety (context-requiring combinators cannot be
chained before their precondition). The same is exercised as tests: DomainDsl
unit tests run a domain DSL through run, DomainDsl.test-d locks the specialised
safety, and Bdd.node.unit demonstrates one scenario interpreted three ways plus a
guardrail asserting the core imports no test runner. examples are type-checked via
tsconfig.examples.json.
Move the five generic DSL modules and their tests into src/core/ (mirroring the
cypher-codegen subfolder convention), leaving the barrel at src/index.ts pointing
to ./core/*. The former Bdd test becomes core/Interpreting (one scenario, many
interpreters). Public API and path alias unchanged.
A dispatcher folds a tagged command value into a core When via a per-tag handler
map, deriving the step description from the command. The command is data, so the
same command type can be driven by multiple dispatchers (e.g. real vs. dry-run) —
the re-interpretability an initial encoding buys. Unit tests cover dispatch,
derived descriptions and re-interpretation; test-d locks exhaustive handlers and
the produced When type.
probe wraps a service in a Layer.mock backed by an eager call-log: the action
runs against the mock, and the recorded calls are read back through .calls
(typically in an assertion). Unimplemented members fail loudly. Unit tests cover
recording and the loud-failure path.
makeHarness bundles initial preconditions + probes + a dispatch function into a
domain-specialized builder: reducer givens fold over the preconditions, when
takes a bare command, then/thenFails/thenDies receive the probes' recorded calls
as an injected observation object, and run auto-provides the merged probe layers
(discharging their services from R) and resets each probe's call-log first, so a
harness can be reused across scenarios. It is a pure composition over the core —
the core term and run are untouched, so scenarios stay runner-agnostic and
inspectable. Probe gains reset; unit + test-d cover injected observations, the
failure path, service discharge and no-accumulation across runs.
Move the combinator-based examples to examples/core/{Cart,Counter}.ts and add
examples/harness/{Cart,Counter}.ts expressing the same two domains via the
preset harness (reified commands, a probed port, injected observations,
auto-provided layers). Document both modes in the README and extend the
changeset to cover the harness layer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant