Move provider normalization logic into a dedicated module to ensure consistent handling of provider defaults and user preference inheritance across client and server. Update layout components to mount ConfigProvider only at the shared layout level, preventing unnecessary remounts and hydration issues during navigation. Adjust app config defaults so user provider settings are empty by default, always inheriting the admin-configured provider unless explicitly set. Add tests for normalization and sync logic to ensure robust provider resolution.
99 lines
3.7 KiB
TypeScript
99 lines
3.7 KiB
TypeScript
import { describe, expect, test } from 'vitest';
|
|
|
|
import {
|
|
sanitizePreferencesPatch,
|
|
type PreferenceNormalizationContext,
|
|
} from '../../src/lib/server/user/preferences-normalize';
|
|
|
|
function makeContext(overrides: Partial<PreferenceNormalizationContext> = {}): PreferenceNormalizationContext {
|
|
return {
|
|
showAllProviderModels: true,
|
|
restrictUserApiKeys: true,
|
|
sharedProviders: [
|
|
{ slug: 'shared-a', providerType: 'openai', defaultModel: 'gpt-4o-mini-tts', defaultInstructions: 'hi' },
|
|
{ slug: 'shared-b', providerType: 'custom-openai', defaultModel: 'kokoro', defaultInstructions: null },
|
|
],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('sanitizePreferencesPatch — inherit-by-default provider model', () => {
|
|
test('preserves an empty providerRef as "inherit" rather than collapsing to a concrete provider', () => {
|
|
const { patch, migrated } = sanitizePreferencesPatch(
|
|
{ providerRef: '', voiceSpeed: 1.5 },
|
|
makeContext(),
|
|
{ fillMissingProvider: true },
|
|
);
|
|
expect(patch.providerRef).toBe('');
|
|
expect(patch.providerType).toBe('unknown');
|
|
expect(patch.ttsModel).toBe('');
|
|
expect(patch.voiceSpeed).toBe(1.5);
|
|
expect(migrated).toBe(false);
|
|
});
|
|
|
|
test('migrates a stale built-in (custom-openai) selection to inherit under restricted mode', () => {
|
|
const { patch, migrated } = sanitizePreferencesPatch(
|
|
{ providerRef: 'custom-openai', providerType: 'custom-openai', ttsModel: 'kokoro' },
|
|
makeContext({ restrictUserApiKeys: true }),
|
|
{ fillMissingProvider: true },
|
|
);
|
|
expect(patch.providerRef).toBe('');
|
|
expect(patch.providerType).toBe('unknown');
|
|
expect(patch.ttsModel).toBe('');
|
|
expect(migrated).toBe(true);
|
|
});
|
|
|
|
test('keeps an explicit built-in selection when API keys are NOT restricted', () => {
|
|
const { patch } = sanitizePreferencesPatch(
|
|
{ providerRef: 'openai', providerType: 'openai', ttsModel: 'tts-1' },
|
|
makeContext({ restrictUserApiKeys: false }),
|
|
{ fillMissingProvider: false },
|
|
);
|
|
expect(patch.providerRef).toBe('openai');
|
|
expect(patch.providerType).toBe('openai');
|
|
expect(patch.ttsModel).toBe('tts-1');
|
|
});
|
|
|
|
test('preserves an explicit shared-provider selection and resolves its type', () => {
|
|
const { patch } = sanitizePreferencesPatch(
|
|
{ providerRef: 'shared-b', ttsModel: 'my-model' },
|
|
makeContext(),
|
|
{ fillMissingProvider: false },
|
|
);
|
|
expect(patch.providerRef).toBe('shared-b');
|
|
expect(patch.providerType).toBe('custom-openai');
|
|
expect(patch.ttsModel).toBe('my-model');
|
|
});
|
|
|
|
test('locks the model to the shared provider default when showAllProviderModels is false', () => {
|
|
const { patch } = sanitizePreferencesPatch(
|
|
{ providerRef: 'shared-a', ttsModel: 'something-else' },
|
|
makeContext({ showAllProviderModels: false }),
|
|
{ fillMissingProvider: false },
|
|
);
|
|
expect(patch.providerRef).toBe('shared-a');
|
|
expect(patch.ttsModel).toBe('gpt-4o-mini-tts');
|
|
});
|
|
|
|
test('treats the legacy "default-openai" sentinel as inherit when it is not a real shared provider', () => {
|
|
const { patch, migrated } = sanitizePreferencesPatch(
|
|
{ providerRef: 'default-openai' },
|
|
makeContext(),
|
|
{ fillMissingProvider: true },
|
|
);
|
|
expect(patch.providerRef).toBe('');
|
|
expect(migrated).toBe(true);
|
|
});
|
|
|
|
test('does not force provider fields on a partial PUT patch that omits them', () => {
|
|
const { patch } = sanitizePreferencesPatch(
|
|
{ voice: 'af_sarah' },
|
|
makeContext(),
|
|
{ fillMissingProvider: false },
|
|
);
|
|
expect(patch.voice).toBe('af_sarah');
|
|
expect('providerRef' in patch).toBe(false);
|
|
expect('providerType' in patch).toBe(false);
|
|
expect('ttsModel' in patch).toBe(false);
|
|
});
|
|
});
|