mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-04-06 16:12:30 +02:00
* fix: allow nested addParams in config schema * Respect no-op task constraint Constraint: Task 2 explicitly forbids code changes Directive: Keep this worker branch code-identical to the assigned base for this task Confidence: high Scope-risk: narrow Tested: git status --short (clean) * fix: align addParams web_search validation with runtime * test: cover addParams edge cases * chore: ignore .codex directory
423 lines
11 KiB
TypeScript
423 lines
11 KiB
TypeScript
import {
|
|
paramDefinitionSchema,
|
|
agentsEndpointSchema,
|
|
azureEndpointSchema,
|
|
endpointSchema,
|
|
configSchema,
|
|
} from '../src/config';
|
|
import { tModelSpecPresetSchema, EModelEndpoint } from '../src/schemas';
|
|
|
|
describe('paramDefinitionSchema', () => {
|
|
it('accepts a minimal definition with only key', () => {
|
|
const result = paramDefinitionSchema.safeParse({ key: 'temperature' });
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('accepts a full definition with all fields', () => {
|
|
const result = paramDefinitionSchema.safeParse({
|
|
key: 'temperature',
|
|
type: 'number',
|
|
component: 'slider',
|
|
default: 0.7,
|
|
label: 'Temperature',
|
|
range: { min: 0, max: 2, step: 0.01 },
|
|
columns: 2,
|
|
columnSpan: 1,
|
|
includeInput: true,
|
|
descriptionSide: 'right',
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('rejects columns > 4', () => {
|
|
const result = paramDefinitionSchema.safeParse({
|
|
key: 'test',
|
|
columns: 5,
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects columns < 1', () => {
|
|
const result = paramDefinitionSchema.safeParse({
|
|
key: 'test',
|
|
columns: 0,
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects non-integer columns', () => {
|
|
const result = paramDefinitionSchema.safeParse({
|
|
key: 'test',
|
|
columns: 2.5,
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects non-integer columnSpan', () => {
|
|
const result = paramDefinitionSchema.safeParse({
|
|
key: 'test',
|
|
columnSpan: 1.5,
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects negative minTags', () => {
|
|
const result = paramDefinitionSchema.safeParse({
|
|
key: 'test',
|
|
minTags: -1,
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects invalid descriptionSide', () => {
|
|
const result = paramDefinitionSchema.safeParse({
|
|
key: 'test',
|
|
descriptionSide: 'diagonal',
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects invalid type enum value', () => {
|
|
const result = paramDefinitionSchema.safeParse({
|
|
key: 'test',
|
|
type: 'invalid',
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects invalid component enum value', () => {
|
|
const result = paramDefinitionSchema.safeParse({
|
|
key: 'test',
|
|
component: 'wheel',
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('allows type and component to be omitted (merged from defaults at runtime)', () => {
|
|
const result = paramDefinitionSchema.safeParse({
|
|
key: 'temperature',
|
|
range: { min: 0, max: 2, step: 0.01 },
|
|
});
|
|
expect(result.success).toBe(true);
|
|
expect(result.data).not.toHaveProperty('type');
|
|
expect(result.data).not.toHaveProperty('component');
|
|
});
|
|
});
|
|
|
|
describe('tModelSpecPresetSchema', () => {
|
|
it('strips system/DB fields from preset', () => {
|
|
const result = tModelSpecPresetSchema.safeParse({
|
|
conversationId: 'conv-123',
|
|
presetId: 'preset-456',
|
|
title: 'My Preset',
|
|
defaultPreset: true,
|
|
order: 3,
|
|
isArchived: true,
|
|
user: 'user123',
|
|
messages: ['msg1'],
|
|
tags: ['tag1'],
|
|
file_ids: ['file1'],
|
|
expiredAt: '2026-12-31',
|
|
parentMessageId: 'parent1',
|
|
model: 'gpt-4o',
|
|
endpoint: EModelEndpoint.openAI,
|
|
});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data).not.toHaveProperty('conversationId');
|
|
expect(result.data).not.toHaveProperty('presetId');
|
|
expect(result.data).not.toHaveProperty('title');
|
|
expect(result.data).not.toHaveProperty('defaultPreset');
|
|
expect(result.data).not.toHaveProperty('order');
|
|
expect(result.data).not.toHaveProperty('isArchived');
|
|
expect(result.data).not.toHaveProperty('user');
|
|
expect(result.data).not.toHaveProperty('messages');
|
|
expect(result.data).not.toHaveProperty('tags');
|
|
expect(result.data).not.toHaveProperty('file_ids');
|
|
expect(result.data).not.toHaveProperty('expiredAt');
|
|
expect(result.data).not.toHaveProperty('parentMessageId');
|
|
expect(result.data).toHaveProperty('model', 'gpt-4o');
|
|
}
|
|
});
|
|
|
|
it('strips deprecated fields', () => {
|
|
const result = tModelSpecPresetSchema.safeParse({
|
|
resendImages: true,
|
|
chatGptLabel: 'old-label',
|
|
model: 'gpt-4o',
|
|
endpoint: EModelEndpoint.openAI,
|
|
});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data).not.toHaveProperty('resendImages');
|
|
expect(result.data).not.toHaveProperty('chatGptLabel');
|
|
}
|
|
});
|
|
|
|
it('strips frontend-only fields', () => {
|
|
const result = tModelSpecPresetSchema.safeParse({
|
|
greeting: 'Hello!',
|
|
iconURL: 'https://example.com/icon.png',
|
|
spec: 'some-spec',
|
|
presetOverride: { model: 'other' },
|
|
model: 'gpt-4o',
|
|
endpoint: EModelEndpoint.openAI,
|
|
});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data).not.toHaveProperty('greeting');
|
|
expect(result.data).not.toHaveProperty('iconURL');
|
|
expect(result.data).not.toHaveProperty('spec');
|
|
expect(result.data).not.toHaveProperty('presetOverride');
|
|
}
|
|
});
|
|
|
|
it('preserves valid preset fields', () => {
|
|
const result = tModelSpecPresetSchema.safeParse({
|
|
model: 'gpt-4o',
|
|
endpoint: EModelEndpoint.openAI,
|
|
temperature: 0.7,
|
|
topP: 0.9,
|
|
maxOutputTokens: 4096,
|
|
});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.model).toBe('gpt-4o');
|
|
expect(result.data.temperature).toBe(0.7);
|
|
expect(result.data.topP).toBe(0.9);
|
|
expect(result.data.maxOutputTokens).toBe(4096);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('endpointSchema deprecated fields', () => {
|
|
const validEndpoint = {
|
|
name: 'CustomEndpoint',
|
|
apiKey: 'test-key',
|
|
baseURL: 'https://api.example.com',
|
|
models: { default: ['model-1'] },
|
|
};
|
|
|
|
it('silently strips deprecated summarize field', () => {
|
|
const result = endpointSchema.safeParse({
|
|
...validEndpoint,
|
|
summarize: true,
|
|
summaryModel: 'gpt-4o',
|
|
});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data).not.toHaveProperty('summarize');
|
|
expect(result.data).not.toHaveProperty('summaryModel');
|
|
}
|
|
});
|
|
|
|
it('silently strips deprecated customOrder field', () => {
|
|
const result = endpointSchema.safeParse({
|
|
...validEndpoint,
|
|
customOrder: 5,
|
|
});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data).not.toHaveProperty('customOrder');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('endpointSchema addParams validation', () => {
|
|
const validEndpoint = {
|
|
name: 'CustomEndpoint',
|
|
apiKey: 'test-key',
|
|
baseURL: 'https://api.example.com',
|
|
models: { default: ['model-1'] },
|
|
};
|
|
const nestedAddParams = {
|
|
provider: {
|
|
only: ['z-ai'],
|
|
quantizations: ['int4'],
|
|
},
|
|
};
|
|
|
|
it('accepts nested addParams objects and arrays', () => {
|
|
const result = endpointSchema.safeParse({
|
|
...validEndpoint,
|
|
addParams: nestedAddParams,
|
|
});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.addParams).toEqual(nestedAddParams);
|
|
}
|
|
});
|
|
|
|
it('keeps configSchema validation intact with nested custom addParams', () => {
|
|
const result = configSchema.safeParse({
|
|
version: '1.0.0',
|
|
endpoints: {
|
|
custom: [
|
|
{
|
|
...validEndpoint,
|
|
addParams: nestedAddParams,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('accepts boolean web_search in addParams', () => {
|
|
const result = endpointSchema.safeParse({
|
|
...validEndpoint,
|
|
addParams: {
|
|
provider: {
|
|
only: ['z-ai'],
|
|
},
|
|
web_search: true,
|
|
},
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('accepts scalar addParams values', () => {
|
|
const result = endpointSchema.safeParse({
|
|
...validEndpoint,
|
|
addParams: {
|
|
model: 'custom-model',
|
|
retries: 2,
|
|
metadata: null,
|
|
},
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('rejects non-boolean web_search objects in addParams', () => {
|
|
const result = endpointSchema.safeParse({
|
|
...validEndpoint,
|
|
addParams: {
|
|
provider: {
|
|
only: ['z-ai'],
|
|
},
|
|
web_search: {
|
|
enabled: true,
|
|
},
|
|
},
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects configSchema entries with non-boolean web_search objects in custom addParams', () => {
|
|
const result = configSchema.safeParse({
|
|
version: '1.0.0',
|
|
endpoints: {
|
|
custom: [
|
|
{
|
|
...validEndpoint,
|
|
addParams: {
|
|
provider: {
|
|
only: ['z-ai'],
|
|
},
|
|
web_search: {
|
|
enabled: true,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('agentsEndpointSchema', () => {
|
|
it('does not accept baseURL', () => {
|
|
const result = agentsEndpointSchema.safeParse({
|
|
baseURL: 'https://example.com',
|
|
});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data).not.toHaveProperty('baseURL');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('azureEndpointSchema', () => {
|
|
it('silently strips plugins field', () => {
|
|
const result = azureEndpointSchema.safeParse({
|
|
groups: [
|
|
{
|
|
group: 'test-group',
|
|
apiKey: 'test-key',
|
|
models: { 'gpt-4': true },
|
|
},
|
|
],
|
|
plugins: true,
|
|
});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data).not.toHaveProperty('plugins');
|
|
}
|
|
});
|
|
|
|
it('accepts nested addParams in azure groups', () => {
|
|
const result = azureEndpointSchema.safeParse({
|
|
groups: [
|
|
{
|
|
group: 'test-group',
|
|
apiKey: 'test-key',
|
|
models: { 'gpt-4': true },
|
|
addParams: {
|
|
provider: {
|
|
only: ['z-ai'],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.groups[0].addParams).toEqual({
|
|
provider: {
|
|
only: ['z-ai'],
|
|
},
|
|
});
|
|
}
|
|
});
|
|
|
|
it('accepts boolean web_search in azure addParams', () => {
|
|
const result = azureEndpointSchema.safeParse({
|
|
groups: [
|
|
{
|
|
group: 'test-group',
|
|
apiKey: 'test-key',
|
|
models: { 'gpt-4': true },
|
|
addParams: {
|
|
provider: {
|
|
only: ['z-ai'],
|
|
},
|
|
web_search: false,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('rejects non-boolean web_search objects in azure addParams', () => {
|
|
const result = azureEndpointSchema.safeParse({
|
|
groups: [
|
|
{
|
|
group: 'test-group',
|
|
apiKey: 'test-key',
|
|
models: { 'gpt-4': true },
|
|
addParams: {
|
|
provider: {
|
|
only: ['z-ai'],
|
|
},
|
|
web_search: {
|
|
enabled: true,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
});
|