Przejdź do treści
Biblioteka skilli
Programowanie · Testowanie

effect-v4-testing

/effect-v4-testing

Pisze i naprawia testy kodu Effect v4 w @effect/vitest: testy błędów, mocki warstw, testy własności i zegar testowy.

Repozytorium
effected ↗
Licencja
MIT
Ostatnia zmiana repozytorium
13.11.2026

Ocena i bezpieczeństwo

95/ 100Klasa: dobry
Format25 / 25
Frontmatter z nazwą i opisem, poprawna nazwa, opis do 1024 znaków, nazwa zgodna z folderem.
Odkrywalność25 / 25
Opis mówi, kiedy użyć skilla, ma sensowną długość i jest w 3. osobie.
Treść30 / 35
Co najmniej 150 słów i do 500 linii, nagłówki, listy kroków, przykłady albo kod.
Higiena15 / 15
Bez zaślepek typu TODO, bez ścieżek Windows, z licencją.
Ryzykobrak sygnałów w skanie
Pliki w paczce
14
Skrypty
0
Komentarze HTML
0
Adresy URL w SKILL.md
0
Przestarzałość

Brak sygnałów. Repozytorium zmienione 13.11.2026, bez odwołań do wycofanych modeli i API.

Licencja

MIT, otwarta: można używać i zmieniać, z zachowaniem informacji o autorze.

Co robi i kiedy

Pisze i naprawia testy kodu Effect v4 w @effect/vitest: testy błędów, mocki warstw, testy własności i zegar testowy.

Oryginalny opis (EN)

Use when writing, reviewing, or fixing tests for Effect v4 code with @effect/vitest — it.effect + Effect.gen as the default runner, asserting typed errors via Effect.flip or Effect.result (Exit + Cause for defects), providing test/mock layers with layer(...) for any service in R (owned or consumed), fault-injecting one method of a real layer, property tests with it.effect.prop over a Schema, TestClock for time-dependent logic, converting a plain-Vitest repo, and the mutate-the-edges discipline for proving a suite can actually fail — the discriminating input wrong in exactly one way, per-clause

Jak działa

  1. The default runner: it.effect + Effect.gen
  2. Asserting on typed errors
  3. Providing test / mock layers
  4. Property testing with it.effect.prop and it.prop
  5. Time-dependent logic: TestClock
  6. it.effect also intercepts CONSOLE output — including Effect.log
  7. A test that cannot fail is worse than no test — mutate the edges
  8. Other false greens, in one place
  9. Test the sync facade over the same fixture as the Effect form
  10. House conventions
Sekcje z nagłówków SKILL.md, w oryginale · 501 linii, 4 024 słów
Podgląd SKILL.md
---
name: effect-v4-testing
description: Use when writing, reviewing, or fixing tests for Effect v4 code with @effect/vitest — it.effect + Effect.gen as the default runner, asserting typed errors via Effect.flip or Effect.result (Exit + Cause for defects), providing test/mock layers with layer(...) for any service in R (owned or consumed), fault-injecting one method of a real layer, property tests with it.effect.prop over a Schema, TestClock for time-dependent logic, converting a plain-Vitest repo, and the mutate-the-edges discipline for proving a suite can actually fail — the discriminating input wrong in exactly one way, per-clause and per-path mutation, and the positive control a "nothing found" result needs before it's believed.
when_to_use: >-
  it.scoped removed, Arbitrary size clamp/-0/exhaustion/dropped-regex traps, Path.layer + @effected/memfs need no platform package, vi.mock must import vi from vitest, layer() memoizing while Effect.provide does not, TestConsole swallowing Effect.log* through ConsoleRef, a small real delay hanging the virtual clock, Tests: 0/0 passed lying while the exit code is honest, a project-filtered run from inside a package not loading the root config, TestClock starting at the epoch (1970), an eagerly-recording layerNoop stub, a narrowing if with no else branch, structural checks over source text (import walkers, export assertions, comment strippers), the two-latch rule for concurrency-leak tests, unhandledErrors and a stray process.exitCode making a green suite lie
targets:
  copilot:
    description: >-
      Use when writing, reviewing, or fixing tests for Effect v4 code with @effect/vitest — it.effect + Effect.gen
      as the default runner, asserting typed errors via Effect.flip or Effect.result (Exit + Cause for defects),
      providing test/mock layers with layer(...) for any service in R (owned or consumed), fault-injecting one
      method of a real layer, property tests with it.effect.prop over a Schema, TestClock for time-dependent logic,
      converting a plain-Vitest repo, and the mutate-the-edges discipline for proving a suite can actually fail —
      the discriminating input wrong in exactly one way, per-clause and per-path mutation, and the positive control
      a "nothing found" result needs before it's believed. Also triggers on: Tests: 0/0 passed lying while the exit
      code is honest, TestClock at the epoch (1970), layer() memoizing while Effect.provide does not, TestConsole
      swallowing Effect.log* through ConsoleRef, vi.mock must import vi from vitest, structural checks over source
      text.
---

# Effect v4 testing with `@effect/vitest`

`@effect/vitest` re-exports Vitest, so it is the single entrypoint for test
APIs — with one exception (`vi.mock`, below). Effect programs run through
`it.effect`, never through a bare `it()` that calls `Effect.runSync`/
`runPromise`. Our house test files (`packages/jsonc/__test__/Jsonc.test.ts`,
`packages/yaml/__test__/Yaml.test.ts`) are the canonical shapes. (The
`effect/testing/*` modules — TestClock, TestConsole, TestSchema — and the
property engine `Arbitrary` are indexed in
`effect-v4-module-index`; this skill owns how to use them. There is no
`FastCheck` module.)

**Migrating a plain-Vitest Effect repo? Adopt `@effect/vitest`.** A repo whose
tests are plain Vitest is not "nothing to migrate on the testing axis": add
`@effect/vitest` and route Effect-returning tests through `it.effect`. The
conversion has its own traps →
**[references/migrating-a-repo.md](./references/migrating-a-repo.md)**.

**Testing a specific front end or a repo-shape check routes elsewhere first.**
This skill owns the general `@effect/vitest` mechanics; a front end's own
testing subpath owns the rest: testing a CLI bin → `effect-v4-cli` (`CliTest`); an Ink screen, a wizard or a live view → `effect-v4-cli` (`CliUiTest`: its `session` transcript, and its snapshot serializer registered through Vitest's `snapshotSerializers` config, snapshots being the one place a test needs `expect`);
testing an MCP server → `effect-v4-mcp` (`McpHarness`, `McpProbe`); a
monorepo's own repo-shape checks (layering, source boundaries, packed
installs) → `@effected/workspaces/testing` (see `effected-packages`).

**Install it from the same catalog as `effect`, so it resolves to the same version** — never bare and never from a dist-tag. Effect releases `effect` and every `@effect/*` package together at one shared version, so `@effect/vitest` must resolve to exactly the version `effect` does. A dist-tag (`latest`, `beta`, `rc`, `snapshot`) is a moving pointer that can resolve to a version other than the one `effect` resolves to.

The failure of a mismatch names neither package: a v3 `@effect/vitest` installed beside v4 `effect` loads with only a one-line `Issues with peer dependencies found` warning; `pnpm peers check` then lists unmet `effect ^3` and `vitest ^3` peers, and the test file fails to load with `Cannot find module '…/effect/dist/Arbitrary.js'`, because the v3 package imports a module the v4 `effect` does not ship. It reads as a broken install rather than a version mismatch. Run `pnpm peers check` and compare `@effect/vitest`'s resolved version with `effect`'s in the lockfile before believing any resolution. With `@effected/pnpm-plugin-effected` the dependency comes from `catalog:effected`, which carries both at the same range.

**`vi.mock` is the one import that must NOT come from `@effect/vitest`.** Vitest
hoists it above all imports, so a `vi` bound through the re-export is not yet
initialized and the file dies at load with `Cannot access '__vi_import_1__'
before initialization` — naming neither `vi` nor `@effect/vitest`. Write
`import { vi } from "vitest"` in any file calling `vi.mock`; `vi.fn` /
`vi.spyOn` work fine through the re-export (measured across 24 files).

**A spy restored in `try`/`finally` inside `Effect.gen` LEAKS.** A failing
assertion leaves through the *error channel*, so the `finally` does not run the
way the shape suggests, and a `vi.spyOn` on a process global that outlives its
test poisons its neighbours — false reds first, false greens later. Acquire and
release spies with `Effect.acquireUseRelease` instead →
[references/false-greens.md](./references/false-greens.md).

**A repo without `@effect/vitest` can still test Effect code**: a plain Vitest
`it` calling a small `Effect.runPromise` helper. What it loses is what
`it.effect` installs: a per-test `Scope` and the virtual `TestClock` and
`TestConsole` (`Effect.scoped` and `TestClock.layer()` add the scope and the clock back
by hand), plus `layer(...)` sharing and `it.effect.prop`. The skill's `it.effect`
examples assume the package is installed.

## The default runner: `it.effect` + `Effect.gen`

```ts
import { assert, describe, it } from "@effect/vitest";
import { Jsonc } from "@effected/jsonc";
import { Effect } from "effect";

describe("Jsonc", () => {
  it.effect("parses objects, arrays and scalars", () =>
    Effect.gen(function* () {
      const value = yield* Jsonc.parse('{ "a": 1 }');
      assert.deepStrictEqual(value, { a: 1 });
    }),
  );
});
```

- **`it.effect` runs the returned Effect** and provides the default test
  environment — `TestEnv = Layer.mergeAll(TestConsole.layer, TestClock.layer())`
  (`packages/vitest/src/internal/internal.ts:68`), piped through
  `flow(Effect.scoped, Effect.provide(TestEnv))` (`internal.ts:490`). Its type is
  `Tester<R | Scope.Scope>`, so scoped effects (`Effect.acquireRelease`, scoped
  layers) run **directly** under `it.effect`.
- **There is no `it.scoped`** (zero `scoped` matches in
  `packages/vitest/src/index.ts`; the v3→v4 migration guide
  spells the replacement out — `it.scoped(...)` becomes `it.effect(...)`,
  `it.scopedLive(...)` becomes `it.live(...)`). The Tester surface is
  `skip`/`skipIf`/`runIf`/`only`/`each`/`fails`/`prop` — **`it.effect.skipIf`
  and `it.effect.runIf` exist and are well-typed** (`packages/vitest/src/index.ts:82-83`);
  reach for them instead of hand-rolling a conditional `describe`.
- **`it.live`** (`Tester<Scope.Scope | R>`) opts into the real `Clock` and live
  runtime services. Use only when a test genuinely needs wall-clock behavior.
- **`it.effect` takes a Vitest timeout as its third argument** —
  `it.effect(name, self, timeout?: number | TestOptions)`. Any real-time elapsed
  assertion above Vitest's 5000ms default is dead code without it (see
  [references/false-greens.md](./references/false-greens.md)).
- **Never** `it("...", () => Effect.runPromise(program))`. Plain `it()` is fine
  only for genuinely non-Effect pure code (`Jsonc.stripComments`, `Yaml.equals`).
- **The rule is UNCONDITIONAL. "But the layer is fully synchronous" is not a
  carve-out.** The tempting shape is a `describe` block with a local
  `const validate = (doc) => Effect.runSync(Effect.provide(program, Svc.layer))`
  helper, justified because the service's implementation happens to be
  synchronous (a `Layer.succeed` over a sync engine such as ajv). It shipped in
  `packages/schemastore/__test__/schema-validator.test.ts`, alongside four
  sibling blocks in the same file that use `layer(...)` + `it.effect`
  correctly. Synchrony is a property of **today's implementation**, never of
  the **contract**: the member's type is `Effect<A, E>`, which permits async,
  and nothing in the type system tells you when that changes. The two costs:
  - Swap one member for an async implementation and every test in the block
    dies at *runtime* with `AsyncFiberError: An asynchronous Effect was
    executed with Effect.runSync` — no type error, no warning, and the blast
    radius is the whole `describe`.
  - A typed failure escapes `runSync` as a **throw** (in v4 the error instance
    itself, `_tag` intact — not a wrapper), so it lands in Vitest's generic
    exception path instead of `Effect.flip` / `Effect.result`, and the test
    asserts on a stack trace instead of the error channel.

  The correct shape costs one line: `layer(Svc.layer)("the shipped engine",
  (it) => { it.effect(…, () => Effect.gen(…)) })` — and it gets you
  `TestClock`/`TestConsole` and layer memoization for free.
- **Never** `it("...", () => Effect.gen(...))` either — the inverse mistake, and
  the worse one. Returning an Effect without running it hands Vitest a
  non-promise it does nothing with: the test reports **green having evaluated
  zero assertions**. The two shapes are inverses and only the first one looks
  wrong:

  | shape | runs? | symptom |
  | --- | --- | --- |
  | `it(..., () => Effect.runPromise(p))` | yes | correct result, execution laundered |
  | `it(..., () => p)` | **no** | always green, no assertion ever evaluated |

  The fix in both directions is `it.effect`. This shipped in
  `@effected/schemastore` and was caught
  only in review — and the vacuous test was the one cited as proof to a
  downstream consumer who had reported the very finding it failed to pin. A
  false green does not merely miss a regression; it gets used as evidence. The
  shape is greppable, so it belongs in a structural check (see
  [references/structural-checks.md](./references/structural-checks.md)).
- **Never launder an Effect into a fixture with `Effect.runSync`.** If a test
  input comes from an Effect (a parse, a decode), the test *is* an `it.effect`
  and you `yield*` it; if you only need a domain value, build it with `X.make`.
- **`yield* import(...)` breaks** — it throws `(intermediate value) is not
  iterable`, naming neither the import nor the yield. A dynamic import inside
  `Effect.gen` is `yield* Effect.promise(() => import("./thing.js"))`; a bulk
  `await` → `yield*` rewrite produces the broken form mechanically.

## Asserting on typed errors

A test for the failure channel must not let the error escape as a defect.
**`Effect.flip`** — swaps channels so the typed error becomes the success value
— is our house pattern:

```ts
import { assert, it } from "@effect/vitest";
import { Jsonc } from "@effected/jsonc";
import { Effect } from "effect";

it.effect("fails with an aggregate JsoncParseError", () =>
  Effect.gen(function* () {
    const error = yield* Effect.flip(Jsonc.parse("{ bad }"));
    assert.strictEqual(error._tag, "JsoncParseError");
    assert.isAbove(error.errors.length, 0);
  }),
);
```

**`Effect.result`** — never fails; returns a `Result` you narrow with
`Result.isSuccess`/`isFailure` (the v4 replacement for the removed `Either`).
Reach for it when one program must assert on *both* channels. **`Effect.exit`**
— the full `Exit` (defects and interrupts included) when you must inspect a
`Cause`. Signatures (verified): `flip: Effect<A,E,R> => Effect<E,A,R>`,
`result: … => Effect<Result<A,E>,never,R>`, `exit: … => Effect<Exit<A,E>,never,R>`.

**`Effect.flip` is WRONG for a defect.** It swaps only the *typed* channel, so a
defect escapes it and the test errors instead of asserting. Defects go through
`Effect.exit`, **with an explicit throw/fail on the non-failure branch**:

```ts
import { assert, it } from "@effect/vitest";
import { Cause, Effect, Exit } from "effect";

const subject = Effect.die(new Error("the expected message"))

it.effect("the subject dies with the expected message", () =>
  Effect.gen(function* () {
    const exit = yield* Effect.exit(subject);
    if (Exit.isFailure(exit)) {
      assert.include(Cause.pretty(exit.cause), "the expected message");
    } else {
      assert.fail("expected a defect, but the effect succeeded");
    }
  }),
);
```

**A narrowing `if` with no `else` asserts nothing on the other path.** A bare
`if (Exit.isFailure(exit)) { …assert… }` passes silently on success — found six
separate times in one package's suite. It is its own anti-pattern, not an
Exit-specific one.

**Genuine defects must NOT be swallowed into the typed channel** — the half a
flip-based test cannot prove. Assert `Effect.exit`, that the cause holds no
`Fail` reason (the discriminating line), and that the `Die` defect is the
ORIGINAL error. **Assert helpers are never type predicates** — narrow with a
real `if`; and `assert.deepStrictEqual` against literal-typed encodes needs an
explicit `<unknown>` type argument. Worked example and both traps →
**[references/asserting-errors.md](./references/asserting-errors.md)**.

## Providing test / mock layers

`layer(...)` applies to **any service in a test's `R` — owned or consumed**. Its
signature takes any `Layer.Layer<R, E>`; nothing requires the package under test
to declare the service. A package that owns no services but *consumes*
`Path.Path` or `FileSystem.FileSystem` needs suite-boundary layers most.

```ts
import { assert, describe, layer } from "@effect/vitest";
import { Context, Effect, Layer } from "effect";

class Foo extends Context.Service<Foo, string>()("Foo") {
  static readonly layer = Layer.succeed(Foo, "foo");
}

describe("foo", () => {
  layer(Foo.layer)((it) => {
    it.effect("gets foo", () =>
      Effect.gen(function* () { assert.strictEqual(yield* Foo, "foo"); }));
  });
});
```

### `layer()` memoizes; plain `Effect.provide` does NOT. That asymmetry is the whole decision

The top-level `layer` builds once per group and keeps the scope open until
`afterAll`; a per-test `.pipe(Effect.provide(L))` rebuilds per test. Nested
provides inside one running effect memoize constituent consts by reference, so
a fault-injected variant can silently fall back to the REAL services already
built outside.

**Per-test provide is the SAFE default; collapsing a suite onto a
suite-boundary `layer()` is the RISKY move.** Build-once means every stateful
resource is cumulative across the group — `TestClock.adjust`, in-memory
stores, TTL expiry, `TestConsole.logLines` — so a clock-driving test must NOT
live inside a `layer()` block. Pre-flight before collapsing, the nested-layer
and `excludeTestServices` forms, and why `MethodsNonLive` has no `.live` →
**[references/providing-layers.md](./references/providing-layers.md)**; worked
failures in [references/migrating-a-repo.md](./references/migrating-a-repo.md).
A mock service is a `Context.Service` with a test `Layer`, swapped `Live` →
`Test` at this boundary, never inside test bodies.

**Testing a boundary-tier package that does real IO needs no platform package.**
`Path.layer` comes from `effect` core (`Path.ts:931`) and is the POSIX `Path`
only (`/` separator, POSIX semantics): provide it in tests, never in code under
`src/`, where it would give Windows users POSIX paths. The filesystem double
is `@effected/memfs`, a pure package with no `node:*` import — core ships
**no** `FileSystem.layer`, only the deny-by-default `layerNoop` (`FileSystem.ts:848`),
which is not a double
([references/providing-layers.md](./references/providing-layers.md)). So `@effected/walker` tests filesystem behavior
with zero `@effect/platform-node` devDependency:

```ts
import { MemoryFileSystem } from "@effected/memfs";
import { assert, layer } from "@effect/vitest";
import { Effect, FileSystem, Layer, Path } from "effect";

layer(Path.layer)("path ops", (it) => {
  it.effect("Path is in R, no Effect.provide in the body", () =>
    Effect.gen(function* () {
      const path = yield* Path.Path;
      assert.strictEqual(path.dirname("/a/b"), "/a");
    }));
});

const Volume = Layer.merge(Path.layer, MemoryFileSystem.layerWith({ "/a/.rc": "" }));

layer(Volume)("seeded filesystem", (it) => {
  it.effect("fs.exists consults the volume", () =>
    Effect.gen(function* () {
      const fs = yield* FileSystem.FileSystem;
      assert.isTrue(yield* fs.exists("/a/.rc"));
      assert.isFalse(yield* fs.exists("/a/other"));
    }));
});
```

**`layerNoop`'s unstubbed members answer in THREE different ways**
(`makeNoop` (`FileSystem.ts:712`) splits them) — typed `NotFound`, silent success (`exists` → `false`, `remove` → `Effect.void`), and
`Effect.die` (`makeDirectory`, `makeTemp*`), which `Effect.catch` cannot
absorb. **None of this is a reason to stub `layerNoop` better — it is the
argument for `@effected/memfs`.** This repo's rule: a `FileSystem` double is
`@effected/memfs`, never a `layerNoop` stub and never a hand-rolled `node:fs`
port stub. The one exception is an **oracle test that runs a third-party
engine** (the tool your code mirrors, such as `@changesets/apply-release-plan`
or a package manager) on the same input as your code: the engine reads
`node:fs` and real git itself, where no volume can reach it. Copy the fixture
into a temporary directory, run both sides there over `@effect/platform-node`,
and keep every other test on memfs. **Every `Effect.provide` of a memfs layer re-seeds a fresh
volume**, so assert inside the one provide or provide a pinned
`handle.layer`. The table, the BOM trap, the seed forms and the
one-`layer(...)`-block-per-fixture shape →
**[references/providing-layers.md](./references/providing-layers.md)**;
picking the memfs form, seeds, faults, case-insensitive volumes and traps →
**[references/memfs.md](./references/memfs.md)**.

**A service built on `@effected/store` is tested over its REAL implementation, never a hand-written double**: share one implementation effect written against `Store` between `layer(filename)` (production, `Store.layerSqlite`) and a `layerMemory()` FACTORY over `Store.layerTest({ migrations })`, which opens a fresh migrated `:memory:` database per build. A factory, not a const, so two consumers in one composed graph never share a database by reference memoization. Pair it with `@effected/memfs` for a fully hermetic pipeline. An application wired through `@effected/app` tests over `AppTest.layer(options)` from `@effected/app/testing` — fixed XDG paths, `:memory:` `Store`/`Cache` and a memfs volume, `R = never`; `@effected/memfs` is that subpath's optional peer, so add it as a devDependency.

**Every stub effect goes through `Effect.suspend`.** A recorder that pushes
eagerly logs calls that never executed. Worked probe →
[references/false-greens.md](./references/false-greens.md).

### Faulting ONE method of a real layer

For "behaves like the real service except this one method fails on demand",
`layerNoop` is the wrong tool. The house recipe is `Layer.effect` + spread the
base + `Layer.provide(base)`, with `Layer.updateService` as the shorter form
and `Layer.mock` for partial stubs that die loudly. Full scaffold, the source
anchors and the three ways to get the spread wrong →
**[references/fault-injection.md](./references/fault-injection.md)**.

### The env seam: swap `ConfigProvider`, never `process.env`

Code reading its environment through `Config.*` already has a test seam, and
it is not in `R`: `ConfigProvider.ConfigProvider` is a `Context.Reference`, so
a test replaces it with `Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: record })))`.
An empty record is the "variable unset" case; the default provider would
answer from whatever the developer's shell exports. Never mutate
`process.env` in `beforeEach`. Worked example →
**[references/env-seam.md](./references/env-seam.md)**.

## Property testing with `it.effect.prop` and `it.prop`

Feed a Schema (or class) directly as an arbitrary. The engine is core's native
**`Arbitrary`**, not fast-check — there is no `FastCheck` module, and **no
`fastCheck: { numRuns }` option**: the bag is `arbitrary: { runs, size, … }`.
The traps a probe settled, each with its fix in
**[references/property-testing.md](./references/property-testing.md)**:

- The `size` clamp (default **10**) silently shrinks a domain — pass
  `size: <cap>` for long-input properties.
- A brand whose check is a bare `makeFilter` EXHAUSTS instead of hanging.
- The generator emits `-0`; round-trip properties over serialized numbers
  exclude it.
- A partial dictionary is a Struct of `optionalKey`, not a `Record`.
- `isPattern` regexes must be lookaround-free, free of `i`/`m`/`v`, and carry
  `u`; otherwise the pattern is silently dropped.
- A custom-alphabet string is `Arbitrary.array` over a `Schema.Literals`
  of characters, mapped through `join("")`; `Arbitrary.array` is under the
  same `size` clamp.
- Reading a failure: the agent reporter compacts it to one line; the full
  shrunk input and replay token are in the `--reporter=json` output file. Pin
  the counterexample as an ordinary regression test.

## Time-dependent logic: `TestClock`

**`it.effect` ALWAYS installs a virtual `TestClock`. This is not opt-in.**

- **A small REAL delay anywhere under the test — usually in `src` — hangs to
  the vitest timeout** with no message pointing at the clock. Any
  `Effect.sleep`, retry schedule, timeout or polling interval needs a driven
  clock or `it.live`; grep the implementation, not only the test. A test that
  hangs for exactly five seconds: suspect wall-clock time first.
- **It starts at the EPOCH**, so clock *reads* return 1970 — set the clock
  with `TestClock.setTime(...)` whenever the code under test reads time.
- **Real async I/O interleaved with sleeps desyncs the drain loop** — use
  `it.live` for exactly those tests, outside the `layer()` block.
- **Do not manually provide `TestClock.layer()` under `it.effect`**, and never
  call `TestClock.adjust` under `it.live`.
- **Stage an interleaving with latches, not sleeps**, and a *leak* test needs
  **two** latches.

The grep pattern, the driving example, the clock-free interrupt restructure
and the source-visible reasoning →
**[references/testclock.md](./references/testclock.md)**; the two-latch probe →
[references/false-greens.md](./references/false-greens.md).

## `it.effect` also intercepts CONSOLE output — including `Effect.log*`

`TestEnv` installs `TestConsole` alongside the clock, so a spy on the real
`console.log` captures **nothing** — and auditing for `Console.*` call sites
is insufficient, because Effect's default logger writes through the same ref.
**A test whose only assertions are negative is the vacuous-pass shape**; a
positive sibling is the cheap proof the sink is live. Source identity and the
immune cases → **[references/test-console.md](./references/test-console.md)**;
`TestConsole.logLines` accumulation →
[references/false-greens.md](./references/false-greens.md).

## A test that cannot fail is worse than no test — mutate the edges

A green suite proves nothing about the properties no test can observe. The
discipline: **capture a baseline** (`git status --porcelain > /tmp/baseline`),
break the implementation in the way the property forbids (with the editor —
never `git checkout`/`git stash`, other work lives in the tree), watch that
exact test go red, revert, and confirm the status matches the **baseline**.

- **The assertion must DISCRIMINATE** — fail for the right reason.
- **The failure to look for is a rule with no input that could falsify it.**
  Ask of every rule: *what input would make this fire alone, and does it
  exist?* Mutate **per clause and per path**; **two code paths implementing
  one rule are two things to pin, not one**.
- **`as const satisfies ReadonlyArray<Union>` enforces NOTHING about
  exhaustiveness** — use a residue-must-be-empty type or a total
  `Record<Union, …>`.
- **One assertion, one rule.** A passing test is evidence about the path it
  takes, not about the rule it appears to test.
- **Before acting on "nothing found", run a control that FIRES**, with a
  non-zero expected answer, against a KNOWN-GOOD input.
- **Read the failure TEXT; never infer a catch from a missing pass line.**
  Empty output is a failed experiment.
- **Never verify a change by grepping for the text you just wrote.** Only a
  mutation finds the emit site.
- **A semantics-preserving perf fix cannot be pinned** — report it as
  fixed-but-unpinned.
- **A surviving mutant is a question about the CODE**; deleting the code is a
  legitimate answer. When sweeping, assert the on-disk state every run, and
  settle disagreeing reads against `git show HEAD:<path>`.

Full discipline, the checklist, the `satisfies` spellings that do fire and the
worked failures →
**[references/mutation-testing.md](./references/mutation-testing.md)**.

### Structural checks over source text

An invariant a type cannot express — "this module does not reach that
dependency", "the entrypoint exports this by name" — gets pinned by asserting
over source text, and those assertions fail silently where ordinary ones shout.
**Raw source is SAFE for a `notInclude` check (a comment mention is a spurious
alarm) and DANGEROUS for an `include` one (a comment mention passes).** Strip
comments — LINE comments **before** block comments — remove module specifiers,
match on a word boundary, and give the stripper its own discriminating test once
it is load-bearing → **[references/structural-checks.md](./references/structural-checks.md)**.

## Other false greens, in one place

`Tests: 0/0 passed` is a FAILED run whose **summary line** says passed (a
filter that matched no test file; the separate `ERR_LOAD_URL` a repo suffers
while its config declares a cwd-relative `globalSetup` path is a config defect
to fix, not a cwd rule to obey); `TestConsole.logLines` accumulation; the eager
`layerNoop` recorder; `PubSub.takeAll` hanging on an empty subscription; a
drain test whose subscriber took every message inside `publish`, so nothing
was ever queued; `PubSub.shutdown` dropping the tail it was meant to drain; a
stream over an ended subscription repeating `PubSub.end`'s sticky final
message forever; a race that only shows below the default
`Scheduler.MaxOpsBeforeYield` budget; timing
gates lying under coverage, and a timing guard that cannot catch a loop
that never returns (its control runs in a subprocess with a hard timeout);
a green suite that fails the vitest **process**
because a test left `process.exitCode` set; a big green count for a surface the
suite never calls; a helper used on **both sides** of every comparison, which
agrees with itself however broken it is; an `Effect.timeout` guard that never
fires — under `it.effect` any guard is inert until `TestClock.adjust` passes
it, and under a real clock a guard of 5 seconds or more loses to vitest's own
default; a forked fiber's failure that is never observed anywhere unless the
fiber is joined. Each with its probe →
**[references/false-greens.md](./references/false-greens.md)**.

**Zero collected tests is never a pass — read BOTH the Tests line and the exit
code.** A filter that matches no test file prints `Tests: 0/0 passed` and exits
**1**: the Tests line is the liar and the exit code is honest. Treat any
disagreement between the two signals as the alarm, and read `unhandledErrors`
alongside both. **Run vitest from the repo root** — from inside a package it
does not load the root config — and never `--passWithNoTests`. In a kit
monorepo a workspace link resolves to a sibling's `src/`, so no sibling needs
building first; only a test that reads its own `dist` does, and a `globalSetup` build through `AgentPlugin.runScript` is
not a guarantee against it: the script is skipped while its done-marker is
younger than `VITEST_AGENT_RUNSCRIPT_BUILT_RECENTLY_MS` (30 s by default),
so pin that to `1` in the setup or a run inside the window reads the
previous build. The `ERR_LOAD_URL` setup-file case and the rest →
**[references/running-the-suite.md](./references/running-the-suite.md)**.

## Test the sync facade over the same fixture as the Effect form

When an external consumer calls a synchronous facade (a `*Sync` function, or a
`parseResult`/`Result` form beside an `Effect` one), the facade is a separate
code path and a green Effect-form suite proves nothing about it. Run the facade
over the **same fixture** the Effect form reads and assert the same answer. The
facade can fail where the service passes: a layout only the Effect suite had a
fixture for (a yarn repository's `package.json#workspaces`, say) discovers
members through the service while the sync facade the consuming tool calls
returns none.

**Piping vitest into `tail` (or `head`, `grep`) hides its exit code.** The pipe
reports the last command's status, so a failing run looks green. Under zsh or
bash use `set -o pipefail` before the pipeline (or read `${PIPESTATUS[0]}` /
`$pipestatus[1]`), or redirect to a file and read the code of the vitest
command itself.

## House conventions

- Tests live in each package's `__test__/` directory (`*.test.ts`), never
  co-located in `src/`.
- **A probe writes no file.** A probe left under `__test__/` is collected by
  the ordinary suite and inflates the `Tests:` count. Run it as a script that
  never touches disk, from inside the package:
  `cd packages/<name> && node --input-type=module -e '<script>'` →
  [references/running-the-suite.md](./references/running-the-suite.md).
- Construct domain values via the schema's `X.make`, never `new`.
- **In this monorepo, assert with `assert.*` from `@effect/vitest`, never
  `expect`** — the root `CLAUDE.md` mandates it and every test file here obeys.
  `toEqual` → `assert.deepStrictEqual`; `toBe` → `assert.strictEqual`;
  `toBeInstanceOf` → `assert.instanceOf`; `toBe(true)` → `assert.isTrue`;
  `toHaveLength` → `assert.lengthOf`. **This is house convention, not a
  technical constraint** — `expect` works unchanged inside `it.effect`, so in a
  repo whose house style is `expect`, do **not** sweep it. Follow the host repo.
- Keep the boundary honest: assert that the package's own error escapes
  (`error._tag === "JsoncParseError"`), and that the schema path surfaces a
  `SchemaError` — the two must not drift.
- **A test that cannot fail is worse than no test.** Two cases beyond the walker
  eight: a prototype-pollution guard whose payload could never mutate the
  asserted object, and a `@ts-expect-error` in a tsconfig-excluded file.
Pliki w paczce (14)
  • SKILL.md 31 KB
  • references/asserting-errors.md 2 KB
  • references/env-seam.md 2 KB
  • references/false-greens.md 45 KB
  • references/fault-injection.md 10 KB
  • references/memfs.md 20 KB
  • references/migrating-a-repo.md 7 KB
  • references/mutation-testing.md 17 KB
  • references/property-testing.md 9 KB
  • references/providing-layers.md 10 KB
  • references/running-the-suite.md 5 KB
  • references/structural-checks.md 8 KB
  • references/test-console.md 1 KB
  • references/testclock.md 7 KB

Wersje

RepozytoriumWynikLicencja
Ta strona · spencerbeggs/effected95MIT
spencerbeggs/effected100MIT

Migawka bazy z 13.11.2026 · wersja paczki 5f6a6d58b36e (skrót SHA-256 całej paczki)

Pobrania w ostatnich 30 dniach: 0 · ranking

Zgłoś problem z tym skillem: szkodliwa treść, błędny opis albo prośba autora o usunięcie.

Podobne skille

  • 91effect-testing
    Pisze testy kodu Effect przez @effect/vitest i zwykłe testy funkcji, także dla warstw, czasu i testów właściwości.
  • 91effect-testing
    Pisze testy dla kodu Effect z @effect/vitest i zwykłych funkcji w vitest, także testy property-based.
  • 95effect-testing
    Opisuje testowanie kodu Effect: Effect.gen w testach, warstwy testowe, mockowanie serwisów i testy scenariuszy błędów.
  • 95effect-best-practices
    Pilnuje wzorców Effect-TS dla serwisów, błędów, warstw i atomów, przy pisaniu Effect.Service i komponentów React.
  • 100effect-ts
    Wspiera nietrywialną pracę z Effect 4: serwisy i warstwy, typowane błędy, Schema, Config, współbieżność, @effect/vitest, effect/ai, effect/sql, Effect Atom i @prb/effect-next.
  • 85vitest
    Pomaga pisać, uruchamiać i debugować testy jednostkowe i komponentowe w Vitest dla projektów TypeScript (Node, bun, React i Next.js, Effect), w tym mocki, narzędzia testowe i pokrycie.

Uwagi i zgłoszenia

Wiadomość trafi do Bartosza Sokolińskiego. Adres e-mail jest potrzebny tylko do odpowiedzi. Szczegóły w polityce prywatności.