| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177 |
- import fs from "fs/promises"
- import path from "path"
- import { describe, expect } from "bun:test"
- import { Effect, Schema } from "effect"
- import { AbsolutePath, Location, Model, OpenCode, Session, Tool } from "@opencode-ai/core/public"
- import { tmpdir } from "./fixture/tmpdir"
- import { testEffect } from "./lib/effect"
- const it = testEffect(OpenCode.layer)
- describe("public native OpenCode API", () => {
- it.effect("exposes only the intentional Session capabilities", () =>
- Effect.gen(function* () {
- const opencode = yield* OpenCode.Service
- expect(Object.keys(opencode).sort()).toEqual(["sessions", "tools"])
- expect(Object.keys(opencode.sessions).sort()).toEqual([
- "context",
- "create",
- "events",
- "get",
- "interrupt",
- "list",
- "message",
- "messages",
- "prompt",
- "switchModel",
- ])
- expect(Session.ID.create()).toStartWith("ses_")
- expect(Session.MessageID.create()).toStartWith("msg_")
- expect(yield* opencode.sessions.list()).toBeArray()
- yield* opencode.tools.attach({
- public_tool: Tool.make({
- description: "Public tool",
- parameters: Schema.Struct({}),
- success: Schema.Struct({ ok: Schema.Boolean }),
- execute: () => Effect.succeed({ ok: true }),
- }),
- })
- }),
- )
- it.effect("switches to an available model and variant", () =>
- Effect.acquireRelease(
- Effect.promise(() => tmpdir()),
- (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
- ).pipe(
- Effect.flatMap((tmp) =>
- Effect.gen(function* () {
- yield* writeProvider(tmp.path)
- const opencode = yield* OpenCode.Service
- const sessionID = Session.ID.make("ses_public_switch_available")
- const model = ref({ variant: "fast" })
- yield* opencode.sessions.create({
- id: sessionID,
- location: Location.Ref.make({ directory: AbsolutePath.make(tmp.path) }),
- })
- yield* opencode.sessions.switchModel({ sessionID, model })
- expect((yield* opencode.sessions.get(sessionID)).model).toEqual(model)
- }),
- ),
- ),
- )
- it.effect("rejects missing and Location-disabled models without changing the Session", () =>
- Effect.acquireRelease(
- Effect.promise(() => Promise.all([tmpdir(), tmpdir()])),
- (dirs) => Effect.promise(() => Promise.all(dirs.map((dir) => dir[Symbol.asyncDispose]())).then(() => undefined)),
- ).pipe(
- Effect.flatMap(([available, disabled]) =>
- Effect.gen(function* () {
- yield* writeProvider(available.path)
- yield* writeProvider(disabled.path, true)
- const opencode = yield* OpenCode.Service
- const availableID = Session.ID.make("ses_public_switch_exact_available")
- const disabledID = Session.ID.make("ses_public_switch_exact_disabled")
- yield* opencode.sessions.create({
- id: availableID,
- location: Location.Ref.make({ directory: AbsolutePath.make(available.path) }),
- })
- yield* opencode.sessions.create({
- id: disabledID,
- location: Location.Ref.make({ directory: AbsolutePath.make(disabled.path) }),
- })
- yield* opencode.sessions.switchModel({ sessionID: availableID, model: ref({ variant: "default" }) })
- const disabledError = yield* opencode.sessions
- .switchModel({ sessionID: disabledID, model: ref() })
- .pipe(Effect.flip)
- const missingError = yield* opencode.sessions
- .switchModel({ sessionID: disabledID, model: ref({ id: "missing" }) })
- .pipe(Effect.flip)
- expect(disabledError).toBeInstanceOf(Session.ModelUnavailableError)
- expect(missingError).toBeInstanceOf(Session.ModelUnavailableError)
- expect((yield* opencode.sessions.get(availableID)).model).toEqual(ref({ variant: "default" }))
- expect((yield* opencode.sessions.get(disabledID)).model).toBeUndefined()
- }),
- ),
- ),
- )
- it.effect("rejects an unavailable variant without changing the Session", () =>
- Effect.acquireRelease(
- Effect.promise(() => tmpdir()),
- (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
- ).pipe(
- Effect.flatMap((tmp) =>
- Effect.gen(function* () {
- yield* writeProvider(tmp.path)
- const opencode = yield* OpenCode.Service
- const sessionID = Session.ID.make("ses_public_switch_variant")
- const selected = ref({ variant: "fast" })
- yield* opencode.sessions.create({
- id: sessionID,
- location: Location.Ref.make({ directory: AbsolutePath.make(tmp.path) }),
- })
- yield* opencode.sessions.switchModel({ sessionID, model: selected })
- const error = yield* opencode.sessions
- .switchModel({ sessionID, model: ref({ variant: "unknown" }) })
- .pipe(Effect.flip)
- expect(error).toBeInstanceOf(Session.VariantUnavailableError)
- expect((yield* opencode.sessions.get(sessionID)).model).toEqual(selected)
- }),
- ),
- ),
- )
- it.effect("preserves the typed not-found error for a missing Session", () =>
- Effect.gen(function* () {
- const opencode = yield* OpenCode.Service
- const sessionID = Session.ID.make("ses_public_switch_missing")
- const error = yield* opencode.sessions
- .switchModel({
- sessionID,
- model: Schema.decodeUnknownSync(Model.Ref)({ id: "claude-sonnet-4-5", providerID: "anthropic" }),
- })
- .pipe(Effect.flip)
- expect(error).toBeInstanceOf(Session.NotFoundError)
- if (error instanceof Session.NotFoundError) expect(error.sessionID).toBe(sessionID)
- }),
- )
- })
- const ref = (input: { id?: string; variant?: string } = {}) =>
- Schema.decodeUnknownSync(Model.Ref)({
- id: input.id ?? "chat",
- providerID: "public-test",
- variant: input.variant,
- })
- const writeProvider = (directory: string, disabled = false) =>
- Effect.promise(() =>
- fs.writeFile(
- path.join(directory, "opencode.json"),
- JSON.stringify({
- providers: {
- "public-test": {
- name: "Public test",
- api: { type: "native", settings: {} },
- models: {
- chat: {
- disabled,
- variants: [{ id: "fast" }],
- },
- },
- },
- },
- }),
- ),
- )
|