Merge pull request #7843 from ouchihao/codex/bug-alert-rule-integer-duration

fix(ops): validate alert rule durations as whole minutes
This commit is contained in:
Wesley Liddick
2026-10-08 11:35:42 +08:00
committed by GitHub
2 changed files with 52 additions and 2 deletions
@@ -327,10 +327,10 @@ const editorValidation = computed(() => {
if (!(typeof r.window_minutes === 'number' && Number.isFinite(r.window_minutes) && [1, 5, 60].includes(r.window_minutes))) {
errors.push(t('admin.ops.alertRules.validation.windowRange'))
}
if (!(typeof r.sustained_minutes === 'number' && Number.isFinite(r.sustained_minutes) && r.sustained_minutes >= 1 && r.sustained_minutes <= 1440)) {
if (!(typeof r.sustained_minutes === 'number' && Number.isInteger(r.sustained_minutes) && r.sustained_minutes >= 1 && r.sustained_minutes <= 1440)) {
errors.push(t('admin.ops.alertRules.validation.sustainedRange'))
}
if (!(typeof r.cooldown_minutes === 'number' && Number.isFinite(r.cooldown_minutes) && r.cooldown_minutes >= 0 && r.cooldown_minutes <= 1440)) {
if (!(typeof r.cooldown_minutes === 'number' && Number.isInteger(r.cooldown_minutes) && r.cooldown_minutes >= 0 && r.cooldown_minutes <= 1440)) {
errors.push(t('admin.ops.alertRules.validation.cooldownRange'))
}
return { valid: errors.length === 0, errors }
@@ -0,0 +1,50 @@
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)
import OpsAlertRulesCard from '../OpsAlertRulesCard.vue'
import { ref } from 'vue'
const { createAlertRule } = vi.hoisted(() => ({ createAlertRule: vi.fn() }))
vi.mock('@vueuse/core', () => ({ useMediaQuery: () => ref(true) }))
vi.mock('@/api', () => ({ adminAPI: { groups: { getAll: vi.fn().mockResolvedValue([]) } } }))
vi.mock('@/api/admin/ops', () => ({ opsAPI: { listAlertRules: vi.fn().mockResolvedValue([]), createAlertRule } }))
vi.mock('@/stores/app', () => ({ useAppStore: () => ({ showError: vi.fn(), showSuccess: vi.fn() }) }))
async function openRule() {
const wrapper = shallowMount(OpsAlertRulesCard, {
global: { stubs: { BaseDialog: { props: ['show'], template: '<div v-if="show"><slot /><slot name="footer" /></div>' } } },
})
await flushPromises()
await wrapper.findAll('button').find(b => b.text().includes('admin.ops.alertRules.create'))!.trigger('click')
await wrapper.find('input[type="text"]').setValue('Error rate')
return wrapper
}
describe('alert rule integer durations', () => {
beforeEach(() => { vi.clearAllMocks() })
it.each(['1.5', '0.5'])('rejects fractional duration %s before submitting', async value => {
const wrapper = await openRule()
const input = wrapper.find(value === '1.5' ? 'input[min="1"][max="1440"]' : 'input[min="0"][max="1440"]')
await input.setValue(value)
const save = wrapper.findAll('button').find(b => b.text() === 'common.save')!
expect(wrapper.text()).toContain(value === '1.5' ? 'admin.ops.alertRules.validation.sustainedRange' : 'admin.ops.alertRules.validation.cooldownRange')
await save.trigger('click')
expect(createAlertRule).not.toHaveBeenCalled()
})
it('allows whole-minute boundaries and a fractional metric threshold', async () => {
const wrapper = await openRule()
await wrapper.get('input[min="1"][max="1440"]').setValue('1440')
await wrapper.get('input[min="0"][max="1440"]').setValue('0')
await wrapper.get('input[type="number"]:not([min])').setValue('1.25')
const save = wrapper.findAll('button').find(b => b.text() === 'common.save')!
expect(save.attributes('disabled')).toBeUndefined()
await save.trigger('click')
await flushPromises()
expect(createAlertRule).toHaveBeenCalledWith(expect.objectContaining({ sustained_minutes: 1440, cooldown_minutes: 0, threshold: 1.25 }))
})
})