diff --git a/desktop/src/store/connectionStore.ts b/desktop/src/store/connectionStore.ts index f209f43..ca05aee 100644 --- a/desktop/src/store/connectionStore.ts +++ b/desktop/src/store/connectionStore.ts @@ -357,23 +357,26 @@ export const useConnectionStore = create((set, get) => { try { const raw = localStorage.getItem('zclaw-saas-account'); if (raw) { - const storedAccount = JSON.parse(raw); - // storedAccount is SaaSAccountInfo (saved directly by saveSaaSSession) - // 类型安全解析: 仅接受 'relay' | 'local' 两个合法值 - const adminRouting = storedAccount?.llm_routing; - if (adminRouting === 'relay') { - // Force SaaS Relay mode — admin override - localStorage.setItem('zclaw-connection-mode', 'saas'); - log.debug('Admin llm_routing=relay: forcing SaaS relay mode'); - } else if (adminRouting === 'local' && isTauriRuntime()) { - // Force local Kernel mode — skip SaaS relay entirely - adminForceLocal = true; - localStorage.setItem('zclaw-connection-mode', 'tauri'); - log.debug('Admin llm_routing=local: forcing local Kernel mode'); + const parsed = JSON.parse(raw); + // Type-safe parsing: only accept 'relay' | 'local' as valid values + if (parsed && typeof parsed === 'object' && 'llm_routing' in parsed) { + const adminRouting = parsed.llm_routing; + if (adminRouting === 'relay') { + // Force SaaS Relay mode — admin override + localStorage.setItem('zclaw-connection-mode', 'saas'); + log.debug('Admin llm_routing=relay: forcing SaaS relay mode'); + } else if (adminRouting === 'local' && isTauriRuntime()) { + // Force local Kernel mode — skip SaaS relay entirely + adminForceLocal = true; + localStorage.setItem('zclaw-connection-mode', 'tauri'); + log.debug('Admin llm_routing=local: forcing local Kernel mode'); + } + // Other values (including undefined/null/invalid) are ignored, fall through to default logic } - // 其他值(含 undefined/null/非法值)忽略,走默认逻辑 } - } catch { /* ignore parse errors, fall through to default logic */ } + } catch (e) { + log.warn('Failed to parse admin routing from localStorage, using default', e); + } // === Internal Kernel Mode: Admin forced local === // If admin forced local mode, skip directly to Tauri Kernel section diff --git a/tests/desktop/connectionStore.adminRouting.test.ts b/tests/desktop/connectionStore.adminRouting.test.ts new file mode 100644 index 0000000..6683c22 --- /dev/null +++ b/tests/desktop/connectionStore.adminRouting.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect } from 'vitest'; + +/** + * Pure function test: adminRouting parsing logic + * Core parsing logic extracted from connectionStore.ts + */ +function parseAdminRouting(raw: string | null): 'relay' | 'local' | null { + if (!raw) return null; + try { + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object' && 'llm_routing' in parsed) { + const routing = parsed.llm_routing; + if (routing === 'relay' || routing === 'local') return routing; + } + return null; + } catch { + return null; + } +} + +describe('parseAdminRouting', () => { + it('returns "relay" for valid relay config', () => { + expect(parseAdminRouting('{"llm_routing":"relay"}')).toBe('relay'); + }); + + it('returns "local" for valid local config', () => { + expect(parseAdminRouting('{"llm_routing":"local"}')).toBe('local'); + }); + + it('returns null for null input', () => { + expect(parseAdminRouting(null)).toBeNull(); + }); + + it('returns null for empty string', () => { + expect(parseAdminRouting('')).toBeNull(); + }); + + it('returns null for malformed JSON', () => { + expect(parseAdminRouting('{not json}')).toBeNull(); + }); + + it('returns null for missing llm_routing field', () => { + expect(parseAdminRouting('{"name":"test"}')).toBeNull(); + }); + + it('returns null for invalid routing value', () => { + expect(parseAdminRouting('{"llm_routing":"invalid"}')).toBeNull(); + }); + + it('returns null for number routing value', () => { + expect(parseAdminRouting('{"llm_routing":123}')).toBeNull(); + }); +});