feat(voice): add session archive control
This commit is contained in:
parent
62c47f467f
commit
87786c5e88
33 changed files with 4827 additions and 3 deletions
152
packages/voice/test/opencode.test.ts
Normal file
152
packages/voice/test/opencode.test.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
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 and archives only sessions discovered by the voice controller", async () => {
|
||||
const requests: Request[] = []
|
||||
const server: { archived?: number } = {}
|
||||
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, time: { ...session.time, archived: server.archived } } })
|
||||
if (url.pathname === "/api/session/ses_known/rename" && request.method === "POST")
|
||||
return new Response(null, { status: 204 })
|
||||
if (url.pathname === "/api/session/ses_known/archive" && request.method === "POST") {
|
||||
server.archived = Date.now()
|
||||
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: () => {},
|
||||
onSession: () => {},
|
||||
completionStore: memoryCompletionStore(),
|
||||
})
|
||||
|
||||
try {
|
||||
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(bridge.definitions.find((tool) => tool.name === "archive_session")).toMatchObject({
|
||||
description: expect.stringContaining("explicitly confirms"),
|
||||
parameters: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: ["session_id"],
|
||||
},
|
||||
})
|
||||
|
||||
expect(await bridge.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(await bridge.execute("archive_session", {})).toEqual({
|
||||
status: "error",
|
||||
message: "Use a session ID returned by find_sessions or start_session.",
|
||||
retryable: false,
|
||||
})
|
||||
expect(await bridge.execute("archive_session", { session_id: "ses_unknown" })).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 bridge.execute("find_sessions", {
|
||||
query: null,
|
||||
scope: "current_project",
|
||||
recency: "any",
|
||||
limit: 10,
|
||||
})
|
||||
expect(await bridge.execute("rename_session", { session_id: "ses_known", title: " " })).toEqual({
|
||||
status: "error",
|
||||
message: "A non-empty session title is required.",
|
||||
retryable: false,
|
||||
})
|
||||
expect(await bridge.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 bridge.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" })
|
||||
|
||||
expect(await bridge.execute("archive_session", { session_id: "ses_known" })).toEqual({
|
||||
status: "archived",
|
||||
title: "Old title",
|
||||
})
|
||||
const archive = requests.find((request) => new URL(request.url).pathname.endsWith("/archive"))
|
||||
expect(archive?.method).toBe("POST")
|
||||
expect(await archive?.text()).toBe("")
|
||||
expect(await bridge.execute("archive_session", { session_id: "ses_known" })).toEqual({
|
||||
status: "already_archived",
|
||||
title: "Old title",
|
||||
})
|
||||
expect(requests.filter((request) => new URL(request.url).pathname.endsWith("/archive"))).toHaveLength(1)
|
||||
expect(requests.some((request) => /\/(prompt|interrupt)$/.test(new URL(request.url).pathname))).toBe(false)
|
||||
} finally {
|
||||
await bridge.close()
|
||||
}
|
||||
})
|
||||
|
||||
function memoryCompletionStore(): CompletionStore {
|
||||
return {
|
||||
entries: () => [],
|
||||
admitting: async () => {},
|
||||
pending: async () => {},
|
||||
completed: async () => {},
|
||||
delivered: async () => {},
|
||||
close: async () => {},
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue