cloudaxe-opencode/packages/opencode/test/testing/simulation/service.test.ts
2026-05-17 15:04:21 -04:00

341 lines
14 KiB
TypeScript

import { describe, expect } from "bun:test"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { streamText, tool, jsonSchema } from "ai"
import { Effect, Layer } from "effect"
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
import { Provider } from "../../../src/provider/provider"
import { SimulationFileSystem } from "../../../src/testing/simulation/filesystem"
import { SimulationNetwork } from "../../../src/testing/simulation/network"
import { SimulationProvider } from "../../../src/testing/simulation/provider"
import { Simulation } from "../../../src/testing/simulation/service"
import { testEffect } from "../../lib/effect"
const fsLayer = SimulationFileSystem.layer({ root: "/opencode" })
const networkLayer = SimulationNetwork.layer({ allowLoopback: false })
const simulationLayer = Simulation.layer.pipe(Layer.provide(fsLayer), Layer.provide(networkLayer))
const providerLayer = SimulationProvider.layer.pipe(Layer.provide(simulationLayer))
const it = testEffect(Layer.mergeAll(fsLayer, networkLayer, simulationLayer, providerLayer))
describe("Simulation", () => {
it.effect("seeds files into the simulated filesystem", () =>
Effect.gen(function* () {
const simulation = yield* Simulation.Service
const fs = yield* AppFileSystem.Service
expect(yield* simulation.seedFilesystem({ files: { "opencode.json": "{}" } })).toEqual({
files: ["opencode.json"],
})
expect(yield* fs.readFileString("/opencode/opencode.json")).toBe("{}")
}),
)
it.effect("writes one file into the simulated filesystem", () =>
Effect.gen(function* () {
const simulation = yield* Simulation.Service
const fs = yield* AppFileSystem.Service
expect(yield* simulation.writeFile({ path: "src/generated.txt", content: "generated" })).toEqual({ file: "src/generated.txt" })
expect(yield* fs.readFileString("/opencode/src/generated.txt")).toBe("generated")
expect((yield* simulation.snapshot()).files).toContain("src/generated.txt")
}),
)
it.effect("registers network responses through control state", () =>
Effect.gen(function* () {
const simulation = yield* Simulation.Service
const http = yield* HttpClient.HttpClient
expect(
yield* simulation.registerNetwork({
kind: "json",
method: "GET",
url: "https://example.com/data",
body: { ok: true },
}),
).toEqual({ registered: "https://example.com/data" })
const response = yield* http.execute(HttpClientRequest.get("https://example.com/data"))
expect(yield* response.json).toEqual({ ok: true })
}),
)
it.effect("snapshots and resets simulation state", () =>
Effect.gen(function* () {
const simulation = yield* Simulation.Service
const http = yield* HttpClient.HttpClient
yield* simulation.seedFilesystem({ files: { "README.md": "hello" } })
yield* simulation.registerNetwork({ kind: "text", url: "https://example.com/page", body: "hello" })
const snapshot = yield* simulation.snapshot()
expect(snapshot.files).toEqual(["README.md"])
expect(snapshot.networkRegistrations).toEqual(["* https://example.com/page"])
expect(snapshot.network.routes.some((route) => route.matcher === "https://example.com/page")).toBe(true)
yield* simulation.reset()
expect((yield* simulation.snapshot()).files).toEqual([])
const exit = yield* http.execute(HttpClientRequest.get("https://example.com/page")).pipe(Effect.exit)
expect(exit._tag).toBe("Failure")
}),
)
it.effect("queues and consumes LLM scripts", () =>
Effect.gen(function* () {
const simulation = yield* Simulation.Service
expect(
yield* simulation.enqueueLLM({
scripts: [{ steps: [[{ type: "text", content: "hello" }]], finish: "stop" }],
}),
).toEqual({ queued: 1 })
expect((yield* simulation.snapshot()).llmQueued).toBe(1)
expect(yield* simulation.nextLLM()).toEqual({ steps: [[{ type: "text", content: "hello" }]], finish: "stop" })
expect((yield* simulation.snapshot()).llmConsumed).toBe(1)
}),
)
it.effect("simulation provider consumes queued text scripts", () =>
Effect.gen(function* () {
const simulation = yield* Simulation.Service
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)
yield* simulation.enqueueLLM({ scripts: [{ steps: [[{ type: "text", content: "assistant text" }]] }] })
const result = yield* Effect.promise(() => language.doGenerate({ prompt: [], abortSignal: undefined }))
expect(result.content).toEqual([{ type: "text", text: "assistant text" }])
expect((yield* simulation.snapshot()).llmConsumed).toBe(1)
}),
)
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
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)
yield* simulation.enqueueLLM({
scripts: [
{
steps: [
[
{ type: "thinking", content: "thinking" },
{ type: "text", content: "answer" },
],
],
},
],
})
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).toEqual([
{ type: "stream-start", warnings: [] },
{ type: "reasoning-start", id: "simulation-thinking-1" },
{ type: "reasoning-delta", id: "simulation-thinking-1", delta: "thinking" },
{ type: "reasoning-end", id: "simulation-thinking-1" },
{ type: "text-start", id: "simulation-text-2" },
{ type: "text-delta", id: "simulation-text-2", delta: "answer" },
{ type: "text-end", id: "simulation-text-2" },
{
type: "finish",
finishReason: { unified: "stop", raw: undefined },
usage: {
inputTokens: { total: 0, noCache: 0, cacheRead: undefined, cacheWrite: undefined },
outputTokens: { total: "thinkinganswer".length, text: "thinkinganswer".length, reasoning: undefined },
raw: undefined,
},
},
])
expect((yield* simulation.snapshot()).llmConsumed).toBe(1)
}),
)
it.effect("simulation provider streams queued tool-call actions", () =>
Effect.gen(function* () {
const simulation = yield* Simulation.Service
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)
yield* simulation.enqueueLLM({
scripts: [
{
steps: [
[
{ type: "text", content: "I'll write that file for you" },
{
type: "tool-call",
toolCallId: "call-1",
toolName: "write",
input: { filePath: "/opencode/hello.txt", content: "hi" },
},
],
],
finish: "tool-calls",
},
],
})
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)
}
const expectedInput = JSON.stringify({ filePath: "/opencode/hello.txt", content: "hi" })
expect(parts).toContainEqual({ type: "tool-input-start", id: "call-1", toolName: "write" })
expect(parts).toContainEqual({ type: "tool-input-delta", id: "call-1", delta: expectedInput })
expect(parts).toContainEqual({ type: "tool-input-end", id: "call-1" })
expect(parts).toContainEqual({ type: "tool-call", toolCallId: "call-1", toolName: "write", input: expectedInput })
const finish = parts.find((p: any) => p?.type === "finish") as any
expect(finish?.finishReason).toEqual({ unified: "tool-calls", raw: "tool-calls" })
}),
)
it.effect("simulation provider doGenerate returns tool-call content", () =>
Effect.gen(function* () {
const simulation = yield* Simulation.Service
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)
yield* simulation.enqueueLLM({
scripts: [
{
steps: [
[
{ type: "text", content: "writing now" },
{
type: "tool-call",
toolCallId: "call-9",
toolName: "write",
input: { filePath: "/opencode/a.txt", content: "x" },
},
],
],
finish: "tool-calls",
},
],
})
const result = yield* Effect.promise(() => language.doGenerate({ prompt: [], abortSignal: undefined }))
expect(result.content).toEqual([
{ type: "text", text: "writing now" },
{
type: "tool-call",
toolCallId: "call-9",
toolName: "write",
input: JSON.stringify({ filePath: "/opencode/a.txt", content: "x" }),
},
])
expect(result.finishReason).toEqual({ unified: "tool-calls", raw: "tool-calls" })
}),
)
it.effect("simulation model advertises toolcall capability", () =>
Effect.gen(function* () {
const provider = yield* Provider.Service
const model = yield* provider.defaultModel().pipe(Effect.flatMap((item) => provider.getModel(item.providerID, item.modelID)))
expect(model.capabilities.toolcall).toBe(true)
}),
)
it.effect("AI SDK streamText invokes tool.execute when the simulated provider emits a tool-call", () =>
Effect.gen(function* () {
const simulation = yield* Simulation.Service
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)
yield* simulation.enqueueLLM({
scripts: [
{
steps: [
[
{ type: "text", content: "writing now" },
{
type: "tool-call",
toolCallId: "call-execute-1",
toolName: "write",
input: { filePath: "/opencode/from-tool.txt", content: "hello from tool" },
},
],
],
finish: "tool-calls",
},
],
})
const executed: Array<{ filePath: string; content: string }> = []
const result = streamText({
model: language,
prompt: "please write the file",
tools: {
write: tool({
description: "Write a file",
inputSchema: jsonSchema<{ filePath: string; content: string }>({
type: "object",
properties: { filePath: { type: "string" }, content: { type: "string" } },
required: ["filePath", "content"],
}),
execute(args) {
executed.push(args)
return Promise.resolve({ ok: true, path: args.filePath })
},
}),
},
})
// Drain the stream so streamText runs to completion.
yield* Effect.promise(async () => {
for await (const _ of result.fullStream) void _
})
expect(executed).toEqual([{ filePath: "/opencode/from-tool.txt", content: "hello from tool" }])
}),
)
})