iper / tests /server.test.ts
amirkabiri's picture
init
55a0975
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
const BASE_URL = "http://localhost:3002";
let server: any;
beforeAll(async () => {
// Start the server for testing
const { spawn } = require("child_process");
server = spawn("bun", ["run", "src/server.ts"], {
env: { ...process.env, PORT: "3002" },
stdio: "pipe",
});
// Wait for server to start and verify it's running
await new Promise((resolve) => setTimeout(resolve, 3000));
// Verify server is responding
let retries = 5;
while (retries > 0) {
try {
const response = await fetch(`${BASE_URL}/health`);
if (response.ok) break;
} catch (e) {
// Server not ready yet
}
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);
// Check model structure
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");
// Check choices structure
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");
// Check message structure
expect(choice.message).toHaveProperty("role", "assistant");
expect(choice.message).toHaveProperty("content");
expect(typeof choice.message.content).toBe("string");
// Allow for fallback messages in case of Duck.ai issues
expect(choice.message.content.length).toBeGreaterThanOrEqual(0);
// Check usage structure
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]");
// Parse first data chunk
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");
// The first chunk should have role, but be flexible about which chunk it appears in
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");
// Just check that we get a valid response structure
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");
});
});
});