From f2fa1a681d6f6b87e6725c083ba48352050089ef Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 31 Mar 2026 12:29:05 -0400 Subject: [PATCH] test: move more prompt cases to mock llm server Migrate the next prompt-effect cases to the HTTP-backed mock server path, keep the shell handoff cases on short live timeouts, and leave the stream-failure case on the in-process fake until the server DSL matches it. --- packages/opencode/test/lib/effect.ts | 6 +- .../test/session/prompt-effect.test.ts | 153 +++++++++++------- 2 files changed, 98 insertions(+), 61 deletions(-) diff --git a/packages/opencode/test/lib/effect.ts b/packages/opencode/test/lib/effect.ts index 36e2b71f61..20a5d69cc8 100644 --- a/packages/opencode/test/lib/effect.ts +++ b/packages/opencode/test/lib/effect.ts @@ -8,7 +8,7 @@ type Body = Effect.Effect | (() => Effect.Effect) const body = (value: Body) => Effect.suspend(() => (typeof value === "function" ? value() : value)) -const run = (value: Body, layer: Layer.Layer) => +const run = (value: Body, layer: Layer.Layer) => Effect.gen(function* () { const exit = yield* body(value).pipe(Effect.scoped, Effect.provide(layer), Effect.exit) if (Exit.isFailure(exit)) { @@ -19,7 +19,7 @@ const run = (value: Body, layer: Layer.Layer return yield* exit }).pipe(Effect.runPromise) -const make = (testLayer: Layer.Layer, liveLayer: Layer.Layer) => { +const make = (testLayer: Layer.Layer, liveLayer: Layer.Layer) => { const effect = (name: string, value: Body, opts?: number | TestOptions) => test(name, () => run(value, testLayer), opts) @@ -49,5 +49,5 @@ const liveEnv = TestConsole.layer export const it = make(testEnv, liveEnv) -export const testEffect = (layer: Layer.Layer) => +export const testEffect = (layer: Layer.Layer) => make(Layer.provideMerge(layer, testEnv), Layer.provideMerge(layer, liveEnv)) diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts index f6dd6f1dda..567bf26e1b 100644 --- a/packages/opencode/test/session/prompt-effect.test.ts +++ b/packages/opencode/test/session/prompt-effect.test.ts @@ -309,8 +309,40 @@ const env = SessionPrompt.layer.pipe( Layer.provideMerge(deps), ) +function makeHttp() { + const deps = Layer.mergeAll( + Session.defaultLayer, + Snapshot.defaultLayer, + LLM.defaultLayer, + AgentSvc.defaultLayer, + Command.defaultLayer, + Permission.layer, + Plugin.defaultLayer, + Config.defaultLayer, + filetime, + lsp, + mcp, + AppFileSystem.defaultLayer, + status, + ).pipe(Layer.provideMerge(infra)) + const registry = ToolRegistry.layer.pipe(Layer.provideMerge(deps)) + const trunc = Truncate.layer.pipe(Layer.provideMerge(deps)) + const proc = SessionProcessor.layer.pipe(Layer.provideMerge(deps)) + const compact = SessionCompaction.layer.pipe(Layer.provideMerge(proc), Layer.provideMerge(deps)) + return Layer.mergeAll( + TestLLMServer.layer, + SessionPrompt.layer.pipe( + Layer.provideMerge(compact), + Layer.provideMerge(proc), + Layer.provideMerge(registry), + Layer.provideMerge(trunc), + Layer.provideMerge(deps), + ), + ) +} + const it = testEffect(env) -const http = testEffect(Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer, TestLLMServer.layer)) +const http = testEffect(makeHttp()) const unix = process.platform !== "win32" ? it.effect : it.effect.skip // Config that registers a custom "test" provider with a "test-model" model @@ -453,23 +485,21 @@ it.live("loop exits immediately when last assistant has stop finish", () => http.live("loop calls LLM and returns assistant message", () => provideTmpdirServer( Effect.fnUntraced(function* ({ llm }) { - const chat = yield* Effect.promise(() => - Session.create({ - title: "Pinned", - permission: [{ permission: "*", pattern: "*", action: "allow" }], - }), - ) - yield* Effect.promise(() => - SessionPrompt.prompt({ - sessionID: chat.id, - agent: "build", - noReply: true, - parts: [{ type: "text", text: "hello" }], - }), - ) + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const chat = yield* sessions.create({ + title: "Pinned", + permission: [{ permission: "*", pattern: "*", action: "allow" }], + }) + yield* prompt.prompt({ + sessionID: chat.id, + agent: "build", + noReply: true, + parts: [{ type: "text", text: "hello" }], + }) yield* llm.text("world") - const result = yield* Effect.promise(() => SessionPrompt.loop({ sessionID: chat.id })) + const result = yield* prompt.loop({ sessionID: chat.id }) expect(result.info.role).toBe("assistant") const parts = result.parts.filter((p) => p.type === "text") expect(parts.some((p) => p.type === "text" && p.text === "world")).toBe(true) @@ -479,24 +509,33 @@ http.live("loop calls LLM and returns assistant message", () => ), ) -it.live("loop continues when finish is tool-calls", () => - provideTmpdirInstance( - (dir) => - Effect.gen(function* () { - const { test, prompt, chat } = yield* boot() - yield* test.reply(...replyToolCalls("first")) - yield* test.reply(...replyStop("second")) - yield* user(chat.id, "hello") +http.live("loop continues when finish is tool-calls", () => + provideTmpdirServer( + Effect.fnUntraced(function* ({ llm }) { + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const session = yield* sessions.create({ + title: "Pinned", + permission: [{ permission: "*", pattern: "*", action: "allow" }], + }) + yield* prompt.prompt({ + sessionID: session.id, + agent: "build", + noReply: true, + parts: [{ type: "text", text: "hello" }], + }) + yield* llm.tool("first", { value: "first" }) + yield* llm.text("second") - const result = yield* prompt.loop({ sessionID: chat.id }) - expect(yield* test.calls).toBe(2) - expect(result.info.role).toBe("assistant") - if (result.info.role === "assistant") { - expect(result.parts.some((part) => part.type === "text" && part.text === "second")).toBe(true) - expect(result.info.finish).toBe("stop") - } - }), - { git: true, config: cfg }, + const result = yield* prompt.loop({ sessionID: session.id }) + expect(yield* llm.calls).toBe(2) + expect(result.info.role).toBe("assistant") + if (result.info.role === "assistant") { + expect(result.parts.some((part) => part.type === "text" && part.text === "second")).toBe(true) + expect(result.info.finish).toBe("stop") + } + }), + { git: true, config: providerCfg }, ), ) @@ -787,7 +826,6 @@ it.live("concurrent loop callers all receive same error result", () => Effect.gen(function* () { const { test, prompt, chat } = yield* boot() - // Push a stream that fails — the loop records the error on the assistant message yield* test.push(Stream.fail(new Error("boom"))) yield* user(chat.id, "hello") @@ -795,7 +833,6 @@ it.live("concurrent loop callers all receive same error result", () => concurrency: "unbounded", }) - // Both callers get the same assistant with an error recorded expect(a.info.id).toBe(b.info.id) expect(a.info.role).toBe("assistant") if (a.info.role === "assistant") { @@ -1040,20 +1077,20 @@ http.live( () => provideTmpdirServer( Effect.fnUntraced(function* ({ llm }) { - const chat = yield* Effect.promise(() => - Session.create({ - title: "Pinned", - permission: [{ permission: "*", pattern: "*", action: "allow" }], - }), - ) + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const chat = yield* sessions.create({ + title: "Pinned", + permission: [{ permission: "*", pattern: "*", action: "allow" }], + }) yield* llm.text("after-shell") - const sh = yield* Effect.promise(() => - SessionPrompt.shell({ sessionID: chat.id, agent: "build", command: "sleep 0.2" }), - ).pipe(Effect.forkChild) + const sh = yield* prompt + .shell({ sessionID: chat.id, agent: "build", command: "sleep 0.2" }) + .pipe(Effect.forkChild) yield* waitMs(50) - const run = yield* Effect.promise(() => SessionPrompt.loop({ sessionID: chat.id })).pipe(Effect.forkChild) + const run = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild) yield* waitMs(50) expect(yield* llm.calls).toBe(0) @@ -1070,7 +1107,7 @@ http.live( }), { git: true, config: providerCfg }, ), - 5_000, + 3_000, ) http.live( @@ -1078,21 +1115,21 @@ http.live( () => provideTmpdirServer( Effect.fnUntraced(function* ({ llm }) { - const chat = yield* Effect.promise(() => - Session.create({ - title: "Pinned", - permission: [{ permission: "*", pattern: "*", action: "allow" }], - }), - ) + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const chat = yield* sessions.create({ + title: "Pinned", + permission: [{ permission: "*", pattern: "*", action: "allow" }], + }) yield* llm.text("done") - const sh = yield* Effect.promise(() => - SessionPrompt.shell({ sessionID: chat.id, agent: "build", command: "sleep 0.2" }), - ).pipe(Effect.forkChild) + const sh = yield* prompt + .shell({ sessionID: chat.id, agent: "build", command: "sleep 0.2" }) + .pipe(Effect.forkChild) yield* waitMs(50) - const a = yield* Effect.promise(() => SessionPrompt.loop({ sessionID: chat.id })).pipe(Effect.forkChild) - const b = yield* Effect.promise(() => SessionPrompt.loop({ sessionID: chat.id })).pipe(Effect.forkChild) + const a = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild) + const b = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild) yield* waitMs(50) expect(yield* llm.calls).toBe(0) @@ -1110,7 +1147,7 @@ http.live( }), { git: true, config: providerCfg }, ), - 5_000, + 3_000, ) unix(