|
|
@@ -1,6 +1,9 @@
|
|
|
+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)
|
|
|
@@ -38,25 +41,94 @@ describe("public native OpenCode API", () => {
|
|
|
}),
|
|
|
)
|
|
|
|
|
|
- it.effect("switches the exact Session to the exact model through the durable facade", () =>
|
|
|
- Effect.gen(function* () {
|
|
|
- const opencode = yield* OpenCode.Service
|
|
|
- const targetID = Session.ID.make("ses_public_switch_target")
|
|
|
- const otherID = Session.ID.make("ses_public_switch_other")
|
|
|
- const model = Schema.decodeUnknownSync(Model.Ref)({
|
|
|
- id: "claude-sonnet-4-5",
|
|
|
- providerID: "anthropic",
|
|
|
- variant: "high",
|
|
|
- })
|
|
|
- const location = Location.Ref.make({ directory: AbsolutePath.make("/public-session-switch-model") })
|
|
|
- yield* opencode.sessions.create({ id: targetID, location })
|
|
|
- yield* opencode.sessions.create({ id: otherID, location })
|
|
|
+ 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: targetID, model })
|
|
|
+ yield* opencode.sessions.switchModel({ sessionID, model })
|
|
|
|
|
|
- expect((yield* opencode.sessions.get(targetID)).model).toEqual(model)
|
|
|
- expect((yield* opencode.sessions.get(otherID)).model).toBeUndefined()
|
|
|
- }),
|
|
|
+ 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", () =>
|
|
|
@@ -71,7 +143,35 @@ describe("public native OpenCode API", () => {
|
|
|
.pipe(Effect.flip)
|
|
|
|
|
|
expect(error).toBeInstanceOf(Session.NotFoundError)
|
|
|
- expect(error.sessionID).toBe(sessionID)
|
|
|
+ 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" }],
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ ),
|
|
|
+ )
|