From 88c479077025332714ec5d9524df3b4be219b91a Mon Sep 17 00:00:00 2001 From: Ryanba <92616678+Gujiassh@users.noreply.github.com> Date: Fri, 17 Jul 2026 17:53:38 +0800 Subject: [PATCH] fix(stage-ui): recover stale stage model selection (#2033) --- .../src/stores/settings/stage-model.test.ts | 63 +++++++++++++++++++ .../src/stores/settings/stage-model.ts | 9 ++- 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 packages/stage-ui/src/stores/settings/stage-model.test.ts diff --git a/packages/stage-ui/src/stores/settings/stage-model.test.ts b/packages/stage-ui/src/stores/settings/stage-model.test.ts new file mode 100644 index 000000000..f35d3be36 --- /dev/null +++ b/packages/stage-ui/src/stores/settings/stage-model.test.ts @@ -0,0 +1,63 @@ +import type { DisplayModelURL } from '../display-models' + +import { createPinia, setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { DisplayModelFormat, useDisplayModelsStore } from '../display-models' +import { useSettingsStageModel } from './stage-model' + +vi.mock('@proj-airi/stage-shared/composables', async () => { + const { refManualReset } = await import('@vueuse/core') + + return { + useLocalStorageManualReset: (_key: string, value: string) => refManualReset(value), + } +}) + +vi.mock('@vueuse/core', async (importOriginal) => { + const actual = await importOriginal() + + return { + ...actual, + useEventListener: vi.fn(), + } +}) + +describe('settings stage model store', () => { + beforeEach(() => { + setActivePinia(createPinia()) + }) + + // https://github.com/moeru-ai/airi/issues/1984 + it('issue #1984: falls back to the default preset when a custom stage model is missing', async () => { + const fallbackModel: DisplayModelURL = { + id: 'preset-live2d-1', + format: DisplayModelFormat.Live2dZip, + type: 'url', + url: 'https://example.com/preset-live2d.zip', + name: 'Preset Live2D', + importedAt: 1, + } + + const displayModelsStore = useDisplayModelsStore() + const getDisplayModelSpy = vi.spyOn(displayModelsStore, 'getDisplayModel').mockImplementation(async (id) => { + if (id === 'display-model-missing') + return undefined + if (id === fallbackModel.id) + return fallbackModel + return undefined + }) + + const store = useSettingsStageModel() + store.stageModelSelected = 'display-model-missing' + + await store.initializeStageModel() + + expect(store.stageModelSelected).toBe(fallbackModel.id) + expect(store.stageModelSelectedDisplayModel).toEqual(fallbackModel) + expect(store.stageModelSelectedUrl).toBe(fallbackModel.url) + expect(store.stageModelRenderer).toBe('live2d') + expect(getDisplayModelSpy).toHaveBeenCalledWith('display-model-missing') + expect(getDisplayModelSpy).toHaveBeenCalledWith(fallbackModel.id) + }) +}) diff --git a/packages/stage-ui/src/stores/settings/stage-model.ts b/packages/stage-ui/src/stores/settings/stage-model.ts index 45e0bc0b3..300d7147a 100644 --- a/packages/stage-ui/src/stores/settings/stage-model.ts +++ b/packages/stage-ui/src/stores/settings/stage-model.ts @@ -14,8 +14,9 @@ export const useSettingsStageModel = defineStore('settings-stage-model', () => { const displayModelsStore = useDisplayModelsStore() let stageModelUpdateSequence = 0 const stageModelStorageKey = 'settings/stage/model' + const defaultStageModelId = 'preset-live2d-1' - const stageModelSelectedState = useLocalStorageManualReset(stageModelStorageKey, 'preset-live2d-1') + const stageModelSelectedState = useLocalStorageManualReset(stageModelStorageKey, defaultStageModelId) const stageModelSelected = computed({ get: () => stageModelSelectedState.value, set: (value) => { @@ -77,6 +78,12 @@ export const useSettingsStageModel = defineStore('settings-stage-model', () => { return if (!model) { + if (selectedModelId !== defaultStageModelId) { + stageModelSelectedState.value = defaultStageModelId + await updateStageModel() + return + } + replaceStageModelUrl(undefined) stageModelSelectedDisplayModel.value = undefined stageModelBuiltInRenderer.value = 'disabled'