import { describe, expect, it } from "bun:test"; import { streamPromptResponse, type PermissionRequestPayload, } from "../../src/routes/chatStream.js"; import { type OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js"; const createEventStream = (events: unknown[]) => ({ async *[Symbol.asyncIterator]() { for (const event of events) { yield event; } }, }); describe("streamPromptResponse", () => { it("forwards opencode permission requests as SSE payloads", async () => { const runtime = { subscribeEvents: async () => createEventStream([ { type: "permission.asked", properties: { id: "perm-1", sessionID: "runtime-session-1", permission: "bash", patterns: ["rm *"], metadata: { command: "rm tmp.txt" }, always: ["rm *"], }, }, { type: "session.idle", properties: { sessionID: "runtime-session-1", }, }, ]), prompt: async () => undefined, messages: async () => [], } as unknown as OpencodeRuntimeAdapter; const events: Array<{ event: string; data: Record }> = []; await streamPromptResponse({ runtime, sessionId: "runtime-session-1", clientSessionId: "client-session-1", message: "delete temp", write: (event, data) => events.push({ event, data }), }); const permissionEvent = events.find((item) => item.event === "permission_request"); expect(permissionEvent?.data).toMatchObject({ session_id: "client-session-1", request_id: "perm-1", permission: "bash", patterns: ["rm *"], metadata: { command: "rm tmp.txt" }, always: ["rm *"], } satisfies Partial); }); it("auto replies always when approval mode is always", async () => { const replies: Array> = []; const runtime = { subscribeEvents: async () => createEventStream([ { type: "permission.asked", properties: { id: "perm-1", sessionID: "runtime-session-1", permission: "bash", patterns: ["npm test"], metadata: { command: "npm test" }, always: ["npm test"], }, }, { type: "session.idle", properties: { sessionID: "runtime-session-1", }, }, ]), prompt: async () => undefined, messages: async () => [], replyPermission: async (options: Record) => { replies.push(options); }, } as unknown as OpencodeRuntimeAdapter; const events: Array<{ event: string; data: Record }> = []; await streamPromptResponse({ runtime, sessionId: "runtime-session-1", clientSessionId: "client-session-1", message: "run tests", approvalMode: "always", write: (event, data) => events.push({ event, data }), }); expect(replies).toEqual([ { requestId: "perm-1", sessionId: "runtime-session-1", reply: "always", }, ]); expect(events.some((item) => item.event === "permission_request")).toBe(false); expect(events.find((item) => item.event === "permission_response")?.data).toEqual({ session_id: "client-session-1", request_id: "perm-1", reply: "always", }); }); it("forwards opencode v2 permission requests as SSE payloads", async () => { const runtime = { subscribeEvents: async () => createEventStream([ { type: "permission.v2.asked", properties: { id: "perm-v2-1", sessionID: "runtime-session-1", action: "external_directory", resources: ["/tmp"], save: ["/tmp"], metadata: { path: "/tmp" }, }, }, { type: "session.idle", properties: { sessionID: "runtime-session-1", }, }, ]), prompt: async () => undefined, messages: async () => [], } as unknown as OpencodeRuntimeAdapter; const events: Array<{ event: string; data: Record }> = []; await streamPromptResponse({ runtime, sessionId: "runtime-session-1", clientSessionId: "client-session-1", message: "read /tmp", write: (event, data) => events.push({ event, data }), }); const permissionEvent = events.find((item) => item.event === "permission_request"); expect(permissionEvent?.data).toMatchObject({ session_id: "client-session-1", request_id: "perm-v2-1", permission: "external_directory", patterns: ["/tmp"], metadata: { path: "/tmp" }, always: ["/tmp"], } satisfies Partial); }); });