@alchemy.run/test 0.0.0-stage → 2.0.0-beta.85

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 (119) hide show
  1. package/LICENSE +201 -0
  2. package/bin/alchemy-test.ts +2 -0
  3. package/lib/Alchemy.d.ts +78 -0
  4. package/lib/Alchemy.d.ts.map +1 -0
  5. package/lib/Alchemy.js +194 -0
  6. package/lib/Alchemy.js.map +1 -0
  7. package/lib/Api.d.ts +134 -0
  8. package/lib/Api.d.ts.map +1 -0
  9. package/lib/Api.js +245 -0
  10. package/lib/Api.js.map +1 -0
  11. package/lib/Bun.d.ts +67 -0
  12. package/lib/Bun.d.ts.map +1 -0
  13. package/lib/Bun.js +143 -0
  14. package/lib/Bun.js.map +1 -0
  15. package/lib/Capture.d.ts +10 -0
  16. package/lib/Capture.d.ts.map +1 -0
  17. package/lib/Capture.js +68 -0
  18. package/lib/Capture.js.map +1 -0
  19. package/lib/Core.d.ts +287 -0
  20. package/lib/Core.d.ts.map +1 -0
  21. package/lib/Core.js +429 -0
  22. package/lib/Core.js.map +1 -0
  23. package/lib/DevCli.d.ts +47 -0
  24. package/lib/DevCli.d.ts.map +1 -0
  25. package/lib/DevCli.js +181 -0
  26. package/lib/DevCli.js.map +1 -0
  27. package/lib/Expect.d.ts +84 -0
  28. package/lib/Expect.d.ts.map +1 -0
  29. package/lib/Expect.js +518 -0
  30. package/lib/Expect.js.map +1 -0
  31. package/lib/FileLog.d.ts +27 -0
  32. package/lib/FileLog.d.ts.map +1 -0
  33. package/lib/FileLog.js +136 -0
  34. package/lib/FileLog.js.map +1 -0
  35. package/lib/Http.d.ts +109 -0
  36. package/lib/Http.d.ts.map +1 -0
  37. package/lib/Http.js +123 -0
  38. package/lib/Http.js.map +1 -0
  39. package/lib/Model.d.ts +98 -0
  40. package/lib/Model.d.ts.map +1 -0
  41. package/lib/Model.js +59 -0
  42. package/lib/Model.js.map +1 -0
  43. package/lib/PlainReporter.d.ts +19 -0
  44. package/lib/PlainReporter.d.ts.map +1 -0
  45. package/lib/PlainReporter.js +348 -0
  46. package/lib/PlainReporter.js.map +1 -0
  47. package/lib/Plan.d.ts +19 -0
  48. package/lib/Plan.d.ts.map +1 -0
  49. package/lib/Plan.js +32 -0
  50. package/lib/Plan.js.map +1 -0
  51. package/lib/Registry.d.ts +34 -0
  52. package/lib/Registry.d.ts.map +1 -0
  53. package/lib/Registry.js +80 -0
  54. package/lib/Registry.js.map +1 -0
  55. package/lib/Reporter.d.ts +160 -0
  56. package/lib/Reporter.d.ts.map +1 -0
  57. package/lib/Reporter.js +9 -0
  58. package/lib/Reporter.js.map +1 -0
  59. package/lib/Runner.d.ts +51 -0
  60. package/lib/Runner.d.ts.map +1 -0
  61. package/lib/Runner.js +1052 -0
  62. package/lib/Runner.js.map +1 -0
  63. package/lib/StrayOutput.d.ts +8 -0
  64. package/lib/StrayOutput.d.ts.map +1 -0
  65. package/lib/StrayOutput.js +259 -0
  66. package/lib/StrayOutput.js.map +1 -0
  67. package/lib/Tags.d.ts +17 -0
  68. package/lib/Tags.d.ts.map +1 -0
  69. package/lib/Tags.js +84 -0
  70. package/lib/Tags.js.map +1 -0
  71. package/lib/TestState.d.ts +12 -0
  72. package/lib/TestState.d.ts.map +1 -0
  73. package/lib/TestState.js +15 -0
  74. package/lib/TestState.js.map +1 -0
  75. package/lib/Tui.d.ts +4 -0
  76. package/lib/Tui.d.ts.map +1 -0
  77. package/lib/Tui.js +1137 -0
  78. package/lib/Tui.js.map +1 -0
  79. package/lib/Vitest.d.ts +58 -0
  80. package/lib/Vitest.d.ts.map +1 -0
  81. package/lib/Vitest.js +132 -0
  82. package/lib/Vitest.js.map +1 -0
  83. package/lib/WorktreeStage.d.ts +17 -0
  84. package/lib/WorktreeStage.d.ts.map +1 -0
  85. package/lib/WorktreeStage.js +48 -0
  86. package/lib/WorktreeStage.js.map +1 -0
  87. package/lib/cli.d.ts +2 -0
  88. package/lib/cli.d.ts.map +1 -0
  89. package/lib/cli.js +222 -0
  90. package/lib/cli.js.map +1 -0
  91. package/lib/index.d.ts +14 -0
  92. package/lib/index.d.ts.map +1 -0
  93. package/lib/index.js +12 -0
  94. package/lib/index.js.map +1 -0
  95. package/package.json +88 -3
  96. package/src/Alchemy.ts +335 -0
  97. package/src/Api.ts +521 -0
  98. package/src/Bun.ts +256 -0
  99. package/src/Capture.ts +72 -0
  100. package/src/Core.ts +872 -0
  101. package/src/DevCli.ts +229 -0
  102. package/src/Expect.ts +763 -0
  103. package/src/FileLog.ts +158 -0
  104. package/src/Http.ts +201 -0
  105. package/src/Model.ts +160 -0
  106. package/src/PlainReporter.ts +426 -0
  107. package/src/Plan.ts +45 -0
  108. package/src/Registry.ts +101 -0
  109. package/src/Reporter.ts +171 -0
  110. package/src/Runner.ts +1341 -0
  111. package/src/StrayOutput.ts +283 -0
  112. package/src/Tags.ts +100 -0
  113. package/src/TestState.ts +35 -0
  114. package/src/Tui.ts +1272 -0
  115. package/src/Vitest.ts +239 -0
  116. package/src/WorktreeStage.ts +45 -0
  117. package/src/cli.ts +330 -0
  118. package/src/index.ts +57 -0
  119. package/README.md +0 -3
package/src/Api.ts ADDED
@@ -0,0 +1,521 @@
1
+ /**
2
+ * The vitest-compatible registration API: `describe`, `it`/`test`, hooks and
3
+ * `layer`. Registration builds the {@link Model} tree; execution is the
4
+ * runner's job. Everything is Effect-first — plain-function tests are wrapped
5
+ * into Effects at registration time.
6
+ */
7
+ import * as Effect from "effect/Effect";
8
+ import * as Exit from "effect/Exit";
9
+ import * as Layer from "effect/Layer";
10
+ import * as Scope from "effect/Scope";
11
+ import * as TestClock from "effect/testing/TestClock";
12
+ import * as TestConsole from "effect/testing/TestConsole";
13
+ import type { Exclusive, Hook, Mode, TestBody } from "./Model.ts";
14
+ import { makeSuite } from "./Model.ts";
15
+ import { currentFileSuite, currentSuite, withSuite } from "./Registry.ts";
16
+ import { mergeTags, type Tags } from "./Tags.ts";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Shared option handling (vitest accepts `number | { timeout?: number, ... }`)
20
+ // ---------------------------------------------------------------------------
21
+
22
+ export type TestOptions =
23
+ | number
24
+ | {
25
+ /** Labels added to the tags inherited from enclosing suites. */
26
+ readonly tags?: Tags;
27
+ /** Tags that must each be explicitly named in the filter to enable this test or suite. */
28
+ readonly optInTags?: Tags;
29
+ readonly timeout?: number;
30
+ readonly retry?: number;
31
+ readonly repeats?: number;
32
+ /**
33
+ * `true` runs this test with the whole-process write lock — no other
34
+ * test in the run executes concurrently with it. Use for tests that
35
+ * mutate process-global state (e.g. `process.env.PATH`): the runner
36
+ * executes everything in ONE bun process, so such mutations are
37
+ * visible to every other running test.
38
+ *
39
+ * A label (e.g. `"hetzner"`) only serializes the test with others
40
+ * holding the same label, so unrelated tests keep running. Use it for
41
+ * a shared cloud quota.
42
+ */
43
+ readonly exclusive?: Exclusive;
44
+ };
45
+
46
+ export const timeoutOf = (options?: TestOptions): number | undefined =>
47
+ typeof options === "number" ? options : options?.timeout;
48
+
49
+ export const retryOf = (options?: TestOptions): number | undefined =>
50
+ typeof options === "object" && options !== null ? options.retry : undefined;
51
+
52
+ export const exclusiveOf = (options?: TestOptions): Exclusive =>
53
+ typeof options === "object" && options !== null ? (options.exclusive ?? false) : false;
54
+
55
+ export const tagsOf = (options?: TestOptions): Tags | undefined =>
56
+ typeof options === "object" ? options?.tags : undefined;
57
+
58
+ export const optInTagsOf = (options?: TestOptions): Tags | undefined =>
59
+ typeof options === "object" ? options?.optInTags : undefined;
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // describe
63
+ // ---------------------------------------------------------------------------
64
+
65
+ type DescribeBody = (() => void) | undefined;
66
+
67
+ export interface DescribeOptions {
68
+ /** Labels inherited by every nested suite and test. */
69
+ readonly tags?: Tags;
70
+ /** Tags that must each be explicitly named in the filter to enable this test or suite. */
71
+ readonly optInTags?: Tags;
72
+ readonly concurrent?: boolean;
73
+ readonly sequential?: boolean;
74
+ readonly timeout?: number;
75
+ }
76
+
77
+ export interface DescribeFn {
78
+ (
79
+ name: string | (() => void),
80
+ body?: DescribeBody | DescribeOptions | number,
81
+ bodyOrTimeout?: DescribeBody | number,
82
+ ): void;
83
+ readonly sequential: DescribeFn;
84
+ readonly concurrent: DescribeFn;
85
+ readonly skip: DescribeFn;
86
+ readonly only: DescribeFn;
87
+ readonly todo: DescribeFn;
88
+ skipIf(condition: unknown): DescribeFn;
89
+ runIf(condition: unknown): DescribeFn;
90
+ each<T>(
91
+ cases: ReadonlyArray<T>,
92
+ ): (name: string, fn: (args: T) => void, options?: TestOptions) => void;
93
+ }
94
+
95
+ interface DescribeConfig {
96
+ readonly mode: Mode;
97
+ readonly sequential: boolean;
98
+ }
99
+
100
+ const makeDescribe = (config: DescribeConfig): DescribeFn => {
101
+ const fn = (
102
+ nameOrBody: string | (() => void),
103
+ second?: DescribeBody | DescribeOptions | number,
104
+ third?: DescribeBody | number,
105
+ ): void => {
106
+ // Normalize the vitest signatures:
107
+ // describe(name, body)
108
+ // describe(name, body, timeout)
109
+ // describe(name, options, body)
110
+ // describe(body) (via describe.skipIf(...)(() => {}))
111
+ let name: string;
112
+ let body: DescribeBody;
113
+ let sequential = config.sequential;
114
+ let tags: Tags | undefined;
115
+ let optInTags: Tags | undefined;
116
+ if (typeof nameOrBody === "function") {
117
+ name = "";
118
+ body = nameOrBody;
119
+ } else {
120
+ name = nameOrBody;
121
+ if (typeof second === "function") {
122
+ body = second;
123
+ } else if (typeof second === "object" && second !== null) {
124
+ tags = second.tags;
125
+ optInTags = second.optInTags;
126
+ if (second.concurrent === true || second.sequential === false) {
127
+ sequential = false;
128
+ } else if (second.concurrent === false || second.sequential === true) {
129
+ sequential = true;
130
+ }
131
+ body = typeof third === "function" ? third : undefined;
132
+ } else {
133
+ body = typeof third === "function" ? third : undefined;
134
+ }
135
+ }
136
+ const parent = currentSuite();
137
+ const suite = makeSuite(name, parent, config.mode, tags, optInTags);
138
+ suite.sequential = sequential;
139
+ parent.children.push(suite);
140
+ // `describe.skip` still collects its children (they're reported as
141
+ // skipped, matching vitest); only `todo` suites skip collection.
142
+ if (body !== undefined && config.mode !== "todo") {
143
+ withSuite(suite, body);
144
+ }
145
+ };
146
+ const withMethods = Object.assign(fn, {
147
+ each:
148
+ <T>(cases: ReadonlyArray<T>) =>
149
+ (name: string, eachBody: (args: T) => void, options?: TestOptions) => {
150
+ cases.forEach((args, index) => {
151
+ fn(
152
+ formatEachName(name, args, index),
153
+ { tags: tagsOf(options), optInTags: optInTagsOf(options) },
154
+ () => eachBody(args),
155
+ );
156
+ });
157
+ },
158
+ skipIf: (condition: unknown) => makeDescribe(condition ? { ...config, mode: "skip" } : config),
159
+ runIf: (condition: unknown) => makeDescribe(condition ? config : { ...config, mode: "skip" }),
160
+ });
161
+ // Lazy getters — Object.assign would evaluate them eagerly and recurse
162
+ // forever (each modifier constructs another DescribeFn).
163
+ Object.defineProperties(withMethods, {
164
+ sequential: { get: () => makeDescribe({ ...config, sequential: true }) },
165
+ concurrent: { get: () => makeDescribe({ ...config, sequential: false }) },
166
+ skip: { get: () => makeDescribe({ ...config, mode: "skip" }) },
167
+ only: { get: () => makeDescribe({ ...config, mode: "only" }) },
168
+ todo: { get: () => makeDescribe({ ...config, mode: "todo" }) },
169
+ });
170
+ return withMethods as DescribeFn;
171
+ };
172
+
173
+ export const describe: DescribeFn = makeDescribe({
174
+ mode: "run",
175
+ sequential: true,
176
+ });
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // Test registration primitives
180
+ // ---------------------------------------------------------------------------
181
+
182
+ export interface RegisterTestOptions {
183
+ readonly tags?: Tags;
184
+ /** Tags that must each be explicitly named in the filter to enable this test or suite. */
185
+ readonly optInTags?: Tags;
186
+ readonly name: string;
187
+ readonly mode: Mode;
188
+ readonly fails?: boolean;
189
+ readonly exclusive?: Exclusive;
190
+ readonly retry?: number | undefined;
191
+ readonly timeout?: number | undefined;
192
+ readonly body: TestBody | undefined;
193
+ }
194
+
195
+ /** Low-level test registration — used by adapters (e.g. alchemy Test.make). */
196
+ export const registerTest = (options: RegisterTestOptions): void => {
197
+ const parent = currentSuite();
198
+ parent.children.push({
199
+ type: "test",
200
+ name: options.name,
201
+ tags: mergeTags(parent.tags, options.tags),
202
+ optInTags: mergeTags(parent.optInTags, options.optInTags),
203
+ mode: options.mode,
204
+ fails: options.fails ?? false,
205
+ exclusive: options.exclusive ?? false,
206
+ retry: options.retry,
207
+ timeout: options.timeout,
208
+ body: options.body,
209
+ parent,
210
+ });
211
+ };
212
+
213
+ /** Wrap a plain (sync or async) test function into a self-contained Effect. */
214
+ const fromFn =
215
+ (fn: (ctx: TestContext) => unknown): TestBody =>
216
+ () =>
217
+ Effect.promise(async () => {
218
+ await fn(emptyContext);
219
+ });
220
+
221
+ /** Minimal stand-in for vitest's TestContext (rarely used by our tests). */
222
+ export interface TestContext {
223
+ readonly signal?: AbortSignal;
224
+ }
225
+
226
+ const emptyContext: TestContext = {};
227
+
228
+ // ---------------------------------------------------------------------------
229
+ // Effect testers (it.effect / it.live)
230
+ // ---------------------------------------------------------------------------
231
+
232
+ /**
233
+ * The environment `it.effect` provides, matching @effect/vitest: a TestClock
234
+ * and a TestConsole on top of a per-test Scope.
235
+ */
236
+ const TestEnv = Layer.mergeAll(TestConsole.layer, TestClock.layer());
237
+
238
+ export type EffectTestFunction<R> = (ctx: TestContext) => Effect.Effect<unknown, unknown, R>;
239
+
240
+ export interface Tester<R> {
241
+ (name: string, self: EffectTestFunction<R>, options?: TestOptions): void;
242
+ skip(name: string, self: EffectTestFunction<R>, options?: TestOptions): void;
243
+ skipIf(
244
+ condition: unknown,
245
+ ): (name: string, self: EffectTestFunction<R>, options?: TestOptions) => void;
246
+ runIf(
247
+ condition: unknown,
248
+ ): (name: string, self: EffectTestFunction<R>, options?: TestOptions) => void;
249
+ only(name: string, self: EffectTestFunction<R>, options?: TestOptions): void;
250
+ fails(name: string, self: EffectTestFunction<R>, options?: TestOptions): void;
251
+ each<T>(
252
+ cases: ReadonlyArray<T>,
253
+ ): (
254
+ name: string,
255
+ self: (args: T) => Effect.Effect<unknown, unknown, R>,
256
+ options?: TestOptions,
257
+ ) => void;
258
+ }
259
+
260
+ export const makeTester = <R>(
261
+ mapEffect: (self: Effect.Effect<unknown, unknown, R>) => Effect.Effect<unknown, unknown, never>,
262
+ ): Tester<R> => {
263
+ const register = (
264
+ name: string,
265
+ self: EffectTestFunction<R>,
266
+ options: TestOptions | undefined,
267
+ overrides?: Partial<RegisterTestOptions>,
268
+ ) =>
269
+ registerTest({
270
+ name,
271
+ mode: "run",
272
+ timeout: timeoutOf(options),
273
+ tags: tagsOf(options),
274
+ optInTags: optInTagsOf(options),
275
+ retry: retryOf(options),
276
+ exclusive: exclusiveOf(options),
277
+ body: () => mapEffect(Effect.suspend(() => self(emptyContext))),
278
+ ...overrides,
279
+ });
280
+
281
+ const fn = ((name, self, options) => register(name, self, options)) as Tester<R>;
282
+ return Object.assign(fn, {
283
+ skip: (name: string, self: EffectTestFunction<R>, options?: TestOptions) =>
284
+ register(name, self, options, { mode: "skip" }),
285
+ skipIf:
286
+ (condition: unknown) => (name: string, self: EffectTestFunction<R>, options?: TestOptions) =>
287
+ register(name, self, options, { mode: condition ? "skip" : "run" }),
288
+ runIf:
289
+ (condition: unknown) => (name: string, self: EffectTestFunction<R>, options?: TestOptions) =>
290
+ register(name, self, options, { mode: condition ? "run" : "skip" }),
291
+ only: (name: string, self: EffectTestFunction<R>, options?: TestOptions) =>
292
+ register(name, self, options, { mode: "only" }),
293
+ fails: (name: string, self: EffectTestFunction<R>, options?: TestOptions) =>
294
+ register(name, self, options, { fails: true }),
295
+ each:
296
+ <T>(cases: ReadonlyArray<T>) =>
297
+ (
298
+ name: string,
299
+ self: (args: T) => Effect.Effect<unknown, unknown, R>,
300
+ options?: TestOptions,
301
+ ) => {
302
+ cases.forEach((args, index) => {
303
+ register(formatEachName(name, args, index), () => self(args), options);
304
+ });
305
+ },
306
+ });
307
+ };
308
+
309
+ const formatEachName = (name: string, args: unknown, index: number): string => {
310
+ let out = name;
311
+ // `$prop` templating (vitest): "Container (dev: $dev)"
312
+ if (out.includes("$") && typeof args === "object" && args !== null) {
313
+ out = out.replace(/\$([a-zA-Z_$][\w.$]*)/g, (match, path: string) => {
314
+ const value = path
315
+ .split(".")
316
+ .reduce<unknown>(
317
+ (acc, key) =>
318
+ acc !== null && typeof acc === "object"
319
+ ? (acc as Record<string, unknown>)[key]
320
+ : undefined,
321
+ args,
322
+ );
323
+ return value === undefined ? match : String(value);
324
+ });
325
+ }
326
+ // printf-style templating: "adds %s"
327
+ if (out.includes("%")) {
328
+ out = out.replace(/%[sdifjo#]/g, () =>
329
+ typeof args === "object" ? JSON.stringify(args) : String(args),
330
+ );
331
+ }
332
+ return out === name ? `${name} [${index}]` : out;
333
+ };
334
+
335
+ // ---------------------------------------------------------------------------
336
+ // it / test
337
+ // ---------------------------------------------------------------------------
338
+
339
+ export interface TestFn {
340
+ (name: string, fn?: (ctx: TestContext) => unknown, options?: TestOptions): void;
341
+ skip(name: string, fn?: (ctx: TestContext) => unknown, options?: TestOptions): void;
342
+ only(name: string, fn?: (ctx: TestContext) => unknown, options?: TestOptions): void;
343
+ todo(name: string, fn?: (ctx: TestContext) => unknown, options?: TestOptions): void;
344
+ fails(name: string, fn?: (ctx: TestContext) => unknown, options?: TestOptions): void;
345
+ skipIf(
346
+ condition: unknown,
347
+ ): (name: string, fn?: (ctx: TestContext) => unknown, options?: TestOptions) => void;
348
+ runIf(
349
+ condition: unknown,
350
+ ): (name: string, fn?: (ctx: TestContext) => unknown, options?: TestOptions) => void;
351
+ each<T>(
352
+ cases: ReadonlyArray<T>,
353
+ ): (name: string, fn: (args: T) => unknown, options?: TestOptions) => void;
354
+ /** Effect tester with TestClock + TestConsole (matches @effect/vitest). */
355
+ readonly effect: Tester<Scope.Scope | TestClock.TestClock | TestConsole.TestConsole>;
356
+ /** Effect tester against the live environment (scope only). */
357
+ readonly live: Tester<Scope.Scope>;
358
+ }
359
+
360
+ const registerFnTest = (
361
+ name: string,
362
+ fn: ((ctx: TestContext) => unknown) | undefined,
363
+ options: TestOptions | undefined,
364
+ mode: Mode,
365
+ fails = false,
366
+ ): void =>
367
+ registerTest({
368
+ name,
369
+ mode: fn === undefined ? "todo" : mode,
370
+ fails,
371
+ timeout: timeoutOf(options),
372
+ tags: tagsOf(options),
373
+ optInTags: optInTagsOf(options),
374
+ retry: retryOf(options),
375
+ exclusive: exclusiveOf(options),
376
+ body: fn === undefined ? undefined : fromFn(fn),
377
+ });
378
+
379
+ const makeTestFn = (): TestFn => {
380
+ const fn = ((name, body, options) => registerFnTest(name, body, options, "run")) as TestFn;
381
+ return Object.assign(fn, {
382
+ skip: (name: string, body?: (ctx: TestContext) => unknown, options?: TestOptions) =>
383
+ registerFnTest(name, body, options, "skip"),
384
+ only: (name: string, body?: (ctx: TestContext) => unknown, options?: TestOptions) =>
385
+ registerFnTest(name, body, options, "only"),
386
+ todo: (name: string, body?: (ctx: TestContext) => unknown, options?: TestOptions) =>
387
+ registerFnTest(name, body, options, "todo"),
388
+ fails: (name: string, body?: (ctx: TestContext) => unknown, options?: TestOptions) =>
389
+ registerFnTest(name, body, options, "run", true),
390
+ skipIf:
391
+ (condition: unknown) =>
392
+ (name: string, body?: (ctx: TestContext) => unknown, options?: TestOptions) =>
393
+ registerFnTest(name, body, options, condition ? "skip" : "run"),
394
+ runIf:
395
+ (condition: unknown) =>
396
+ (name: string, body?: (ctx: TestContext) => unknown, options?: TestOptions) =>
397
+ registerFnTest(name, body, options, condition ? "run" : "skip"),
398
+ each:
399
+ <T>(cases: ReadonlyArray<T>) =>
400
+ (name: string, body: (args: T) => unknown, options?: TestOptions) => {
401
+ cases.forEach((args, index) => {
402
+ registerFnTest(formatEachName(name, args, index), () => body(args), options, "run");
403
+ });
404
+ },
405
+ effect: makeTester<Scope.Scope | TestClock.TestClock | TestConsole.TestConsole>(
406
+ (self) =>
407
+ self.pipe(Effect.scoped, Effect.provide(TestEnv)) as Effect.Effect<unknown, unknown, never>,
408
+ ),
409
+ live: makeTester<Scope.Scope>((self) => Effect.scoped(self)),
410
+ });
411
+ };
412
+
413
+ export const it: TestFn = makeTestFn();
414
+ export const test: TestFn = it;
415
+
416
+ // ---------------------------------------------------------------------------
417
+ // Hooks
418
+ // ---------------------------------------------------------------------------
419
+
420
+ export type HookKind = "beforeAll" | "afterAll" | "beforeEach" | "afterEach";
421
+
422
+ /** Framework cleanup after user teardown, including files with no runnable tests. */
423
+ export const registerFileCleanup = (hook: Hook): void => {
424
+ currentFileSuite().cleanups.push(hook);
425
+ };
426
+
427
+ /** Low-level hook registration with an Effect body — used by adapters. */
428
+ export const registerHook = (kind: HookKind, hook: Hook): void => {
429
+ currentSuite()[kind].push(hook);
430
+ };
431
+
432
+ const hookFromFn =
433
+ (fn: () => unknown): Hook["body"] =>
434
+ () =>
435
+ Effect.promise(async () => {
436
+ await fn();
437
+ });
438
+
439
+ export const beforeAll = (fn: () => unknown, timeout?: number): void =>
440
+ registerHook("beforeAll", { body: hookFromFn(fn), timeout });
441
+ export const afterAll = (fn: () => unknown, timeout?: number): void =>
442
+ registerHook("afterAll", { body: hookFromFn(fn), timeout });
443
+ export const beforeEach = (fn: () => unknown, timeout?: number): void =>
444
+ registerHook("beforeEach", { body: hookFromFn(fn), timeout });
445
+ export const afterEach = (fn: () => unknown, timeout?: number): void =>
446
+ registerHook("afterEach", { body: hookFromFn(fn), timeout });
447
+
448
+ // ---------------------------------------------------------------------------
449
+ // layer — share a Layer across the tests of a block (@effect/vitest parity)
450
+ // ---------------------------------------------------------------------------
451
+
452
+ export interface LayerMethods<R> {
453
+ effect: Tester<R | Scope.Scope | TestClock.TestClock | TestConsole.TestConsole>;
454
+ layer<R2, E2>(
455
+ nested: Layer.Layer<R2, E2, R>,
456
+ options?: { readonly timeout?: number },
457
+ ): {
458
+ (f: (it: LayerMethods<R | R2>) => void): void;
459
+ (name: string, f: (it: LayerMethods<R | R2>) => void): void;
460
+ };
461
+ }
462
+
463
+ export const layer =
464
+ <R, E>(
465
+ layer_: Layer.Layer<R, E>,
466
+ options?: {
467
+ readonly memoMap?: Layer.MemoMap;
468
+ readonly timeout?: number;
469
+ readonly excludeTestServices?: boolean;
470
+ },
471
+ ): {
472
+ (f: (it: LayerMethods<R>) => void): void;
473
+ (name: string, f: (it: LayerMethods<R>) => void): void;
474
+ } =>
475
+ (
476
+ ...args: [name: string, f: (it: LayerMethods<R>) => void] | [f: (it: LayerMethods<R>) => void]
477
+ ): void => {
478
+ const excludeTestServices = options?.excludeTestServices ?? false;
479
+ const withTestEnv = excludeTestServices
480
+ ? (layer_ as Layer.Layer<R, E>)
481
+ : Layer.provideMerge(layer_, TestEnv);
482
+ const memoMap = options?.memoMap ?? Effect.runSync(Layer.makeMemoMap);
483
+ const scope = Scope.makeUnsafe();
484
+ const contextEffect = Layer.buildWithMemoMap(withTestEnv, memoMap, scope).pipe(
485
+ Effect.orDie,
486
+ Effect.cached,
487
+ Effect.runSync,
488
+ );
489
+
490
+ const makeMethods = (): LayerMethods<R> => ({
491
+ effect: makeTester<R | Scope.Scope | TestClock.TestClock | TestConsole.TestConsole>(
492
+ (self) =>
493
+ Effect.flatMap(contextEffect, (context) =>
494
+ self.pipe(Effect.scoped, Effect.provide(context)),
495
+ ) as Effect.Effect<unknown, unknown, never>,
496
+ ),
497
+ layer: (nested, nestedOptions) =>
498
+ layer(Layer.provideMerge(nested, withTestEnv) as Layer.Layer<any, any>, {
499
+ ...nestedOptions,
500
+ memoMap: Layer.forkMemoMapUnsafe(memoMap),
501
+ excludeTestServices,
502
+ }) as any,
503
+ });
504
+
505
+ const register = (): void => {
506
+ const cleanup: Hook = {
507
+ body: () => Scope.close(scope, Exit.void),
508
+ timeout: options?.timeout,
509
+ };
510
+ registerHook("afterAll", cleanup);
511
+ registerFileCleanup(cleanup);
512
+ const f = (args.length === 1 ? args[0] : args[1]) as (it: LayerMethods<R>) => void;
513
+ f(makeMethods());
514
+ };
515
+
516
+ if (args.length === 2) {
517
+ describe(args[0], register);
518
+ } else {
519
+ register();
520
+ }
521
+ };