Spaces:
Paused
Paused
| /** | |
| * Settings Reducer Tests | |
| * | |
| * Tests the reducer logic independently from React context | |
| */ | |
| import { describe, it, expect, beforeEach } from 'vitest'; | |
| // Type definitions matching SettingsContext | |
| interface ModelSettings { | |
| temperature: number; | |
| maxOutputTokens: number; | |
| topP: number; | |
| thinkingLevel: string; | |
| enableThinking: boolean; | |
| enableManualBudget: boolean; | |
| thinkingBudget: number; | |
| enableGoogleSearch: boolean; | |
| systemPrompt: string; | |
| stopSequences: string[]; | |
| } | |
| type ThinkingLevel = 'minimal' | 'low' | 'medium' | 'high'; | |
| type SettingsAction = | |
| | { type: 'SET_TEMPERATURE'; payload: number } | |
| | { type: 'SET_MAX_TOKENS'; payload: number } | |
| | { type: 'SET_TOP_P'; payload: number } | |
| | { type: 'SET_THINKING_LEVEL'; payload: ThinkingLevel | string } | |
| | { type: 'SET_ENABLE_THINKING'; payload: boolean } | |
| | { type: 'SET_ENABLE_MANUAL_BUDGET'; payload: boolean } | |
| | { type: 'SET_THINKING_BUDGET'; payload: number } | |
| | { type: 'SET_ENABLE_GOOGLE_SEARCH'; payload: boolean } | |
| | { type: 'SET_SYSTEM_PROMPT'; payload: string } | |
| | { type: 'SET_STOP_SEQUENCES'; payload: string[] } | |
| | { type: 'RESET_TO_DEFAULTS' } | |
| | { type: 'LOAD_SETTINGS'; payload: Partial<ModelSettings> }; | |
| // Default settings matching SettingsContext | |
| const defaultSettings: ModelSettings = { | |
| temperature: 1.0, | |
| maxOutputTokens: 8192, | |
| topP: 0.95, | |
| thinkingLevel: 'high', | |
| enableThinking: true, | |
| enableManualBudget: false, | |
| thinkingBudget: 8192, | |
| enableGoogleSearch: false, | |
| systemPrompt: '', | |
| stopSequences: [], | |
| }; | |
| // Reducer logic extracted from SettingsContext | |
| function settingsReducer(state: ModelSettings, action: SettingsAction): ModelSettings { | |
| switch (action.type) { | |
| case 'SET_TEMPERATURE': | |
| return { ...state, temperature: action.payload }; | |
| case 'SET_MAX_TOKENS': | |
| return { ...state, maxOutputTokens: action.payload }; | |
| case 'SET_TOP_P': | |
| return { ...state, topP: action.payload }; | |
| case 'SET_THINKING_LEVEL': | |
| return { ...state, thinkingLevel: action.payload }; | |
| case 'SET_ENABLE_THINKING': | |
| return { ...state, enableThinking: action.payload }; | |
| case 'SET_ENABLE_MANUAL_BUDGET': | |
| return { ...state, enableManualBudget: action.payload }; | |
| case 'SET_THINKING_BUDGET': | |
| return { ...state, thinkingBudget: action.payload }; | |
| case 'SET_ENABLE_GOOGLE_SEARCH': | |
| return { ...state, enableGoogleSearch: action.payload }; | |
| case 'SET_SYSTEM_PROMPT': | |
| return { ...state, systemPrompt: action.payload }; | |
| case 'SET_STOP_SEQUENCES': | |
| return { ...state, stopSequences: action.payload }; | |
| case 'RESET_TO_DEFAULTS': | |
| return { ...defaultSettings }; | |
| case 'LOAD_SETTINGS': | |
| return { ...state, ...action.payload }; | |
| default: | |
| return state; | |
| } | |
| } | |
| describe('settingsReducer', () => { | |
| let initialState: ModelSettings; | |
| beforeEach(() => { | |
| initialState = { ...defaultSettings }; | |
| }); | |
| describe('SET_TEMPERATURE', () => { | |
| it('updates temperature', () => { | |
| const result = settingsReducer(initialState, { type: 'SET_TEMPERATURE', payload: 0.7 }); | |
| expect(result.temperature).toBe(0.7); | |
| }); | |
| it('preserves other settings', () => { | |
| const result = settingsReducer(initialState, { type: 'SET_TEMPERATURE', payload: 0.5 }); | |
| expect(result.maxOutputTokens).toBe(defaultSettings.maxOutputTokens); | |
| expect(result.topP).toBe(defaultSettings.topP); | |
| }); | |
| it('allows 0 temperature', () => { | |
| const result = settingsReducer(initialState, { type: 'SET_TEMPERATURE', payload: 0 }); | |
| expect(result.temperature).toBe(0); | |
| }); | |
| it('allows 2.0 temperature', () => { | |
| const result = settingsReducer(initialState, { type: 'SET_TEMPERATURE', payload: 2.0 }); | |
| expect(result.temperature).toBe(2.0); | |
| }); | |
| }); | |
| describe('SET_MAX_TOKENS', () => { | |
| it('updates maxOutputTokens', () => { | |
| const result = settingsReducer(initialState, { type: 'SET_MAX_TOKENS', payload: 4096 }); | |
| expect(result.maxOutputTokens).toBe(4096); | |
| }); | |
| it('allows very large values', () => { | |
| const result = settingsReducer(initialState, { type: 'SET_MAX_TOKENS', payload: 65536 }); | |
| expect(result.maxOutputTokens).toBe(65536); | |
| }); | |
| }); | |
| describe('SET_TOP_P', () => { | |
| it('updates topP', () => { | |
| const result = settingsReducer(initialState, { type: 'SET_TOP_P', payload: 0.8 }); | |
| expect(result.topP).toBe(0.8); | |
| }); | |
| it('allows 0', () => { | |
| const result = settingsReducer(initialState, { type: 'SET_TOP_P', payload: 0 }); | |
| expect(result.topP).toBe(0); | |
| }); | |
| it('allows 1.0', () => { | |
| const result = settingsReducer(initialState, { type: 'SET_TOP_P', payload: 1.0 }); | |
| expect(result.topP).toBe(1.0); | |
| }); | |
| }); | |
| describe('SET_THINKING_LEVEL', () => { | |
| it('updates thinking level', () => { | |
| const result = settingsReducer(initialState, { type: 'SET_THINKING_LEVEL', payload: 'low' }); | |
| expect(result.thinkingLevel).toBe('low'); | |
| }); | |
| it('accepts string values', () => { | |
| const result = settingsReducer(initialState, { type: 'SET_THINKING_LEVEL', payload: 'minimal' }); | |
| expect(result.thinkingLevel).toBe('minimal'); | |
| }); | |
| it('accepts all valid levels', () => { | |
| const levels: ThinkingLevel[] = ['minimal', 'low', 'medium', 'high']; | |
| levels.forEach(level => { | |
| const result = settingsReducer(initialState, { type: 'SET_THINKING_LEVEL', payload: level }); | |
| expect(result.thinkingLevel).toBe(level); | |
| }); | |
| }); | |
| }); | |
| describe('SET_ENABLE_THINKING', () => { | |
| it('enables thinking', () => { | |
| const stateWithThinkingOff = { ...initialState, enableThinking: false }; | |
| const result = settingsReducer(stateWithThinkingOff, { type: 'SET_ENABLE_THINKING', payload: true }); | |
| expect(result.enableThinking).toBe(true); | |
| }); | |
| it('disables thinking', () => { | |
| const result = settingsReducer(initialState, { type: 'SET_ENABLE_THINKING', payload: false }); | |
| expect(result.enableThinking).toBe(false); | |
| }); | |
| }); | |
| describe('SET_ENABLE_MANUAL_BUDGET', () => { | |
| it('enables manual budget', () => { | |
| const result = settingsReducer(initialState, { type: 'SET_ENABLE_MANUAL_BUDGET', payload: true }); | |
| expect(result.enableManualBudget).toBe(true); | |
| }); | |
| it('disables manual budget', () => { | |
| const stateWithBudget = { ...initialState, enableManualBudget: true }; | |
| const result = settingsReducer(stateWithBudget, { type: 'SET_ENABLE_MANUAL_BUDGET', payload: false }); | |
| expect(result.enableManualBudget).toBe(false); | |
| }); | |
| }); | |
| describe('SET_THINKING_BUDGET', () => { | |
| it('updates thinking budget', () => { | |
| const result = settingsReducer(initialState, { type: 'SET_THINKING_BUDGET', payload: 16384 }); | |
| expect(result.thinkingBudget).toBe(16384); | |
| }); | |
| it('allows 0 budget', () => { | |
| const result = settingsReducer(initialState, { type: 'SET_THINKING_BUDGET', payload: 0 }); | |
| expect(result.thinkingBudget).toBe(0); | |
| }); | |
| it('allows large budget values', () => { | |
| const result = settingsReducer(initialState, { type: 'SET_THINKING_BUDGET', payload: 32768 }); | |
| expect(result.thinkingBudget).toBe(32768); | |
| }); | |
| }); | |
| describe('SET_ENABLE_GOOGLE_SEARCH', () => { | |
| it('enables Google Search', () => { | |
| const result = settingsReducer(initialState, { type: 'SET_ENABLE_GOOGLE_SEARCH', payload: true }); | |
| expect(result.enableGoogleSearch).toBe(true); | |
| }); | |
| it('disables Google Search', () => { | |
| const stateWithSearch = { ...initialState, enableGoogleSearch: true }; | |
| const result = settingsReducer(stateWithSearch, { type: 'SET_ENABLE_GOOGLE_SEARCH', payload: false }); | |
| expect(result.enableGoogleSearch).toBe(false); | |
| }); | |
| }); | |
| describe('SET_SYSTEM_PROMPT', () => { | |
| it('updates system prompt', () => { | |
| const prompt = 'You are a helpful assistant.'; | |
| const result = settingsReducer(initialState, { type: 'SET_SYSTEM_PROMPT', payload: prompt }); | |
| expect(result.systemPrompt).toBe(prompt); | |
| }); | |
| it('allows empty prompt', () => { | |
| const stateWithPrompt = { ...initialState, systemPrompt: 'Some prompt' }; | |
| const result = settingsReducer(stateWithPrompt, { type: 'SET_SYSTEM_PROMPT', payload: '' }); | |
| expect(result.systemPrompt).toBe(''); | |
| }); | |
| it('handles unicode characters', () => { | |
| const unicodePrompt = '你是一个有帮助的助手。🤖'; | |
| const result = settingsReducer(initialState, { type: 'SET_SYSTEM_PROMPT', payload: unicodePrompt }); | |
| expect(result.systemPrompt).toBe(unicodePrompt); | |
| }); | |
| }); | |
| describe('SET_STOP_SEQUENCES', () => { | |
| it('updates stop sequences', () => { | |
| const sequences = ['###', '---', '\n\n']; | |
| const result = settingsReducer(initialState, { type: 'SET_STOP_SEQUENCES', payload: sequences }); | |
| expect(result.stopSequences).toEqual(sequences); | |
| }); | |
| it('allows empty array', () => { | |
| const stateWithSequences = { ...initialState, stopSequences: ['###'] }; | |
| const result = settingsReducer(stateWithSequences, { type: 'SET_STOP_SEQUENCES', payload: [] }); | |
| expect(result.stopSequences).toEqual([]); | |
| }); | |
| }); | |
| describe('RESET_TO_DEFAULTS', () => { | |
| it('resets all settings to defaults', () => { | |
| const modifiedState: ModelSettings = { | |
| temperature: 0.5, | |
| maxOutputTokens: 2048, | |
| topP: 0.5, | |
| thinkingLevel: 'low', | |
| enableThinking: false, | |
| enableManualBudget: true, | |
| thinkingBudget: 4096, | |
| enableGoogleSearch: true, | |
| systemPrompt: 'Custom prompt', | |
| stopSequences: ['###'], | |
| }; | |
| const result = settingsReducer(modifiedState, { type: 'RESET_TO_DEFAULTS' }); | |
| expect(result.temperature).toBe(defaultSettings.temperature); | |
| expect(result.maxOutputTokens).toBe(defaultSettings.maxOutputTokens); | |
| expect(result.topP).toBe(defaultSettings.topP); | |
| expect(result.thinkingLevel).toBe(defaultSettings.thinkingLevel); | |
| expect(result.enableThinking).toBe(defaultSettings.enableThinking); | |
| expect(result.enableManualBudget).toBe(defaultSettings.enableManualBudget); | |
| expect(result.thinkingBudget).toBe(defaultSettings.thinkingBudget); | |
| expect(result.enableGoogleSearch).toBe(defaultSettings.enableGoogleSearch); | |
| }); | |
| }); | |
| describe('LOAD_SETTINGS', () => { | |
| it('merges partial settings', () => { | |
| const result = settingsReducer(initialState, { | |
| type: 'LOAD_SETTINGS', | |
| payload: { temperature: 0.8, topP: 0.9 }, | |
| }); | |
| expect(result.temperature).toBe(0.8); | |
| expect(result.topP).toBe(0.9); | |
| expect(result.maxOutputTokens).toBe(defaultSettings.maxOutputTokens); | |
| }); | |
| it('preserves unloaded settings', () => { | |
| const result = settingsReducer(initialState, { | |
| type: 'LOAD_SETTINGS', | |
| payload: { enableGoogleSearch: true }, | |
| }); | |
| expect(result.enableGoogleSearch).toBe(true); | |
| expect(result.temperature).toBe(defaultSettings.temperature); | |
| expect(result.systemPrompt).toBe(defaultSettings.systemPrompt); | |
| }); | |
| it('handles empty payload', () => { | |
| const result = settingsReducer(initialState, { | |
| type: 'LOAD_SETTINGS', | |
| payload: {}, | |
| }); | |
| expect(result).toEqual(initialState); | |
| }); | |
| }); | |
| describe('Unknown action', () => { | |
| it('returns current state for unknown action', () => { | |
| // @ts-expect-error Testing unknown action type | |
| const result = settingsReducer(initialState, { type: 'UNKNOWN_ACTION' }); | |
| expect(result).toEqual(initialState); | |
| }); | |
| }); | |
| describe('Immutability', () => { | |
| it('does not mutate original state', () => { | |
| const originalState = { ...initialState }; | |
| const frozen = Object.freeze(originalState); | |
| // This would throw if we tried to mutate frozen object | |
| const result = settingsReducer(frozen as ModelSettings, { type: 'SET_TEMPERATURE', payload: 0.5 }); | |
| expect(result).not.toBe(frozen); | |
| expect(result.temperature).toBe(0.5); | |
| expect(frozen.temperature).toBe(initialState.temperature); | |
| }); | |
| }); | |
| }); | |