import { ServiceUnavailableException } from "@nestjs/common"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ChatProxyService } from "./chat-proxy.service"; describe("ChatProxyService", () => { const userId = "user-123"; const prisma = { userAgentConfig: { findUnique: vi.fn(), }, }; const containerLifecycle = { ensureRunning: vi.fn(), touch: vi.fn(), }; let service: ChatProxyService; let fetchMock: ReturnType; beforeEach(() => { fetchMock = vi.fn(); vi.stubGlobal("fetch", fetchMock); service = new ChatProxyService(prisma as never, containerLifecycle as never); }); afterEach(() => { vi.unstubAllGlobals(); vi.clearAllMocks(); }); describe("getContainerUrl", () => { it("calls ensureRunning and touch for the user", async () => { containerLifecycle.ensureRunning.mockResolvedValue({ url: "http://mosaic-user-user-123:19000", token: "gateway-token", }); containerLifecycle.touch.mockResolvedValue(undefined); const url = await service.getContainerUrl(userId); expect(url).toBe("http://mosaic-user-user-123:19000"); expect(containerLifecycle.ensureRunning).toHaveBeenCalledWith(userId); expect(containerLifecycle.touch).toHaveBeenCalledWith(userId); }); }); describe("proxyChat", () => { it("forwards the request to the user's OpenClaw container", async () => { containerLifecycle.ensureRunning.mockResolvedValue({ url: "http://mosaic-user-user-123:19000", token: "gateway-token", }); containerLifecycle.touch.mockResolvedValue(undefined); fetchMock.mockResolvedValue(new Response("event: token\ndata: hello\n\n")); const messages = [{ role: "user", content: "Hello from Mosaic" }]; const response = await service.proxyChat(userId, messages); expect(response).toBeInstanceOf(Response); expect(fetchMock).toHaveBeenCalledWith( "http://mosaic-user-user-123:19000/v1/chat/completions", expect.objectContaining({ method: "POST", headers: { Authorization: "Bearer gateway-token", "Content-Type": "application/json", }, }) ); const [, request] = fetchMock.mock.calls[0] as [string, RequestInit]; const parsedBody = JSON.parse(String(request.body)); expect(parsedBody).toEqual({ messages, model: "openclaw:default", stream: true, }); }); it("throws ServiceUnavailableException on connection refused errors", async () => { containerLifecycle.ensureRunning.mockResolvedValue({ url: "http://mosaic-user-user-123:19000", token: "gateway-token", }); containerLifecycle.touch.mockResolvedValue(undefined); fetchMock.mockRejectedValue(new Error("connect ECONNREFUSED 127.0.0.1:19000")); await expect(service.proxyChat(userId, [])).rejects.toBeInstanceOf( ServiceUnavailableException ); }); it("throws ServiceUnavailableException on timeout errors", async () => { containerLifecycle.ensureRunning.mockResolvedValue({ url: "http://mosaic-user-user-123:19000", token: "gateway-token", }); containerLifecycle.touch.mockResolvedValue(undefined); fetchMock.mockRejectedValue(new Error("The operation was aborted due to timeout")); await expect(service.proxyChat(userId, [])).rejects.toBeInstanceOf( ServiceUnavailableException ); }); }); });