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:
Wesley Liddick
2026-10-08 11:36:19 +08:00
committed by GitHub
2 changed files with 77 additions and 2 deletions
@@ -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 },
@@ -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()
})
})