109 lines
3.5 KiB
TypeScript
109 lines
3.5 KiB
TypeScript
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<typeof vi.fn>;
|
|
|
|
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
|
|
);
|
|
});
|
|
});
|
|
});
|