Files
chatgpt-mcp/test/openai/responses.test.js
T

217 lines
6.7 KiB
JavaScript

import { describe, it, expect, vi, beforeEach } from "vitest";
// --- Helpers ---
function createMockClient(responseBody) {
const mockCreate = vi.fn(async () => ({
output_text: responseBody?.output_text ?? "OK response text",
model: responseBody?.model ?? "gpt-5.1",
usage: responseBody?.usage ?? { inputTokens: 10, outputTokens: 20 },
}));
return { responses: { create: mockCreate } };
}
function makeAuthError() {
const err = new Error("Authentication failed. Provide a valid OPENAI_API_KEY.");
err.status = 401;
return err;
}
function makeRateLimitError() {
const err = new Error("Rate limit exceeded.");
err.status = 429;
return err;
}
function makeNetworkError() {
const err = new Error("fetch failed");
err.status = undefined;
return err;
}
function makeServerError() {
const err = new Error("Service unavailable");
err.status = 503;
return err;
}
function makeTimeoutError() {
const err = new Error("timeout");
err.code = "ETIMEDOUT";
return err;
}
// --- Import ---
const { sendOpenAIResponse } = await import("../../src/openai/responses.js");
describe("sendOpenAIResponse", () => {
let mockClient;
let mockCreate;
beforeEach(() => {
mockClient = createMockClient();
({ responses: { create: mockCreate } } = mockClient);
});
// ===== Success (3) =====
it("passes correct OpenAI params to client.responses.create() including max_output_tokens", async () => {
const input = [{ type: "input_text", text: "Hello" }];
await sendOpenAIResponse(mockClient, {
input,
model: "gpt-5.1",
temperature: 0.2,
maxOutputTokens: 2000,
});
expect(mockCreate).toHaveBeenCalledTimes(1);
const callArgs = mockCreate.mock.calls[0][0];
expect(callArgs.model).toBe("gpt-5.1");
expect(callArgs.input).toBe(input);
expect(callArgs.temperature).toBe(0.2);
expect(callArgs.max_output_tokens).toBe(2000);
});
it("omits max_output_tokens when maxOutputTokens is not provided", async () => {
const input = [{ type: "input_text", text: "Hello" }];
await sendOpenAIResponse(mockClient, {
input,
model: "gpt-5.1",
temperature: 0.2,
});
const callArgs = mockCreate.mock.calls[0][0];
expect(callArgs).not.toHaveProperty("max_output_tokens");
});
it("returns response.output_text as content", async () => {
const client = createMockClient({ output_text: "Custom text" });
const result = await sendOpenAIResponse(client, {
input: [{ type: "input_text", text: "x" }],
model: "gpt-5.1",
});
expect(result.content).toBe("Custom text");
});
// ===== Validation (4) =====
it("throws ValidationError when client is missing", async () => {
await expect(
sendOpenAIResponse(null, { input: ["x"], model: "m" })
).rejects.toHaveProperty("kind", "ValidationError");
});
it("throws ValidationError when client.responses.create is missing", async () => {
const badClient = { responses: {} };
await expect(
sendOpenAIResponse(badClient, { input: ["x"], model: "m" })
).rejects.toHaveProperty("kind", "ValidationError");
});
it("throws ValidationError when params.input is missing", async () => {
await expect(
sendOpenAIResponse(mockClient, { model: "m" })
).rejects.toHaveProperty("kind", "ValidationError");
});
it("throws ValidationError when params.model is missing", async () => {
await expect(
sendOpenAIResponse(mockClient, { input: ["x"] })
).rejects.toHaveProperty("kind", "ValidationError");
});
// ===== Error mapping (3) =====
it("maps 401 to OpenAIAuthError", async () => {
mockCreate.mockRejectedValueOnce(makeAuthError());
await expect(
sendOpenAIResponse(mockClient, { input: ["x"], model: "m" })
).rejects.toHaveProperty("kind", "OpenAIAuthError");
});
it("maps 429 to OpenAIRateLimitError", async () => {
mockCreate.mockRejectedValueOnce(makeRateLimitError());
await expect(
sendOpenAIResponse(mockClient, { input: ["x"], model: "m" })
).rejects.toHaveProperty("kind", "OpenAIRateLimitError");
});
it("maps generic error to OpenAIRequestError", async () => {
mockCreate.mockRejectedValueOnce(makeNetworkError());
await expect(
sendOpenAIResponse(mockClient, { input: ["x"], model: "m" })
).rejects.toHaveProperty("kind", "OpenAIRequestError");
});
// ===== Security (1) =====
it("does not leak API key in error message", async () => {
const authErr = makeAuthError();
authErr.message = "Invalid OPENAI_API_KEY: sk-my-secret-key-12345.";
mockCreate.mockRejectedValueOnce(authErr);
try {
await sendOpenAIResponse(mockClient, { input: ["x"], model: "m" });
expect.fail("Should have thrown");
} catch (err) {
expect(err.message).not.toContain("sk-my-secret-key-12345");
expect(err.kind).toBe("OpenAIAuthError");
}
});
// ===== Empty content (1) =====
it("returns empty content when output_text is empty", async () => {
const client = createMockClient({ output_text: "" });
const result = await sendOpenAIResponse(client, {
input: ["x"],
model: "gpt-5.1",
});
expect(result.content).toBe("");
});
// ===== Validation: invalid maxOutputTokens (1) =====
it("throws ValidationError when maxOutputTokens is negative", async () => {
await expect(
sendOpenAIResponse(mockClient, { input: ["x"], model: "m", maxOutputTokens: -5 })
).rejects.toHaveProperty("kind", "ValidationError");
});
it("throws ValidationError when maxOutputTokens is zero", async () => {
await expect(
sendOpenAIResponse(mockClient, { input: ["x"], model: "m", maxOutputTokens: 0 })
).rejects.toHaveProperty("kind", "ValidationError");
});
it("throws ValidationError when maxOutputTokens is a non-positive float", async () => {
await expect(
sendOpenAIResponse(mockClient, { input: ["x"], model: "m", maxOutputTokens: 0.5 })
).rejects.toHaveProperty("kind", "ValidationError");
});
it("throws ValidationError when maxOutputTokens is NaN", async () => {
await expect(
sendOpenAIResponse(mockClient, { input: ["x"], model: "m", maxOutputTokens: NaN })
).rejects.toHaveProperty("kind", "ValidationError");
});
// ===== Error mapping edge cases (2) =====
it("maps status 503 to OpenAIRequestError", async () => {
mockCreate.mockRejectedValueOnce(makeServerError());
await expect(
sendOpenAIResponse(mockClient, { input: ["x"], model: "m" })
).rejects.toHaveProperty("kind", "OpenAIRequestError");
});
it("maps timeout error (code ETIMEDOUT) to OpenAIRequestError", async () => {
mockCreate.mockRejectedValueOnce(makeTimeoutError());
await expect(
sendOpenAIResponse(mockClient, { input: ["x"], model: "m" })
).rejects.toHaveProperty("kind", "OpenAIRequestError");
});
});