diff --git a/packages/ai/README.md b/packages/ai/README.md index 6f088d2b2a4..d1ddf209f16 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -214,22 +214,20 @@ the requests sent by code under test: import { Effect } from "effect" import { TestLLM } from "@opencode-ai/ai/testing" -const testLLM = TestLLM.layer({ - fallback: TestLLM.text("Hello from the test model", "text-1"), +const testLLM = TestLLM.layerWithClient({ + fallback: TestLLM.text("Hello from the test model"), }) -// TestLLM.clientLayer provides LLMClient.Service and consumes TestLLM.Service. const programWithTestClient = Effect.gen(function* () { const result = yield* program - const test = yield* TestLLM.Service - console.log(test.requests) + console.log(yield* TestLLM.requests) return result -}).pipe(Effect.provide(TestLLM.clientLayer), Effect.provide(testLLM)) +}).pipe(Effect.provide(testLLM)) ``` `TestLLM.push(...)` scripts one-shot responses, `TestLLM.always(...)` changes the fallback, and `TestLLM.wait(...)` lets concurrent tests wait until a request has arrived. Every received canonical request is -available on the yielded `TestLLM.Service`. +available from `TestLLM.requests`. ## Caching diff --git a/packages/ai/src/testing.ts b/packages/ai/src/testing.ts index 475569a24ec..612db94be6d 100644 --- a/packages/ai/src/testing.ts +++ b/packages/ai/src/testing.ts @@ -53,7 +53,7 @@ const textEvents = (value: string, id: string) => [ LLMEvent.textEnd({ id }), ] -export const text = (value: string, id: string) => stop(...textEvents(value, id)) +export const text = (value: string, id = "text-0") => stop(...textEvents(value, id)) export const textWithUsage = (value: string, id: string, inputTokens: number) => complete( @@ -147,6 +147,10 @@ export const clientLayer = Layer.effect( Effect.map(Service, (service) => service.client), ) +export const layerWithClient = (options: LayerOptions = {}) => clientLayer.pipe(Layer.provideMerge(layer(options))) + +export const requests = Service.use((service) => Effect.succeed(service.requests)) + export const push = (...responses: readonly Response[]) => Service.use((service) => service.push(...responses)) export const always = (response: Response) => Service.use((service) => service.always(response)) diff --git a/packages/ai/test/exports.test.ts b/packages/ai/test/exports.test.ts index b0003b55d7b..1a6bf99031e 100644 --- a/packages/ai/test/exports.test.ts +++ b/packages/ai/test/exports.test.ts @@ -32,6 +32,8 @@ describe("public exports", () => { expect(Provider.make).toBeFunction() expect(ProviderSubpath.make).toBe(Provider.make) expect(TestLLM.layer).toBeFunction() + expect(TestLLM.layerWithClient).toBeFunction() + expect(TestLLM.requests).toBeDefined() }) test("route barrel exposes route-authoring APIs", () => { diff --git a/packages/ai/test/testing.test.ts b/packages/ai/test/testing.test.ts new file mode 100644 index 00000000000..f7cafb54161 --- /dev/null +++ b/packages/ai/test/testing.test.ts @@ -0,0 +1,19 @@ +import { expect } from "bun:test" +import { Effect } from "effect" +import { LLM, LLMClient, LLMEvent } from "../src" +import { OpenAIChat } from "../src/protocols" +import { TestLLM } from "../src/testing" +import { testEffect } from "./lib/effect" + +const model = OpenAIChat.route.model({ id: "test" }) +const it = testEffect(TestLLM.layerWithClient({ fallback: TestLLM.text("Hello") })) + +it.effect("provides a client and exposes received requests", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate(LLM.request({ model, prompt: "Say hello" })) + + expect(response.text).toBe("Hello") + expect(response.events.filter(LLMEvent.is.textDelta)).toEqual([{ type: "text-delta", id: "text-0", text: "Hello" }]) + expect(yield* TestLLM.requests).toHaveLength(1) + }), +) diff --git a/packages/core/test/generate.test.ts b/packages/core/test/generate.test.ts index 68d56235a00..3a4bef63fe0 100644 --- a/packages/core/test/generate.test.ts +++ b/packages/core/test/generate.test.ts @@ -65,7 +65,7 @@ const aisdk = Layer.mock(AISDK.Service, { }, model: () => Effect.succeed(runtime), }) -const client = TestLLM.clientLayer.pipe(Layer.provide(TestLLM.layer({ fallback: TestLLM.text("OK", "generate") }))) +const client = TestLLM.layerWithClient({ fallback: TestLLM.text("OK") }) const resolver = ModelResolver.layer.pipe(Layer.provide(Layer.mergeAll(catalog, integrations, npm, aisdk))) const it = testEffect(Generate.layer.pipe(Layer.provide(Layer.merge(resolver, client))))