Skip to content

testing: effectTest harness — return an Effect, get pass/fail - #10

Merged
GraemeF merged 1 commit into
mainfrom
comms-30hq
Jun 13, 2026
Merged

GraemeF merged 1 commit into
mainfrom
comms-30hq

Conversation

@GraemeF

@GraemeF GraemeF commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

What

Adds effectTest to @commy/testing — a bun:test harness that lets a test body return a success/failure Effect instead of hand-wrapping Effect.runPromise(Effect.scoped(Effect.gen(...))) at every call site.

Foundation for the hexagonal test architecture (comms-e5vm); first consumer is the Tier-2 proof (comms-e5vm.2).

Why

Nearly every test in the suite repeats the Effect.runPromise(Effect.scoped(...)) wrapper, and a failing Effect rejects with a FiberFailure that buries the real error/assertion behind the promise boundary — so a failed expect() shows up wrapped, not as itself.

How

effectTest(name, body, options?):

  • always wraps the body in Effect.scoped, giving every test a per-test Scope whose finalizers run on both the success and failure paths;
  • options.layer threads the test's requirements (R) without a global — the caller composes captureLogger / TestContext (TestClock) / the stub HttpClient layer (comms-e5vm.1) and passes them as one layer. The base harness stays on the live clock, so TestClock is opt-in and live-clock tests are unaffected;
  • runs via Effect.runPromiseExit and, on failure, throws Cause.squash(cause) so the raw error surfaces. squash (not prettyErrors) deliberately returns the original thrown object untouched, keeping bun's assertion-diff renderer intact.

runTestEffect is the engine behind the registrar, exported so the pass/fail mapping is itself testable (and usable inside a custom test.each).

Design decisions (resolving the bead's open questions)

  • One function, no .scoped / .live variants — the base is already always-scoped + live-clock; everything else is opt-in via options.layer.
  • Generic layer slot, no build-time dep on comms-e5vm.1 — the harness never knows the stub's concrete shape; the two compose at the consumer's call sites. Keeps both foundation beads landing in parallel.
  • Migration is out of scope (adopt-going-forward) — that's comms-e5vm.2.

Tests

11 tests covering: success resolves with value; typed failure and thrown defect both surface the raw object (not a FiberFailure); per-test Scope finalizers run on success and failure; layer provision via a Context.Tag service, captureLogger, and TestContext/TestClock; layer-build failure surfaces; plus the effectTest registrar wired end-to-end.

Tests across the suite repeat
`test(name, () => Effect.runPromise(Effect.scoped(Effect.gen(...))))`,
and a failing Effect rejects with a FiberFailure that buries the real
error/assertion behind the promise boundary.

`effectTest(name, body, options?)` lets a test body return an Effect:
- always wraps the body in `Effect.scoped`, giving every test a per-test
  Scope whose finalizers run on both the success and failure paths;
- `options.layer` threads the test's requirements (R) without a global —
  the caller composes captureLogger / TestContext (TestClock) / the stub
  HttpClient layer and passes them as one layer. The base stays on the
  live clock, so TestClock is opt-in;
- runs via `Effect.runPromiseExit` and, on failure, throws
  `Cause.squash(cause)` so the raw error surfaces — squash (not
  prettyErrors) preserves the original thrown object, keeping bun's
  assertion-diff renderer intact.

`runTestEffect` is the engine behind the registrar, exported so the
pass/fail mapping is itself testable.

comms-30hq
@GraemeF
GraemeF merged commit 2e0f52c into main Jun 13, 2026
2 checks passed
@GraemeF
GraemeF deleted the comms-30hq branch June 13, 2026 09:23
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