service.test.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import { describe, expect } from "bun:test"
  2. import { AppFileSystem } from "@opencode-ai/core/filesystem"
  3. import { Effect, Layer } from "effect"
  4. import { HttpClient, HttpClientRequest } from "effect/unstable/http"
  5. import { Provider } from "../../../src/provider/provider"
  6. import { SimulationFileSystem } from "../../../src/testing/simulation/filesystem"
  7. import { SimulationNetwork } from "../../../src/testing/simulation/network"
  8. import { SimulationProvider } from "../../../src/testing/simulation/provider"
  9. import { Simulation } from "../../../src/testing/simulation/service"
  10. import { testEffect } from "../../lib/effect"
  11. const fsLayer = SimulationFileSystem.layer({ root: "/opencode" })
  12. const networkLayer = SimulationNetwork.layer({ allowLoopback: false })
  13. const simulationLayer = Simulation.layer.pipe(Layer.provide(fsLayer), Layer.provide(networkLayer))
  14. const providerLayer = SimulationProvider.layer.pipe(Layer.provide(simulationLayer))
  15. const it = testEffect(Layer.mergeAll(fsLayer, networkLayer, simulationLayer, providerLayer))
  16. describe("Simulation", () => {
  17. it.effect("seeds files into the simulated filesystem", () =>
  18. Effect.gen(function* () {
  19. const simulation = yield* Simulation.Service
  20. const fs = yield* AppFileSystem.Service
  21. expect(yield* simulation.seedFilesystem({ files: { "opencode.json": "{}" } })).toEqual({
  22. files: ["opencode.json"],
  23. })
  24. expect(yield* fs.readFileString("/opencode/opencode.json")).toBe("{}")
  25. }),
  26. )
  27. it.effect("registers network responses through control state", () =>
  28. Effect.gen(function* () {
  29. const simulation = yield* Simulation.Service
  30. const http = yield* HttpClient.HttpClient
  31. expect(
  32. yield* simulation.registerNetwork({
  33. kind: "json",
  34. method: "GET",
  35. url: "https://example.com/data",
  36. body: { ok: true },
  37. }),
  38. ).toEqual({ registered: "https://example.com/data" })
  39. const response = yield* http.execute(HttpClientRequest.get("https://example.com/data"))
  40. expect(yield* response.json).toEqual({ ok: true })
  41. }),
  42. )
  43. it.effect("snapshots and resets simulation state", () =>
  44. Effect.gen(function* () {
  45. const simulation = yield* Simulation.Service
  46. const http = yield* HttpClient.HttpClient
  47. yield* simulation.seedFilesystem({ files: { "README.md": "hello" } })
  48. yield* simulation.registerNetwork({ kind: "text", url: "https://example.com/page", body: "hello" })
  49. const snapshot = yield* simulation.snapshot()
  50. expect(snapshot.files).toEqual(["README.md"])
  51. expect(snapshot.networkRegistrations).toEqual(["* https://example.com/page"])
  52. expect(snapshot.network.routes.some((route) => route.matcher === "https://example.com/page")).toBe(true)
  53. yield* simulation.reset()
  54. expect((yield* simulation.snapshot()).files).toEqual([])
  55. const exit = yield* http.execute(HttpClientRequest.get("https://example.com/page")).pipe(Effect.exit)
  56. expect(exit._tag).toBe("Failure")
  57. }),
  58. )
  59. it.effect("queues and consumes LLM scripts", () =>
  60. Effect.gen(function* () {
  61. const simulation = yield* Simulation.Service
  62. expect(
  63. yield* simulation.enqueueLLM({
  64. scripts: [{ steps: [[{ type: "text", content: "hello" }]], finish: "stop" }],
  65. }),
  66. ).toEqual({ queued: 1 })
  67. expect((yield* simulation.snapshot()).llmQueued).toBe(1)
  68. expect(yield* simulation.nextLLM()).toEqual({ steps: [[{ type: "text", content: "hello" }]], finish: "stop" })
  69. expect((yield* simulation.snapshot()).llmConsumed).toBe(1)
  70. }),
  71. )
  72. it.effect("simulation provider consumes queued text scripts", () =>
  73. Effect.gen(function* () {
  74. const simulation = yield* Simulation.Service
  75. const provider = yield* Provider.Service
  76. const model = yield* provider.defaultModel().pipe(Effect.flatMap((item) => provider.getModel(item.providerID, item.modelID)))
  77. const language = yield* provider.getLanguage(model)
  78. yield* simulation.enqueueLLM({ scripts: [{ steps: [[{ type: "text", content: "assistant text" }]] }] })
  79. const result = yield* Effect.promise(() => language.doGenerate({ prompt: [], abortSignal: undefined }))
  80. expect(result.content).toEqual([{ type: "text", text: "assistant text" }])
  81. expect((yield* simulation.snapshot()).llmConsumed).toBe(1)
  82. }),
  83. )
  84. it.effect("simulation provider returns a default response when no script is queued", () =>
  85. Effect.gen(function* () {
  86. const provider = yield* Provider.Service
  87. const model = yield* provider.defaultModel().pipe(Effect.flatMap((item) => provider.getModel(item.providerID, item.modelID)))
  88. const language = yield* provider.getLanguage(model)
  89. const result = yield* Effect.promise(() => language.doGenerate({ prompt: [], abortSignal: undefined }))
  90. expect(result.content).toEqual([{ type: "text", text: "Simulation mock response." }])
  91. }),
  92. )
  93. it.effect("simulation provider streams a default response when no script is queued", () =>
  94. Effect.gen(function* () {
  95. const provider = yield* Provider.Service
  96. const model = yield* provider.defaultModel().pipe(Effect.flatMap((item) => provider.getModel(item.providerID, item.modelID)))
  97. const language = yield* provider.getLanguage(model)
  98. const result = yield* Effect.promise(() => language.doStream({ prompt: [], abortSignal: undefined }))
  99. const reader = result.stream.getReader()
  100. const parts: unknown[] = []
  101. while (true) {
  102. const next = yield* Effect.promise(() => reader.read())
  103. if (next.done) break
  104. parts.push(next.value)
  105. }
  106. expect(parts).toContainEqual({ type: "text-delta", id: "simulation-text-1", delta: "Simulation mock response." })
  107. }),
  108. )
  109. it.effect("simulation provider streams queued script actions", () =>
  110. Effect.gen(function* () {
  111. const simulation = yield* Simulation.Service
  112. const provider = yield* Provider.Service
  113. const model = yield* provider.defaultModel().pipe(Effect.flatMap((item) => provider.getModel(item.providerID, item.modelID)))
  114. const language = yield* provider.getLanguage(model)
  115. yield* simulation.enqueueLLM({
  116. scripts: [
  117. {
  118. steps: [
  119. [
  120. { type: "thinking", content: "thinking" },
  121. { type: "text", content: "answer" },
  122. ],
  123. ],
  124. },
  125. ],
  126. })
  127. const result = yield* Effect.promise(() => language.doStream({ prompt: [], abortSignal: undefined }))
  128. const reader = result.stream.getReader()
  129. const parts: unknown[] = []
  130. while (true) {
  131. const next = yield* Effect.promise(() => reader.read())
  132. if (next.done) break
  133. parts.push(next.value)
  134. }
  135. expect(parts).toEqual([
  136. { type: "stream-start", warnings: [] },
  137. { type: "reasoning-start", id: "simulation-thinking-1" },
  138. { type: "reasoning-delta", id: "simulation-thinking-1", delta: "thinking" },
  139. { type: "reasoning-end", id: "simulation-thinking-1" },
  140. { type: "text-start", id: "simulation-text-2" },
  141. { type: "text-delta", id: "simulation-text-2", delta: "answer" },
  142. { type: "text-end", id: "simulation-text-2" },
  143. {
  144. type: "finish",
  145. finishReason: { unified: "stop", raw: undefined },
  146. usage: {
  147. inputTokens: { total: 0, noCache: 0, cacheRead: undefined, cacheWrite: undefined },
  148. outputTokens: { total: "thinkinganswer".length, text: "thinkinganswer".length, reasoning: undefined },
  149. raw: undefined,
  150. },
  151. },
  152. ])
  153. expect((yield* simulation.snapshot()).llmConsumed).toBe(1)
  154. }),
  155. )
  156. })