fix(desktop): expire unknown readiness notices and clear on recovery

This commit is contained in:
funky-xamarin
2026-09-27 07:59:25 +08:00
committed by brooklyn!
parent 4c3da4fbe5
commit 52276088c9
2 changed files with 27 additions and 24 deletions

View File

@@ -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 () => {

View File

@@ -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.'