浏览代码

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.
Kit Langton 2 周之前
父节点
当前提交
f2fa1a681d
共有 2 个文件被更改,包括 99 次插入62 次删除
  1. 3 3
      packages/opencode/test/lib/effect.ts
  2. 96 59
      packages/opencode/test/session/prompt-effect.test.ts

+ 3 - 3
packages/opencode/test/lib/effect.ts

@@ -8,7 +8,7 @@ type Body<A, E, R> = Effect.Effect<A, E, R> | (() => Effect.Effect<A, E, R>)
 
 const body = <A, E, R>(value: Body<A, E, R>) => Effect.suspend(() => (typeof value === "function" ? value() : value))
 
-const run = <A, E, R, E2>(value: Body<A, E, R | Scope.Scope>, layer: Layer.Layer<R, E2, never>) =>
+const run = <A, E, R, E2, S>(value: Body<A, E, R | Scope.Scope>, layer: Layer.Layer<R, E2, S>) =>
   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 = <A, E, R, E2>(value: Body<A, E, R | Scope.Scope>, layer: Layer.Layer
     return yield* exit
   }).pipe(Effect.runPromise)
 
-const make = <R, E>(testLayer: Layer.Layer<R, E, never>, liveLayer: Layer.Layer<R, E, never>) => {
+const make = <R, E, S, T>(testLayer: Layer.Layer<R, E, S>, liveLayer: Layer.Layer<R, E, T>) => {
   const effect = <A, E2>(name: string, value: Body<A, E2, R | Scope.Scope>, 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 = <R, E>(layer: Layer.Layer<R, E, never>) =>
+export const testEffect = <R, E, S>(layer: Layer.Layer<R, E, S>) =>
   make(Layer.provideMerge(layer, testEnv), Layer.provideMerge(layer, liveEnv))

+ 96 - 59
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")
-
-        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 },
+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: 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(