From 7270c1d14ff6e6316af760d80fa9488a26de10c4 Mon Sep 17 00:00:00 2001 From: Samuel Laycock Date: Tue, 12 May 2026 22:10:45 +0100 Subject: [PATCH 1/2] test: add example smoke coverage Add a Bun smoke test that discovers and executes every TypeScript example so examples fail CI when they drift from the public API. Make the batch-job example deterministic by injecting a test UserApi instead of performing a real network request during example execution. Document the examples smoke workflow and include a patch changeset. --- .changeset/runnable-examples-smoke.md | 5 +++++ conventions/TESTING.md | 1 + examples/batch-job.ts | 25 ++++++++++++++++++------- package.json | 1 + test/examples-smoke.test.ts | 22 ++++++++++++++++++++++ 5 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 .changeset/runnable-examples-smoke.md create mode 100644 test/examples-smoke.test.ts diff --git a/.changeset/runnable-examples-smoke.md b/.changeset/runnable-examples-smoke.md new file mode 100644 index 0000000..aed828e --- /dev/null +++ b/.changeset/runnable-examples-smoke.md @@ -0,0 +1,5 @@ +--- +"fluent-effect": patch +--- + +Add runnable Bun smoke coverage for every TypeScript example. diff --git a/conventions/TESTING.md b/conventions/TESTING.md index 9222f55..6c39e40 100644 --- a/conventions/TESTING.md +++ b/conventions/TESTING.md @@ -4,3 +4,4 @@ Use descriptive test names. Group related tests using describe blocks. Aim for high code coverage. Avoid duplication of testing scenarios. +Examples under `examples/` are executed by `bun run examples:smoke`; keep them deterministic, fast, and free of real network calls. diff --git a/examples/batch-job.ts b/examples/batch-job.ts index 4702af1..1a4b803 100644 --- a/examples/batch-job.ts +++ b/examples/batch-job.ts @@ -5,18 +5,25 @@ interface User { readonly name: string; } +interface UserApi { + readonly fetchUser: (id: string, signal: AbortSignal) => Promise; +} + const AppError = fx.errors<{ NetworkError: { cause: unknown }; TimeoutError: { operation: string }; }>(); +const UserApi = fx.dependency("UserApi"); + const fetchUser = (id: string) => - fx.try({ - try: async (signal) => { - const response = await fetch(`https://example.com/users/${id}`, { signal }); - return (await response.json()) as User; - }, - catch: (cause) => AppError.NetworkError({ cause }), + fx.task(function* () { + const userApi = yield* fx.getDependency(UserApi); + + return yield* fx.try({ + try: (signal) => userApi.fetchUser(id, signal), + catch: (cause) => AppError.NetworkError({ cause }), + }); }); const loadUser = (id: string) => @@ -48,4 +55,8 @@ const loadUsers = (ids: readonly string[]) => return users; }); -export const main = fx.run(loadUsers(["1", "2", "3"])); +const testUserApi = fx.provideDependency(UserApi, { + fetchUser: async (id) => ({ id, name: `User ${id}` }), +}); + +export const main = fx.runWith(loadUsers(["1", "2", "3"]), fx.dependencies(testUserApi)); diff --git a/package.json b/package.json index dccac1d..24f0332 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ }, "scripts": { "test": "bun test", + "examples:smoke": "bun test test/examples-smoke.test.ts", "lint": "oxlint . --type-aware", "lint:fix": "oxlint . --type-aware --fix", "fmt": "oxfmt .", diff --git a/test/examples-smoke.test.ts b/test/examples-smoke.test.ts new file mode 100644 index 0000000..1a9d3a7 --- /dev/null +++ b/test/examples-smoke.test.ts @@ -0,0 +1,22 @@ +import { $ } from "bun"; +import { describe, expect, test } from "bun:test"; +import { readdir } from "node:fs/promises"; +import { join } from "node:path"; + +const repositoryDirectory = join(import.meta.dir, ".."); +const examplesDirectory = join(repositoryDirectory, "examples"); + +const getExampleFiles = async () => + (await readdir(examplesDirectory)).filter((file) => file.endsWith(".ts")).sort(); + +describe("examples smoke tests", () => { + test("every TypeScript example executes with Bun", async () => { + const exampleFiles = await getExampleFiles(); + + expect(exampleFiles.length).toBeGreaterThan(0); + + for (const exampleFile of exampleFiles) { + await $`bun ${join(examplesDirectory, exampleFile)}`.cwd(repositoryDirectory).quiet(); + } + }); +}); From 58b769100deb513baf3ad2efdee1ab420b41abb5 Mon Sep 17 00:00:00 2001 From: Samuel Laycock Date: Wed, 13 May 2026 15:29:20 +0100 Subject: [PATCH 2/2] test: address example smoke review feedback Register one smoke test per TypeScript example so multiple failures are reported independently. Rename the batch-job example dependency provider from testUserApi to stubUserApi to better communicate that it is example stub wiring. --- examples/batch-job.ts | 4 ++-- test/examples-smoke.test.ts | 19 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/examples/batch-job.ts b/examples/batch-job.ts index 1a4b803..6cfaa91 100644 --- a/examples/batch-job.ts +++ b/examples/batch-job.ts @@ -55,8 +55,8 @@ const loadUsers = (ids: readonly string[]) => return users; }); -const testUserApi = fx.provideDependency(UserApi, { +const stubUserApi = fx.provideDependency(UserApi, { fetchUser: async (id) => ({ id, name: `User ${id}` }), }); -export const main = fx.runWith(loadUsers(["1", "2", "3"]), fx.dependencies(testUserApi)); +export const main = fx.runWith(loadUsers(["1", "2", "3"]), fx.dependencies(stubUserApi)); diff --git a/test/examples-smoke.test.ts b/test/examples-smoke.test.ts index 1a9d3a7..3a0477c 100644 --- a/test/examples-smoke.test.ts +++ b/test/examples-smoke.test.ts @@ -1,22 +1,21 @@ import { $ } from "bun"; import { describe, expect, test } from "bun:test"; -import { readdir } from "node:fs/promises"; +import { readdirSync } from "node:fs"; import { join } from "node:path"; const repositoryDirectory = join(import.meta.dir, ".."); const examplesDirectory = join(repositoryDirectory, "examples"); -const getExampleFiles = async () => - (await readdir(examplesDirectory)).filter((file) => file.endsWith(".ts")).sort(); +const exampleFiles = readdirSync(examplesDirectory) + .filter((file) => file.endsWith(".ts")) + .sort(); describe("examples smoke tests", () => { - test("every TypeScript example executes with Bun", async () => { - const exampleFiles = await getExampleFiles(); + expect(exampleFiles.length).toBeGreaterThan(0); - expect(exampleFiles.length).toBeGreaterThan(0); - - for (const exampleFile of exampleFiles) { + for (const exampleFile of exampleFiles) { + test(`${exampleFile} executes with Bun`, async () => { await $`bun ${join(examplesDirectory, exampleFile)}`.cwd(repositoryDirectory).quiet(); - } - }); + }); + } });