diff --git a/packages/stage-ui/src/stores/modules/airi-card.browser.test.ts b/packages/stage-ui/src/stores/modules/airi-card.browser.test.ts index 1e97da5608..042c168be0 100644 --- a/packages/stage-ui/src/stores/modules/airi-card.browser.test.ts +++ b/packages/stage-ui/src/stores/modules/airi-card.browser.test.ts @@ -276,3 +276,39 @@ describe('issue #3124 restores the active Character after a restart', () => { expect(localStorage.getItem(activeCardIdKey)).toBeNull() }) }) + +// ROOT CAUSE: +// +// `initialize()` looked up the module stores after an `await`, through the active Pinia. +// A stray action of a disposed Pinia could make that one active, so CI failed with `Pinia destroyed`. +// +// We fixed this by passing the card store's own Pinia to these lookups. +describe('airi card store keeps to its own Pinia', () => { + afterEach(() => { + localStorage.clear() + }) + + it('initializes while another Pinia becomes active', async () => { + const stale = createPinia() + createApp({}).use(stale) + disposePinia(stale) + + const pinia = createPinia() + // Stands in for that stray action: every action of this Pinia hands the active Pinia back to the stale one. + pinia.use(({ store }) => { + store.$onAction(({ after, onError }) => { + after(() => setActivePinia(stale)) + onError(() => setActivePinia(stale)) + }) + }) + createApp({}).use(pinia).use(PiniaColada) + setActivePinia(pinia) + const cardStore = useAiriCardStore() + const initialized = cardStore.initialize() + setActivePinia(stale) + + await expect(initialized).resolves.toBeUndefined() + expect(cardStore.cards.has('default')).toBe(true) + disposePinia(pinia) + }) +}) diff --git a/packages/stage-ui/src/stores/modules/airi-card.ts b/packages/stage-ui/src/stores/modules/airi-card.ts index 1c7a993568..8f86f6a59b 100644 --- a/packages/stage-ui/src/stores/modules/airi-card.ts +++ b/packages/stage-ui/src/stores/modules/airi-card.ts @@ -226,10 +226,10 @@ export const useAiriCardStore = defineStore('airi-card', () => { function useRuntimeModuleStores() { return { - artistry: useArtistryStore(), - consciousness: useConsciousnessStore(), - speech: useSpeechStore(), - vision: useVisionStore(), + artistry: useArtistryStore(pinia), + consciousness: useConsciousnessStore(pinia), + speech: useSpeechStore(pinia), + vision: useVisionStore(pinia), } } @@ -353,7 +353,7 @@ export const useAiriCardStore = defineStore('airi-card', () => { /** Loads the effective auth-owned voice catalog after card setup finishes. */ async function loadAuthenticatedSpeechVoices(): Promise { const { speech } = useRuntimeModuleStores() - const provider = useProviderConfigStore().providers[speech.activeSpeechProvider] + const provider = useProviderConfigStore(pinia).providers[speech.activeSpeechProvider] if (provider?.configuredBy !== 'authentication') return @@ -647,14 +647,14 @@ export const useAiriCardStore = defineStore('airi-card', () => { */ async function selectActiveCardVisionProvider(provider: string) { await pendingAuthenticationSetup - const vision = useVisionStore() + const vision = useVisionStore(pinia) vision.enabled = true if (vision.activeProvider === provider) return true vision.activeProvider = provider vision.resetModelSelection() await vision.loadModelsForProvider(provider) - const model = useProviderStore().getDefaultModelForProvider(provider) ?? '' + const model = useProviderStore(pinia).getDefaultModelForProvider(provider) ?? '' return await updateActiveCardVision({ provider, model }) } @@ -1383,7 +1383,7 @@ export const useAiriCardStore = defineStore('airi-card', () => { ), }, } - const providers = useProviderConfigStore().providers + const providers = useProviderConfigStore(pinia).providers for (const module of ['consciousness', 'vision', 'speech'] as const) { const provider = providers[resolved[module].provider] // Logout disables authenticated providers without deleting card choices. @@ -1422,7 +1422,7 @@ export const useAiriCardStore = defineStore('airi-card', () => { rememberInheritedSettings() // The runtime holds a mix of two cards until the write completes. See followInheritedSettings. appliedModules = undefined - const artistry = useArtistryStore() + const artistry = useArtistryStore(pinia) artistry.resetToGlobal() diff --git a/packages/stage-ui/src/stores/settings/stage-model.ts b/packages/stage-ui/src/stores/settings/stage-model.ts index 088997ae44..c7c7771de0 100644 --- a/packages/stage-ui/src/stores/settings/stage-model.ts +++ b/packages/stage-ui/src/stores/settings/stage-model.ts @@ -42,6 +42,8 @@ export const useSettingsStageModel = defineStore('settings-stage-model', () => { let disposed = false onScopeDispose(() => { disposed = true + // Cancel a pending update. Its next store action would make this disposed Pinia active again. + stageModelUpdateSequence++ }) const stageModelSelected = computed({ get: () => stageModelSelectedState.value,