embedded.test.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. import { expect, test } from "bun:test"
  2. import { mkdtemp, rm } from "node:fs/promises"
  3. import { tmpdir } from "node:os"
  4. import { join } from "node:path"
  5. import { Flag } from "@opencode-ai/core/flag/flag"
  6. import { Effect, Option, Schema, Stream } from "effect"
  7. test("embedded client uses the real router and handlers", async () => {
  8. const directory = await mkdtemp(join(tmpdir(), "opencode-embedded-"))
  9. const database = Flag.OPENCODE_DB
  10. Flag.OPENCODE_DB = join(directory, "opencode.sqlite")
  11. const { AbsolutePath, Agent, Location, Model, OpenCode, Prompt, Provider, Session, Tool } = await import("../src")
  12. const sessionID = Session.ID.make(`ses_embedded_${crypto.randomUUID()}`)
  13. const model = Model.Ref.make({ id: Model.ID.make("embedded"), providerID: Provider.ID.make("test") })
  14. try {
  15. const program = Effect.gen(function* () {
  16. const opencode = yield* OpenCode.create()
  17. yield* opencode.tools.register({
  18. embedded_tool: Tool.make({
  19. description: "Embedded test tool",
  20. input: Schema.Struct({}),
  21. output: Schema.Struct({ ok: Schema.Boolean }),
  22. execute: () => Effect.succeed({ ok: true }),
  23. }),
  24. })
  25. const created = yield* opencode.sessions.create({
  26. id: sessionID,
  27. agent: Agent.ID.make("build"),
  28. location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
  29. })
  30. yield* opencode.sessions.switchModel({ sessionID, model })
  31. const selected = yield* opencode.sessions.get({ sessionID })
  32. const page = yield* opencode.sessions.list({ directory: AbsolutePath.make(directory) })
  33. const admitted = yield* opencode.sessions.prompt({
  34. sessionID,
  35. prompt: Prompt.make({ text: "Do not run" }),
  36. resume: false,
  37. })
  38. const context = yield* opencode.sessions.context({ sessionID })
  39. const event = yield* opencode.sessions
  40. .events({ sessionID })
  41. .pipe(Stream.take(1), Stream.runHead, Effect.map(Option.getOrUndefined))
  42. const modelMessage = Option.fromNullishOr(context.find((message) => message.type === "model-switched")).pipe(
  43. Option.getOrThrow,
  44. )
  45. const message = yield* opencode.sessions.message({ sessionID, messageID: modelMessage.id })
  46. yield* opencode.sessions.interrupt({ sessionID })
  47. const other = yield* opencode.sessions.create({
  48. location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
  49. })
  50. const missingSessionID = Session.ID.make(`ses_missing_${crypto.randomUUID()}`)
  51. const missing = yield* Effect.all(
  52. [
  53. opencode.sessions.events({ sessionID: missingSessionID }).pipe(Stream.runHead, Effect.flip),
  54. opencode.sessions.interrupt({ sessionID: missingSessionID }).pipe(Effect.flip),
  55. opencode.sessions.message({ sessionID: missingSessionID, messageID: modelMessage.id }).pipe(Effect.flip),
  56. ],
  57. { concurrency: "unbounded" },
  58. )
  59. const missingMessage = yield* Effect.flip(
  60. opencode.sessions.message({
  61. sessionID: other.id,
  62. messageID: modelMessage.id,
  63. }),
  64. )
  65. expect(created.id).toBe(sessionID)
  66. expect(selected.model?.id).toBe(model.id)
  67. expect(selected.model?.providerID).toBe(model.providerID)
  68. expect(page.data.some((session) => session.id === sessionID)).toBe(true)
  69. expect(admitted.sessionID).toBe(sessionID)
  70. expect(context.some((message) => message.type === "model-switched")).toBe(true)
  71. expect(event).toMatchObject({ type: "session.next.model.switched", durable: { seq: 1 } })
  72. expect(message).toEqual(modelMessage)
  73. expect(missing.map((error) => error._tag)).toEqual([
  74. "SessionNotFoundError",
  75. "SessionNotFoundError",
  76. "SessionNotFoundError",
  77. ])
  78. expect(missingMessage._tag).toBe("MessageNotFoundError")
  79. })
  80. await Effect.runPromise(Effect.scoped(program))
  81. } finally {
  82. Flag.OPENCODE_DB = database
  83. await rm(directory, { recursive: true, force: true })
  84. }
  85. })
  86. test("embedded client is available as a Layer service", async () => {
  87. const directory = await mkdtemp(join(tmpdir(), "opencode-embedded-layer-"))
  88. const database = Flag.OPENCODE_DB
  89. Flag.OPENCODE_DB = join(directory, "opencode.sqlite")
  90. const { AbsolutePath, Location, OpenCode, Session } = await import("../src")
  91. const sessionID = Session.ID.make(`ses_embedded_${crypto.randomUUID()}`)
  92. try {
  93. const created = await Effect.runPromise(
  94. Effect.gen(function* () {
  95. const opencode = yield* OpenCode.Service
  96. return yield* opencode.sessions.create({
  97. id: sessionID,
  98. location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
  99. })
  100. }).pipe(Effect.provide(OpenCode.layer), Effect.scoped),
  101. )
  102. expect(created.id).toBe(sessionID)
  103. } finally {
  104. Flag.OPENCODE_DB = database
  105. await rm(directory, { recursive: true, force: true })
  106. }
  107. })