feat(api): chat proxy (MS22-P1i) (#615)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #615.
This commit is contained in:
107
apps/api/src/chat-proxy/chat-proxy.service.spec.ts
Normal file
107
apps/api/src/chat-proxy/chat-proxy.service.spec.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
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: {
|
||||
"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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user