diff --git a/packages/ui/src/lib/persistence.test.ts b/packages/ui/src/lib/persistence.test.ts index 38057e9c..bd59bcfe 100644 --- a/packages/ui/src/lib/persistence.test.ts +++ b/packages/ui/src/lib/persistence.test.ts @@ -241,6 +241,47 @@ describe('updateDesktopSettings', () => { } }); + test('sanitizes a successful fallback settings response before applying it', async () => { + const previousFetch = globalThis.fetch; + const fallbackFetch: typeof fetch = async () => new Response(JSON.stringify({ terminalShell: 'zsh' }), { + headers: { 'Content-Type': 'application/json' }, + }); + try { + globalThis.fetch = fallbackFetch; + useUIStore.getState().setTerminalShell('fish'); + + await updateDesktopSettings({ terminalShell: 'zsh' }); + + expect(useUIStore.getState().terminalShell).toBe('zsh'); + expect(getSettingsSaveState()).toBe('idle'); + } finally { + globalThis.fetch = previousFetch; + } + }); + + test('reports an error without applying a malformed fallback settings response', async () => { + const previousFetch = globalThis.fetch; + const fallbackFetch: typeof fetch = async () => new Response(JSON.stringify('ok'), { + headers: { 'Content-Type': 'application/json' }, + }); + const states: string[] = []; + const unsubscribe = subscribeToSettingsSaveState(() => { + states.push(getSettingsSaveState()); + }); + try { + globalThis.fetch = fallbackFetch; + useUIStore.getState().setTerminalShell('fish'); + + await updateDesktopSettings({ terminalShell: 'zsh' }); + + expect(useUIStore.getState().terminalShell).toBe('fish'); + expect(states).toEqual(['saving', 'error']); + } finally { + unsubscribe(); + globalThis.fetch = previousFetch; + } + }); + test('drains a pending save to the previous runtime and ignores its stale response', async () => { switchRuntimeEndpoint({ apiBaseUrl: 'https://settings-a.example', runtimeKey: 'settings-a' }); const saveResult = deferred(); diff --git a/packages/ui/src/lib/persistence.ts b/packages/ui/src/lib/persistence.ts index c02fc88d..c53c4c4c 100644 --- a/packages/ui/src/lib/persistence.ts +++ b/packages/ui/src/lib/persistence.ts @@ -130,7 +130,7 @@ const persistToLocalStorage = (settings: DesktopSettings) => { if (Array.isArray(settings.projects) && settings.projects.length > 0) { const collapsed = settings.projects - .filter((project) => (project as unknown as { sidebarCollapsed?: boolean }).sidebarCollapsed === true) + .filter((project) => project.sidebarCollapsed === true) .map((project) => project.id) .filter((id): id is string => typeof id === 'string' && id.length > 0); if (collapsed.length > 0) { @@ -273,13 +273,14 @@ const sanitizeSkillCatalogs = (value: unknown): DesktopSettings['skillCatalogs'] if (seen.has(id)) continue; seen.add(id); - result.push({ + const catalog: NonNullable[number] = { id, label, source, - ...(subpath ? { subpath } : {}), - ...(gitIdentityId ? { gitIdentityId } : {}), - }); + }; + if (subpath) catalog.subpath = subpath; + if (gitIdentityId) catalog.gitIdentityId = gitIdentityId; + result.push(catalog); } return result; @@ -393,7 +394,7 @@ const sanitizeProjects = (value: unknown): DesktopSettings['projects'] | undefin project.icon = candidate.icon.trim(); } if (candidate.iconImage === null) { - (project as unknown as Record).iconImage = null; + project.iconImage = null; } else if (candidate.iconImage && typeof candidate.iconImage === 'object') { const iconImage = candidate.iconImage as Record; const mime = typeof iconImage.mime === 'string' ? iconImage.mime.trim() : ''; @@ -404,18 +405,18 @@ const sanitizeProjects = (value: unknown): DesktopSettings['projects'] | undefin ? iconImage.source : null; if (mime && updatedAt > 0 && source) { - (project as unknown as Record).iconImage = { mime, updatedAt, source }; + project.iconImage = { mime, updatedAt, source }; } } if (typeof candidate.color === 'string' && candidate.color.trim().length > 0) { project.color = candidate.color.trim(); } if (candidate.iconBackground === null) { - (project as unknown as Record).iconBackground = null; + project.iconBackground = null; } else { const iconBackground = normalizeIconBackground(candidate.iconBackground); if (iconBackground) { - (project as unknown as Record).iconBackground = iconBackground; + project.iconBackground = iconBackground; } } if (typeof candidate.addedAt === 'number' && Number.isFinite(candidate.addedAt) && candidate.addedAt >= 0) { @@ -429,7 +430,7 @@ const sanitizeProjects = (value: unknown): DesktopSettings['projects'] | undefin project.lastOpenedAt = candidate.lastOpenedAt; } if (typeof candidate.sidebarCollapsed === 'boolean') { - (project as unknown as Record).sidebarCollapsed = candidate.sidebarCollapsed; + project.sidebarCollapsed = candidate.sidebarCollapsed; } result.push(project); } @@ -507,7 +508,7 @@ const sanitizeModelRefs = (value: unknown, limit: number): Array<{ providerID: s }; const getPersistApi = (): PersistApi | undefined => { - const candidate = (useUIStore as unknown as { persist?: PersistApi }).persist; + const candidate = useUIStore.persist; if (candidate && typeof candidate === 'object') { return candidate; } @@ -1325,11 +1326,7 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => { for (const [providerId, config] of Object.entries(candidate.usageModelGroups)) { if (config && typeof config === 'object') { const typedConfig = config as Record; - const providerConfig: { - customGroups?: Array<{id: string; label: string; models: string[]; order: number}>; - modelAssignments?: Record; - renamedGroups?: Record; - } = {}; + const providerConfig: NonNullable[string] = {}; // Parse customGroups if (Array.isArray(typedConfig.customGroups)) { @@ -1875,7 +1872,7 @@ async function _flushSettingsUpdate(): Promise { return; } - const updated = (await response.json().catch(() => null)) as DesktopSettings | null; + const updated = sanitizeWebSettings(await response.json().catch(() => null)); if (!isSettingsRuntimeContextCurrent(context)) return; if (updated) { applyDesktopUiPreferences(updated);