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