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