James Long пре 3 месеци
родитељ
комит
02b7367b4e

+ 40 - 28
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 { Effect, Layer } from "effect"
 import { Provider } from "@/provider/provider"
 import { Provider } from "@/provider/provider"
 import { ModelID, ProviderID } from "@/provider/schema"
 import { ModelID, ProviderID } from "@/provider/schema"
@@ -39,6 +40,15 @@ const provider: Provider.Info = {
   models: { [modelID]: model },
   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) {
 function text(script: LLMScript) {
   return script.steps[0]?.flatMap((item) => (item.type === "text" || item.type === "thinking" ? [item.content] : []))
   return script.steps[0]?.flatMap((item) => (item.type === "text" || item.type === "thinking" ? [item.content] : []))
     .join("") ?? ""
     .join("") ?? ""
@@ -49,31 +59,33 @@ function error(script: LLMScript) {
 }
 }
 
 
 function stream(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,
     modelId: modelID,
     supportedUrls: {},
     supportedUrls: {},
     async doGenerate(_options: LanguageModelV3CallOptions) {
     async doGenerate(_options: LanguageModelV3CallOptions) {
-      const script = await Effect.runPromise(simulation.nextLLM())
+      const script = await nextScript(simulation)
       const err = error(script)
       const err = error(script)
       if (err?.type === "error") throw new Error(err.message)
       if (err?.type === "error") throw new Error(err.message)
       return {
       return {
@@ -116,7 +128,7 @@ function language(simulation: Simulation.Interface): LanguageModelV3 {
       }
       }
     },
     },
     async doStream(_options: LanguageModelV3CallOptions) {
     async doStream(_options: LanguageModelV3CallOptions) {
-      const script = await Effect.runPromise(simulation.nextLLM())
+      const script = await nextScript(simulation)
       return { stream: stream(script) }
       return { stream: stream(script) }
     },
     },
   }
   }

+ 8 - 2
packages/opencode/src/testing/simulation/service.ts

@@ -108,11 +108,17 @@ export const layer = Layer.effect(
   Effect.gen(function* () {
   Effect.gen(function* () {
     const fs = yield* AppFileSystem.Service
     const fs = yield* AppFileSystem.Service
     const network = yield* SimulationNetwork.Service
     const network = yield* SimulationNetwork.Service
-    const state = yield* Ref.make<State>({ files: [], networkRegistrations: [], llmScripts: [], consumedLLMScripts: 0 })
+    const empty: State = {
+      files: [],
+      networkRegistrations: [],
+      llmScripts: [],
+      consumedLLMScripts: 0,
+    }
+    const state = yield* Ref.make<State>(empty)
 
 
     const reset = Effect.fn("Simulation.reset")(function* () {
     const reset = Effect.fn("Simulation.reset")(function* () {
       yield* network.reset()
       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) {
     const seedFilesystem = Effect.fn("Simulation.seedFilesystem")(function* (input: typeof FilesystemSeedInput.Type) {

+ 30 - 0
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", () =>
   it.effect("simulation provider streams queued script actions", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const simulation = yield* Simulation.Service
       const simulation = yield* Simulation.Service