import { createChatRunAccumulator } from "../../src/routes/chat-run-accumulator.js"; import { expect, it } from "bun:test"; import express from "express"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { getLocalAgentContext } from "../../src/context/localContext.js"; import { registerChatInteractionRoutes } from "../../src/routes/chatInteractionRoutes.js"; import type { ActiveRun } from "../../src/routes/chatUiState.js"; import type { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js"; import { SessionMetadataStore } from "../../src/sessions/metadataStore.js"; import { SessionUiStateStore } from "../../src/sessions/uiStateStore.js"; import { SessionWorkspaceStore } from "../../src/sessions/workspace-store.js"; it.each([false, true])("persists permission replies in the owner's workspace (native failure=%s)", async (nativeFailure) => { const root = await mkdtemp(join(tmpdir(), "permission-persistence-")); const workspaces = new SessionWorkspaceStore(join(root, "workspaces")); const sessions = new SessionMetadataStore(join(root, "metadata"), workspaces); const states = new SessionUiStateStore(workspaces); const context = { ...getLocalAgentContext(), sessionId: "permission-session" }; await sessions.ensure(context); const messages = [{ id: "answer", role: "assistant", permissions: [] }]; await states.write(context, { sessionId: context.sessionId, isTitleManuallyEdited: true, messages }); let calls = 0; const runtime = { replyPermission: async () => { calls++; if (nativeFailure) throw new Error("native unavailable"); } } as unknown as OpencodeRuntimeAdapter; const run: ActiveRun = { clientSessionId: context.sessionId, controller: new AbortController(), messages, pendingPermissions: new Map([["permission", { request_id: "permission", session_id: context.sessionId, permission: "shell", patterns: [], always: [], created_at: Date.now() }]]), pendingQuestions: new Map(), subscribers: new Set(), status: "running", }; const accumulator = createChatRunAccumulator({ run, messages: [], messageId: "answer", sessionId: context.sessionId, collaboration: false, write() {}, onStatus() {} }); accumulator.apply("permission_request", run.pendingPermissions.get("permission")! as unknown as Record); await states.write(context, { sessionId: context.sessionId, isTitleManuallyEdited: true, messages: run.messages }); const app = express(); app.use(express.json()); registerChatInteractionRoutes(app, { activeRuns: new Map([[context.sessionId, run]]), runtime, sessionMetadataStore: sessions, sessionUiStateStore: states }); const server = app.listen(0, "127.0.0.1"); await new Promise((resolve) => server.once("listening", resolve)); const url = `http://127.0.0.1:${(server.address() as { port: number }).port}/permission/permission/reply`; const reply = (headers = {}) => fetch(url, { method: "POST", headers: { "Content-Type": "application/json", ...headers }, body: JSON.stringify({ session_id: context.sessionId, reply: "once" }) }); try { expect((await reply({ "X-Agent-Local-User": "other" })).status).toBe(404); expect(calls).toBe(0); expect((await reply()).status).toBe(nativeFailure ? 502 : 202); expect(calls).toBe(1); expect(run.pendingPermissions.has("permission")).toBe(nativeFailure); const saved = await states.read(context); expect(saved?.isTitleManuallyEdited).toBe(true); const parts = (saved?.messages[0] as { parts: Array<{ type: string; data: Record }> }).parts; expect(parts.find((part) => part.type === "data-permission_response")?.data).toMatchObject(nativeFailure ? { request_id: "permission", status: "error", error: "native unavailable" } : { request_id: "permission", reply: "once" }); expect(run.messages[0]).toMatchObject({ permissions: [{ requestId: "permission", status: nativeFailure ? "error" : "approved_once" }] }); } finally { await new Promise((resolve) => server.close(() => resolve())); await rm(root, { recursive: true, force: true }); } });