opencode-effect-enforcer 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/guidance/effect-first-development.md +1247 -0
  4. package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
  5. package/guidance/post__parse-dont-validate.md +109 -0
  6. package/guidance/progressive-disclosure-guidance.md +38 -0
  7. package/package.json +63 -0
  8. package/patterns/avoid-any.md +37 -0
  9. package/patterns/avoid-data-tagged-error.md +34 -0
  10. package/patterns/avoid-direct-json.md +51 -0
  11. package/patterns/avoid-direct-tag-checks.md +54 -0
  12. package/patterns/avoid-expect-in-if.md +52 -0
  13. package/patterns/avoid-mutable-state.md +70 -0
  14. package/patterns/avoid-native-fetch.md +61 -0
  15. package/patterns/avoid-node-imports.md +86 -0
  16. package/patterns/avoid-non-null-assertion.md +44 -0
  17. package/patterns/avoid-object-type.md +46 -0
  18. package/patterns/avoid-option-getorthrow.md +39 -0
  19. package/patterns/avoid-platform-coupling.md +43 -0
  20. package/patterns/avoid-process-env.md +43 -0
  21. package/patterns/avoid-react-hooks.md +73 -0
  22. package/patterns/avoid-schema-suffix.md +45 -0
  23. package/patterns/avoid-sync-fs.md +68 -0
  24. package/patterns/avoid-try-catch.md +47 -0
  25. package/patterns/avoid-ts-ignore.md +38 -0
  26. package/patterns/avoid-untagged-errors.md +67 -0
  27. package/patterns/avoid-yield-ref.md +46 -0
  28. package/patterns/casting-awareness.md +46 -0
  29. package/patterns/context-tag-extends.md +84 -0
  30. package/patterns/effect-catchall-default.md +61 -0
  31. package/patterns/effect-promise-vs-trypromise.md +47 -0
  32. package/patterns/effect-run-in-body.md +58 -0
  33. package/patterns/imperative-loops.md +76 -0
  34. package/patterns/prefer-arr-sort.md +52 -0
  35. package/patterns/prefer-duration-values.md +56 -0
  36. package/patterns/prefer-effect-fn.md +161 -0
  37. package/patterns/prefer-match-over-switch.md +48 -0
  38. package/patterns/prefer-option-over-null.md +56 -0
  39. package/patterns/prefer-redacted-config.md +70 -0
  40. package/patterns/prefer-schema-class.md +54 -0
  41. package/patterns/require-effect-concurrency.md +83 -0
  42. package/patterns/stream-large-files.md +63 -0
  43. package/patterns/throw-in-effect-gen.md +62 -0
  44. package/patterns/use-clock-service.md +45 -0
  45. package/patterns/use-command-executor-service.md +54 -0
  46. package/patterns/use-console-service.md +54 -0
  47. package/patterns/use-filesystem-service.md +59 -0
  48. package/patterns/use-http-client-service.md +77 -0
  49. package/patterns/use-path-service.md +53 -0
  50. package/patterns/use-random-service.md +45 -0
  51. package/patterns/use-temp-file-scoped.md +66 -0
  52. package/patterns/vm-in-wrong-file.md +51 -0
  53. package/patterns/yield-in-for-loop.md +61 -0
  54. package/skills/effect-ai-chat/SKILL.md +472 -0
  55. package/skills/effect-ai-language-model/SKILL.md +652 -0
  56. package/skills/effect-ai-prompt/SKILL.md +752 -0
  57. package/skills/effect-ai-provider/SKILL.md +668 -0
  58. package/skills/effect-ai-streaming/SKILL.md +418 -0
  59. package/skills/effect-ai-tool/SKILL.md +1132 -0
  60. package/skills/effect-atom-rpc/SKILL.md +488 -0
  61. package/skills/effect-atom-state/SKILL.md +640 -0
  62. package/skills/effect-batching/SKILL.md +614 -0
  63. package/skills/effect-cache/SKILL.md +570 -0
  64. package/skills/effect-cli/SKILL.md +523 -0
  65. package/skills/effect-command-executor/SKILL.md +675 -0
  66. package/skills/effect-concurrency-testing/SKILL.md +612 -0
  67. package/skills/effect-config/SKILL.md +580 -0
  68. package/skills/effect-context-witness/SKILL.md +274 -0
  69. package/skills/effect-domain-modeling/SKILL.md +1212 -0
  70. package/skills/effect-domain-predicates/SKILL.md +867 -0
  71. package/skills/effect-error-handling/SKILL.md +1581 -0
  72. package/skills/effect-fiber/SKILL.md +731 -0
  73. package/skills/effect-filesystem/SKILL.md +624 -0
  74. package/skills/effect-graph/SKILL.md +571 -0
  75. package/skills/effect-http-api/SKILL.md +1760 -0
  76. package/skills/effect-http-client/SKILL.md +989 -0
  77. package/skills/effect-http-server/SKILL.md +920 -0
  78. package/skills/effect-incremental-migration/SKILL.md +362 -0
  79. package/skills/effect-layer-design/SKILL.md +642 -0
  80. package/skills/effect-managed-runtime/SKILL.md +395 -0
  81. package/skills/effect-mcp-server/SKILL.md +608 -0
  82. package/skills/effect-observability/SKILL.md +719 -0
  83. package/skills/effect-optics/SKILL.md +554 -0
  84. package/skills/effect-parallelization/SKILL.md +668 -0
  85. package/skills/effect-path/SKILL.md +296 -0
  86. package/skills/effect-pattern-matching/SKILL.md +914 -0
  87. package/skills/effect-platform-abstraction/SKILL.md +1175 -0
  88. package/skills/effect-platform-layers/SKILL.md +514 -0
  89. package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
  90. package/skills/effect-react-composition/SKILL.md +986 -0
  91. package/skills/effect-react-vm/SKILL.md +675 -0
  92. package/skills/effect-rpc-api/SKILL.md +624 -0
  93. package/skills/effect-rpc-client/SKILL.md +666 -0
  94. package/skills/effect-rpc-cluster/SKILL.md +1623 -0
  95. package/skills/effect-rpc-server/SKILL.md +767 -0
  96. package/skills/effect-scheduling/SKILL.md +124 -0
  97. package/skills/effect-schema-composition/SKILL.md +975 -0
  98. package/skills/effect-schema-v4/SKILL.md +691 -0
  99. package/skills/effect-scope/SKILL.md +682 -0
  100. package/skills/effect-service-implementation/SKILL.md +656 -0
  101. package/skills/effect-socket/SKILL.md +703 -0
  102. package/skills/effect-sql/SKILL.md +781 -0
  103. package/skills/effect-stream/SKILL.md +765 -0
  104. package/skills/effect-testing/SKILL.md +1331 -0
  105. package/skills/effect-typeclass-design/SKILL.md +161 -0
  106. package/skills/effect-wide-events/Article.md +66 -0
  107. package/skills/effect-wide-events/SKILL.md +95 -0
  108. package/skills/effect-workflow/SKILL.md +810 -0
  109. package/src/agent-policy.ts +22 -0
  110. package/src/enforcer.ts +104 -0
  111. package/src/frontmatter.ts +34 -0
  112. package/src/guidance.ts +66 -0
  113. package/src/index.ts +38 -0
  114. package/src/pattern-catalog.ts +115 -0
  115. package/src/pattern-matcher.ts +178 -0
  116. package/src/pattern.ts +97 -0
  117. package/src/skills.ts +29 -0
  118. package/src/write-projection.ts +66 -0
@@ -0,0 +1,1331 @@
1
+ ---
2
+ name: effect-testing
3
+ description: Write comprehensive tests using @effect/vitest for Effect code and vitest for pure functions. Use this skill when implementing tests for Effect-based applications, including services, layers, time-dependent effects, error handling, and property-based testing.
4
+ ---
5
+
6
+ # Effect Testing Skill
7
+
8
+ This skill provides comprehensive guidance for testing Effect-based applications using `@effect/vitest` and standard `vitest`.
9
+
10
+ ## Effect Source Reference
11
+
12
+ The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
13
+ Browse and read files there directly to look up APIs, types, and implementations.
14
+
15
+ Reference this for:
16
+
17
+ - Testing utilities: `packages/effect/src/Testing.ts`
18
+ - @effect/vitest source: `packages/vitest/`
19
+ - Migration guide: `MIGRATION.md`
20
+ - Effect source: `packages/effect/src/`
21
+
22
+ ## Framework Selection
23
+
24
+ **CRITICAL**: Choose the correct testing framework based on the code being tested.
25
+
26
+ ### Use @effect/vitest for Effect Code
27
+
28
+ Use `@effect/vitest` when testing:
29
+
30
+ - Functions that return `Effect<A, E, R>`
31
+ - Code that uses services and layers
32
+ - Time-dependent operations with `TestClock`
33
+ - Asynchronous operations coordinated with Effect
34
+ - STM (Software Transactional Memory) operations
35
+
36
+ ```typescript
37
+ import { it, expect } from '@effect/vitest';
38
+ import { Effect } from 'effect';
39
+
40
+ declare const fetchUser: (id: string) => Effect.Effect<{ id: string }, Error>;
41
+
42
+ it.effect('should fetch user', () =>
43
+ Effect.gen(function* () {
44
+ const user = yield* fetchUser('123');
45
+ expect(user.id).toBe('123');
46
+ })
47
+ );
48
+ ```
49
+
50
+ ### Use Regular vitest for Pure Functions
51
+
52
+ Use standard `vitest` for:
53
+
54
+ - Pure functions with no Effect wrapper
55
+ - Simple data transformations
56
+ - Helper utilities
57
+ - Type constructors (brands, newtypes)
58
+
59
+ ```typescript
60
+ import { describe, expect, it } from 'vitest';
61
+
62
+ declare const Cents: {
63
+ make: (value: bigint) => bigint;
64
+ add: (a: bigint, b: bigint) => bigint;
65
+ };
66
+
67
+ describe('Cents', () => {
68
+ it('should add cents correctly', () => {
69
+ const result = Cents.add(Cents.make(100n), Cents.make(50n));
70
+ expect(result).toBe(150n);
71
+ });
72
+ });
73
+ ```
74
+
75
+ ## Test Variants
76
+
77
+ ### it.effect - Default Test Environment
78
+
79
+ Use `it.effect` by default. It scopes the test and installs Effect's `TestClock` and `TestConsole` services. Other services are not automatically test implementations and must be provided explicitly.
80
+
81
+ The callback argument is Vitest's `TestContext` (task metadata, cancellation signal, fixtures), not Effect's service context. Access Effect test services by yielding them, for example with `TestClock.adjust`.
82
+
83
+ ```typescript
84
+ import { it, expect } from '@effect/vitest';
85
+ import { Effect } from 'effect';
86
+
87
+ declare const someEffect: Effect.Effect<number>;
88
+ declare const expected: number;
89
+
90
+ it.effect('test name', (context) =>
91
+ Effect.gen(function* () {
92
+ // context is Vitest's TestContext; TestClock is in the Effect context.
93
+ const result = yield* someEffect;
94
+ expect(result).toBe(expected);
95
+ })
96
+ );
97
+ ```
98
+
99
+ ### it.live - Explicit Live Environment
100
+
101
+ Uses real services (real clock, real random, etc.).
102
+
103
+ ```typescript
104
+ import { it } from '@effect/vitest';
105
+ import { Effect, Clock } from 'effect';
106
+
107
+ it.live('test with real time', () =>
108
+ Effect.gen(function* () {
109
+ const now = yield* Clock.currentTimeMillis;
110
+ // Uses actual system time
111
+ })
112
+ );
113
+ ```
114
+
115
+ Use `it.live` only when real time or live runtime services are behavior under test. It scopes the test without installing `TestClock` or `TestConsole`. Real databases, HTTP clients, and filesystems still require their explicit layers; `it.live` does not provide them.
116
+
117
+ ### Resource Management in Tests
118
+
119
+ `it.effect` already handles scoping internally — there is no separate `it.scoped` or `it.scopedLive` variant. Use `Effect.acquireRelease` or `Effect.scoped` directly within `it.effect`:
120
+
121
+ ```typescript
122
+ import { it } from '@effect/vitest';
123
+ import { Effect } from 'effect';
124
+
125
+ declare const acquire: Effect.Effect<unknown>;
126
+ declare const release: Effect.Effect<void>;
127
+
128
+ it.effect('test with resources', () =>
129
+ Effect.gen(function* () {
130
+ const resource = yield* Effect.acquireRelease(acquire, () => release);
131
+ // Resource automatically cleaned up when the test's scope closes
132
+ })
133
+ );
134
+ ```
135
+
136
+ ## Assertions
137
+
138
+ ### Use expect from vitest
139
+
140
+ For all assertions, use the standard `expect` from vitest:
141
+
142
+ ```typescript
143
+ import { it, expect } from '@effect/vitest';
144
+ import { Effect } from 'effect';
145
+
146
+ declare const computation: Effect.Effect<number>;
147
+ declare const array: unknown[];
148
+
149
+ it.effect('assertions', () =>
150
+ Effect.gen(function* () {
151
+ const result = yield* computation;
152
+ expect(result).toBe(42);
153
+ expect(result).toBeGreaterThan(0);
154
+ expect(array).toHaveLength(3);
155
+ })
156
+ );
157
+ ```
158
+
159
+ ### Effect-Specific Utilities
160
+
161
+ `@effect/vitest` provides additional assertion utilities in `utils`:
162
+
163
+ ```typescript
164
+ import { it } from '@effect/vitest';
165
+ import {
166
+ assertEquals, // Uses Effect's Equal.equals
167
+ assertTrue,
168
+ assertFalse,
169
+ assertSome, // For Option.Some
170
+ assertNone, // For Option.None
171
+ assertSuccess, // For Either.Right / Exit.Success
172
+ assertFailure // For Either.Left / Exit.Failure
173
+ } from '@effect/vitest/utils';
174
+ import { Effect, Option, Either } from 'effect';
175
+
176
+ declare const someOptionalEffect: Effect.Effect<Option.Option<number>>;
177
+ declare const someEitherEffect: Effect.Effect<Either.Either<number, Error>>;
178
+ declare const expectedValue: number;
179
+
180
+ it.effect('with effect assertions', () =>
181
+ Effect.gen(function* () {
182
+ const option = yield* someOptionalEffect;
183
+ assertSome(option, expectedValue);
184
+
185
+ const either = yield* someEitherEffect;
186
+ assertSuccess(either, expectedValue);
187
+ })
188
+ );
189
+ ```
190
+
191
+ ## Testing with Services and Layers
192
+
193
+ ### Providing Services to Tests
194
+
195
+ Use `Effect.provide` to supply test implementations:
196
+
197
+ ```typescript
198
+ import { it, expect } from '@effect/vitest';
199
+ import { Effect, Context, Layer } from 'effect';
200
+
201
+ class UserService extends Context.Service<
202
+ UserService,
203
+ {
204
+ getUser: (id: string) => Effect.Effect<{ name: string }>;
205
+ }
206
+ >()('UserService') {}
207
+
208
+ declare const TestUserServiceLayer: Layer.Layer<UserService>;
209
+
210
+ it.effect('should work with dependencies', () =>
211
+ Effect.gen(function* () {
212
+ const userService = yield* UserService;
213
+ const result = yield* userService.getUser('123');
214
+ expect(result.name).toBe('John');
215
+ }).pipe(Effect.provide(TestUserServiceLayer))
216
+ );
217
+ ```
218
+
219
+ ```typescript
220
+ // Concise alternative using Layer.mock (v4)
221
+ const TestUserService = Layer.mock(UserService)({
222
+ getUser: (id) => Effect.succeed({ name: 'John' })
223
+ });
224
+ ```
225
+
226
+ `Layer.mock(Service)({...})` is shorthand for `Layer.succeed(Service, Service.of({...}))` — use whichever reads more clearly in context.
227
+
228
+ ### First-Class Controllable Test Services
229
+
230
+ For reusable stateful fakes, define file-local `TestInterface extends Interface`, a separate `TestService` tag, and a `testLayer` that provides one implementation under both tags. Production code depends only on `Service`; tests yield `TestService` to inspect calls and trigger failures or lifecycle transitions. The owning leaf self-exports its canonical identity at the bottom; sibling modules import that identity from the leaf, while folder/package barrels only relay it. This intentional self-reference requires toolchain and runtime support, so preserve an established project convention when it differs.
231
+
232
+ ```typescript
233
+ // notifier.ts
234
+ import { Context, Effect, Layer, Option, Ref } from 'effect';
235
+ import * as Arr from 'effect/Array';
236
+
237
+ export interface Interface {
238
+ readonly send: (message: Message) => Effect.Effect<void, SendError>;
239
+ }
240
+
241
+ export class Service extends Context.Service<Service, Interface>()(
242
+ '@app/Notifier'
243
+ ) {}
244
+
245
+ export interface TestInterface extends Interface {
246
+ readonly sentMessages: () => Effect.Effect<ReadonlyArray<Message>>;
247
+ readonly failNextSend: (error: SendError) => Effect.Effect<void>;
248
+ }
249
+
250
+ export class TestService extends Context.Service<TestService, TestInterface>()(
251
+ '@app/Notifier/Test'
252
+ ) {}
253
+
254
+ export const testLayer = Layer.effectContext(
255
+ Effect.gen(function* () {
256
+ const sent = yield* Ref.make<ReadonlyArray<Message>>([]);
257
+ const nextFailure = yield* Ref.make<Option.Option<SendError>>(
258
+ Option.none()
259
+ );
260
+ const service = TestService.of({
261
+ send: Effect.fn('Notifier.Test.send')(function* (message) {
262
+ const failure = yield* Ref.getAndSet(nextFailure, Option.none());
263
+ if (Option.isSome(failure)) return yield* Effect.fail(failure.value);
264
+ yield* Ref.update(sent, Arr.append(message));
265
+ }),
266
+ sentMessages: () => Ref.get(sent),
267
+ failNextSend: (error) => Ref.set(nextFailure, Option.some(error))
268
+ });
269
+
270
+ return Context.empty().pipe(
271
+ Context.add(Service, service),
272
+ Context.add(TestService, service)
273
+ );
274
+ })
275
+ );
276
+
277
+ export * as Notifier from './notifier.js';
278
+ ```
279
+
280
+ Use `Layer.succeed` for complete static fakes. Reserve `Layer.mock` for tiny local partial mocks where omitted methods should fail loudly.
281
+
282
+ ### Using layer Helper
283
+
284
+ Share a layer across multiple tests with the `layer` function:
285
+
286
+ ```typescript
287
+ import { layer, it, expect } from '@effect/vitest';
288
+ import { Effect, Context, Layer } from 'effect';
289
+
290
+ class Database extends Context.Service<
291
+ Database,
292
+ {
293
+ query: (sql: string) => Effect.Effect<Array<unknown>>;
294
+ }
295
+ >()('Database') {
296
+ static Test = Layer.succeed(Database, {
297
+ query: (sql) => Effect.succeed([])
298
+ });
299
+ }
300
+
301
+ layer(Database.Test)((it) => {
302
+ it.effect('test 1', () =>
303
+ Effect.gen(function* () {
304
+ const db = yield* Database;
305
+ const results = yield* db.query('SELECT *');
306
+ expect(results).toEqual([]);
307
+ })
308
+ );
309
+
310
+ it.effect('test 2', () =>
311
+ Effect.gen(function* () {
312
+ const db = yield* Database;
313
+ // Database available in all tests
314
+ })
315
+ );
316
+ });
317
+
318
+ // With name for describe block
319
+ layer(Database.Test)('Database tests', (it) => {
320
+ it.effect('query test', () => Effect.succeed(true));
321
+ });
322
+ ```
323
+
324
+ ### Nested Layers
325
+
326
+ Compose layers for complex dependencies:
327
+
328
+ ```typescript
329
+ import { layer, it } from '@effect/vitest';
330
+ import { Effect, Context, Layer } from 'effect';
331
+
332
+ class Database extends Context.Service<
333
+ Database,
334
+ {
335
+ query: (sql: string) => Effect.Effect<Array<unknown>>;
336
+ }
337
+ >()('Database') {}
338
+
339
+ class UserService extends Context.Service<
340
+ UserService,
341
+ {
342
+ getUser: (id: string) => Effect.Effect<unknown>;
343
+ }
344
+ >()('UserService') {}
345
+
346
+ declare const DatabaseLayer: Layer.Layer<Database>;
347
+ declare const UserServiceLayer: Layer.Layer<UserService, never, Database>;
348
+
349
+ layer(DatabaseLayer)((it) => {
350
+ it.layer(UserServiceLayer)('user tests', (it) => {
351
+ it.effect('has both dependencies', () =>
352
+ Effect.gen(function* () {
353
+ const db = yield* Database;
354
+ const userService = yield* UserService;
355
+ // Both available
356
+ })
357
+ );
358
+ });
359
+ });
360
+ ```
361
+
362
+ A nested `it.layer` suite **reuses** the parent suite's memoized layer allocations rather than rebuilding them. As of beta.67, each nested suite also **forks its own memo map**, so layers allocated locally inside one nested suite are isolated from sibling nested suites and are released independently when that suite finishes. The practical effect: shared parent layers (e.g. `DatabaseLayer`) are built once and reused, while sibling-local allocations do not leak across siblings even in concurrent suites.
363
+
364
+ ### Excluding Test Services
365
+
366
+ Use live services instead of test services:
367
+
368
+ ```typescript
369
+ import { layer, it } from '@effect/vitest';
370
+ import { Effect, Layer } from 'effect';
371
+
372
+ declare const MyServiceLayer: Layer.Layer<never>;
373
+
374
+ layer(MyServiceLayer, { excludeTestServices: true })((it) => {
375
+ it.effect('uses real clock', () =>
376
+ Effect.gen(function* () {
377
+ // Uses actual Clock, not TestClock
378
+ })
379
+ );
380
+ });
381
+ ```
382
+
383
+ ## Time-Dependent Testing with TestClock
384
+
385
+ ### Basic TestClock Usage
386
+
387
+ `TestClock` allows controlling time without waiting:
388
+
389
+ ```typescript
390
+ import { it, expect } from '@effect/vitest';
391
+ import { Effect, Fiber } from 'effect';
392
+ import { TestClock } from 'effect/testing';
393
+
394
+ it.effect('should handle delays', () =>
395
+ Effect.gen(function* () {
396
+ const fiber = yield* Effect.forkChild(
397
+ Effect.sleep('5 seconds').pipe(Effect.as('done'))
398
+ );
399
+
400
+ // Advance time by 5 seconds instantly
401
+ yield* TestClock.adjust('5 seconds');
402
+
403
+ const result = yield* Fiber.join(fiber);
404
+ expect(result).toBe('done');
405
+ })
406
+ );
407
+ ```
408
+
409
+ ### Testing Recurring Effects
410
+
411
+ Test periodic operations efficiently:
412
+
413
+ ```typescript
414
+ import { it, expect } from '@effect/vitest';
415
+ import { Effect, Queue, Option } from 'effect';
416
+ import { TestClock } from 'effect/testing';
417
+
418
+ it.effect('should execute every minute', () =>
419
+ Effect.gen(function* () {
420
+ const queue = yield* Queue.unbounded<number>();
421
+
422
+ // Fork effect that repeats every minute
423
+ yield* Effect.forkChild(
424
+ Queue.offer(queue, 1).pipe(
425
+ Effect.delay('60 seconds'),
426
+ Effect.forever
427
+ )
428
+ );
429
+
430
+ // No effect before time passes
431
+ const empty = yield* Queue.poll(queue);
432
+ expect(Option.isNone(empty)).toBe(true);
433
+
434
+ // Advance time
435
+ yield* TestClock.adjust('60 seconds');
436
+
437
+ // Effect executed once
438
+ const value = yield* Queue.take(queue);
439
+ expect(value).toBe(1);
440
+
441
+ // Verify only one execution
442
+ const stillEmpty = yield* Queue.poll(queue);
443
+ expect(Option.isNone(stillEmpty)).toBe(true);
444
+ })
445
+ );
446
+ ```
447
+
448
+ ### Testing Clock Methods
449
+
450
+ ```typescript
451
+ import { it, expect } from '@effect/vitest';
452
+ import { Effect, Clock } from 'effect';
453
+ import { TestClock } from 'effect/testing';
454
+
455
+ it.effect('should track time correctly', () =>
456
+ Effect.gen(function* () {
457
+ const start = yield* Clock.currentTimeMillis;
458
+
459
+ yield* TestClock.adjust('1 minute');
460
+
461
+ const end = yield* Clock.currentTimeMillis;
462
+
463
+ expect(end - start).toBeGreaterThanOrEqual(60_000);
464
+ })
465
+ );
466
+ ```
467
+
468
+ The clock separates Unix wall time (`Clock.currentTimeMillis` / `currentTimeNanos`) from monotonic elapsed time (`Clock.monotonicTimeNanos`). `TestClock.adjust` advances both, while `TestClock.setTime` may move wall time backward without decreasing monotonic time. Duration measurements such as `Effect.timed` therefore remain stable across wall-clock corrections. Nanosecond wall time also remains precise for large finite timestamps, and reads stay total after an infinite adjustment.
469
+
470
+ ### TestClock with Deferred
471
+
472
+ ```typescript
473
+ import { it, expect } from '@effect/vitest';
474
+ import { Effect, Deferred } from 'effect';
475
+ import { TestClock } from 'effect/testing';
476
+
477
+ it.effect('should handle deferred with delays', () =>
478
+ Effect.gen(function* () {
479
+ const deferred = yield* Deferred.make<number>();
480
+
481
+ yield* Effect.forkChild(
482
+ Effect.gen(function* () {
483
+ yield* Effect.sleep('60 seconds');
484
+ yield* Deferred.succeed(deferred, 42);
485
+ })
486
+ );
487
+
488
+ yield* TestClock.adjust('60 seconds');
489
+
490
+ const result = yield* Deferred.await(deferred);
491
+ expect(result).toBe(42);
492
+ })
493
+ );
494
+ ```
495
+
496
+ ## Error Testing
497
+
498
+ ### Testing Expected Failures
499
+
500
+ Use `Effect.flip` to convert failures to successes:
501
+
502
+ ```typescript
503
+ import { it, expect } from '@effect/vitest';
504
+ import { Effect, Schema } from 'effect';
505
+
506
+ class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()(
507
+ 'UserNotFoundError',
508
+ {
509
+ userId: Schema.String
510
+ }
511
+ ) {}
512
+
513
+ declare const failingOperation: () => Effect.Effect<never, UserNotFoundError>;
514
+
515
+ it.effect('should fail with error', () =>
516
+ Effect.gen(function* () {
517
+ const error = yield* Effect.flip(failingOperation());
518
+ expect(error).toBeInstanceOf(UserNotFoundError);
519
+ expect(error.userId).toBe('123');
520
+ })
521
+ );
522
+ ```
523
+
524
+ ### Testing with Exit
525
+
526
+ Use `Effect.exit` to capture both success and failure:
527
+
528
+ ```typescript
529
+ import { it, expect } from '@effect/vitest';
530
+ import { Effect, Exit } from 'effect';
531
+
532
+ declare const divide: (a: number, b: number) => Effect.Effect<number, string>;
533
+
534
+ it.effect('should handle success', () =>
535
+ Effect.gen(function* () {
536
+ const exit = yield* Effect.exit(divide(4, 2));
537
+ expect(exit).toEqual(Exit.succeed(2));
538
+ })
539
+ );
540
+
541
+ it.effect('should handle failure', () =>
542
+ Effect.gen(function* () {
543
+ const exit = yield* Effect.exit(divide(4, 0));
544
+ expect(exit).toEqual(Exit.fail('Cannot divide by zero'));
545
+ })
546
+ );
547
+ ```
548
+
549
+ ### Testing Error Types
550
+
551
+ ```typescript
552
+ import { it, expect } from '@effect/vitest';
553
+ import { Effect, Exit, Cause, Schema } from 'effect';
554
+ import * as Option from 'effect/Option';
555
+
556
+ class NotFoundError extends Schema.TaggedError<NotFoundError>()(
557
+ 'NotFoundError',
558
+ {
559
+ id: Schema.String
560
+ }
561
+ ) {}
562
+
563
+ class UserService extends Context.Service<
564
+ UserService,
565
+ {
566
+ getUser: (id: string) => Effect.Effect<unknown, NotFoundError>;
567
+ }
568
+ >()('UserService') {}
569
+
570
+ declare const userService: {
571
+ getUser: (id: string) => Effect.Effect<unknown, NotFoundError>;
572
+ };
573
+
574
+ it.effect('should fail with specific error', () =>
575
+ Effect.gen(function* () {
576
+ const exit = yield* Effect.exit(userService.getUser('nonexistent'));
577
+
578
+ if (Exit.isFailure(exit)) {
579
+ const cause = exit.cause;
580
+ // v4: use hasFails (not isFailType) and findErrorOption (not failureOrCause)
581
+ expect(Cause.hasFails(cause)).toBe(true);
582
+ const errorOpt = Cause.findErrorOption(cause);
583
+ expect(Option.isSome(errorOpt)).toBe(true);
584
+ if (Option.isSome(errorOpt)) {
585
+ expect(errorOpt.value).toBeInstanceOf(NotFoundError);
586
+ }
587
+ } else {
588
+ throw new Error('Expected failure');
589
+ }
590
+ })
591
+ );
592
+ ```
593
+
594
+ ## Property-Based Testing
595
+
596
+ ### Using it.prop for Pure Properties
597
+
598
+ ```typescript
599
+ import { FastCheck } from 'effect/testing';
600
+ import { it } from '@effect/vitest';
601
+
602
+ it.prop(
603
+ 'addition is commutative',
604
+ [FastCheck.integer(), FastCheck.integer()],
605
+ ([a, b]) => a + b === b + a
606
+ );
607
+
608
+ // With object syntax
609
+ it.prop(
610
+ 'multiplication distributes',
611
+ { a: FastCheck.integer(), b: FastCheck.integer(), c: FastCheck.integer() },
612
+ ({ a, b, c }) => a * (b + c) === a * b + a * c
613
+ );
614
+ ```
615
+
616
+ ### Using it.effect.prop for Effect Properties
617
+
618
+ ```typescript
619
+ import { it } from '@effect/vitest';
620
+ import { Effect, Context } from 'effect';
621
+ import { FastCheck } from 'effect/testing';
622
+
623
+ class Database extends Context.Service<
624
+ Database,
625
+ {
626
+ set: (key: string, value: number) => Effect.Effect<void>;
627
+ get: (key: string) => Effect.Effect<number>;
628
+ }
629
+ >()('Database') {}
630
+
631
+ it.effect.prop(
632
+ 'database operations are idempotent',
633
+ [FastCheck.string(), FastCheck.integer()],
634
+ ([key, value]) =>
635
+ Effect.gen(function* () {
636
+ const db = yield* Database;
637
+
638
+ yield* db.set(key, value);
639
+ const result1 = yield* db.get(key);
640
+
641
+ yield* db.set(key, value);
642
+ const result2 = yield* db.get(key);
643
+
644
+ return result1 === result2;
645
+ })
646
+ );
647
+ ```
648
+
649
+ ### With Schema Arbitraries
650
+
651
+ ```typescript
652
+ import { it, expect } from '@effect/vitest';
653
+ import { Effect, Schema } from 'effect';
654
+
655
+ const User = Schema.Struct({
656
+ id: Schema.String,
657
+ age: Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 120 }))
658
+ });
659
+
660
+ it.effect.prop('user validation works', { user: User }, ({ user }) =>
661
+ Effect.gen(function* () {
662
+ expect(user.age).toBeGreaterThanOrEqual(0);
663
+ expect(user.age).toBeLessThanOrEqual(120);
664
+ return true;
665
+ })
666
+ );
667
+ ```
668
+
669
+ `it.prop` and `it.effect.prop` accept schemas directly and derive their arbitraries internally. For manual use, beta.106 consolidated derivation into `Schema.toArbitrary(schema)`, which returns a factory that must receive the fast-check module:
670
+
671
+ ```typescript
672
+ import { Schema } from 'effect';
673
+ import { FastCheck } from 'effect/testing';
674
+
675
+ const UserArbitrary = Schema.toArbitrary(User)(FastCheck);
676
+ const samples = FastCheck.sample(UserArbitrary, 10);
677
+ ```
678
+
679
+ `Schema.toArbitraryLazy` and arbitrary derivation reports no longer exist.
680
+
681
+ ### Configuring FastCheck
682
+
683
+ ```typescript
684
+ import { it } from '@effect/vitest';
685
+ import { Effect } from 'effect';
686
+ import { FastCheck } from 'effect/testing';
687
+
688
+ it.effect.prop(
689
+ 'property test',
690
+ [FastCheck.integer()],
691
+ ([n]) => Effect.succeed(n >= 0 || n < 0),
692
+ {
693
+ timeout: 10000,
694
+ fastCheck: {
695
+ numRuns: 1000,
696
+ seed: 42,
697
+ verbose: true
698
+ }
699
+ }
700
+ );
701
+ ```
702
+
703
+ ## Test Control
704
+
705
+ ### Skipping Tests
706
+
707
+ ```typescript
708
+ import { it } from '@effect/vitest';
709
+ import { Effect } from 'effect';
710
+
711
+ declare const condition: boolean;
712
+
713
+ it.effect.skip('not ready yet', () =>
714
+ Effect.gen(function* () {
715
+ // Will not run
716
+ })
717
+ );
718
+
719
+ it.effect.skipIf(condition)('conditional skip', () =>
720
+ Effect.gen(function* () {
721
+ // Only runs if condition is false
722
+ })
723
+ );
724
+ ```
725
+
726
+ ### Running Single Tests
727
+
728
+ ```typescript
729
+ import { it } from '@effect/vitest';
730
+ import { Effect } from 'effect';
731
+
732
+ it.effect.only('debug this test', () =>
733
+ Effect.gen(function* () {
734
+ // Only this test runs
735
+ })
736
+ );
737
+ ```
738
+
739
+ ### Running Conditionally
740
+
741
+ ```typescript
742
+ import { it } from '@effect/vitest';
743
+ import { Effect } from 'effect';
744
+
745
+ it.effect.runIf(process.env.INTEGRATION_TESTS)('integration test', () =>
746
+ Effect.gen(function* () {
747
+ // Only runs if condition is true
748
+ })
749
+ );
750
+ ```
751
+
752
+ ### Expecting Failures
753
+
754
+ ```typescript
755
+ import { it, expect } from '@effect/vitest';
756
+ import { Effect } from 'effect';
757
+
758
+ it.effect.fails('known failing test', () =>
759
+ Effect.gen(function* () {
760
+ // This test is expected to fail
761
+ // Will pass if it fails, fail if it passes
762
+ expect(1).toBe(2);
763
+ })
764
+ );
765
+ ```
766
+
767
+ ## Testing Flaky Operations
768
+
769
+ Use `it.flakyTest` for operations that may fail intermittently:
770
+
771
+ ```typescript
772
+ import { it } from '@effect/vitest';
773
+ import { Effect, Random } from 'effect';
774
+
775
+ it.effect('retrying flaky operation', () =>
776
+ it.flakyTest(
777
+ Effect.gen(function* () {
778
+ const random = yield* Random.nextBoolean;
779
+ if (random) {
780
+ yield* Effect.fail('Random failure');
781
+ }
782
+ }),
783
+ '5 seconds' // Retry timeout
784
+ )
785
+ );
786
+ ```
787
+
788
+ ## Logging in Tests
789
+
790
+ ### Default Behavior (Suppressed)
791
+
792
+ ```typescript
793
+ import { it } from '@effect/vitest';
794
+ import { Effect } from 'effect';
795
+
796
+ it.effect('logs are suppressed', () =>
797
+ Effect.gen(function* () {
798
+ yield* Effect.log("This won't appear");
799
+ })
800
+ );
801
+ ```
802
+
803
+ ### Enabling Logs
804
+
805
+ ```typescript
806
+ import { it } from '@effect/vitest';
807
+ import { Effect, Logger } from 'effect';
808
+
809
+ it.effect('logs visible', () =>
810
+ Effect.gen(function* () {
811
+ yield* Effect.log('This will appear');
812
+ }).pipe(Effect.provide(Logger.pretty))
813
+ );
814
+
815
+ // Use it.live only when the live console itself is under test.
816
+ it.live('logs visible', () =>
817
+ Effect.gen(function* () {
818
+ yield* Effect.log('This will appear');
819
+ })
820
+ );
821
+ ```
822
+
823
+ ## Testing Patterns
824
+
825
+ ### Arrange-Act-Assert Pattern
826
+
827
+ ```typescript
828
+ import { describe, it, expect } from '@effect/vitest';
829
+ import { Effect, Context, Layer } from 'effect';
830
+
831
+ class UserService extends Context.Service<
832
+ UserService,
833
+ {
834
+ getUser: (id: string) => Effect.Effect<{ id: string; name: string }>;
835
+ }
836
+ >()('UserService') {}
837
+
838
+ declare const TestUserServiceLayer: Layer.Layer<UserService>;
839
+
840
+ describe('UserService', () => {
841
+ describe('getUser', () => {
842
+ it.effect('should return user by id', () =>
843
+ Effect.gen(function* () {
844
+ // Arrange
845
+ const userId = 'user-123';
846
+ const expectedUser = { id: userId, name: 'Alice' };
847
+
848
+ // Act
849
+ const service = yield* UserService;
850
+ const user = yield* service.getUser(userId);
851
+
852
+ // Assert
853
+ expect(user).toEqual(expectedUser);
854
+ }).pipe(Effect.provide(TestUserServiceLayer))
855
+ );
856
+ });
857
+ });
858
+ ```
859
+
860
+ ### Testing STM Operations
861
+
862
+ ```typescript
863
+ import { it, expect } from '@effect/vitest';
864
+ import { Effect, STM, TRef } from 'effect';
865
+
866
+ it.effect('should handle concurrent updates', () =>
867
+ Effect.gen(function* () {
868
+ const counter = yield* TRef.make(0);
869
+
870
+ const increment = STM.updateAndGet(counter, (n) => n + 1);
871
+
872
+ yield* STM.commit(increment);
873
+ yield* STM.commit(increment);
874
+
875
+ const final = yield* STM.commit(TRef.get(counter));
876
+ expect(final).toBe(2);
877
+ })
878
+ );
879
+ ```
880
+
881
+ ### Testing CRDT Operations
882
+
883
+ ```typescript
884
+ import { it, expect } from '@effect/vitest';
885
+ import { Effect, STM } from 'effect';
886
+
887
+ declare const GCounter: {
888
+ make: (id: string) => Effect.Effect<unknown>;
889
+ increment: (counter: unknown, value: number) => STM.STM<void>;
890
+ query: (counter: unknown) => STM.STM<unknown>;
891
+ merge: (counter: unknown, state: unknown) => STM.STM<void>;
892
+ value: (counter: unknown) => STM.STM<number>;
893
+ };
894
+
895
+ declare const ReplicaId: (id: string) => string;
896
+
897
+ it.effect('should merge states correctly', () =>
898
+ Effect.gen(function* () {
899
+ const counter1 = yield* GCounter.make(ReplicaId('replica-1'));
900
+ const counter2 = yield* GCounter.make(ReplicaId('replica-2'));
901
+
902
+ yield* STM.commit(GCounter.increment(counter1, 10));
903
+ yield* STM.commit(GCounter.increment(counter2, 20));
904
+
905
+ const state2 = yield* STM.commit(GCounter.query(counter2));
906
+ yield* STM.commit(GCounter.merge(counter1, state2));
907
+
908
+ const result = yield* STM.commit(GCounter.value(counter1));
909
+ expect(result).toBe(30);
910
+ })
911
+ );
912
+ ```
913
+
914
+ ## HTTP Mock Server Testing
915
+
916
+ For services that speak HTTP protocols (REST, SSE, streaming), **prefer HTTP mock server testing over service-level fakes**. This approach tests the full HTTP integration path — serialization, status codes, retries, SSE framing — and catches bugs that service fakes miss.
917
+
918
+ **When to use HTTP mock servers vs service fakes:**
919
+
920
+ - **HTTP mock server** (preferred): when the service communicates over HTTP/SSE and transport-level correctness matters. The real service layer (`MyService.defaultLayer`) is used, backed by a mock HTTP server at port 0.
921
+ - **Service fake** (`Layer.succeed`): when the service is a pure domain abstraction with no protocol-level concerns.
922
+
923
+ The pattern:
924
+
925
+ 1. **Define a `TestServer` service** as a `Context.Service` with semantic helper methods (not raw `push(reply)`). For an LLM server, expose `text(content)`, `tool(call)`, `fail(error)`, `hang`, `hold(promise)`. Each method pushes a typed Step onto a queue.
926
+ 2. **Implement with `Layer.effect`** — build an `HttpRouter` that dequeues steps on each request, use `HttpServerResponse.stream` for SSE endpoints, and bind to a random port with `NodeHttpServer.layer(() => Http.createServer(), { port: 0 })`.
927
+ 3. **Use `Deferred`-based request counting** for `wait(count)` — the server increments a counter on each request and completes a `Deferred` when the count is reached, letting the test block until the expected number of calls arrive. This eliminates all `setTimeout`/polling from tests.
928
+ 4. **Wire the mock URL into test config** via a callback: `config: (url) => ({ baseUrl: url })`.
929
+
930
+ ### Typed Step ADT for Mock Responses
931
+
932
+ Define mock response types as a discriminated union so test intent is readable:
933
+
934
+ ```typescript
935
+ type Step =
936
+ | { readonly kind: 'text'; readonly content: string }
937
+ | { readonly kind: 'tool'; readonly call: ToolCall }
938
+ | { readonly kind: 'fail'; readonly error: string }
939
+ | { readonly kind: 'hang' } // Keeps connection open indefinitely
940
+ | { readonly kind: 'hold'; readonly wait: Promise<void> }; // Blocks until resolved
941
+
942
+ // Semantic helpers on the TestServer service:
943
+ // yield* server.text("hello") — enqueue a text response
944
+ // yield* server.tool(toolCall) — enqueue a tool call response
945
+ // yield* server.fail("error") — enqueue a mid-stream SSE failure
946
+ // server.hang — enqueue a response that never completes
947
+ ```
948
+
949
+ ### SSE Response Patterns
950
+
951
+ For SSE endpoints, use `HttpServerResponse.stream` with different `Stream` constructors per step type:
952
+
953
+ ```typescript
954
+ import { Effect, Stream } from 'effect';
955
+ import { HttpServerResponse } from 'effect/unstable/http';
956
+
957
+ // Normal SSE response — stream JSON lines, then [DONE]
958
+ const sse = (lines: ReadonlyArray<unknown>) =>
959
+ HttpServerResponse.stream(
960
+ Stream.fromIterable([
961
+ [
962
+ ...lines.map((line) => `data: ${JSON.stringify(line)}`),
963
+ 'data: [DONE]'
964
+ ].join('\n\n') + '\n\n'
965
+ ]).pipe(Stream.encodeText),
966
+ { contentType: 'text/event-stream' }
967
+ );
968
+
969
+ // Hang — connection stays open forever (for testing cancellation)
970
+ const hang = HttpServerResponse.stream(Stream.never, {
971
+ contentType: 'text/event-stream'
972
+ });
973
+
974
+ // Mid-stream failure — partial data then error
975
+ const fail = (error: string) =>
976
+ HttpServerResponse.stream(
977
+ Stream.concat(
978
+ Stream.fromIterable([`data: {"partial": true}\n\n`]),
979
+ Stream.fail(new Error(error))
980
+ ).pipe(Stream.encodeText),
981
+ { contentType: 'text/event-stream' }
982
+ );
983
+
984
+ // Hold — blocks until a promise resolves (for testing timing)
985
+ const hold = (wait: Promise<void>) =>
986
+ HttpServerResponse.stream(
987
+ Stream.fromEffect(Effect.promise(() => wait)).pipe(
988
+ Stream.flatMap(() => Stream.fromIterable(['data: [DONE]\n\n'])),
989
+ Stream.encodeText
990
+ ),
991
+ { contentType: 'text/event-stream' }
992
+ );
993
+ ```
994
+
995
+ ### Test Fixture Composition
996
+
997
+ ```typescript
998
+ // Test fixture composition
999
+ const withMockServer = <A, E, R>(
1000
+ self: (server: TestApiServer) => Effect.Effect<A, E, R>
1001
+ ) =>
1002
+ Effect.gen(function* () {
1003
+ const server = yield* TestApiServer;
1004
+ return yield* self(server);
1005
+ });
1006
+
1007
+ // In test — uses the REAL service layer, backed by mock HTTP
1008
+ it.live('calls API correctly', () =>
1009
+ withMockServer((server) =>
1010
+ Effect.gen(function* () {
1011
+ yield* server.text('hello world');
1012
+ const result = yield* MyService.use((svc) => svc.callApi());
1013
+ expect(result).toEqual('hello world');
1014
+ })
1015
+ ).pipe(
1016
+ Effect.provide(MyService.defaultLayer), // Real service, not a fake
1017
+ Effect.provide(TestApiServer.layer)
1018
+ )
1019
+ );
1020
+ ```
1021
+
1022
+ ### Test Fake Factories
1023
+
1024
+ When providing fake service layers in tests, return the test data alongside the layer to avoid duplicating constants between setup and assertions:
1025
+
1026
+ ```typescript
1027
+ export function fakeUserRepo(overrides?: { user?: User }) {
1028
+ const user = overrides?.user ?? new User({ id: '1', name: 'Test' });
1029
+ return {
1030
+ user,
1031
+ layer: Layer.succeed(
1032
+ UserRepo,
1033
+ UserRepo.of({
1034
+ findById: Effect.fn('TestUserRepo.findById')(function* (
1035
+ id: string
1036
+ ) {
1037
+ if (id === user.id) return user;
1038
+ return yield* Effect.die(
1039
+ new Error(`Unknown test user: ${id}`)
1040
+ );
1041
+ })
1042
+ })
1043
+ )
1044
+ };
1045
+ }
1046
+
1047
+ // Usage in test:
1048
+ const { user, layer } = fakeUserRepo();
1049
+ // assert against `user` values, provide `layer`
1050
+ ```
1051
+
1052
+ > Returning test data alongside the layer avoids duplicating constants between test setup and assertions.
1053
+
1054
+ ## Lifecycle State-Machine Fakes
1055
+
1056
+ For long-lived services such as connection managers, runners, registries, or background sync loops, prefer explicit controllable test doubles over broad end-to-end flows when transport-level correctness is not the thing under test.
1057
+
1058
+ Model the fake as a small state machine with semantic controls:
1059
+
1060
+ - counters for call counts
1061
+ - explicit transition methods like `disconnect()`, `reloadTools()`, `failNext()`
1062
+ - `Deferred` values for blocking and release points
1063
+ - direct assertions against cache/status transitions after each step
1064
+
1065
+ This keeps race and lifecycle tests fast, deterministic, and reviewable.
1066
+
1067
+ ## Instance-Scoped Harness Tests
1068
+
1069
+ When testing tools or services that depend on instance-local context or `InstanceState`, prefer the real layer graph and a real instance/test harness over ad hoc module wrappers.
1070
+
1071
+ ```typescript
1072
+ const layer = Layer.mergeAll(
1073
+ MyTool.defaultLayer,
1074
+ Instruction.defaultLayer,
1075
+ OtherDependency.defaultLayer
1076
+ );
1077
+
1078
+ it.live('runs with the real harness', () =>
1079
+ withTestInstance((dir) =>
1080
+ Effect.gen(function* () {
1081
+ const tool = yield* MyTool.Service;
1082
+ yield* tool.run(dir);
1083
+ })
1084
+ ).pipe(Effect.provide(layer))
1085
+ );
1086
+ ```
1087
+
1088
+ Use fake services when you are isolating pure domain behavior. Use the real harness when correctness depends on instance context, layer composition, or production orchestration.
1089
+
1090
+ ## Interrupt Tests Should Prove Cleanup
1091
+
1092
+ If interruption is part of the contract, do not stop at asserting that the fiber was interrupted. Assert the cleanup effect too:
1093
+
1094
+ - busy/idle status reset
1095
+ - pending work marked aborted/cancelled
1096
+ - finalizers or teardown callbacks ran
1097
+ - replacement work can start without a second manual cleanup call
1098
+
1099
+ ## Testing with Non-Vitest Runners (bun:test)
1100
+
1101
+ When using `bun:test` or other non-vitest runners, `@effect/vitest`'s `it.effect`, `it.live`, and `layer()` helpers are unavailable. Build a custom test harness that replicates the same semantics:
1102
+
1103
+ ```typescript
1104
+ import { Effect, Layer } from 'effect';
1105
+ import { TestClock, TestConsole } from 'effect/testing';
1106
+ import { describe, it } from 'bun:test';
1107
+
1108
+ // Two layer stacks: one with TestClock, one without
1109
+ const testEnv = Layer.mergeAll(TestConsole.layer, TestClock.layer());
1110
+ const liveEnv = TestConsole.layer;
1111
+
1112
+ export const testEffect = <R, E>(layer: Layer.Layer<R, E>) => {
1113
+ const testLayer = Layer.provideMerge(layer, testEnv);
1114
+ const liveLayer = Layer.provideMerge(layer, liveEnv);
1115
+
1116
+ return {
1117
+ effect: (name: string, fn: () => Effect.Effect<void, unknown, R>) =>
1118
+ it(name, () =>
1119
+ Effect.runPromise(fn().pipe(Effect.provide(testLayer)))
1120
+ ),
1121
+
1122
+ live: (name: string, fn: () => Effect.Effect<void, unknown, R>) =>
1123
+ it(name, () =>
1124
+ Effect.runPromise(fn().pipe(Effect.provide(liveLayer)))
1125
+ )
1126
+ };
1127
+ };
1128
+
1129
+ // Usage:
1130
+ const deps = Layer.mergeAll(MyService.defaultLayer, OtherService.defaultLayer);
1131
+ const it = testEffect(deps);
1132
+
1133
+ describe('MyService', () => {
1134
+ it.live('handles request', () =>
1135
+ Effect.gen(function* () {
1136
+ const svc = yield* MyService.Service;
1137
+ const result = yield* svc.handle('input');
1138
+ expect(result).toBe('expected');
1139
+ })
1140
+ );
1141
+ });
1142
+ ```
1143
+
1144
+ Key differences from `@effect/vitest`:
1145
+
1146
+ - Use `Effect.runPromise` manually — bun:test expects `Promise<void>` from async tests
1147
+ - Layer composition uses `Layer.provideMerge` here because the harness intentionally exposes both application and test services; do not use it blindly
1148
+ - Keep `effect` as the default; use `live` only when real time or live runtime services are under test
1149
+
1150
+ **`TestClock` without an ambient `Scope` (beta.70):** earlier betas required a surrounding `Scope` for `TestClock.adjust` to advance time. As of beta.70, `TestClock.layer()` works when provided directly to a program run with `Effect.runPromise` (no ambient `Scope`) — which is exactly what the harness above relies on. `testEffect(...).effect(...)` merges `TestClock.layer()` into the layer stack and runs with `Effect.runPromise`, and `TestClock.adjust` still drives time correctly.
1151
+
1152
+ ## Testing Checklist
1153
+
1154
+ Before completing a testing task, verify:
1155
+
1156
+ - [ ] Correct framework chosen (@effect/vitest vs vitest vs bun:test harness)
1157
+ - [ ] Test variant appropriate — `it.effect` by default; `it.live` only for explicitly live runtime behavior
1158
+ - [ ] Services provided via layers when needed
1159
+ - [ ] HTTP-speaking services tested with HTTP mock server, not service fakes
1160
+ - [ ] TestClock used only for tests that explicitly need time simulation
1161
+ - [ ] Errors tested with Effect.flip or Effect.exit
1162
+ - [ ] Edge cases covered
1163
+ - [ ] Property-based tests for general properties
1164
+ - [ ] Tests are deterministic (no polling/setTimeout — use Deferred-based synchronization)
1165
+ - [ ] Interrupt tests assert resulting cleanup state, not just interruption itself
1166
+ - [ ] Test names describe behavior clearly
1167
+ - [ ] Resources properly scoped and cleaned up
1168
+ - [ ] All tests pass
1169
+
1170
+ ## Common Pitfalls
1171
+
1172
+ ### Assertion Style
1173
+
1174
+ Effect v4 canonically uses `import { assert } from "@effect/vitest"` with methods like `assert.deepStrictEqual`, `assert.strictEqual`, and `assert.isTrue`. The `expect` API from vitest is still available and works fine. Pick one style and stay consistent within a test file.
1175
+
1176
+ ```typescript
1177
+ // ✅ Option A - assert style (canonical v4)
1178
+ import { it, assert } from '@effect/vitest';
1179
+ import { Effect } from 'effect';
1180
+
1181
+ declare const result: unknown;
1182
+ declare const expected: unknown;
1183
+
1184
+ it.effect('test', () =>
1185
+ Effect.gen(function* () {
1186
+ assert.strictEqual(result, expected);
1187
+ assert.deepStrictEqual(result, { id: '123' });
1188
+ assert.isTrue(true);
1189
+ })
1190
+ );
1191
+
1192
+ // ✅ Option B - expect style (still works)
1193
+ import { it, expect } from '@effect/vitest';
1194
+
1195
+ it.effect('test', () =>
1196
+ Effect.gen(function* () {
1197
+ expect(result).toBe(expected);
1198
+ })
1199
+ );
1200
+ ```
1201
+
1202
+ ### Don't Forget to Fork for TestClock
1203
+
1204
+ ```typescript
1205
+ import { it } from '@effect/vitest';
1206
+ import { Effect, Fiber } from 'effect';
1207
+ import { TestClock } from 'effect/testing';
1208
+
1209
+ // ❌ Wrong - will hang waiting for real time
1210
+ it.effect('test', () =>
1211
+ Effect.gen(function* () {
1212
+ yield* Effect.sleep('5 seconds'); // Blocks!
1213
+ yield* TestClock.adjust('5 seconds');
1214
+ })
1215
+ );
1216
+
1217
+ // ✅ Correct - fork the effect
1218
+ it.effect('test', () =>
1219
+ Effect.gen(function* () {
1220
+ const fiber = yield* Effect.forkChild(Effect.sleep('5 seconds'));
1221
+ yield* TestClock.adjust('5 seconds');
1222
+ yield* Fiber.join(fiber);
1223
+ })
1224
+ );
1225
+ ```
1226
+
1227
+ ### Provide Layers to Effect, Not Test
1228
+
1229
+ ```typescript
1230
+ import { it, expect } from '@effect/vitest';
1231
+ import { Effect, Layer } from 'effect';
1232
+
1233
+ declare const someEffect: Effect.Effect<number>;
1234
+ declare const expected: number;
1235
+ declare const layer: Layer.Layer<never>;
1236
+
1237
+ // ❌ Wrong - providing to wrong level
1238
+ it.effect('test', () =>
1239
+ Effect.gen(function* () {
1240
+ const result = yield* someEffect;
1241
+ expect(result).toBe(expected);
1242
+ })
1243
+ ); // ❌ Can't provide to test function
1244
+ // .pipe(Effect.provide(layer))
1245
+
1246
+ // ✅ Correct - provide to Effect
1247
+ it.effect(
1248
+ 'test',
1249
+ () =>
1250
+ Effect.gen(function* () {
1251
+ const result = yield* someEffect;
1252
+ expect(result).toBe(expected);
1253
+ }).pipe(Effect.provide(layer)) // ✅ Provide to Effect
1254
+ );
1255
+ ```
1256
+
1257
+ ## Running Tests
1258
+
1259
+ ```bash
1260
+ # Run all tests
1261
+ bun run test
1262
+
1263
+ # Run specific file
1264
+ bunx vitest run path/to/file.test.ts
1265
+
1266
+ # Full check (format + lint + typecheck + test)
1267
+ bun run check && bun run test
1268
+ ```
1269
+
1270
+ ## Example: Complete Test Suite
1271
+
1272
+ ```typescript
1273
+ import { describe, expect, it, layer } from '@effect/vitest';
1274
+ import { Effect, Context, Layer, Exit } from 'effect';
1275
+
1276
+ // Service definition
1277
+ class Counter extends Context.Service<
1278
+ Counter,
1279
+ {
1280
+ increment: () => Effect.Effect<void>;
1281
+ value: () => Effect.Effect<number>;
1282
+ }
1283
+ >()('Counter') {
1284
+ static Live = Layer.effect(
1285
+ Counter,
1286
+ Effect.gen(function* () {
1287
+ let count = 0;
1288
+ return {
1289
+ increment: () =>
1290
+ Effect.sync(() => {
1291
+ count++;
1292
+ }),
1293
+ value: () => Effect.succeed(count)
1294
+ };
1295
+ })
1296
+ );
1297
+ }
1298
+
1299
+ // Tests
1300
+ layer(Counter.Live)('Counter', (it) => {
1301
+ it.effect('should start at 0', () =>
1302
+ Effect.gen(function* () {
1303
+ const counter = yield* Counter;
1304
+ const value = yield* counter.value();
1305
+ expect(value).toBe(0);
1306
+ })
1307
+ );
1308
+
1309
+ it.effect('should increment', () =>
1310
+ Effect.gen(function* () {
1311
+ const counter = yield* Counter;
1312
+ yield* counter.increment();
1313
+ const value = yield* counter.value();
1314
+ expect(value).toBe(1);
1315
+ })
1316
+ );
1317
+
1318
+ it.effect('should handle multiple increments', () =>
1319
+ Effect.gen(function* () {
1320
+ const counter = yield* Counter;
1321
+ yield* counter.increment();
1322
+ yield* counter.increment();
1323
+ yield* counter.increment();
1324
+ const value = yield* counter.value();
1325
+ expect(value).toBe(3);
1326
+ })
1327
+ );
1328
+ });
1329
+ ```
1330
+
1331
+ This skill ensures comprehensive, reliable testing of Effect-based applications following best practices.