diff --git a/apps/desktop/src/store/onboarding.test.ts b/apps/desktop/src/store/onboarding.test.ts index 4b85880d89..7e73f3ce94 100644 --- a/apps/desktop/src/store/onboarding.test.ts +++ b/apps/desktop/src/store/onboarding.test.ts @@ -246,28 +246,31 @@ describe('refreshOnboarding', () => { expect(window.localStorage.getItem('hermes-desktop-onboarded-v1')).toBe('1') }) - it('shows a non-blocking notification when preserving configured on fallback', async () => { - const notifySpy = vi.spyOn(notifications, 'notify') - + it('keeps an unknown readiness notice temporary and clears it on recovery (#124545)', async () => { + vi.useFakeTimers() + notifications.clearNotifications() installApiMock(vi.fn()) - $desktopOnboarding.set( - baseState({ - configured: true, - providers: [makeOAuthProvider('cached')], - reason: null, - requested: false - }) - ) + $desktopOnboarding.set(baseState({ configured: true })) - await refreshOnboarding(onboardingContext(fallbackTimeoutGateway())) + try { + await refreshOnboarding(onboardingContext(fallbackTimeoutGateway())) + expect(notifications.$notifications.get()).toEqual([ + expect.objectContaining({ id: 'runtime-not-ready', kind: 'info' }) + ]) + expect($desktopOnboarding.get().configured).toBe(true) + await vi.advanceTimersByTimeAsync(5_000) + expect(notifications.$notifications.get()).toEqual([]) - expect(notifySpy).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'runtime-not-ready', - kind: 'error' - }) - ) - expect($desktopOnboarding.get().configured).toBe(true) + // A later outage can show a fresh notice; an authoritative ready clears it + // without waiting for its timer, and must not dismiss unrelated errors. + await refreshOnboarding(onboardingContext(fallbackTimeoutGateway())) + notifications.notify({ id: 'unrelated', kind: 'error', message: 'Keep me' }) + await refreshOnboarding(onboardingContext(keylessCustomGateway())) + expect(notifications.$notifications.get().map(item => item.id)).toEqual(['unrelated']) + } finally { + notifications.clearNotifications() + vi.useRealTimers() + } }) it('enters setup when the selected OpenRouter credential is genuinely empty', async () => { diff --git a/apps/desktop/src/store/onboarding.ts b/apps/desktop/src/store/onboarding.ts index 85aac808c0..3368a801a0 100644 --- a/apps/desktop/src/store/onboarding.ts +++ b/apps/desktop/src/store/onboarding.ts @@ -18,7 +18,7 @@ import { isProviderSetupErrorMessage } from '@/lib/provider-setup-errors' import { evaluateRuntimeReadiness, type RuntimeReadinessResult } from '@/lib/runtime-readiness' import { ackFreeTierNotice, freeTierReadyPending, refreshFreeTierStatus, setFreeTierRoute } from '@/store/free-tier' import { setMainModelAssignment } from '@/store/model-assignment' -import { notify, notifyError } from '@/store/notifications' +import { dismissNotification, notify, notifyError } from '@/store/notifications' import { guidedOnboardingActive } from '@/store/onboarding-gate' import { captureOnboardingScope, type OnboardingScope } from '@/store/onboarding-scope' import type { OAuthProvider, OAuthStartResponse } from '@/types/hermes' @@ -645,6 +645,7 @@ export function closeManualOnboarding() { export function completeDesktopOnboarding() { clearPoll() + dismissNotification('runtime-not-ready') writeCachedConfigured(true) // A real provider is now connected, so any earlier "choose later" skip is // moot — clear it so the flag never lingers in a configured install. @@ -721,12 +722,11 @@ export async function refreshOnboarding(ctx: OnboardingContext, stillWanted?: () if (shouldPreserveConfiguredOnFallback(runtime, state)) { // Gateway probes timed out but the user was already configured — don't - // downgrade to the blocking onboarding overlay. Surface a non-blocking - // notification with a stable id so repeated calls during an outage dedup - // instead of stacking toasts. + // downgrade to the blocking onboarding overlay or claim an error verdict. + // Use the temporary informational notice; recovery clears it early. notify({ id: 'runtime-not-ready', - kind: 'error', + kind: 'info', title: 'Runtime not ready', message: 'Hermes Desktop could not verify the running backend on startup. Some features may be unavailable until the gateway is reachable.'