From personal-skills
Provides testing patterns for Effect-TS services, errors, layers, and effects using @effect/vitest. Use when writing tests for Effect-based TypeScript code.
npx claudepluginhub enitrat/skill-issue --plugin personal-skillsThis skill uses the workspace's default tool permissions.
Comprehensive testing patterns for Effect-TS services, errors, layers, and effects. Use this skill when writing tests for Effect-based code.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Comprehensive testing patterns for Effect-TS services, errors, layers, and effects. Use this skill when writing tests for Effect-based code.
import { describe, it, expect } from "@effect/vitest"
import { Effect } from "effect"
// Basic test - it.effect provides TestContext automatically
it.effect("test name", () =>
Effect.gen(function* () {
const result = yield* someEffect
expect(result).toBe(expected)
})
)
// Test with layers - provide dependencies to all tests
it.layer(MyServiceLive)("test with service", () =>
Effect.gen(function* () {
const service = yield* MyService
const result = yield* service.doSomething()
expect(result).toBe(expected)
})
)
// Scoped tests - automatically handles resource cleanup
it.scoped("test with resources", () =>
Effect.gen(function* () {
const resource = yield* acquireResource
// resource automatically released after test
yield* useResource(resource)
})
)
Key Features:
it.effect - automatic TestContext provision (TestClock, TestRandom, etc.)it.layer - provide layers to test suite, shared across testsit.scoped - automatic resource cleanupimport { Effect, Context, Layer } from "effect"
// Service definition
class DatabaseService extends Context.Tag("DatabaseService")<
DatabaseService,
{
readonly query: (sql: string) => Effect.Effect<unknown>
}
>() {}
// Live implementation (production)
export const DatabaseServiceLive = Layer.succeed(
DatabaseService,
{
query: (sql) => Effect.promise(() => realDatabase.query(sql))
}
)
// Test implementation (mocked)
export const DatabaseServiceTest = Layer.succeed(
DatabaseService,
{
query: (sql) => Effect.succeed({ rows: [{ id: 1, name: "test" }] })
}
)
// Usage in test
it.layer(DatabaseServiceTest)("queries database", () =>
Effect.gen(function* () {
const db = yield* DatabaseService
const result = yield* db.query("SELECT * FROM users")
expect(result).toEqual({ rows: [{ id: 1, name: "test" }] })
})
)
Convention: Use "Live" suffix for production, "Test" suffix for mocks.
import { Layer } from "effect"
// Partial mock - only implement methods you need
const PartialDatabaseMock = Layer.mock(DatabaseService, {
query: (sql) => Effect.succeed({ rows: [] })
// Other methods throw UnimplementedError when called
})
// Full mock with test doubles
const MockWithSpy = Layer.succeed(DatabaseService, {
query: vi.fn().mockReturnValue(Effect.succeed({ rows: [] }))
})
class UserService extends Context.Tag("UserService")<
UserService,
{
readonly getUser: (id: number) => Effect.Effect<User, UserNotFound>
}
>() {}
class EmailService extends Context.Tag("EmailService")<
EmailService,
{
readonly sendEmail: (to: string, body: string) => Effect.Effect<void>
}
>() {}
// Service that depends on other services
class NotificationService extends Context.Tag("NotificationService")<
NotificationService,
{
readonly notifyUser: (userId: number) => Effect.Effect<void, UserNotFound>
}
>() {
static Live = Layer.effect(
NotificationService,
Effect.gen(function* () {
const users = yield* UserService
const email = yield* EmailService
return {
notifyUser: (userId) =>
Effect.gen(function* () {
const user = yield* users.getUser(userId)
yield* email.sendEmail(user.email, "Notification")
})
}
})
)
}
// Test with all dependencies mocked
const TestLayer = Layer.mergeAll(
UserServiceTest,
EmailServiceTest
).pipe(Layer.provideMerge(NotificationService.Live))
it.layer(TestLayer)("sends notification", () =>
Effect.gen(function* () {
const notif = yield* NotificationService
yield* notif.notifyUser(1)
// Verify email was sent using mocked EmailService
})
)
import { Effect, Exit } from "effect"
class MyError extends Data.TaggedError("MyError")<{
readonly message: string
}> {}
it.effect("handles expected errors", () =>
Effect.gen(function* () {
const result = yield* Effect.exit(
Effect.fail(new MyError({ message: "test error" }))
)
// Check error occurred
expect(Exit.isFailure(result)).toBe(true)
// Check error type
if (Exit.isFailure(result)) {
const cause = result.cause
expect(Cause.isFailType(cause)).toBe(true)
// Extract error value
const error = Cause.failureOption(cause)
expect(error).toBeSome()
expect(Option.getOrThrow(error)).toBeInstanceOf(MyError)
}
})
)
// Alternative: use catchTag to verify error
it.effect("catches specific error type", () =>
Effect.gen(function* () {
let caught = false
yield* effectThatFails.pipe(
Effect.catchTag("MyError", (error) =>
Effect.sync(() => {
caught = true
expect(error.message).toBe("test error")
})
)
)
expect(caught).toBe(true)
})
)
type UserServiceError = UserNotFound | DatabaseError | ValidationError
it.effect("handles multiple error types", () =>
Effect.gen(function* () {
const result = yield* Effect.either(service.getUser(999))
expect(Either.isLeft(result)).toBe(true)
if (Either.isLeft(result)) {
// Pattern match on error type
const error = result.left
if (error._tag === "UserNotFound") {
expect(error.userId).toBe(999)
}
}
})
)
it.effect("handles unexpected errors (defects)", () =>
Effect.gen(function* () {
const result = yield* Effect.exit(
Effect.die(new Error("Unexpected"))
)
expect(Exit.isFailure(result)).toBe(true)
if (Exit.isFailure(result)) {
expect(Cause.isDie(result.cause)).toBe(true)
}
})
)
import { TestClock } from "effect"
it.effect("delays execution", () =>
Effect.gen(function* () {
let executed = false
// Fork effect that delays 1 second
const fiber = yield* Effect.fork(
Effect.delay("1 second")(
Effect.sync(() => { executed = true })
)
)
// Verify not executed yet
expect(executed).toBe(false)
// Advance time by 1 second
yield* TestClock.adjust("1 second")
// Wait for fiber to complete
yield* Fiber.join(fiber)
// Verify executed after time advance
expect(executed).toBe(true)
})
)
// Testing intervals
it.effect("processes scheduled tasks", () =>
Effect.gen(function* () {
const results: number[] = []
const fiber = yield* Effect.fork(
Effect.repeat(
Effect.sync(() => results.push(Date.now())),
Schedule.spaced("100 millis")
).pipe(Effect.timeout("1 second"))
)
// Advance time in increments
yield* TestClock.adjust("100 millis")
yield* TestClock.adjust("100 millis")
yield* TestClock.adjust("100 millis")
yield* Fiber.join(fiber)
expect(results.length).toBeGreaterThan(0)
})
)
import { TestRandom } from "effect"
it.effect("deterministic random values", () =>
Effect.gen(function* () {
// Set fixed random seed for reproducibility
yield* TestRandom.setSeed(42)
const random1 = yield* Random.next
const random2 = yield* Random.next
// Reset seed - same values again
yield* TestRandom.setSeed(42)
const random3 = yield* Random.next
expect(random3).toBe(random1)
})
)
// Helper to create fresh layer for each test
const makeFreshLayer = () =>
Layer.succeed(MyService, {
state: { counter: 0 }, // Fresh state per test
increment: function() {
this.state.counter++
return Effect.succeed(this.state.counter)
}
})
describe("isolated tests", () => {
it.effect("test 1", () =>
Effect.gen(function* () {
const service = yield* MyService
const result = yield* service.increment()
expect(result).toBe(1) // Fresh state
}).pipe(Effect.provide(makeFreshLayer()))
)
it.effect("test 2", () =>
Effect.gen(function* () {
const service = yield* MyService
const result = yield* service.increment()
expect(result).toBe(1) // Fresh state again
}).pipe(Effect.provide(makeFreshLayer()))
)
})
// Test layer dependencies are wired correctly
it.effect("layer composition", () =>
Effect.gen(function* () {
// This test verifies all dependencies resolve
const service = yield* TopLevelService
const result = yield* service.operation()
expect(result).toBeDefined()
}).pipe(
Effect.provide(
TopLevelService.Live.pipe(
Layer.provide(MiddleService.Live),
Layer.provide(BottomService.Live)
)
)
)
)
import { ConfigProvider, Layer } from "effect"
// Mock config for tests
const TestConfig = Layer.setConfigProvider(
ConfigProvider.fromMap(
new Map([
["API_KEY", "test-key"],
["DATABASE_URL", "postgres://test"],
["PORT", "3000"]
])
)
)
it.layer(TestConfig)("uses test configuration", () =>
Effect.gen(function* () {
const apiKey = yield* Config.string("API_KEY")
expect(apiKey).toBe("test-key")
})
)
import { FetchHttpClient, HttpClient } from "@effect/platform"
// Mock fetch for testing
const MockFetch = Layer.succeed(
FetchHttpClient.Fetch,
() => Promise.resolve(
new Response(JSON.stringify({ data: "test" }), {
status: 200,
headers: { "content-type": "application/json" }
})
)
)
it.layer(MockFetch)("makes HTTP request", () =>
Effect.gen(function* () {
const client = yield* HttpClient.HttpClient
const response = yield* client.get("https://api.example.com/data")
const json = yield* response.json
expect(json).toEqual({ data: "test" })
})
)
it.effect("race condition handling", () =>
Effect.gen(function* () {
const ref = yield* Ref.make(0)
// Run 100 concurrent increments
yield* Effect.all(
Array.from({ length: 100 }, () =>
Ref.update(ref, (n) => n + 1)
),
{ concurrency: "unbounded" }
)
const final = yield* Ref.get(ref)
expect(final).toBe(100) // Ref is atomic
})
)
it.effect("parallel execution", () =>
Effect.gen(function* () {
const fiber1 = yield* Effect.fork(longRunningTask1)
const fiber2 = yield* Effect.fork(longRunningTask2)
const [result1, result2] = yield* Effect.all([
Fiber.join(fiber1),
Fiber.join(fiber2)
])
expect(result1).toBeDefined()
expect(result2).toBeDefined()
})
)
import { Stream, Chunk } from "effect"
it.effect("stream processing", () =>
Effect.gen(function* () {
const stream = Stream.range(1, 5)
const result = yield* Stream.runCollect(
stream.pipe(Stream.map((n) => n * 2))
)
expect(Chunk.toArray(result)).toEqual([2, 4, 6, 8, 10])
})
)
it.effect("stream error handling", () =>
Effect.gen(function* () {
const stream = Stream.range(1, 10).pipe(
Stream.map((n) =>
n === 5 ? Effect.fail("error") : Effect.succeed(n)
),
Stream.flatMap(identity)
)
const result = yield* Effect.exit(Stream.runCollect(stream))
expect(Exit.isFailure(result)).toBe(true)
})
)
// For integrating with Promise-based test frameworks
describe("promise integration", () => {
it("converts effect to promise", async () => {
const effect = Effect.succeed(42)
const result = await Effect.runPromise(effect)
expect(result).toBe(42)
})
it("rejects on failure", async () => {
const effect = Effect.fail(new MyError({ message: "fail" }))
await expect(Effect.runPromise(effect)).rejects.toThrow()
})
it("provides dependencies for promise", async () => {
const effect = Effect.gen(function* () {
const service = yield* MyService
return yield* service.doSomething()
})
const result = await Effect.runPromise(
effect.pipe(Effect.provide(MyServiceTest))
)
expect(result).toBeDefined()
})
})
// Good
it.effect("returns user when found in database", () => ...)
it.effect("fails with UserNotFound when user does not exist", () => ...)
// Bad
it.effect("test 1", () => ...)
it.effect("works", () => ...)
describe("UserService.getUser", () => {
it.effect("returns user when exists", () => ...)
it.effect("fails with UserNotFound when not exists", () => ...)
it.effect("fails with DatabaseError on connection failure", () => ...)
})
// Good - each test gets fresh state
it.effect("test 1", () =>
Effect.provide(effect, makeFreshLayer())
)
// Bad - shared state between tests
const sharedLayer = makeLayer()
it.layer(sharedLayer)("test 1", () => ...)
it.layer(sharedLayer)("test 2", () => ...) // May see state from test 1
// Good - instant, deterministic
it.effect("delays 1 hour", () =>
Effect.gen(function* () {
const fiber = yield* Effect.fork(Effect.delay("1 hour")(task))
yield* TestClock.adjust("1 hour")
yield* Fiber.join(fiber)
})
)
// Bad - actually waits 1 hour
it.effect("delays 1 hour", () =>
Effect.delay("1 hour")(task)
)
// Good - all external services mocked
const TestLayer = Layer.mergeAll(
DatabaseServiceTest,
EmailServiceTest,
PaymentServiceTest
)
// Bad - tests hit real services (slow, flaky, expensive)
const TestLayer = Layer.mergeAll(
DatabaseServiceLive, // ❌ Real DB
EmailServiceLive, // ❌ Sends real emails
PaymentServiceLive // ❌ Charges real money
)
it.effect("propagates errors through effect chain", () =>
Effect.gen(function* () {
const result = yield* Effect.exit(
service.getUser(999).pipe(
Effect.flatMap((user) => service.processUser(user)),
Effect.flatMap((processed) => service.saveUser(processed))
)
)
// Verify UserNotFound from getUser propagated through chain
expect(Exit.isFailure(result)).toBe(true)
})
)
// Good - automatic cleanup
it.scoped("uses database connection", () =>
Effect.gen(function* () {
const conn = yield* acquireConnection // Scope manages lifecycle
yield* useConnection(conn)
// Connection automatically closed after test
})
)
// Bad - manual cleanup (easy to forget)
it.effect("uses database connection", () =>
Effect.gen(function* () {
const conn = yield* acquireConnection
try {
yield* useConnection(conn)
} finally {
yield* releaseConnection(conn)
}
})
)
it.effect("retries on failure", () =>
Effect.gen(function* () {
let attempts = 0
const effect = Effect.gen(function* () {
attempts++
if (attempts < 3) {
yield* Effect.fail("retry me")
}
return "success"
}).pipe(
Effect.retry(Schedule.recurs(5))
)
const result = yield* effect
expect(result).toBe("success")
expect(attempts).toBe(3)
})
)
it.effect("times out long operations", () =>
Effect.gen(function* () {
const longOp = Effect.delay("5 seconds")(Effect.succeed("done"))
const result = yield* Effect.exit(
longOp.pipe(Effect.timeout("1 second"))
)
expect(Exit.isFailure(result)).toBe(true)
// Verify it's a TimeoutException
}).pipe(Effect.provide(TestClock.layer))
)
it.effect("handles interruption gracefully", () =>
Effect.gen(function* () {
const ref = yield* Ref.make("initial")
const fiber = yield* Effect.fork(
Effect.gen(function* () {
yield* Ref.set(ref, "started")
yield* Effect.sleep("1 hour")
yield* Ref.set(ref, "completed") // Should never reach here
})
)
yield* TestClock.adjust("1 second")
yield* Fiber.interrupt(fiber)
const value = yield* Ref.get(ref)
expect(value).toBe("started") // Not "completed"
})
)