opencode/packages/server/test/voice.test.ts

148 lines
5.4 KiB
TypeScript

import fs from "node:fs/promises"
import path from "node:path"
import { expect } from "bun:test"
import { Voice } from "@opencode/core/voice"
import { makeLocationNode } from "@opencode/util/effect/app-node"
import { Effect, Layer } from "effect"
import { tmpdir } from "../../core/test/fixture/tmpdir"
import { it } from "../../core/test/lib/effect"
import { ServerFetch } from "../src/fetch"
const voice = makeLocationNode({
service: Voice.Service,
layer: Layer.succeed(
Voice.Service,
Voice.Service.of({
models: () =>
Effect.succeed({
models: [
{
model: "gpt-live-1",
provider: "openai",
transports: ["webrtc"],
available: true,
integration: "opencode",
workspace: "Anomaly",
},
{ model: "gpt-realtime-2.1", provider: "openai", transports: ["webrtc", "websocket"], available: true },
{ model: "grok-voice-3.0", provider: "xai", transports: ["websocket"], available: false },
{ model: "gemini-3.8-live", provider: "google", transports: ["websocket"], available: false },
],
default: "gpt-live-1",
}),
session: (input) => {
if (input.model === "invalid")
return Effect.fail(new Voice.InvalidRequestError({ message: "Unsupported voice model" }))
if (input.model === "unavailable")
return Effect.fail(new Voice.UnavailableError({ message: "OpenAI API key is required", service: "openai" }))
return Effect.succeed(
input.transport.type === "webrtc"
? { transport: { type: "webrtc" as const, sdp: `answer:${input.model}:${input.transport.sdp}` }, id: "sess_1" }
: {
transport: {
type: "websocket" as const,
url: `wss://api.x.ai/v1/realtime?model=${input.model}`,
token: "ek_test",
expiresAt: 1760000000,
},
},
)
},
instruct: (input) =>
input.id === "sess_1"
? Effect.void
: Effect.fail(new Voice.InvalidRequestError({ message: "Only hosted voice sessions take instructions" })),
host: () => Effect.void,
}),
),
deps: [],
})
it.live("routes experimental voice models and session endpoints", () =>
Effect.gen(function* () {
const tmp = yield* Effect.acquireDisposable(Effect.promise(() => tmpdir("opencode-voice-endpoint-")))
const global = path.join(tmp.path, "global")
yield* Effect.promise(() => fs.mkdir(global))
const handler = yield* ServerFetch.make(
{
database: { path: ":memory:" },
config: { directory: global },
fs: { filewatcher: false },
models: { fetch: false },
},
{ overrides: [Voice.node.replace(voice)] },
)
const modelsRes = yield* Effect.promise(() =>
handler(new Request("http://opencode.local/api/experimental/voice/models")),
)
expect(modelsRes.status).toBe(200)
expect(yield* Effect.promise(() => modelsRes.json())).toEqual({
data: {
models: [
{
model: "gpt-live-1",
provider: "openai",
transports: ["webrtc"],
available: true,
integration: "opencode",
workspace: "Anomaly",
},
{ model: "gpt-realtime-2.1", provider: "openai", transports: ["webrtc", "websocket"], available: true },
{ model: "grok-voice-3.0", provider: "xai", transports: ["websocket"], available: false },
{ model: "gemini-3.8-live", provider: "google", transports: ["websocket"], available: false },
],
default: "gpt-live-1",
},
})
const post = (body: unknown) =>
Effect.promise(() =>
handler(
new Request("http://opencode.local/api/experimental/voice/session", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
}),
),
)
const rtcRes = yield* post({
model: "gpt-live-1",
session: { voice: "marin" },
transport: { type: "webrtc", sdp: "v=0", dcid: 4 },
})
expect(rtcRes.status).toBe(200)
expect(yield* Effect.promise(() => rtcRes.json())).toEqual({
data: { transport: { type: "webrtc", sdp: "answer:gpt-live-1:v=0" }, session: { id: "sess_1" } },
})
const wsRes = yield* post({ model: "grok-voice-3.0", transport: { type: "websocket" } })
expect(wsRes.status).toBe(200)
expect(yield* Effect.promise(() => wsRes.json())).toEqual({
data: {
transport: {
type: "websocket",
url: "wss://api.x.ai/v1/realtime?model=grok-voice-3.0",
token: "ek_test",
expiresAt: 1760000000,
},
},
})
expect((yield* post({ model: "invalid", transport: { type: "websocket" } })).status).toBe(400)
expect((yield* post({ model: "unavailable", transport: { type: "websocket" } })).status).toBe(503)
const instruct = (id: string) =>
Effect.promise(() =>
handler(
new Request(`http://opencode.local/api/experimental/voice/session/${id}/instructions`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ instructions: "The screen shows ses_1." }),
}),
),
)
expect((yield* instruct("sess_1")).status).toBe(204)
expect((yield* instruct("sess_unknown")).status).toBe(400)
}),
)