diff --git a/packages/stage-ui/src/stores/onboarding.test.ts b/packages/stage-ui/src/stores/onboarding.test.ts index 8984e5cad..e392869a1 100644 --- a/packages/stage-ui/src/stores/onboarding.test.ts +++ b/packages/stage-ui/src/stores/onboarding.test.ts @@ -2,8 +2,10 @@ import { createPinia, setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { nextTick } from 'vue' import { useOnboardingStore } from './onboarding' +import { useProviderConfigStore } from './providers/config' vi.mock('./auth', async () => { const { defineStore } = await import('pinia') @@ -24,7 +26,7 @@ vi.mock('./providers/config', async () => { return { useProviderConfigStore: defineStore('provider-config', { state: () => ({ - configuredProviders: {}, + configuredProviders: {} as Record, }), actions: { getProviderConfig: () => undefined, @@ -39,6 +41,51 @@ describe('onboarding store', () => { setActivePinia(createPinia()) }) + it('suppresses onboarding for an essential provider configured at startup', () => { + const providerStore = useProviderConfigStore() + providerStore.configuredProviders.openai = true + + const store = useOnboardingStore() + + expect(store.needsOnboarding).toBe(false) + }) + + // ROOT CAUSE: + // + // Reactive provider status closed onboarding before model selection (P1). + // The startup snapshot prevents provider changes from closing the dialog. + // https://github.com/moeru-ai/airi/pull/1900 + it('keeps onboarding open when a provider becomes configured during setup', async () => { + const providerStore = useProviderConfigStore() + const store = useOnboardingStore() + store.forceShowSetup() + + expect(store.needsOnboarding).toBe(true) + + providerStore.configuredProviders.openai = true + await nextTick() + + expect(store.hasEssentialProviderConfigured).toBe(true) + expect(store.needsOnboarding).toBe(true) + expect(store.showingSetup).toBe(true) + }) + + // ROOT CAUSE: + // + // An azure-openai apiKey without a baseUrl suppressed onboarding even without a validated provider (P2). + // Only validated provider status at startup can suppress onboarding. + // https://github.com/moeru-ai/airi/pull/1900 + it('requires onboarding when startup credentials have no validated provider', () => { + const providerStore = useProviderConfigStore() + vi.spyOn(providerStore, 'getProviderConfig').mockImplementation(providerId => providerId === 'azure-openai' ? { apiKey: 'sk-x', baseUrl: '' } : undefined) + + const store = useOnboardingStore() + + expect(store.hasEssentialProviderCredentialConfigured).toBe(true) + expect(store.hasEssentialProviderConfigured).toBe(false) + expect(store.needsOnboarding).toBe(true) + }) + // ROOT CAUSE: // // The standalone onboarding renderer previously depended on a localStorage diff --git a/packages/stage-ui/src/stores/onboarding.ts b/packages/stage-ui/src/stores/onboarding.ts index e79849771..8e5b5e1ae 100644 --- a/packages/stage-ui/src/stores/onboarding.ts +++ b/packages/stage-ui/src/stores/onboarding.ts @@ -99,6 +99,12 @@ export const useOnboardingStore = defineStore('onboarding', () => { return essentialProviderIds.some(providerId => providerStore.configuredProviders[providerId]) }) + // Persisted provider status ('configured') is restored synchronously from + // localStorage by useProviderConfigStore, so this snapshot is available at + // store creation. It is intentionally NOT reactive: providers configured + // during the onboarding flow itself must not close the dialog mid-flow. + const hadEssentialProviderConfiguredAtStartup = hasEssentialProviderConfigured.value + // Fallback for app startup timing: // If configured state has not been revalidated yet, infer "configured" // from persisted essential credentials. @@ -120,6 +126,7 @@ export const useOnboardingStore = defineStore('onboarding', () => { && !authStore.token && !hasSkippedSetup.value && !hasCompletedSetup.value + && !hadEssentialProviderConfiguredAtStartup && !skipOnboardingPath.includes(document.location.pathname), )