import { describe, expect, it } from "vitest"; import { isModelId } from "@/lib/chat/types"; import { ChatValidationError, validateChatRequest, validatePersistentChatRequest, } from "@/lib/chat/validation"; describe("isModelId", () => { it.each(["auto", "qwen", "claude-sonnet.4", `m${"x".repeat(127)}`])( "accepts the Gate model slug %j", (value) => expect(isModelId(value)).toBe(true), ); it.each(["", "Auto", "model/path", ".leading", "-leading", `m${"x".repeat(128)}`])( "rejects the invalid Gate model slug %j", (value) => expect(isModelId(value)).toBe(false), ); }); describe("validateChatRequest", () => { it("returns a normalized request containing only supported fields", () => { expect( validateChatRequest({ model: "chatgpt", ignored: "value", messages: [ { role: "user", content: "Привет", ignored: "value" }, { role: "assistant", content: "Здравствуйте" }, ], }), ).toEqual({ model: "chatgpt", messages: [ { role: "user", content: "Привет" }, { role: "assistant", content: "Здравствуйте" }, ], }); }); it.each([null, [], "request", 1])("rejects a non-object request: %j", (value) => { expect(() => validateChatRequest(value)).toThrow(ChatValidationError); }); it("rejects an unknown model", () => { expect(() => validateChatRequest({ model: "other", messages: [] })).toThrow( ChatValidationError, ); }); it("accepts a dynamic Gate model in both chat request shapes", () => { expect( validateChatRequest({ model: "claude-sonnet.4", messages: [{ role: "user", content: "Привет" }], }).model, ).toBe("claude-sonnet.4"); expect( validatePersistentChatRequest({ model: "claude-sonnet.4", content: "Привет", }).model, ).toBe("claude-sonnet.4"); }); it("rejects an empty message array", () => { expect(() => validateChatRequest({ model: "qwen", messages: [] })).toThrow( ChatValidationError, ); }); it("rejects more than 100 messages", () => { expect(() => validateChatRequest({ model: "qwen", messages: Array.from({ length: 101 }, () => ({ role: "user", content: "x" })), }), ).toThrow(ChatValidationError); }); it("accepts the exact message-count and character boundaries", () => { expect( validateChatRequest({ model: "qwen", messages: Array.from({ length: 100 }, () => ({ role: "user", content: "x", })), }).messages, ).toHaveLength(100); expect( validateChatRequest({ model: "qwen", messages: Array.from({ length: 4 }, () => ({ role: "user", content: "x".repeat(32_000), })), }).messages, ).toHaveLength(4); }); it("rejects missing entries in a sparse message array", () => { expect(() => validateChatRequest({ model: "qwen", messages: new Array(1) }), ).toThrow(ChatValidationError); }); it.each(["system", "tool", null])("rejects an unsupported message role: %j", (role) => { expect(() => validateChatRequest({ model: "deepseek", messages: [{ role, content: "x" }] }), ).toThrow(ChatValidationError); }); it.each(["", " \n\t", null])("rejects empty or non-string content: %j", (content) => { expect(() => validateChatRequest({ model: "deepseek", messages: [{ role: "user", content }] }), ).toThrow(ChatValidationError); }); it("rejects a message longer than 32,000 characters", () => { expect(() => validateChatRequest({ model: "chatgpt", messages: [{ role: "user", content: "x".repeat(32_001) }], }), ).toThrow(ChatValidationError); }); it("rejects more than 128,000 total characters", () => { expect(() => validateChatRequest({ model: "chatgpt", messages: Array.from({ length: 5 }, () => ({ role: "user", content: "x".repeat(25_600), })).concat({ role: "user", content: "x" }), }), ).toThrow(ChatValidationError); }); });