From 02b7367b4e69f7c97112069887602a2e5765c549 Mon Sep 17 00:00:00 2001 From: James Long Date: Thu, 14 May 2026 14:59:31 -0400 Subject: [PATCH] add default script --- .../src/testing/simulation/provider.ts | 68 +++++++++++-------- .../src/testing/simulation/service.ts | 10 ++- .../test/testing/simulation/service.test.ts | 30 ++++++++ 3 files changed, 78 insertions(+), 30 deletions(-) diff --git a/packages/opencode/src/testing/simulation/provider.ts b/packages/opencode/src/testing/simulation/provider.ts index 9f7121ec7..0c61ca083 100644 --- a/packages/opencode/src/testing/simulation/provider.ts +++ b/packages/opencode/src/testing/simulation/provider.ts @@ -1,4 +1,5 @@ -import type { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3FinishReason } from "@ai-sdk/provider" +import type { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3FinishReason, LanguageModelV3StreamPart } from "@ai-sdk/provider" +import { simulateReadableStream } from "ai" import { Effect, Layer } from "effect" import { Provider } from "@/provider/provider" import { ModelID, ProviderID } from "@/provider/schema" @@ -39,6 +40,15 @@ const provider: Provider.Info = { models: { [modelID]: model }, } +const defaultScript: LLMScript = { + steps: [[{ type: "text", content: "Simulation mock response." }]], + finish: "stop", +} + +function nextScript(simulation: Simulation.Interface) { + return Effect.runPromise(simulation.nextLLM().pipe(Effect.catch(() => Effect.succeed(defaultScript)))) +} + function text(script: LLMScript) { return script.steps[0]?.flatMap((item) => (item.type === "text" || item.type === "thinking" ? [item.content] : [])) .join("") ?? "" @@ -49,31 +59,33 @@ function error(script: LLMScript) { } function stream(script: LLMScript) { - return new ReadableStream({ - start(controller) { - controller.enqueue({ type: "stream-start", warnings: [] }) - let index = 0 - for (const item of script.steps[0] ?? []) { - index++ - if (item.type === "error") { - controller.enqueue({ type: "error", error: new Error(item.message) }) - controller.close() - return - } - const id = `simulation-${item.type}-${index}` - if (item.type === "thinking") { - controller.enqueue({ type: "reasoning-start", id }) - controller.enqueue({ type: "reasoning-delta", id, delta: item.content }) - controller.enqueue({ type: "reasoning-end", id }) - continue - } - controller.enqueue({ type: "text-start", id }) - controller.enqueue({ type: "text-delta", id, delta: item.content }) - controller.enqueue({ type: "text-end", id }) - } - controller.enqueue({ type: "finish", finishReason: finishReason(script), usage: usage(script) }) - controller.close() - }, + const chunks: LanguageModelV3StreamPart[] = [{ type: "stream-start", warnings: [] }] + for (const [index, item] of (script.steps[0] ?? []).entries()) { + if (item.type === "error") { + chunks.push({ type: "error", error: new Error(item.message) }) + continue + } + const id = `simulation-${item.type}-${index + 1}` + if (item.type === "thinking") { + chunks.push( + { type: "reasoning-start", id }, + { type: "reasoning-delta", id, delta: item.content }, + { type: "reasoning-end", id }, + ) + continue + } + chunks.push( + { type: "text-start", id }, + { type: "text-delta", id, delta: item.content }, + { type: "text-end", id }, + ) + } + chunks.push({ type: "finish", finishReason: finishReason(script), usage: usage(script) }) + + return simulateReadableStream({ + chunks, + initialDelayInMs: 0, + chunkDelayInMs: 0, }) } @@ -105,7 +117,7 @@ function language(simulation: Simulation.Interface): LanguageModelV3 { modelId: modelID, supportedUrls: {}, async doGenerate(_options: LanguageModelV3CallOptions) { - const script = await Effect.runPromise(simulation.nextLLM()) + const script = await nextScript(simulation) const err = error(script) if (err?.type === "error") throw new Error(err.message) return { @@ -116,7 +128,7 @@ function language(simulation: Simulation.Interface): LanguageModelV3 { } }, async doStream(_options: LanguageModelV3CallOptions) { - const script = await Effect.runPromise(simulation.nextLLM()) + const script = await nextScript(simulation) return { stream: stream(script) } }, } diff --git a/packages/opencode/src/testing/simulation/service.ts b/packages/opencode/src/testing/simulation/service.ts index 222c1d333..36e90e159 100644 --- a/packages/opencode/src/testing/simulation/service.ts +++ b/packages/opencode/src/testing/simulation/service.ts @@ -108,11 +108,17 @@ export const layer = Layer.effect( Effect.gen(function* () { const fs = yield* AppFileSystem.Service const network = yield* SimulationNetwork.Service - const state = yield* Ref.make({ files: [], networkRegistrations: [], llmScripts: [], consumedLLMScripts: 0 }) + const empty: State = { + files: [], + networkRegistrations: [], + llmScripts: [], + consumedLLMScripts: 0, + } + const state = yield* Ref.make(empty) const reset = Effect.fn("Simulation.reset")(function* () { yield* network.reset() - yield* Ref.set(state, { files: [], networkRegistrations: [], llmScripts: [], consumedLLMScripts: 0 }) + yield* Ref.set(state, empty) }) const seedFilesystem = Effect.fn("Simulation.seedFilesystem")(function* (input: typeof FilesystemSeedInput.Type) { diff --git a/packages/opencode/test/testing/simulation/service.test.ts b/packages/opencode/test/testing/simulation/service.test.ts index e1bb20486..f099cc687 100644 --- a/packages/opencode/test/testing/simulation/service.test.ts +++ b/packages/opencode/test/testing/simulation/service.test.ts @@ -98,6 +98,36 @@ describe("Simulation", () => { }), ) + it.effect("simulation provider returns a default response when no script is queued", () => + Effect.gen(function* () { + const provider = yield* Provider.Service + const model = yield* provider.defaultModel().pipe(Effect.flatMap((item) => provider.getModel(item.providerID, item.modelID))) + const language = yield* provider.getLanguage(model) + + const result = yield* Effect.promise(() => language.doGenerate({ prompt: [], abortSignal: undefined })) + expect(result.content).toEqual([{ type: "text", text: "Simulation mock response." }]) + }), + ) + + it.effect("simulation provider streams a default response when no script is queued", () => + Effect.gen(function* () { + const provider = yield* Provider.Service + const model = yield* provider.defaultModel().pipe(Effect.flatMap((item) => provider.getModel(item.providerID, item.modelID))) + const language = yield* provider.getLanguage(model) + + const result = yield* Effect.promise(() => language.doStream({ prompt: [], abortSignal: undefined })) + const reader = result.stream.getReader() + const parts: unknown[] = [] + while (true) { + const next = yield* Effect.promise(() => reader.read()) + if (next.done) break + parts.push(next.value) + } + + expect(parts).toContainEqual({ type: "text-delta", id: "simulation-text-1", delta: "Simulation mock response." }) + }), + ) + it.effect("simulation provider streams queued script actions", () => Effect.gen(function* () { const simulation = yield* Simulation.Service