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 { 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("reconciles persisted legacy forms and replies after the active run was lost", async () => { const root = await mkdtemp(join(tmpdir(), "legacy-question-routes-")); const workspaces = new SessionWorkspaceStore(join(root, "workspaces")); const sessions = new SessionMetadataStore(join(root, "metadata"), workspaces); const states = new SessionUiStateStore(join(root, "unused"), workspaces); const context = { ...getLocalAgentContext(), sessionId: "session" }; await sessions.ensure(context); await states.write(context, { sessionId: "session", messages: [{ id: "old", role: "assistant", questions: [{ requestId: "form", status: "pending", tool: { callID: "call" } }] }, { id: "new", role: "assistant", text: "later message", parts: [{ type: "data-question_request", data: { request_id: "form2", session_id: "session", questions: [], tool: { callID: "call2" } } }, { type: "data-question_request", data: { request_id: "form3", session_id: "session", questions: [] } }, { type: "data-question_request", data: { request_id: "form4", session_id: "session", questions: [] } }] }] }); let status: "pending" | "answered" | "cancelled" | "missing" = "pending"; let unavailable = false; let reads = 0; let replies = 0; const runtime = { getQuestionStatus: async () => { reads++; if (unavailable) throw new Error("private-native-path"); return status; }, replyQuestion: async () => { replies++; status = "answered"; }, rejectQuestion: async () => { status = "cancelled"; } } as unknown as OpencodeRuntimeAdapter; const app = express(); app.use(express.json()); registerChatInteractionRoutes(app, { activeRuns: new Map(), runtime, sessionMetadataStore: sessions, sessionUiStateStore: states }); const server = app.listen(0, "127.0.0.1"); await new Promise((resolve) => server.once("listening", resolve)); const base = `http://127.0.0.1:${(server.address() as { port: number }).port}`; const get = (id = "form", headers = {}) => fetch(`${base}/session/session/questions/${id}`, { headers }); const reply = (id = "form") => fetch(`${base}/question/${id}/reply`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_id: "session", answers: [["A"]] }) }); try { expect((await get("unknown")).status).toBe(404); expect(reads).toBe(0); expect((await get("form", { "X-Agent-Local-User": "other" })).status).toBe(404); expect(reads).toBe(0); expect(await (await get("call")).json()).toMatchObject({ request_id: "form", status: "pending" }); unavailable = true; const failed = await get(); expect(failed.status).toBe(503); expect(await failed.text()).not.toContain("private-native-path"); expect((await states.read(context))?.messages[0]).toMatchObject({ questions: [{ status: "pending" }] }); unavailable = false; expect((await reply()).status).toBe(202); expect(replies).toBe(1); expect((await states.read(context))?.messages[0]).toMatchObject({ questions: [{ status: "answered", answers: [["A"]] }] }); expect((await states.read(context))?.messages[1]).toMatchObject({ text: "later message" }); expect((await reply()).status).toBe(409); expect(replies).toBe(1); status = "missing"; expect(await (await get()).json()).toMatchObject({ status: "answered" }); status = "cancelled"; expect(await (await get("call2")).json()).toMatchObject({ request_id: "form2", status: "rejected" }); expect((await states.read(context))?.messages[1]).toMatchObject({ parts: [{ type: "data-question_request" }, { type: "data-question_request" }, { type: "data-question_request" }, { type: "data-question_response", data: { request_id: "form2", status: "rejected" } }] }); status = "missing"; expect(await (await get("form2")).json()).toMatchObject({ status: "rejected" }); expect(await (await get("form3")).json()).toMatchObject({ status: "invalidated" }); status = "pending"; expect((await reply("form4")).status).toBe(202); const canonical = (await states.read(context))?.messages[1] as { parts: Array<{ type: string; data?: unknown }> }; expect(canonical.parts.find((part) => part.type === "data-question_response" && (part.data as { request_id: string }).request_id === "form4")).toMatchObject({ data: { request_id: "form4", status: "answered", answers: [["A"]] } }); status = "missing"; expect(await (await get("form4")).json()).toMatchObject({ status: "answered" }); await sessions.remove((await sessions.get(context, "session"))!); expect((await get()).status).toBe(404); } finally { await new Promise((resolve) => server.close(() => resolve())); await rm(root, { recursive: true, force: true }); } });