opencode/packages/voice/test/opencode.test.ts
2026-07-28 21:00:35 -04:00

248 lines
9.8 KiB
TypeScript

import { expect, test } from "bun:test"
import { OpenCode } from "@opencode-ai/client/promise"
import { createOpenCodeBridge } from "../src/opencode"
import type { CompletionStore } from "../src/completion-store"
test("renames only a session discovered by the voice controller", async () => {
const requests: Request[] = []
const session = {
id: "ses_known",
projectID: "project-1",
cost: "0",
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 1, updated: 2 },
title: "Old title",
location: { directory: "/workspace" },
}
const fetch = Object.assign(
async (input: string | URL | Request, init?: RequestInit) => {
const request = input instanceof Request ? input : new Request(input.toString(), init)
requests.push(request)
const url = new URL(request.url)
if (url.pathname === "/api/event") return new Response("", { headers: { "content-type": "text/event-stream" } })
if (url.pathname === "/api/project")
return Response.json([
{
id: "project-1",
worktree: "/workspace",
time: { created: 1, updated: 2 },
sandboxes: [],
},
])
if (url.pathname === "/api/session" && request.method === "GET")
return Response.json({ data: [session], cursor: {} })
if (url.pathname === "/api/session/ses_known" && request.method === "GET") return Response.json({ data: session })
if (url.pathname === "/api/session/ses_known/rename" && request.method === "POST")
return new Response(null, { status: 204 })
return new Response("Not found", { status: 404 })
},
{ preconnect: () => {} },
)
const client = OpenCode.make({
baseUrl: "https://opencode.test",
fetch,
})
const bridge = await createOpenCodeBridge({
client,
directory: "/workspace",
model: { providerID: "openai", id: "gpt-test" },
notify: () => {},
completionStore: memoryCompletionStore(),
})
const execute = async (name: string, input: Record<string, unknown>) => (await bridge.execute(name, input)).output
try {
expect(new Set(bridge.definitions.map((tool) => tool.name)).size).toBe(bridge.definitions.length)
expect(bridge.definitions.every((tool) => tool.name.length > 0 && tool.description.length > 0)).toBe(true)
expect(bridge.definitions.find((tool) => tool.name === "reply_form")).toMatchObject({
parameters: {
properties: { answer: { type: "object", additionalProperties: true } },
},
})
expect(bridge.definitions.find((tool) => tool.name === "rename_session")).toMatchObject({
description: expect.stringContaining("does not prompt, wake, or interrupt"),
parameters: {
type: "object",
additionalProperties: false,
required: ["session_id", "title"],
},
})
expect(await execute("rename_session", { session_id: "ses_unknown", title: "Nope" })).toEqual({
status: "error",
message: "Use a session ID returned by find_sessions or start_session.",
retryable: false,
})
expect(requests.some((request) => new URL(request.url).pathname.includes("ses_unknown"))).toBe(false)
await execute("find_sessions", {
query: null,
scope: "current_project",
recency: "any",
limit: 10,
})
expect(await execute("rename_session", { session_id: "ses_known", title: " " })).toEqual({
status: "error",
message: "A non-empty session title is required.",
retryable: false,
})
expect(await execute("rename_session", { session_id: "ses_known", title: "Old title" })).toEqual({
status: "unchanged",
session_id: "ses_known",
previous_title: "Old title",
title: "Old title",
})
expect(requests.some((request) => new URL(request.url).pathname.endsWith("/rename"))).toBe(false)
expect(await execute("rename_session", { session_id: "ses_known", title: " New title " })).toEqual({
status: "renamed",
session_id: "ses_known",
previous_title: "Old title",
title: "New title",
})
const rename = requests.find((request) => new URL(request.url).pathname.endsWith("/rename"))
expect(rename?.method).toBe("POST")
expect(await rename?.json()).toEqual({ title: "New title" })
} finally {
await bridge.close()
}
})
test("retries a lost completion wait and prompts a started session by ID", async () => {
const requests: Request[] = []
const prompts: Array<{ readonly id: string; readonly text: string }> = []
const notifications: Array<unknown> = []
const traces: Array<{ readonly event: string; readonly data?: Record<string, unknown> }> = []
let waits = 0
const fetch = Object.assign(
async (input: string | URL | Request, init?: RequestInit) => {
const request = input instanceof Request ? input : new Request(input.toString(), init)
requests.push(request)
const url = new URL(request.url)
if (url.pathname === "/api/event") return new Response("", { headers: { "content-type": "text/event-stream" } })
if (url.pathname === "/api/session" && request.method === "POST")
return Response.json({
data: {
id: "ses_focused",
projectID: "project-1",
cost: "0",
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 1, updated: 1 },
title: "Focused session",
location: { directory: "/workspace" },
},
})
if (url.pathname === "/api/session/ses_focused/prompt" && request.method === "POST") {
const body = await request.json()
if (
typeof body !== "object" ||
body === null ||
!("id" in body) ||
typeof body.id !== "string" ||
!("text" in body) ||
typeof body.text !== "string"
)
return new Response("Invalid prompt", { status: 400 })
prompts.push({ id: body.id, text: body.text })
return Response.json({ data: { id: body.id, admittedSeq: prompts.length } })
}
if (url.pathname === "/api/session/ses_focused/wait" && request.method === "POST") {
waits += 1
if (waits === 1) throw new Error("connection closed")
return new Response(null, { status: 204 })
}
if (url.pathname === "/api/session/ses_focused/message" && request.method === "GET")
return Response.json({
data: prompts.toReversed().map((prompt) => ({ id: prompt.id, type: "user", text: prompt.text })),
cursor: {},
})
return new Response("Not found", { status: 404 })
},
{ preconnect: () => {} },
)
const bridge = await createOpenCodeBridge({
client: OpenCode.make({ baseUrl: "https://opencode.test", fetch }),
directory: "/workspace",
model: { providerID: "openai", id: "gpt-test" },
notify: (notification) => notifications.push(notification),
trace: (event, data) => traces.push({ event, data }),
completionStore: memoryCompletionStore(),
})
const execute = async (name: string, input: Record<string, unknown>) => (await bridge.execute(name, input)).output
try {
expect(bridge.definitions.some((tool) => tool.name === "continue_session")).toBe(false)
const started = await bridge.execute("start_session", { text: "First task", project_id: null })
expect(started.output).toMatchObject({
status: "started",
session_id: "ses_focused",
})
expect(started.admittedPrompt).toEqual({ sessionID: "ses_focused", promptID: prompts[0].id })
expect(
await execute("prompt_session", { session_id: "ses_focused", text: "Follow-up task" }),
).toMatchObject({
status: "started",
session_id: "ses_focused",
})
await Bun.sleep(1_100)
expect(prompts.map((prompt) => prompt.text)).toEqual(["First task", "Follow-up task"])
expect(requests.filter((request) => new URL(request.url).pathname.endsWith("/wait")).length).toBeGreaterThanOrEqual(
3,
)
expect(traces.some((entry) => entry.event === "opencode.wait.retrying")).toBe(true)
expect(notifications.some((value) => JSON.stringify(value).includes("opencode.prompt.failed"))).toBe(false)
} finally {
await bridge.close()
}
})
test("discards a restored prompt whose session no longer exists", async () => {
const removed: Array<{ readonly sessionID: string; readonly promptID: string }> = []
const handle = { sessionID: "ses_missing", promptID: "msg_orphaned" }
const fetch = Object.assign(
async (input: string | URL | Request) => {
const request = input instanceof Request ? input : new Request(input.toString())
const url = new URL(request.url)
if (url.pathname === "/api/event") return new Response("", { headers: { "content-type": "text/event-stream" } })
if (url.pathname === "/api/session/ses_missing")
return Response.json(
{ _tag: "SessionNotFoundError", sessionID: handle.sessionID, message: `Session not found: ${handle.sessionID}` },
{ status: 404 },
)
return new Response("Not found", { status: 404 })
},
{ preconnect: () => {} },
)
const store = {
...memoryCompletionStore(),
entries: () => [{ status: "pending" as const, handle }],
remove: async (entry) => {
removed.push(entry)
},
} satisfies CompletionStore
const bridge = await createOpenCodeBridge({
client: OpenCode.make({ baseUrl: "https://opencode.test", fetch }),
directory: "/workspace",
model: { providerID: "openai", id: "gpt-test" },
notify: () => {},
completionStore: store,
})
try {
expect(removed).toEqual([handle])
} finally {
await bridge.close()
}
})
function memoryCompletionStore(): CompletionStore {
return {
entries: () => [],
admitting: async () => {},
pending: async () => {},
completed: async () => {},
remove: async () => {},
close: async () => {},
}
}