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,612 @@
1
+ ---
2
+ name: effect-concurrency-testing
3
+ description: Test Effect concurrency primitives including PubSub, Deferred, Latch, Fiber coordination, SubscriptionRef, and Stream. Use this skill when testing concurrent effects, event-driven systems, or fiber coordination.
4
+ ---
5
+
6
+ # Effect Concurrency Testing Skill
7
+
8
+ This skill provides patterns for testing Effect's concurrency primitives: fibers, latches, deferreds, PubSub, SubscriptionRef, and streams.
9
+
10
+ ## Core Principles
11
+
12
+ **CRITICAL**: Choose the correct coordination primitive based on what you need to synchronize.
13
+
14
+ | Need | Use |
15
+ | --------------------------------- | ------------------------------------------------------------------------------------- |
16
+ | Simple fiber yield | `Effect.yieldNow` |
17
+ | Wait for subscriber ready | `Deferred.make()` + `Deferred.await` |
18
+ | Wait for stream element | `Latch.make()` + `Stream.tap(() => latch.open)` |
19
+ | Passive subscription registration | explicit readiness signal if possible; otherwise a tiny one-tick yield/sleep fallback |
20
+ | Time-dependent behavior | `TestClock.adjust` |
21
+ | Verify events published | `PubSub.subscribe` + `PubSub.takeUpTo` |
22
+ | Check fiber status | `fiber.pollUnsafe()` |
23
+
24
+ For `Stream.fromPubSub` subscription registration, prefer an explicit readiness signal when you control the stream. If the API offers no readiness hook and you only need registration to settle before publishing, a tiny `Effect.yieldNow()` or very short sleep is an acceptable last resort. Avoid broad polling or arbitrary delays.
25
+
26
+ ## Fiber Coordination Patterns
27
+
28
+ ### Effect.yieldNow - Simple Fiber Scheduling
29
+
30
+ Use `Effect.yieldNow` when you need to allow other fibers to execute. This is preferred over `TestClock.adjust` for non-time-dependent code.
31
+
32
+ ```typescript
33
+ import { it } from '@effect/vitest';
34
+ import { Effect, Exit, Fiber, Latch } from 'effect';
35
+
36
+ it.effect('fiber polling with yieldNow', () =>
37
+ Effect.gen(function* () {
38
+ const latch = yield* Latch.make();
39
+
40
+ const fiber = yield* latch.await.pipe(Effect.forkChild);
41
+
42
+ yield* Effect.yieldNow();
43
+
44
+ expect(fiber.pollUnsafe()).toBeUndefined();
45
+
46
+ yield* latch.open;
47
+
48
+ expect(yield* fiber.await).toEqual(Exit.void);
49
+ })
50
+ );
51
+ ```
52
+
53
+ ### Latch - Explicit Coordination
54
+
55
+ `Latch.make()` creates a gate that blocks fibers until opened:
56
+
57
+ ```typescript
58
+ import { it } from '@effect/vitest';
59
+ import { Effect, Fiber, Latch } from 'effect';
60
+
61
+ it.effect('latch coordination', () =>
62
+ Effect.gen(function* () {
63
+ const latch = yield* Latch.make();
64
+
65
+ const fiber = yield* Effect.gen(function* () {
66
+ yield* latch.await;
67
+ return 'completed';
68
+ }).pipe(Effect.forkChild);
69
+
70
+ yield* Effect.yieldNow();
71
+ expect(fiber.pollUnsafe()).toBeUndefined();
72
+
73
+ yield* latch.open;
74
+
75
+ const result = yield* Fiber.join(fiber);
76
+ expect(result).toBe('completed');
77
+ })
78
+ );
79
+ ```
80
+
81
+ ### Latch Operations
82
+
83
+ ```typescript
84
+ import { Latch } from 'effect';
85
+
86
+ declare const latch: Latch.Latch;
87
+
88
+ latch.await; // Wait until latch is open
89
+ latch.open; // Open the latch (current and future waiters proceed)
90
+ latch.close; // Close the latch (future waiters suspend again)
91
+ latch.release; // Wake current waiters only; latch stays closed for future waiters
92
+ latch.whenOpen; // Run effect only when latch is open
93
+ ```
94
+
95
+ ### Deferred - Signal Readiness Between Fibers
96
+
97
+ Use `Deferred` when one fiber needs to signal another with a value:
98
+
99
+ ```typescript
100
+ import { it } from '@effect/vitest';
101
+ import { Effect, Deferred, Fiber } from 'effect';
102
+
103
+ it.effect('deferred signaling', () =>
104
+ Effect.gen(function* () {
105
+ const signal = yield* Deferred.make<number>();
106
+
107
+ const consumer = yield* Effect.gen(function* () {
108
+ const value = yield* Deferred.await(signal);
109
+ return value * 2;
110
+ }).pipe(Effect.forkChild);
111
+
112
+ yield* Deferred.succeed(signal, 21);
113
+
114
+ const result = yield* Fiber.join(consumer);
115
+ expect(result).toBe(42);
116
+ })
117
+ );
118
+ ```
119
+
120
+ ### fiber.pollUnsafe() - Check Completion Without Blocking
121
+
122
+ ```typescript
123
+ import { Exit, Fiber } from 'effect';
124
+
125
+ declare const fiber: Fiber.Fiber<string>;
126
+
127
+ fiber.pollUnsafe();
128
+ // Returns undefined if running
129
+ // Returns Exit<A, E> if completed (success, failure, or interrupted)
130
+
131
+ // Check if still running
132
+ expect(fiber.pollUnsafe()).toBeUndefined();
133
+
134
+ // Check if completed
135
+ expect(fiber.pollUnsafe()).toBeDefined();
136
+
137
+ // Check specific completion
138
+ expect(fiber.pollUnsafe()).toEqual(Exit.succeed('result'));
139
+ ```
140
+
141
+ ## PubSub Event Testing
142
+
143
+ ### Direct Event Verification
144
+
145
+ Use `Effect.scoped` to manage PubSub subscription lifecycle:
146
+
147
+ ```typescript
148
+ import { it } from '@effect/vitest';
149
+ import { Effect, PubSub } from 'effect';
150
+
151
+ it.effect('verify published events', () =>
152
+ Effect.gen(function* () {
153
+ const pubsub = yield* PubSub.unbounded<string>();
154
+
155
+ yield* Effect.scoped(
156
+ Effect.gen(function* () {
157
+ const sub = yield* PubSub.subscribe(pubsub);
158
+
159
+ yield* PubSub.publish(pubsub, 'event-1');
160
+ yield* PubSub.publish(pubsub, 'event-2');
161
+
162
+ const events = yield* PubSub.takeAll(sub);
163
+
164
+ expect(events).toEqual(['event-1', 'event-2']);
165
+ })
166
+ );
167
+ })
168
+ );
169
+ ```
170
+
171
+ Both events are published **before** `PubSub.takeAll`, so it returns immediately. `PubSub.takeAll` suspends when the subscription is empty and always returns a `NonEmptyArray`; for non-blocking drains or “no more events” assertions, use `PubSub.takeUpTo(sub, n)` instead, which returns whatever is buffered (possibly an empty array).
172
+
173
+ ### Testing Event Publishers
174
+
175
+ When testing a service that publishes events:
176
+
177
+ ```typescript
178
+ import { it } from '@effect/vitest';
179
+ import { Effect, PubSub, Context, Layer } from 'effect';
180
+
181
+ interface UserEvent {
182
+ readonly type: 'created' | 'deleted';
183
+ readonly userId: string;
184
+ }
185
+
186
+ class EventBus extends Context.Service<EventBus, PubSub.PubSub<UserEvent>>()(
187
+ 'EventBus'
188
+ ) {}
189
+
190
+ class UserService extends Context.Service<
191
+ UserService,
192
+ { readonly createUser: (id: string) => Effect.Effect<void> }
193
+ >()('UserService') {}
194
+
195
+ declare const UserServiceLive: Layer.Layer<UserService, never, EventBus>;
196
+
197
+ it.effect('should publish user created event', () =>
198
+ Effect.gen(function* () {
199
+ const pubsub = yield* PubSub.unbounded<UserEvent>();
200
+
201
+ yield* Effect.scoped(
202
+ Effect.gen(function* () {
203
+ const sub = yield* PubSub.subscribe(pubsub);
204
+
205
+ const service = yield* UserService;
206
+ yield* service.createUser('user-123');
207
+
208
+ const events = yield* PubSub.takeAll(sub);
209
+
210
+ expect(events).toHaveLength(1);
211
+ expect(events[0]).toEqual({
212
+ type: 'created',
213
+ userId: 'user-123'
214
+ });
215
+ })
216
+ );
217
+ }).pipe(
218
+ Effect.provide(UserServiceLive),
219
+ Effect.provide(Layer.succeed(EventBus, pubsub))
220
+ )
221
+ );
222
+ ```
223
+
224
+ ### Concurrent Publisher/Subscriber Testing
225
+
226
+ ```typescript
227
+ import { it } from '@effect/vitest';
228
+ import { Deferred, Effect, PubSub, Fiber, Latch, Array as A } from 'effect';
229
+
230
+ it.effect('concurrent publishers and subscribers', () =>
231
+ Effect.gen(function* () {
232
+ const values = A.range(0, 9);
233
+ const latch = yield* Latch.make();
234
+ const ready = yield* Deferred.make<void>();
235
+ const pubsub = yield* PubSub.bounded<number>(10);
236
+
237
+ const subscriber = yield* PubSub.subscribe(pubsub).pipe(
238
+ Effect.flatMap((sub) =>
239
+ Effect.gen(function* () {
240
+ // Signal that the subscription is registered before publishing
241
+ yield* Deferred.succeed(ready, undefined);
242
+ yield* latch.await;
243
+ return yield* Effect.forEach(values, () =>
244
+ PubSub.take(sub)
245
+ );
246
+ })
247
+ ),
248
+ Effect.scoped,
249
+ Effect.forkScoped
250
+ );
251
+
252
+ // Wait until the subscriber has actually subscribed (PubSub is not
253
+ // an event log — publishing before subscription would drop messages)
254
+ yield* Deferred.await(ready);
255
+
256
+ yield* PubSub.publishAll(pubsub, values);
257
+ yield* latch.open;
258
+
259
+ const result = yield* Fiber.join(subscriber);
260
+ expect(result).toEqual(values);
261
+ })
262
+ );
263
+ ```
264
+
265
+ ## SubscriptionRef Testing
266
+
267
+ ### Testing Stream Changes with Latches
268
+
269
+ The latch pattern ensures the stream subscription is ready before mutations:
270
+
271
+ ```typescript
272
+ import { it } from '@effect/vitest';
273
+ import { Effect, Fiber, Latch, Number } from 'effect';
274
+ import { Stream, SubscriptionRef } from 'effect';
275
+
276
+ it.effect('multiple subscribers can receive changes', () =>
277
+ Effect.gen(function* () {
278
+ const ref = yield* SubscriptionRef.make(0);
279
+ const latch1 = yield* Latch.make();
280
+ const latch2 = yield* Latch.make();
281
+
282
+ const fiber1 = yield* SubscriptionRef.changes(ref).pipe(
283
+ Stream.tap(() => latch1.open),
284
+ Stream.take(3),
285
+ Stream.runCollect,
286
+ Effect.forkScoped
287
+ );
288
+
289
+ yield* latch1.await;
290
+ yield* SubscriptionRef.update(ref, Number.increment);
291
+
292
+ const fiber2 = yield* SubscriptionRef.changes(ref).pipe(
293
+ Stream.tap(() => latch2.open),
294
+ Stream.take(2),
295
+ Stream.runCollect,
296
+ Effect.forkScoped
297
+ );
298
+
299
+ yield* latch2.await;
300
+ yield* SubscriptionRef.update(ref, Number.increment);
301
+
302
+ const result1 = yield* Fiber.join(fiber1);
303
+ const result2 = yield* Fiber.join(fiber2);
304
+
305
+ expect(result1).toEqual([0, 1, 2]);
306
+ expect(result2).toEqual([1, 2]);
307
+ })
308
+ );
309
+ ```
310
+
311
+ ### Testing Subscription Interruption
312
+
313
+ ```typescript
314
+ import { it } from '@effect/vitest';
315
+ import { Effect, Exit, Fiber, Latch, Number } from 'effect';
316
+ import { Pull, Stream, SubscriptionRef } from 'effect';
317
+
318
+ it.effect('subscriptions are interruptible', () =>
319
+ Effect.gen(function* () {
320
+ const ref = yield* SubscriptionRef.make(0);
321
+ const latch = yield* Latch.make();
322
+
323
+ const fiber = yield* SubscriptionRef.changes(ref).pipe(
324
+ Stream.tap(() => latch.open),
325
+ Stream.take(10),
326
+ Stream.runCollect,
327
+ Effect.forkScoped
328
+ );
329
+
330
+ yield* latch.await;
331
+ yield* SubscriptionRef.update(ref, Number.increment);
332
+ yield* Fiber.interrupt(fiber);
333
+
334
+ const result = yield* Fiber.await(fiber);
335
+
336
+ expect(Exit.isFailure(result) && Pull.isDoneCause(result.cause)).toBe(
337
+ true
338
+ );
339
+ })
340
+ );
341
+ ```
342
+
343
+ ## Stream Testing
344
+
345
+ ### Collecting Stream Results
346
+
347
+ ```typescript
348
+ import { it } from '@effect/vitest';
349
+ import { Effect } from 'effect';
350
+ import { Stream } from 'effect';
351
+
352
+ it.effect('should collect stream elements', () =>
353
+ Effect.gen(function* () {
354
+ const result = yield* Stream.make(1, 2, 3, 4, 5).pipe(
355
+ Stream.filter((n) => n % 2 === 0),
356
+ Stream.runCollect
357
+ );
358
+
359
+ expect(result).toEqual([2, 4]);
360
+ })
361
+ );
362
+ ```
363
+
364
+ ### Testing Stream Side Effects
365
+
366
+ ```typescript
367
+ import { it } from '@effect/vitest';
368
+ import { Effect, Ref } from 'effect';
369
+ import { Stream } from 'effect';
370
+
371
+ it.effect('should track side effects', () =>
372
+ Effect.gen(function* () {
373
+ const log = yield* Ref.make<string[]>([]);
374
+
375
+ yield* Stream.make('a', 'b', 'c').pipe(
376
+ Stream.tap((item) => Ref.update(log, (items) => [...items, item])),
377
+ Stream.runDrain
378
+ );
379
+
380
+ const logged = yield* Ref.get(log);
381
+ expect(logged).toEqual(['a', 'b', 'c']);
382
+ })
383
+ );
384
+ ```
385
+
386
+ ### Testing Stream Errors
387
+
388
+ ```typescript
389
+ import { it } from '@effect/vitest';
390
+ import { Effect, Exit, Schema } from 'effect';
391
+ import { Stream } from 'effect';
392
+
393
+ class StreamError extends Schema.TaggedError<StreamError>()(
394
+ 'StreamError',
395
+ {
396
+ message: Schema.String
397
+ }
398
+ ) {}
399
+
400
+ it.effect('should handle stream errors', () =>
401
+ Effect.gen(function* () {
402
+ const result = yield* Stream.make(1, 2, 3).pipe(
403
+ Stream.mapEffect((n) =>
404
+ n === 2
405
+ ? Effect.fail(new StreamError({ message: 'boom' }))
406
+ : Effect.succeed(n)
407
+ ),
408
+ Stream.runCollect,
409
+ Effect.exit
410
+ );
411
+
412
+ expect(Exit.isFailure(result)).toBe(true);
413
+ })
414
+ );
415
+ ```
416
+
417
+ ### Testing Stream Finalization
418
+
419
+ ```typescript
420
+ import { it } from '@effect/vitest';
421
+ import { Effect, Ref } from 'effect';
422
+ import { Stream } from 'effect';
423
+
424
+ it.effect('should run finalizers', () =>
425
+ Effect.gen(function* () {
426
+ const finalized = yield* Ref.make(false);
427
+
428
+ yield* Stream.make(1, 2, 3).pipe(
429
+ Stream.ensuring(Ref.set(finalized, true)),
430
+ Stream.take(1),
431
+ Stream.runDrain
432
+ );
433
+
434
+ expect(yield* Ref.get(finalized)).toBe(true);
435
+ })
436
+ );
437
+ ```
438
+
439
+ ## Interruption Testing
440
+
441
+ ### Testing Fiber Interruption
442
+
443
+ ```typescript
444
+ import { it } from '@effect/vitest';
445
+ import { Effect, Exit, Fiber, Cause } from 'effect';
446
+
447
+ it.effect('should handle interruption', () =>
448
+ Effect.gen(function* () {
449
+ const fiber = yield* Effect.never.pipe(Effect.forkChild);
450
+
451
+ yield* Fiber.interrupt(fiber);
452
+
453
+ const result = yield* Fiber.await(fiber);
454
+
455
+ expect(Exit.hasInterrupts(result)).toBe(true);
456
+ })
457
+ );
458
+ ```
459
+
460
+ ### Testing Interrupted-Only Cause
461
+
462
+ ```typescript
463
+ import { it } from '@effect/vitest';
464
+ import { Effect, Exit, Fiber, Cause } from 'effect';
465
+
466
+ it.effect('should have interrupted-only cause', () =>
467
+ Effect.gen(function* () {
468
+ const fiber = yield* Effect.never.pipe(Effect.forkChild);
469
+
470
+ yield* Fiber.interrupt(fiber);
471
+
472
+ const result = yield* Fiber.await(fiber);
473
+
474
+ expect(
475
+ Exit.isFailure(result) && Cause.hasInterruptsOnly(result.cause)
476
+ ).toBe(true);
477
+ })
478
+ );
479
+ ```
480
+
481
+ ## Time-Dependent Concurrency Testing
482
+
483
+ Use `TestClock` only when testing time-dependent behavior like delays, timeouts, or schedules.
484
+
485
+ ```typescript
486
+ import { it } from '@effect/vitest';
487
+ import { Effect, Fiber, Duration } from 'effect';
488
+ import { TestClock } from 'effect/testing';
489
+
490
+ it.effect('should handle delayed concurrent operations', () =>
491
+ Effect.gen(function* () {
492
+ const fiber = yield* Effect.gen(function* () {
493
+ yield* Effect.sleep(Duration.seconds(5));
494
+ return 'done';
495
+ }).pipe(Effect.forkChild);
496
+
497
+ yield* TestClock.adjust(Duration.seconds(5));
498
+
499
+ const result = yield* Fiber.join(fiber);
500
+ expect(result).toBe('done');
501
+ })
502
+ );
503
+ ```
504
+
505
+ ## Anti-Patterns
506
+
507
+ ### DON'T use TestClock for non-time-dependent code
508
+
509
+ ```typescript
510
+ import { Effect, Duration } from 'effect';
511
+ import { TestClock } from 'effect/testing';
512
+
513
+ // BAD - Using TestClock when not needed
514
+ Effect.gen(function* () {
515
+ const fiber = yield* someEffect.pipe(Effect.forkChild);
516
+ yield* TestClock.adjust(Duration.millis(100));
517
+ yield* Fiber.join(fiber);
518
+ });
519
+
520
+ // GOOD - Use yieldNow for simple yielding
521
+ Effect.gen(function* () {
522
+ const fiber = yield* someEffect.pipe(Effect.forkChild);
523
+ yield* Effect.yieldNow();
524
+ yield* Fiber.join(fiber);
525
+ });
526
+ ```
527
+
528
+ ### DON'T poll in a loop without yieldNow
529
+
530
+ ```typescript
531
+ import { Effect, Fiber } from 'effect';
532
+
533
+ declare const fiber: Fiber.Fiber<void>;
534
+
535
+ // BAD - Busy loop
536
+ while (fiber.pollUnsafe() === undefined) {
537
+ // Spins forever!
538
+ }
539
+
540
+ // GOOD - Yield between polls or use Fiber.await
541
+ Effect.gen(function* () {
542
+ while (fiber.pollUnsafe() === undefined) {
543
+ yield* Effect.yieldNow();
544
+ }
545
+ });
546
+
547
+ // BETTER - Just await the fiber
548
+ Effect.gen(function* () {
549
+ yield* Fiber.await(fiber);
550
+ });
551
+ ```
552
+
553
+ ### DON'T forget Effect.scoped for PubSub subscriptions
554
+
555
+ ```typescript
556
+ import { Effect, PubSub } from 'effect';
557
+
558
+ declare const pubsub: PubSub.PubSub<string>;
559
+
560
+ // BAD - Subscription leaks
561
+ Effect.gen(function* () {
562
+ const sub = yield* PubSub.subscribe(pubsub);
563
+ // Sub is never cleaned up!
564
+ });
565
+
566
+ // GOOD - Scoped subscription
567
+ Effect.gen(function* () {
568
+ yield* Effect.scoped(
569
+ Effect.gen(function* () {
570
+ const sub = yield* PubSub.subscribe(pubsub);
571
+ // takeUpTo never suspends; takeAll would hang here (nothing published)
572
+ const events = yield* PubSub.takeUpTo(sub, 10);
573
+ // Sub cleaned up when scope closes
574
+ })
575
+ );
576
+ });
577
+ ```
578
+
579
+ ### DON'T start subscriptions after mutations
580
+
581
+ ```typescript
582
+ import { Effect, Fiber } from 'effect';
583
+ import { Stream, SubscriptionRef } from 'effect';
584
+
585
+ declare const ref: SubscriptionRef.SubscriptionRef<number>;
586
+
587
+ // BAD - May miss events: the subscription starts AFTER the first mutation
588
+ Effect.gen(function* () {
589
+ yield* SubscriptionRef.update(ref, (n) => n + 1);
590
+
591
+ const fiber = yield* SubscriptionRef.changes(ref).pipe(
592
+ Stream.take(1),
593
+ Stream.runCollect,
594
+ Effect.forkChild
595
+ );
596
+
597
+ yield* SubscriptionRef.update(ref, (n) => n + 1);
598
+
599
+ const result = yield* Fiber.join(fiber);
600
+ });
601
+ ```
602
+
603
+ ## Quality Checklist
604
+
605
+ - [ ] Using correct coordination primitive for the use case
606
+ - [ ] `Effect.scoped` wraps PubSub subscriptions
607
+ - [ ] Latches ensure stream subscriptions are ready before mutations
608
+ - [ ] `Effect.yieldNow` used instead of TestClock for non-time-dependent code
609
+ - [ ] Fiber interruption tested with `Exit.hasInterrupts` or `Cause.hasInterruptsOnly`
610
+ - [ ] Stream finalizers verified with `Stream.ensuring`
611
+ - [ ] No busy polling without yields
612
+ - [ ] Test is deterministic (no race conditions)