| import { describe, it, expect, beforeAll, afterAll } from "bun:test"; |
|
|
| const BASE_URL = "http://localhost:3002"; |
| let server: any; |
|
|
| beforeAll(async () => { |
| |
| const { spawn } = require("child_process"); |
| server = spawn("bun", ["run", "src/server.ts"], { |
| env: { ...process.env, PORT: "3002" }, |
| stdio: "pipe", |
| }); |
|
|
| |
| await new Promise((resolve) => setTimeout(resolve, 3000)); |
|
|
| |
| let retries = 5; |
| while (retries > 0) { |
| try { |
| const response = await fetch(`${BASE_URL}/health`); |
| if (response.ok) break; |
| } catch (e) { |
| |
| } |
| retries--; |
| await new Promise((resolve) => setTimeout(resolve, 1000)); |
| } |
| }); |
|
|
| afterAll(() => { |
| if (server) { |
| server.kill(); |
| } |
| }); |
|
|
| describe("OpenAI Compatible Server", () => { |
| describe("Health Check", () => { |
| it("should return health status", async () => { |
| const response = await fetch(`${BASE_URL}/health`); |
| expect(response.status).toBe(200); |
|
|
| const data = await response.json(); |
| expect(data).toEqual({ status: "ok" }); |
| }); |
| }); |
|
|
| describe("Models Endpoint", () => { |
| it("should return list of available models", async () => { |
| const response = await fetch(`${BASE_URL}/v1/models`); |
| expect(response.status).toBe(200); |
|
|
| const data = await response.json(); |
| expect(data.object).toBe("list"); |
| expect(Array.isArray(data.data)).toBe(true); |
| expect(data.data.length).toBeGreaterThan(0); |
|
|
| |
| const model = data.data[0]; |
| expect(model).toHaveProperty("id"); |
| expect(model).toHaveProperty("object", "model"); |
| expect(model).toHaveProperty("created"); |
| expect(model).toHaveProperty("owned_by", "duckai"); |
| }); |
| }); |
|
|
| describe("Chat Completions", () => { |
| it("should handle basic chat completion", async () => { |
| const requestBody = { |
| model: "gpt-4o-mini", |
| messages: [{ role: "user", content: "Say hello" }], |
| }; |
|
|
| const response = await fetch(`${BASE_URL}/v1/chat/completions`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify(requestBody), |
| }); |
|
|
| expect(response.status).toBe(200); |
|
|
| const data = await response.json(); |
| expect(data).toHaveProperty("id"); |
| expect(data).toHaveProperty("object", "chat.completion"); |
| expect(data).toHaveProperty("created"); |
| expect(data).toHaveProperty("model", "gpt-4o-mini"); |
| expect(data).toHaveProperty("choices"); |
| expect(data).toHaveProperty("usage"); |
|
|
| |
| expect(Array.isArray(data.choices)).toBe(true); |
| expect(data.choices.length).toBe(1); |
|
|
| const choice = data.choices[0]; |
| expect(choice).toHaveProperty("index", 0); |
| expect(choice).toHaveProperty("message"); |
| expect(choice).toHaveProperty("finish_reason", "stop"); |
|
|
| |
| expect(choice.message).toHaveProperty("role", "assistant"); |
| expect(choice.message).toHaveProperty("content"); |
| expect(typeof choice.message.content).toBe("string"); |
| |
| expect(choice.message.content.length).toBeGreaterThanOrEqual(0); |
|
|
| |
| expect(data.usage).toHaveProperty("prompt_tokens"); |
| expect(data.usage).toHaveProperty("completion_tokens"); |
| expect(data.usage).toHaveProperty("total_tokens"); |
| expect(typeof data.usage.prompt_tokens).toBe("number"); |
| expect(typeof data.usage.completion_tokens).toBe("number"); |
| expect(typeof data.usage.total_tokens).toBe("number"); |
| }); |
|
|
| it("should handle streaming chat completion", async () => { |
| const requestBody = { |
| model: "gpt-4o-mini", |
| messages: [{ role: "user", content: "Count to 3" }], |
| stream: true, |
| }; |
|
|
| const response = await fetch(`${BASE_URL}/v1/chat/completions`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify(requestBody), |
| }); |
|
|
| expect(response.status).toBe(200); |
| expect(response.headers.get("content-type")).toBe("text/event-stream"); |
|
|
| const reader = response.body?.getReader(); |
| expect(reader).toBeDefined(); |
|
|
| if (reader) { |
| const decoder = new TextDecoder(); |
| let chunks: string[] = []; |
| let done = false; |
|
|
| while (!done) { |
| const { value, done: readerDone } = await reader.read(); |
| done = readerDone; |
|
|
| if (value) { |
| const chunk = decoder.decode(value); |
| chunks.push(chunk); |
| } |
| } |
|
|
| const fullResponse = chunks.join(""); |
| expect(fullResponse).toContain("data: "); |
| expect(fullResponse).toContain("[DONE]"); |
|
|
| |
| const lines = fullResponse.split("\n"); |
| const firstDataLine = lines.find( |
| (line) => line.startsWith("data: ") && !line.includes("[DONE]") |
| ); |
| expect(firstDataLine).toBeDefined(); |
|
|
| if (firstDataLine) { |
| const jsonStr = firstDataLine.replace("data: ", ""); |
| const data = JSON.parse(jsonStr); |
|
|
| expect(data).toHaveProperty("id"); |
| expect(data).toHaveProperty("object", "chat.completion.chunk"); |
| expect(data).toHaveProperty("created"); |
| expect(data).toHaveProperty("model", "gpt-4o-mini"); |
| expect(data).toHaveProperty("choices"); |
|
|
| const choice = data.choices[0]; |
| expect(choice).toHaveProperty("index", 0); |
| expect(choice).toHaveProperty("delta"); |
| |
| if (choice.delta.role) { |
| expect(choice.delta.role).toBe("assistant"); |
| } |
| } |
| } |
| }); |
|
|
| it("should validate required fields", async () => { |
| const response = await fetch(`${BASE_URL}/v1/chat/completions`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({}), |
| }); |
|
|
| expect(response.status).toBe(400); |
|
|
| const data = await response.json(); |
| expect(data).toHaveProperty("error"); |
| expect(data.error).toHaveProperty("message"); |
| expect(data.error).toHaveProperty("type", "invalid_request_error"); |
| }); |
|
|
| it("should validate message structure", async () => { |
| const response = await fetch(`${BASE_URL}/v1/chat/completions`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ |
| model: "gpt-4o-mini", |
| messages: [{ role: "invalid", content: "test" }], |
| }), |
| }); |
|
|
| expect(response.status).toBe(400); |
|
|
| const data = await response.json(); |
| expect(data.error.message).toContain("valid role"); |
| }); |
|
|
| it("should handle multiple messages", async () => { |
| const requestBody = { |
| model: "gpt-4o-mini", |
| messages: [{ role: "user", content: "What is 2+2?" }], |
| }; |
|
|
| const response = await fetch(`${BASE_URL}/v1/chat/completions`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify(requestBody), |
| }); |
|
|
| expect(response.status).toBe(200); |
|
|
| const data = await response.json(); |
| expect(data.choices[0].message.content).toBeDefined(); |
| expect(typeof data.choices[0].message.content).toBe("string"); |
| |
| expect(data.choices[0].message.role).toBe("assistant"); |
| }); |
| }); |
|
|
| describe("CORS", () => { |
| it("should handle preflight requests", async () => { |
| const response = await fetch(`${BASE_URL}/v1/chat/completions`, { |
| method: "OPTIONS", |
| }); |
|
|
| expect(response.status).toBe(200); |
| expect(response.headers.get("access-control-allow-origin")).toBe("*"); |
| expect(response.headers.get("access-control-allow-methods")).toContain( |
| "POST" |
| ); |
| expect(response.headers.get("access-control-allow-headers")).toContain( |
| "Content-Type" |
| ); |
| }); |
| }); |
|
|
| describe("Error Handling", () => { |
| it("should return 404 for unknown endpoints", async () => { |
| const response = await fetch(`${BASE_URL}/unknown`); |
| expect(response.status).toBe(404); |
|
|
| const data = await response.json(); |
| expect(data.error.message).toBe("Not found"); |
| expect(data.error.type).toBe("invalid_request_error"); |
| }); |
|
|
| it("should handle malformed JSON", async () => { |
| const response = await fetch(`${BASE_URL}/v1/chat/completions`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: "invalid json", |
| }); |
|
|
| expect(response.status).toBe(500); |
|
|
| const data = await response.json(); |
| expect(data).toHaveProperty("error"); |
| }); |
| }); |
| }); |
|
|