diff --git a/apps/desktop/src/app/shell/hooks/use-status-snapshot.test.ts b/apps/desktop/src/app/shell/hooks/use-status-snapshot.test.ts index e6be2a018d..055be9ccba 100644 --- a/apps/desktop/src/app/shell/hooks/use-status-snapshot.test.ts +++ b/apps/desktop/src/app/shell/hooks/use-status-snapshot.test.ts @@ -1,4 +1,5 @@ import { act, cleanup, fireEvent, render, renderHook, screen } from '@testing-library/react' +import { MotionGlobalConfig } from 'motion/react' import { createElement, type ReactElement, type ReactNode } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -24,6 +25,11 @@ async function flushAsync() { }) } +// This file is about status RPC cadence, not card choreography: the notification +// stack's exit animation would otherwise straddle the fake→real timer swap between +// locales and leave a departing card in the next test's DOM. +MotionGlobalConfig.skipAnimations = true + beforeEach(() => { vi.useFakeTimers() vi.spyOn(document, 'hasFocus').mockReturnValue(true) @@ -71,7 +77,11 @@ describe('useStatusSnapshot', () => { await vi.advanceTimersByTimeAsync(60_000) }) expect(screen.getByText(warning)).toBeTruthy() - fireEvent.click(screen.getByRole('button', { name: copy.notifications.dismiss })) + // Dismiss departs through the card stack (store row on a microtask, then the exit). + await act(async (): Promise => { + fireEvent.click(screen.getByRole('button', { name: copy.notifications.dismiss })) + await vi.advanceTimersByTimeAsync(100) + }) expect(screen.queryByText(warning)).toBeNull() await act(async (): Promise => { await vi.advanceTimersByTimeAsync(60_000) @@ -90,6 +100,9 @@ describe('useStatusSnapshot', () => { vi.mocked(getStatus).mockResolvedValue({ shared_profile_warning: false } as never) rerender({ scope: 'local-work' }) await flushAsync() + await act(async (): Promise => { + await vi.advanceTimersByTimeAsync(100) + }) expect(screen.queryByText(warning)).toBeNull() } ) diff --git a/apps/desktop/src/components/boot-failure-overlay.test.tsx b/apps/desktop/src/components/boot-failure-overlay.test.tsx index eff8691906..6d9ab1ca99 100644 --- a/apps/desktop/src/components/boot-failure-overlay.test.tsx +++ b/apps/desktop/src/components/boot-failure-overlay.test.tsx @@ -1,6 +1,7 @@ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { en } from '@/i18n/en' import { $desktopBoot } from '@/store/boot' import { $notifications } from '@/store/notifications' import { $desktopOnboarding } from '@/store/onboarding' @@ -330,7 +331,7 @@ describe('BootFailureOverlay', () => { fireEvent.click(await screen.findByRole('button', { name: /repair install/i })) const message = failure === 'thrown' ? 'installer permission denied' - : failure === 'refused' ? 'bundled-immutable' : 'Desktop IPC bridge is unavailable.' + : failure === 'refused' ? 'bundled-immutable' : en.boot.errors.ipcBridgeUnavailable await waitFor(() => expect($notifications.get()).toEqual(expect.arrayContaining([ expect.objectContaining({ kind: 'error', message }) diff --git a/apps/desktop/src/components/model-picker.test.tsx b/apps/desktop/src/components/model-picker.test.tsx index be5aeec50e..303d4e221c 100644 --- a/apps/desktop/src/components/model-picker.test.tsx +++ b/apps/desktop/src/components/model-picker.test.tsx @@ -13,7 +13,12 @@ import type { LocalRuntimeJob } from '@/types/hermes' import { ModelPickerDialog } from './model-picker' +// The jobs query refetches on mount and would replace a seeded cache entry with +// whatever the backend answers; answering with the seeded jobs keeps the two equal. +const seededJobs: { current: readonly LocalRuntimeJob[] } = vi.hoisted(() => ({ current: [] })) + vi.mock('@/hermes', () => ({ + getLocalModelsJobs: vi.fn(async () => ({ jobs: [...seededJobs.current] })), getLocalModelsStatus: vi.fn().mockResolvedValue({ loading: {} }) })) vi.mock('@/lib/model-options', async importOriginal => ({ @@ -60,18 +65,18 @@ const DOWNLOAD_JOB: LocalRuntimeJob = { error: null } -let seedClient: QueryClient | null = null +// One client per test, created in beforeEach: the tests seed jobs BEFORE +// rendering, so the picker must mount against the client that was seeded. +let client: QueryClient = new QueryClient() function setRuntimeJobs(jobs: readonly LocalRuntimeJob[]): void { - // Banana's store keeps jobs in react-query, not the old atom: seed the cache + // The jobs store keeps jobs in react-query, not an atom: seed the cache // directly the way production populates it (localModelsKey(owner, 'jobs')). - seedClient?.setQueryData(localModelsKey(localModelsOwner(), 'jobs'), jobs) + seededJobs.current = jobs + client.setQueryData(localModelsKey(localModelsOwner(), 'jobs'), jobs) } function renderPicker(ui?: Partial[0]>) { - const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }) - seedClient = client - const element: ReactElement = ( @@ -91,6 +96,7 @@ function renderPicker(ui?: Partial[0]>) { } beforeEach(() => { + client = new QueryClient({ defaultOptions: { queries: { retry: false } } }) vi.mocked(requestModelOptions).mockResolvedValue(OPTIONS) setRuntimeJobs([]) // These suites exercise the local-models rows, which ship behind --local. diff --git a/apps/desktop/src/store/local-models-surfaces.test.tsx b/apps/desktop/src/store/local-models-surfaces.test.tsx index 2409efaf03..00f9acd5cc 100644 --- a/apps/desktop/src/store/local-models-surfaces.test.tsx +++ b/apps/desktop/src/store/local-models-surfaces.test.tsx @@ -15,7 +15,13 @@ vi.mock('@/hermes', async (): Promise => ({ vi.mock('@/store/profile', async (): Promise => { const { atom } = await import('nanostores') - return { $activeGatewayProfile: atom('work') } + // settings-scope compares the selected profile against the roster's default; 'work' IS the + // default here so the settings surfaces render without the non-default warning. + return { + $activeGatewayProfile: atom('work'), + $profiles: atom([{ name: 'work', is_default: true }]), + normalizeProfileKey: (name: string | null | undefined): string => (name ?? '').trim() || 'default' + } }) vi.mock('@/store/session', async (): Promise => { const { atom } = await import('nanostores')