mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
Merge pull request #7840 from ouchihao/codex/bug-monitor-template-list-race
fix(monitor): ignore obsolete template manager list responses
This commit is contained in:
@@ -227,7 +227,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { extractApiErrorMessage } from '@/utils/apiError'
|
||||
@@ -287,6 +287,8 @@ const providerTabs = computed<{ value: Provider; label: string }[]>(() => [
|
||||
const activeProvider = ref<Provider>(PROVIDER_ANTHROPIC)
|
||||
const templates = ref<ChannelMonitorTemplate[]>([])
|
||||
const loading = ref(false)
|
||||
let listRequestId = 0
|
||||
onBeforeUnmount(() => { listRequestId++ })
|
||||
|
||||
const templatesForActiveProvider = computed(() =>
|
||||
templates.value.filter((t) => t.provider === activeProvider.value),
|
||||
@@ -354,14 +356,17 @@ function backToList() {
|
||||
|
||||
// --- data fetch ---
|
||||
async function fetchTemplates() {
|
||||
const requestId = ++listRequestId
|
||||
loading.value = true
|
||||
try {
|
||||
const { items } = await adminAPI.channelMonitorTemplate.list()
|
||||
if (requestId !== listRequestId) return
|
||||
templates.value = items
|
||||
} catch (err: unknown) {
|
||||
if (requestId !== listRequestId) return
|
||||
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (requestId === listRequestId) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,6 +376,9 @@ watch(
|
||||
if (show) {
|
||||
editing.value = null
|
||||
fetchTemplates()
|
||||
} else {
|
||||
listRequestId++
|
||||
loading.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { enableAutoUnmount, flushPromises, shallowMount } from '@vue/test-utils'
|
||||
|
||||
vi.mock('vue-i18n', async (importOriginal) => ({
|
||||
...await importOriginal<typeof import('vue-i18n')>(),
|
||||
useI18n: () => ({ t: (key: string) => key }),
|
||||
}))
|
||||
enableAutoUnmount(afterEach)
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void
|
||||
let reject!: (reason: unknown) => void
|
||||
const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej })
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
|
||||
import MonitorTemplateManagerDialog from '../MonitorTemplateManagerDialog.vue'
|
||||
const { list, showError } = vi.hoisted(() => ({ list: vi.fn(), showError: vi.fn() }))
|
||||
vi.mock('@/api/admin', () => ({ adminAPI: { channelMonitorTemplate: { list } } }))
|
||||
vi.mock('@/stores/app', () => ({ useAppStore: () => ({ showError }) }))
|
||||
const response = (name: string) => ({ items: [{ id: 1, name, provider: 'anthropic', body_override_mode: 'off' }] })
|
||||
function mountDialog() {
|
||||
return shallowMount(MonitorTemplateManagerDialog, {
|
||||
props: { show: true },
|
||||
global: { stubs: { BaseDialog: { template: '<div><slot /></div>' } } },
|
||||
})
|
||||
}
|
||||
describe('monitor template list requests', () => {
|
||||
beforeEach(() => { vi.resetAllMocks() })
|
||||
it.each(['resolve', 'reject'] as const)('ignores an old dialog request that %ss', async outcome => {
|
||||
const old = deferred<ReturnType<typeof response>>()
|
||||
list.mockReturnValueOnce(old.promise).mockResolvedValueOnce(response('current-template'))
|
||||
const wrapper = mountDialog()
|
||||
await wrapper.setProps({ show: false })
|
||||
await wrapper.setProps({ show: true })
|
||||
await flushPromises()
|
||||
if (outcome === 'resolve') old.resolve(response('old-template'))
|
||||
else old.reject(new Error('obsolete error'))
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('current-template')
|
||||
expect(wrapper.text()).not.toContain('old-template')
|
||||
expect(showError).not.toHaveBeenCalled()
|
||||
})
|
||||
it('keeps the reopened dialog loading until its own request completes', async () => {
|
||||
const old = deferred<ReturnType<typeof response>>()
|
||||
const current = deferred<ReturnType<typeof response>>()
|
||||
list.mockReturnValueOnce(old.promise).mockReturnValueOnce(current.promise)
|
||||
const wrapper = mountDialog()
|
||||
await wrapper.setProps({ show: false })
|
||||
await wrapper.setProps({ show: true })
|
||||
old.resolve(response('old-template'))
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('common.loading')
|
||||
current.resolve(response('current-template'))
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('current-template')
|
||||
})
|
||||
it('does not report an error after closing the dialog', async () => {
|
||||
const request = deferred<ReturnType<typeof response>>()
|
||||
list.mockReturnValueOnce(request.promise)
|
||||
const wrapper = mountDialog()
|
||||
await wrapper.setProps({ show: false })
|
||||
request.reject(new Error('closed dialog'))
|
||||
await flushPromises()
|
||||
expect(showError).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user