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