| | 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"); |
| | }); |
| | }); |
| | }); |
| |
|