import { afterAll, beforeAll, describe, expect, it, mock } from "bun:test"; import express, { Router } from "express"; import type { Server } from "node:http"; import { CredentialRefreshCoordinator } from "../../src/auth/credentialRefresh.js"; import { registerChatInteractionRoutes } from "../../src/routes/chatInteractionRoutes.js"; import type { ActiveRun } from "../../src/routes/chatUiState.js"; describe("chat interaction routes", () => { let baseUrl = ""; let server: Server; const replyQuestion = mock(async () => ({ ok: true })); const replyPermission = mock(async () => ({ ok: true })); beforeAll(async () => { const activeRuns = new Map(); activeRuns.set("runtime-session", { clientSessionId: "client-session", controller: new AbortController(), messages: [ { id: "assistant-1", role: "assistant", permissions: [ { requestId: "permission-1", sessionId: "runtime-session", permission: "bash", patterns: ["npm test"], always: ["npm test"], createdAt: 1, status: "pending", }, ], questions: [{ requestId: "question-1", status: "pending" }], }, ], pendingPermissions: new Map([ [ "permission-1", { session_id: "runtime-session", request_id: "permission-1", permission: "bash", patterns: ["npm test"], always: ["npm test"], created_at: 1, }, ], ]), pendingQuestions: new Map([ [ "question-1", { created_at: 1, request_id: "question-1", session_id: "runtime-session", questions: [], }, ], ]), status: "running", subscribers: new Set(), }); const router = Router(); router.use((req, _res, next) => { req.agentAuth = { accessToken: "access-token", userId: "user-1", keycloakSub: "keycloak-1", username: "tester", role: "user", isSuperuser: false, projectId: "project-1", network: "network-1", projectRole: "member", }; next(); }); registerChatInteractionRoutes(router, { activeRuns, credentialRefreshCoordinator: new CredentialRefreshCoordinator(), runtime: { replyPermission, replyQuestion } as never, sessionMetadataStore: { get: async () => ({ sessionId: "runtime-session" }), } as never, sessionUiStateStore: { read: async () => null, write: async () => undefined, } as never, }); const app = express(); app.use(express.json()); app.use(router); server = app.listen(0); await new Promise((resolve) => server.once("listening", resolve)); const address = server.address(); if (!address || typeof address === "string") { throw new Error("test server did not expose a TCP port"); } baseUrl = `http://127.0.0.1:${address.port}`; }); afterAll(() => { server.close(); }); it("submits answers to the stable OpenCode question adapter", async () => { const response = await fetch( `${baseUrl}/sessions/client-session/question-responses`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ request_id: "question-1", action: "reply", answers: [["继续"]], }), }, ); expect(response.status).toBe(202); expect(replyQuestion).toHaveBeenCalledWith({ requestId: "question-1", sessionId: "runtime-session", answers: [["继续"]], }); }); it("forwards saved permission grants to OpenCode", async () => { const response = await fetch( `${baseUrl}/sessions/client-session/permission-responses`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ request_id: "permission-1", reply: "always", }), }, ); expect(response.status).toBe(202); expect(replyPermission).toHaveBeenCalledWith({ requestId: "permission-1", sessionId: "runtime-session", reply: "always", message: undefined, }); }); });