| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129 |
- import { expect, test } from "bun:test"
- import { mkdtemp, rm } from "node:fs/promises"
- import { tmpdir } from "node:os"
- import { join } from "node:path"
- import { Flag } from "@opencode-ai/core/flag/flag"
- import { Effect, Option, Schema, Stream } from "effect"
- test("embedded client uses the real router and handlers", async () => {
- const directory = await mkdtemp(join(tmpdir(), "opencode-embedded-"))
- const database = Flag.OPENCODE_DB
- Flag.OPENCODE_DB = join(directory, "opencode.sqlite")
- const { AbsolutePath, Agent, Location, Model, OpenCode, Prompt, Provider, Session, Tool } = await import("../src")
- const sessionID = Session.ID.make(`ses_embedded_${crypto.randomUUID()}`)
- const model = Model.Ref.make({ id: Model.ID.make("embedded"), providerID: Provider.ID.make("test") })
- try {
- const program = Effect.gen(function* () {
- const opencode = yield* OpenCode.create()
- yield* opencode.tools.register({
- embedded_tool: Tool.make({
- description: "Embedded test tool",
- input: Schema.Struct({}),
- output: Schema.Struct({ ok: Schema.Boolean }),
- execute: () => Effect.succeed({ ok: true }),
- }),
- })
- const created = yield* opencode.sessions.create({
- id: sessionID,
- agent: Agent.ID.make("build"),
- location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
- })
- yield* opencode.sessions.switchModel({ sessionID, model })
- const selected = yield* opencode.sessions.get({ sessionID })
- const page = yield* opencode.sessions.list({ directory: AbsolutePath.make(directory) })
- const active = yield* opencode.sessions.active()
- const admitted = yield* opencode.sessions.prompt({
- sessionID,
- prompt: Prompt.make({ text: "Do not run" }),
- resume: false,
- })
- const context = yield* opencode.sessions.context({ sessionID })
- const wake = yield* opencode.sessions.prompt({
- sessionID,
- prompt: Prompt.make({ text: "Promote this input" }),
- })
- const prompted = yield* opencode.sessions.events({ sessionID }).pipe(
- Stream.filter((event) => event.type === "session.next.prompted" && event.data.messageID === wake.id),
- Stream.runHead,
- Effect.timeout("10 seconds"),
- Effect.map(Option.getOrThrow),
- )
- const wakeContext = yield* opencode.sessions.context({ sessionID })
- const event = yield* opencode.sessions
- .events({ sessionID })
- .pipe(Stream.take(1), Stream.runHead, Effect.map(Option.getOrUndefined))
- const modelMessage = Option.fromNullishOr(context.find((message) => message.type === "model-switched")).pipe(
- Option.getOrThrow,
- )
- const message = yield* opencode.sessions.message({ sessionID, messageID: modelMessage.id })
- yield* opencode.sessions.interrupt({ sessionID })
- const other = yield* opencode.sessions.create({
- location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
- })
- const missingSessionID = Session.ID.make(`ses_missing_${crypto.randomUUID()}`)
- const missing = yield* Effect.all(
- [
- opencode.sessions.events({ sessionID: missingSessionID }).pipe(Stream.runHead, Effect.flip),
- opencode.sessions.interrupt({ sessionID: missingSessionID }).pipe(Effect.flip),
- opencode.sessions.message({ sessionID: missingSessionID, messageID: modelMessage.id }).pipe(Effect.flip),
- ],
- { concurrency: "unbounded" },
- )
- const missingMessage = yield* Effect.flip(
- opencode.sessions.message({
- sessionID: other.id,
- messageID: modelMessage.id,
- }),
- )
- expect(created.id).toBe(sessionID)
- expect(selected.model?.id).toBe(model.id)
- expect(selected.model?.providerID).toBe(model.providerID)
- expect(page.data.some((session) => session.id === sessionID)).toBe(true)
- expect(active).toEqual({})
- expect(admitted.sessionID).toBe(sessionID)
- expect(prompted.type).toBe("session.next.prompted")
- expect(wakeContext).toContainEqual(expect.objectContaining({ id: wake.id, type: "user" }))
- expect(context.some((message) => message.type === "model-switched")).toBe(true)
- expect(event).toMatchObject({ type: "session.next.model.switched", durable: { seq: 1 } })
- expect(message).toEqual(modelMessage)
- expect(missing.map((error) => error._tag)).toEqual([
- "SessionNotFoundError",
- "SessionNotFoundError",
- "SessionNotFoundError",
- ])
- expect(missingMessage._tag).toBe("MessageNotFoundError")
- })
- await Effect.runPromise(Effect.scoped(program))
- } finally {
- Flag.OPENCODE_DB = database
- await rm(directory, { recursive: true, force: true })
- }
- })
- test("embedded client is available as a Layer service", async () => {
- const directory = await mkdtemp(join(tmpdir(), "opencode-embedded-layer-"))
- const database = Flag.OPENCODE_DB
- Flag.OPENCODE_DB = join(directory, "opencode.sqlite")
- const { AbsolutePath, Location, OpenCode, Session } = await import("../src")
- const sessionID = Session.ID.make(`ses_embedded_${crypto.randomUUID()}`)
- try {
- const created = await Effect.runPromise(
- Effect.gen(function* () {
- const opencode = yield* OpenCode.Service
- return yield* opencode.sessions.create({
- id: sessionID,
- location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
- })
- }).pipe(Effect.provide(OpenCode.layer), Effect.scoped),
- )
- expect(created.id).toBe(sessionID)
- } finally {
- Flag.OPENCODE_DB = database
- await rm(directory, { recursive: true, force: true })
- }
- })
|