@shivaedev/effect-test 0.0.0-bootstrap.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## 0.1.0 - 2026-08-30
6
+
7
+ ### Added
8
+
9
+ - Add `makeEffectIt` for generator Effect tests on a worker-scoped Layer, with
10
+ TestClock by default, an optional `around` hook, and a Clock-aware
11
+ `eventually` helper.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ShivaeDev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # `@shivaedev/effect-test`
2
+
3
+ A Vitest runner for Effect programs. The test Layer is built once per worker.
4
+ Each test gets a fresh Scope, a generator body, and TestClock unless you ask
5
+ for the live clock.
6
+
7
+ Prisma and tRPC testing helpers in this repository are thin wrappers around
8
+ `makeEffectIt`. Use this package directly when those wrappers are the wrong
9
+ shape.
10
+
11
+ This package currently targets exact early-access versions of Effect v4. It is
12
+ built for ShivaeDev applications and may change without a deprecation period.
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ pnpm add --save-dev @shivaedev/effect-test @effect/vitest effect vitest
18
+ ```
19
+
20
+ ## `makeEffectIt`
21
+
22
+ ```ts
23
+ import { Layer } from "effect"
24
+ import { makeEffectIt } from "@shivaedev/effect-test"
25
+
26
+ const { effectApp } = makeEffectIt({
27
+ layer: TestLive,
28
+ makeHarness: () => Effect.succeed({}),
29
+ })
30
+
31
+ effectApp("reads a service from the test Layer", function* (_harness, _context) {
32
+ const movies = yield* Movies
33
+ expect(yield* movies.list()).toEqual([])
34
+ })
35
+ ```
36
+
37
+ `makeHarness` runs inside the worker Layer and any `around` wrapper, then the
38
+ generator receives that value as its first argument.
39
+
40
+ ```ts
41
+ const { effectApp } = makeEffectIt({
42
+ layer: DatabaseLive,
43
+ around: (effect) => withTestTransaction(Database, effect),
44
+ makeHarness: () => Database,
45
+ })
46
+ ```
47
+
48
+ `effectApp` supports `skip`, `skipIf`, `runIf`, `only`, `each`, and `fails`.
49
+
50
+ ## Clock
51
+
52
+ Tests install TestClock and TestConsole. Pass `clock: "live"` on the factory or
53
+ on a single test when the program must use wall-clock time:
54
+
55
+ ```ts
56
+ effectApp("talks to a real timer", function* () {
57
+ yield* Effect.sleep("10 millis")
58
+ }, { clock: "live" })
59
+ ```
60
+
61
+ ## `eventually`
62
+
63
+ Retries until the effect succeeds. Under TestClock it advances time with
64
+ `TestClock.adjust`. Under `clock: "live"` it uses `Schedule.spaced`.
65
+
66
+ ```ts
67
+ import { eventually } from "@shivaedev/effect-test"
68
+
69
+ effectApp("sees a delayed write", function* ({ db }) {
70
+ const row = yield* eventually(db.Job.where({ id }).first(), {
71
+ interval: "10 millis",
72
+ times: 50,
73
+ })
74
+ expect(row.status).toBe("ready")
75
+ })
76
+ ```
@@ -0,0 +1,7 @@
1
+ import { type Duration, Effect } from "effect";
2
+ export interface EventuallyOptions {
3
+ readonly interval?: Duration.Input;
4
+ readonly times?: number;
5
+ }
6
+ export declare const eventually: <A, E, R>(effect: Effect.Effect<A, E, R>, options?: EventuallyOptions) => Effect.Effect<A, E, R>;
7
+ //# sourceMappingURL=eventually.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eventually.d.ts","sourceRoot":"","sources":["../src/eventually.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,QAAQ,EAAE,MAAM,EAAkB,MAAM,QAAQ,CAAC;AAGtE,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACxB;AAOD,eAAO,MAAM,UAAU,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EACjC,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC9B,UAAU,iBAAiB,KACzB,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CA4BrB,CAAC"}
@@ -0,0 +1,31 @@
1
+ import { Clock, Effect, Exit, Schedule } from "effect";
2
+ import * as TestClock from "effect/testing/TestClock";
3
+ const defaultInterval = "10 millis";
4
+ const isTestClock = (clock) => "adjust" in clock;
5
+ export const eventually = (effect, options) => Effect.gen(function* () {
6
+ const interval = options?.interval ?? defaultInterval;
7
+ const clock = yield* Clock.Clock;
8
+ if (!isTestClock(clock)) {
9
+ return yield* options?.times === undefined
10
+ ? Effect.retry(effect, Schedule.spaced(interval))
11
+ : Effect.retry(effect, {
12
+ schedule: Schedule.spaced(interval),
13
+ times: options.times,
14
+ });
15
+ }
16
+ let retriesLeft = options?.times;
17
+ for (;;) {
18
+ const exit = yield* Effect.exit(effect);
19
+ if (Exit.isSuccess(exit)) {
20
+ return exit.value;
21
+ }
22
+ if (retriesLeft === 0) {
23
+ return yield* exit;
24
+ }
25
+ if (retriesLeft !== undefined) {
26
+ retriesLeft -= 1;
27
+ }
28
+ yield* TestClock.adjust(interval);
29
+ }
30
+ });
31
+ //# sourceMappingURL=eventually.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eventually.js","sourceRoot":"","sources":["../src/eventually.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAiB,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACtE,OAAO,KAAK,SAAS,MAAM,0BAA0B,CAAC;AAOtD,MAAM,eAAe,GAAG,WAAW,CAAC;AAEpC,MAAM,WAAW,GAAG,CAAC,KAAkB,EAAgC,EAAE,CACxE,QAAQ,IAAI,KAAK,CAAC;AAEnB,MAAM,CAAC,MAAM,UAAU,GAAG,CACzB,MAA8B,EAC9B,OAA2B,EACF,EAAE,CAC3B,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IACnB,MAAM,QAAQ,GAAG,OAAO,EAAE,QAAQ,IAAI,eAAe,CAAC;IACtD,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;IAEjC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,CAAC,OAAO,EAAE,KAAK,KAAK,SAAS;YACzC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YACjD,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE;gBACrB,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC;gBACnC,KAAK,EAAE,OAAO,CAAC,KAAK;aACpB,CAAC,CAAC;IACN,CAAC;IAED,IAAI,WAAW,GAAG,OAAO,EAAE,KAAK,CAAC;IACjC,SAAS,CAAC;QACT,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACxC,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1B,OAAO,IAAI,CAAC,KAAK,CAAC;QACnB,CAAC;QACD,IAAI,WAAW,KAAK,CAAC,EAAE,CAAC;YACvB,OAAO,KAAK,CAAC,CAAC,IAAI,CAAC;QACpB,CAAC;QACD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YAC/B,WAAW,IAAI,CAAC,CAAC;QAClB,CAAC;QACD,KAAK,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACnC,CAAC;AACF,CAAC,CAAC,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { type EventuallyOptions, eventually } from "./eventually.js";
2
+ export type { EffectClock, EffectTest, EffectTester, EffectTestOptions, MakeEffectItOptions, MakeEffectItResult, } from "./types.js";
3
+ export { makeEffectIt } from "./vitest.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,iBAAiB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACrE,YAAY,EACX,WAAW,EACX,UAAU,EACV,YAAY,EACZ,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,GAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { eventually } from "./eventually.js";
2
+ export { makeEffectIt } from "./vitest.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,UAAU,EAAE,MAAM,iBAAiB,CAAC;AASrE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,25 @@
1
+ import type { TestContext, TestOptions } from "@effect/vitest";
2
+ import type { Effect, Layer } from "effect";
3
+ export type EffectClock = "test" | "live";
4
+ export type EffectTestOptions = TestOptions & {
5
+ readonly clock?: EffectClock;
6
+ };
7
+ export type EffectTest<Harness, Provided> = <A, Eff extends Effect.Effect<unknown, unknown, Provided>>(name: string, body: (harness: Harness, context: TestContext) => Generator<Eff, A, never>, options?: number | EffectTestOptions) => void;
8
+ export interface EffectTester<Harness, Provided> extends EffectTest<Harness, Provided> {
9
+ readonly skip: EffectTest<Harness, Provided>;
10
+ readonly skipIf: (condition: unknown) => EffectTest<Harness, Provided>;
11
+ readonly runIf: (condition: unknown) => EffectTest<Harness, Provided>;
12
+ readonly only: EffectTest<Harness, Provided>;
13
+ readonly each: <Item>(cases: ReadonlyArray<Item>) => <A, Eff extends Effect.Effect<unknown, unknown, Provided>>(name: string, body: (item: Item, harness: Harness, context: TestContext) => Generator<Eff, A, never>, options?: number | EffectTestOptions) => void;
14
+ readonly fails: EffectTest<Harness, Provided>;
15
+ }
16
+ export interface MakeEffectItOptions<Harness, TestLayer extends Layer.Layer<any, any, never>> {
17
+ readonly layer: TestLayer;
18
+ readonly around?: <A, E>(effect: Effect.Effect<A, E, Layer.Success<TestLayer>>) => Effect.Effect<A, unknown, Layer.Success<TestLayer>>;
19
+ readonly makeHarness: (context: TestContext) => Effect.Effect<Harness, unknown, Layer.Success<TestLayer>>;
20
+ readonly clock?: EffectClock;
21
+ }
22
+ export interface MakeEffectItResult<Harness, Provided> {
23
+ readonly effectApp: EffectTester<Harness, Provided>;
24
+ }
25
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAE5C,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,CAAC;AAE1C,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG;IAC7C,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,UAAU,CAAC,OAAO,EAAE,QAAQ,IAAI,CAC3C,CAAC,EACD,GAAG,SAAS,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAErD,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,KAAK,SAAS,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,CAAC,EAC1E,OAAO,CAAC,EAAE,MAAM,GAAG,iBAAiB,KAChC,IAAI,CAAC;AAEV,MAAM,WAAW,YAAY,CAAC,OAAO,EAAE,QAAQ,CAC9C,SAAQ,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAC7C,QAAQ,CAAC,MAAM,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IACvE,QAAQ,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IACtE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAC7C,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EACnB,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,KACtB,CAAC,CAAC,EAAE,GAAG,SAAS,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAC7D,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,CACL,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,WAAW,KAChB,SAAS,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,CAAC,EAC7B,OAAO,CAAC,EAAE,MAAM,GAAG,iBAAiB,KAChC,IAAI,CAAC;IACV,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;CAC9C;AAED,MAAM,WAAW,mBAAmB,CACnC,OAAO,EAEP,SAAS,SAAS,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC;IAE9C,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EACtB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,KACjD,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;IACzD,QAAQ,CAAC,WAAW,EAAE,CACrB,OAAO,EAAE,WAAW,KAChB,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;IAC/D,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB,CAAC,OAAO,EAAE,QAAQ;IACpD,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;CACpD"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,4 @@
1
+ import { Layer } from "effect";
2
+ import type { MakeEffectItOptions, MakeEffectItResult } from "./types.js";
3
+ export declare const makeEffectIt: <Harness, TestLayer extends Layer.Layer<any, any, never>>(options: MakeEffectItOptions<Harness, TestLayer>) => MakeEffectItResult<Harness, Layer.Success<TestLayer>>;
4
+ //# sourceMappingURL=vitest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vitest.d.ts","sourceRoot":"","sources":["../src/vitest.ts"],"names":[],"mappings":"AAKA,OAAO,EAAqC,KAAK,EAAS,MAAM,QAAQ,CAAC;AAGzE,OAAO,KAAK,EAKX,mBAAmB,EACnB,kBAAkB,EAClB,MAAM,YAAY,CAAC;AAyGpB,eAAO,MAAM,YAAY,GACxB,OAAO,EAEP,SAAS,SAAS,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,EAE9C,SAAS,mBAAmB,CAAC,OAAO,EAAE,SAAS,CAAC,KAC9C,kBAAkB,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAuHtD,CAAC"}
package/dist/vitest.js ADDED
@@ -0,0 +1,103 @@
1
+ import { it as effectIt, } from "@effect/vitest";
2
+ import { Cause, Effect, Exit, Layer, Scope } from "effect";
3
+ import * as TestClock from "effect/testing/TestClock";
4
+ import * as TestConsole from "effect/testing/TestConsole";
5
+ const fixtureName = "__effectTestContext";
6
+ const TestClockEnvironment = Layer.mergeAll(TestConsole.layer, TestClock.layer());
7
+ const LiveClockEnvironment = TestConsole.layer;
8
+ const environmentFor = (clock) => clock === "live" ? LiveClockEnvironment : TestClockEnvironment;
9
+ const restoreContext = (fixture, context) => ({
10
+ ...context,
11
+ [fixtureName]: fixture,
12
+ });
13
+ const contextFrom = (context) => context[fixtureName];
14
+ const runEffectTest = (effect, context, clock) => Effect.runPromise(Effect.gen(function* () {
15
+ const exit = yield* Effect.exit(effect);
16
+ if (Exit.isFailure(exit)) {
17
+ for (const error of Cause.prettyErrors(exit.cause)) {
18
+ yield* Effect.logError(error);
19
+ }
20
+ }
21
+ return yield* exit;
22
+ }).pipe(Effect.scoped, Effect.provide(environmentFor(clock))), { signal: context.signal });
23
+ const splitOptions = (options) => {
24
+ if (typeof options === "number") {
25
+ return { clock: undefined, vitest: { timeout: options } };
26
+ }
27
+ if (options === undefined) {
28
+ return { clock: undefined, vitest: {} };
29
+ }
30
+ const { clock, ...vitest } = options;
31
+ return { clock, vitest };
32
+ };
33
+ const makeFixtureIt = (layer) => effectIt.extend(fixtureName, { scope: "worker" },
34
+ // biome-ignore lint/correctness/noEmptyPattern: Vitest fixtures require a destructured context parameter.
35
+ async ({}, { onCleanup }) => {
36
+ const scope = Effect.runSync(Scope.make());
37
+ onCleanup(() => Effect.runPromise(Scope.close(scope, Exit.void)));
38
+ try {
39
+ return await Effect.runPromise(Layer.buildWithScope(layer, scope));
40
+ }
41
+ catch (error) {
42
+ await Effect.runPromise(Scope.close(scope, Exit.void));
43
+ throw error;
44
+ }
45
+ });
46
+ export const makeEffectIt = (options) => {
47
+ const fixtureIt = makeFixtureIt(options.layer);
48
+ const defaultClock = options.clock ?? "test";
49
+ const run = (body, context) => {
50
+ const program = Effect.gen(function* () {
51
+ const harness = yield* options.makeHarness(context);
52
+ return yield* Effect.gen(() => body(harness, context));
53
+ });
54
+ // The body constraint proves every yielded service is part of Provided.
55
+ // TypeScript cannot reduce that generic generator requirement here.
56
+ const ready = program;
57
+ const wrapped = options.around?.(ready) ?? ready;
58
+ return wrapped.pipe(Effect.provide(contextFrom(context)));
59
+ };
60
+ const register = (current) => (name, body, testOptions) => {
61
+ const split = splitOptions(testOptions);
62
+ const clock = split.clock ?? defaultClock;
63
+ current(name, split.vitest, ({ __effectTestContext, task, signal, onTestFailed, onTestFinished, skip, annotate, expect, _local, }) => {
64
+ const context = restoreContext(__effectTestContext, {
65
+ annotate,
66
+ expect,
67
+ onTestFailed,
68
+ onTestFinished,
69
+ signal,
70
+ skip,
71
+ task,
72
+ _local,
73
+ });
74
+ return runEffectTest(run(body, context), context, clock);
75
+ });
76
+ };
77
+ const effectApp = Object.assign(register(fixtureIt), {
78
+ skip: register(fixtureIt.skip),
79
+ skipIf: (condition) => register(fixtureIt.skipIf(condition)),
80
+ runIf: (condition) => register(fixtureIt.runIf(condition)),
81
+ only: register(fixtureIt.only),
82
+ each: (cases) => (name, body, testOptions) => {
83
+ const split = splitOptions(testOptions);
84
+ const clock = split.clock ?? defaultClock;
85
+ fixtureIt.for(cases)(name, split.vitest, (item, { __effectTestContext, task, signal, onTestFailed, onTestFinished, skip, annotate, expect, _local, }) => {
86
+ const context = restoreContext(__effectTestContext, {
87
+ annotate,
88
+ expect,
89
+ onTestFailed,
90
+ onTestFinished,
91
+ signal,
92
+ skip,
93
+ task,
94
+ _local,
95
+ });
96
+ return runEffectTest(run((harness, testContext) => body(item, harness, testContext), context), context, clock);
97
+ });
98
+ },
99
+ fails: register(fixtureIt.fails),
100
+ });
101
+ return { effectApp };
102
+ };
103
+ //# sourceMappingURL=vitest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vitest.js","sourceRoot":"","sources":["../src/vitest.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@shivaedev/effect-test",
3
+ "version": "0.0.0-bootstrap.0",
4
+ "description": "Generic Effect Vitest runner with worker-scoped Layers and TestClock",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ShivaeDev/platform.git",
10
+ "directory": "packages/effect-test"
11
+ },
12
+ "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/effect-test#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/ShivaeDev/platform/issues"
15
+ },
16
+ "engines": {
17
+ "node": ">=24"
18
+ },
19
+ "sideEffects": false,
20
+ "files": [
21
+ "dist",
22
+ "src",
23
+ "CHANGELOG.md",
24
+ "README.md"
25
+ ],
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "source": "./src/index.ts",
30
+ "import": "./dist/index.js",
31
+ "default": "./dist/index.js"
32
+ },
33
+ "./package.json": "./package.json"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public",
37
+ "provenance": true
38
+ },
39
+ "peerDependencies": {
40
+ "@effect/vitest": "4.0.0-rc.108",
41
+ "effect": "4.0.0-rc.108"
42
+ },
43
+ "peerDependenciesMeta": {
44
+ "@effect/vitest": {
45
+ "optional": true
46
+ }
47
+ },
48
+ "devDependencies": {
49
+ "@effect/vitest": "4.0.0-rc.108",
50
+ "@types/node": "24.10.1",
51
+ "@typescript/native": "npm:typescript@7.0.2",
52
+ "effect": "4.0.0-rc.108",
53
+ "typescript": "npm:@typescript/typescript6@6.0.2",
54
+ "vitest": "4.1.9"
55
+ },
56
+ "scripts": {
57
+ "build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc6 --project tsconfig.build.json",
58
+ "check": "biome check .",
59
+ "test": "vitest run",
60
+ "test:package": "node scripts/test-package.mjs",
61
+ "typecheck": "tsc --noEmit",
62
+ "typecheck:compat": "tsc6 --noEmit",
63
+ "ready": "pnpm check && pnpm typecheck && pnpm typecheck:compat && pnpm test && pnpm build && pnpm test:package"
64
+ }
65
+ }
@@ -0,0 +1,45 @@
1
+ import { Clock, type Duration, Effect, Exit, Schedule } from "effect";
2
+ import * as TestClock from "effect/testing/TestClock";
3
+
4
+ export interface EventuallyOptions {
5
+ readonly interval?: Duration.Input;
6
+ readonly times?: number;
7
+ }
8
+
9
+ const defaultInterval = "10 millis";
10
+
11
+ const isTestClock = (clock: Clock.Clock): clock is TestClock.TestClock =>
12
+ "adjust" in clock;
13
+
14
+ export const eventually = <A, E, R>(
15
+ effect: Effect.Effect<A, E, R>,
16
+ options?: EventuallyOptions,
17
+ ): Effect.Effect<A, E, R> =>
18
+ Effect.gen(function* () {
19
+ const interval = options?.interval ?? defaultInterval;
20
+ const clock = yield* Clock.Clock;
21
+
22
+ if (!isTestClock(clock)) {
23
+ return yield* options?.times === undefined
24
+ ? Effect.retry(effect, Schedule.spaced(interval))
25
+ : Effect.retry(effect, {
26
+ schedule: Schedule.spaced(interval),
27
+ times: options.times,
28
+ });
29
+ }
30
+
31
+ let retriesLeft = options?.times;
32
+ for (;;) {
33
+ const exit = yield* Effect.exit(effect);
34
+ if (Exit.isSuccess(exit)) {
35
+ return exit.value;
36
+ }
37
+ if (retriesLeft === 0) {
38
+ return yield* exit;
39
+ }
40
+ if (retriesLeft !== undefined) {
41
+ retriesLeft -= 1;
42
+ }
43
+ yield* TestClock.adjust(interval);
44
+ }
45
+ });
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export { type EventuallyOptions, eventually } from "./eventually.js";
2
+ export type {
3
+ EffectClock,
4
+ EffectTest,
5
+ EffectTester,
6
+ EffectTestOptions,
7
+ MakeEffectItOptions,
8
+ MakeEffectItResult,
9
+ } from "./types.js";
10
+ export { makeEffectIt } from "./vitest.js";
package/src/types.ts ADDED
@@ -0,0 +1,56 @@
1
+ import type { TestContext, TestOptions } from "@effect/vitest";
2
+ import type { Effect, Layer } from "effect";
3
+
4
+ export type EffectClock = "test" | "live";
5
+
6
+ export type EffectTestOptions = TestOptions & {
7
+ readonly clock?: EffectClock;
8
+ };
9
+
10
+ export type EffectTest<Harness, Provided> = <
11
+ A,
12
+ Eff extends Effect.Effect<unknown, unknown, Provided>,
13
+ >(
14
+ name: string,
15
+ body: (harness: Harness, context: TestContext) => Generator<Eff, A, never>,
16
+ options?: number | EffectTestOptions,
17
+ ) => void;
18
+
19
+ export interface EffectTester<Harness, Provided>
20
+ extends EffectTest<Harness, Provided> {
21
+ readonly skip: EffectTest<Harness, Provided>;
22
+ readonly skipIf: (condition: unknown) => EffectTest<Harness, Provided>;
23
+ readonly runIf: (condition: unknown) => EffectTest<Harness, Provided>;
24
+ readonly only: EffectTest<Harness, Provided>;
25
+ readonly each: <Item>(
26
+ cases: ReadonlyArray<Item>,
27
+ ) => <A, Eff extends Effect.Effect<unknown, unknown, Provided>>(
28
+ name: string,
29
+ body: (
30
+ item: Item,
31
+ harness: Harness,
32
+ context: TestContext,
33
+ ) => Generator<Eff, A, never>,
34
+ options?: number | EffectTestOptions,
35
+ ) => void;
36
+ readonly fails: EffectTest<Harness, Provided>;
37
+ }
38
+
39
+ export interface MakeEffectItOptions<
40
+ Harness,
41
+ // biome-ignore lint/suspicious/noExplicitAny: Layer output and error are recovered with Layer utility types
42
+ TestLayer extends Layer.Layer<any, any, never>,
43
+ > {
44
+ readonly layer: TestLayer;
45
+ readonly around?: <A, E>(
46
+ effect: Effect.Effect<A, E, Layer.Success<TestLayer>>,
47
+ ) => Effect.Effect<A, unknown, Layer.Success<TestLayer>>;
48
+ readonly makeHarness: (
49
+ context: TestContext,
50
+ ) => Effect.Effect<Harness, unknown, Layer.Success<TestLayer>>;
51
+ readonly clock?: EffectClock;
52
+ }
53
+
54
+ export interface MakeEffectItResult<Harness, Provided> {
55
+ readonly effectApp: EffectTester<Harness, Provided>;
56
+ }
package/src/vitest.ts ADDED
@@ -0,0 +1,246 @@
1
+ import {
2
+ it as effectIt,
3
+ type TestContext,
4
+ type TestOptions,
5
+ } from "@effect/vitest";
6
+ import { Cause, type Context, Effect, Exit, Layer, Scope } from "effect";
7
+ import * as TestClock from "effect/testing/TestClock";
8
+ import * as TestConsole from "effect/testing/TestConsole";
9
+ import type {
10
+ EffectClock,
11
+ EffectTest,
12
+ EffectTester,
13
+ EffectTestOptions,
14
+ MakeEffectItOptions,
15
+ MakeEffectItResult,
16
+ } from "./types.js";
17
+
18
+ const fixtureName = "__effectTestContext";
19
+
20
+ type EffectFixture<Provided> = TestContext & {
21
+ readonly [fixtureName]: Context.Context<Provided>;
22
+ };
23
+
24
+ interface FixtureTestApi<Provided> {
25
+ (
26
+ name: string,
27
+ options: TestOptions,
28
+ body: (context: EffectFixture<Provided>) => Promise<unknown>,
29
+ ): void;
30
+ readonly skip: FixtureTestApi<Provided>;
31
+ readonly skipIf: (condition: unknown) => FixtureTestApi<Provided>;
32
+ readonly runIf: (condition: unknown) => FixtureTestApi<Provided>;
33
+ readonly only: FixtureTestApi<Provided>;
34
+ readonly fails: FixtureTestApi<Provided>;
35
+ readonly for: <Item>(
36
+ cases: ReadonlyArray<Item>,
37
+ ) => (
38
+ name: string,
39
+ options: TestOptions,
40
+ body: (item: Item, context: EffectFixture<Provided>) => Promise<unknown>,
41
+ ) => void;
42
+ }
43
+
44
+ const TestClockEnvironment = Layer.mergeAll(
45
+ TestConsole.layer,
46
+ TestClock.layer(),
47
+ );
48
+ const LiveClockEnvironment = TestConsole.layer;
49
+
50
+ const environmentFor = (clock: EffectClock) =>
51
+ clock === "live" ? LiveClockEnvironment : TestClockEnvironment;
52
+
53
+ const restoreContext = <Provided>(
54
+ fixture: Context.Context<Provided>,
55
+ context: Omit<EffectFixture<Provided>, typeof fixtureName>,
56
+ ): EffectFixture<Provided> =>
57
+ ({
58
+ ...context,
59
+ [fixtureName]: fixture,
60
+ }) as EffectFixture<Provided>;
61
+
62
+ const contextFrom = <Provided>(
63
+ context: TestContext,
64
+ ): Context.Context<Provided> =>
65
+ (context as EffectFixture<Provided>)[fixtureName];
66
+
67
+ const runEffectTest = <A, E>(
68
+ effect: Effect.Effect<A, E, Scope.Scope>,
69
+ context: TestContext,
70
+ clock: EffectClock,
71
+ ): Promise<A> =>
72
+ Effect.runPromise(
73
+ Effect.gen(function* () {
74
+ const exit = yield* Effect.exit(effect);
75
+ if (Exit.isFailure(exit)) {
76
+ for (const error of Cause.prettyErrors(exit.cause)) {
77
+ yield* Effect.logError(error);
78
+ }
79
+ }
80
+ return yield* exit;
81
+ }).pipe(Effect.scoped, Effect.provide(environmentFor(clock))),
82
+ { signal: context.signal },
83
+ );
84
+
85
+ const splitOptions = (
86
+ options: number | EffectTestOptions | undefined,
87
+ ): {
88
+ readonly vitest: TestOptions;
89
+ readonly clock: EffectClock | undefined;
90
+ } => {
91
+ if (typeof options === "number") {
92
+ return { clock: undefined, vitest: { timeout: options } };
93
+ }
94
+ if (options === undefined) {
95
+ return { clock: undefined, vitest: {} };
96
+ }
97
+ const { clock, ...vitest } = options;
98
+ return { clock, vitest };
99
+ };
100
+
101
+ const makeFixtureIt = <Provided, LayerError>(
102
+ layer: Layer.Layer<Provided, LayerError>,
103
+ ): FixtureTestApi<Provided> =>
104
+ effectIt.extend(
105
+ fixtureName,
106
+ { scope: "worker" },
107
+ // biome-ignore lint/correctness/noEmptyPattern: Vitest fixtures require a destructured context parameter.
108
+ async ({}, { onCleanup }) => {
109
+ const scope = Effect.runSync(Scope.make());
110
+ onCleanup(() => Effect.runPromise(Scope.close(scope, Exit.void)));
111
+
112
+ try {
113
+ return await Effect.runPromise(Layer.buildWithScope(layer, scope));
114
+ } catch (error) {
115
+ await Effect.runPromise(Scope.close(scope, Exit.void));
116
+ throw error;
117
+ }
118
+ },
119
+ ) as unknown as FixtureTestApi<Provided>;
120
+
121
+ export const makeEffectIt = <
122
+ Harness,
123
+ // biome-ignore lint/suspicious/noExplicitAny: Layer output and error are recovered with Layer utility types
124
+ TestLayer extends Layer.Layer<any, any, never>,
125
+ >(
126
+ options: MakeEffectItOptions<Harness, TestLayer>,
127
+ ): MakeEffectItResult<Harness, Layer.Success<TestLayer>> => {
128
+ type Provided = Layer.Success<TestLayer>;
129
+ const fixtureIt = makeFixtureIt(options.layer);
130
+ const defaultClock = options.clock ?? "test";
131
+
132
+ const run = <A, Eff extends Effect.Effect<unknown, unknown, Provided>>(
133
+ body: (harness: Harness, context: TestContext) => Generator<Eff, A, never>,
134
+ context: TestContext,
135
+ ): Effect.Effect<A, unknown, Scope.Scope> => {
136
+ const program = Effect.gen(function* () {
137
+ const harness = yield* options.makeHarness(context);
138
+ return yield* Effect.gen(() => body(harness, context));
139
+ });
140
+ // The body constraint proves every yielded service is part of Provided.
141
+ // TypeScript cannot reduce that generic generator requirement here.
142
+ const ready = program as Effect.Effect<A, unknown, Provided>;
143
+ const wrapped = options.around?.(ready) ?? ready;
144
+ return wrapped.pipe(
145
+ Effect.provide(contextFrom<Provided>(context)),
146
+ ) as Effect.Effect<A, unknown, Scope.Scope>;
147
+ };
148
+
149
+ const register =
150
+ (current: FixtureTestApi<Provided>): EffectTest<Harness, Provided> =>
151
+ (name, body, testOptions) => {
152
+ const split = splitOptions(testOptions);
153
+ const clock = split.clock ?? defaultClock;
154
+ current(
155
+ name,
156
+ split.vitest,
157
+ ({
158
+ __effectTestContext,
159
+ task,
160
+ signal,
161
+ onTestFailed,
162
+ onTestFinished,
163
+ skip,
164
+ annotate,
165
+ expect,
166
+ _local,
167
+ }) => {
168
+ const context = restoreContext(__effectTestContext, {
169
+ annotate,
170
+ expect,
171
+ onTestFailed,
172
+ onTestFinished,
173
+ signal,
174
+ skip,
175
+ task,
176
+ _local,
177
+ });
178
+ return runEffectTest(run(body, context), context, clock);
179
+ },
180
+ );
181
+ };
182
+
183
+ const effectApp: EffectTester<Harness, Provided> = Object.assign(
184
+ register(fixtureIt),
185
+ {
186
+ skip: register(fixtureIt.skip),
187
+ skipIf: (condition: unknown) => register(fixtureIt.skipIf(condition)),
188
+ runIf: (condition: unknown) => register(fixtureIt.runIf(condition)),
189
+ only: register(fixtureIt.only),
190
+ each:
191
+ <Item>(cases: ReadonlyArray<Item>) =>
192
+ <A, Eff extends Effect.Effect<unknown, unknown, Provided>>(
193
+ name: string,
194
+ body: (
195
+ item: Item,
196
+ harness: Harness,
197
+ context: TestContext,
198
+ ) => Generator<Eff, A, never>,
199
+ testOptions?: number | EffectTestOptions,
200
+ ) => {
201
+ const split = splitOptions(testOptions);
202
+ const clock = split.clock ?? defaultClock;
203
+ fixtureIt.for(cases)(
204
+ name,
205
+ split.vitest,
206
+ (
207
+ item,
208
+ {
209
+ __effectTestContext,
210
+ task,
211
+ signal,
212
+ onTestFailed,
213
+ onTestFinished,
214
+ skip,
215
+ annotate,
216
+ expect,
217
+ _local,
218
+ },
219
+ ) => {
220
+ const context = restoreContext(__effectTestContext, {
221
+ annotate,
222
+ expect,
223
+ onTestFailed,
224
+ onTestFinished,
225
+ signal,
226
+ skip,
227
+ task,
228
+ _local,
229
+ });
230
+ return runEffectTest(
231
+ run(
232
+ (harness, testContext) => body(item, harness, testContext),
233
+ context,
234
+ ),
235
+ context,
236
+ clock,
237
+ );
238
+ },
239
+ );
240
+ },
241
+ fails: register(fixtureIt.fails),
242
+ },
243
+ );
244
+
245
+ return { effectApp };
246
+ };