Skip to content

fix: preserve Effect equality and scoped test lifetimes - #5

Merged
Nsttt merged 1 commit into
Nsttt:mainfrom
BleedingDev:fix/effect-equality-and-scoped-test-lifetimes
Sep 19, 2026
Merged

Nsttt merged 1 commit into
Nsttt:mainfrom
BleedingDev:fix/effect-equality-and-scoped-test-lifetimes

Conversation

@BleedingDev

@BleedingDev BleedingDev commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator

ScriptedAlchemy/effect-rstest#4 (origin of these fixes) | TechsioCZ/ontos#507 (consumer currently carrying them as a pnpm patch)

Why the change

Timed-out Effect tests and layer setups keep running after Rstest has moved on, and addEqualityTesters() breaks asymmetric matchers, so consumers patch the published package; this PR lands those fixes upstream so the patch can go away.

Special things to note

  • Cleanup is awaited, not bounded: a finalizer that never completes will hold the suite. Native afterEach hooks run before onTestFinished and are outside the guarantee. Both are documented in the README.
  • The two lifetime tests spawn a child Rstest (test/fixtures/run-fixture.ts) and assert the JSON report verbatim, including runner error strings like beforeAll hook timed out in 100ms. A future @rstest/core wording change will need those expectations updated.
  • Schema property inputs and Effect.asVoid at the test boundary already exist here, so only the three behavioral fixes from the upstream PR are ported.

Change outline

Everything lives in src/internal/internal.ts; the rest is tests and docs.

 src/internal/internal.ts
   addEqualityTesters   # both sides must be Equal, else defer to Rstest
   runTest              # onTestFinished barrier awaits fiber settlement
   layer                # track setup fiber, interrupt it before Scope.close
 test/
+├── equality.test.ts               # semantic equal / not-equal, matchers intact
+├── test-values.test.ts            # thenable success values never resolved
+├── test-lifetime.test.ts          # child runner, hookTimeout 50
+├── layer-lifetime.test.ts         # child runner, hookTimeout 100
+└── fixtures/
+    ├── run-fixture.ts             # spawns rstest, decodes JSON report
+    ├── test-lifetime.fixture.ts
+    └── layer-lifetime.fixture.ts
 typetest/
+└── prop-schema.tst.ts             # mixed schema/arbitrary inference

Equality: with ||, a plain object versus an Equal value returned a hard false, so expect.objectContaining never got a turn.

 addEqualityTesters
-  if isEqual(a) || isEqual(b) -> Equal.equals(a, b)
+  if isEqual(a) && isEqual(b) -> Equal.equals(a, b)
   else -> undefined   # Rstest's own deep equality and asymmetric matchers

Test settlement: Rstest reports a timeout and immediately starts the next test; the fiber and its finalizers were still running.

 runTest(ctx)(effect)
+  ctx.onTestFinished(() => settlement, 0)   # registered before the run
   result = runPromise(effect, ctx)            # abort signal still cancels the fiber
+  settlement = result.then(noop, noop)        # swallow only for the barrier
   return result                               # timeout / failure / .fails unchanged

Layer teardown: suite hooks have no AbortSignal, so a beforeAll that exceeded its timeout kept building the layer while afterAll closed the scope underneath it.

 layer(...)
-  beforeAll -> runPromise(contextEffect)
+  beforeAll -> buildContext
+    withFiber(f => setupFiber = f; contextEffect)
   afterAll  -> closeScope
+    interrupt(setupFiber) if set
     Scope.close(scope)

How the lifetime tests verify it:

test-lifetime.test.ts
  runFixture("test-lifetime", 50)
    write temp rstest.config.mjs  (pool: threads, json reporter)
    execFile rstest run --config
    decode report.json via Schema
  assert summary {tests 25, failed 3, passed 20, skipped 2}
  assert failing names + messages are exactly the runner's own
  assert no unhandledErrors, no file errors

🤖 Generated with Claude Code

- addEqualityTesters only delegates to Equal.equals when both values
  implement Equal, so plain objects and asymmetric matchers keep Rstest's
  native behavior.
- Timed-out Effect tests register an onTestFinished barrier that awaits
  fiber settlement and scoped finalizers before later sequential tests and
  suite-layer release, without changing runner outcomes.
- Layer suites track the setup fiber and interrupt it before closing the
  scope, so a timed-out or early-failed beforeAll cannot outlive teardown
  or leak resources acquired before the failure.

Ported from ScriptedAlchemy/effect-rstest#4 with runner-conformance
fixtures for test and layer lifetimes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Nsttt
Nsttt merged commit 0a5c85d into Nsttt:main Sep 19, 2026
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.

2 participants