mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-28 08:08:32 +00:00
Merge pull request #5780 from Randark-JMT/fix/channel-monitor-p2
fix(monitor): P2 audit follow-ups — credential/balance semantics, capability checks, single account load, quota placeholder UI
This commit is contained in:
@@ -134,7 +134,9 @@ export interface CreateParams {
|
||||
api_key: string
|
||||
/** 缺省 probe;antigravity 仅支持 quota */
|
||||
check_mode?: CheckMode
|
||||
/** 配额模式必填:数据源账号(provider 需与账号平台一致) */
|
||||
/** 配额模式必填:数据源账号(provider 需与账号平台一致)。
|
||||
* update 语义:>0=换绑,0=解绑(切回 probe 模式时前端发 0 清空存量关联);
|
||||
* create 绝不发 0——后端会把 0 存成 &0 触发外键违约。 */
|
||||
account_id?: number | null
|
||||
primary_model: string
|
||||
extra_models?: string[]
|
||||
@@ -148,7 +150,8 @@ export interface CreateParams {
|
||||
body_override?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
// Update request: api_key 空串 = 不修改;clear_template=true 时把 template_id 置空
|
||||
// Update request: api_key 空串 = 不修改;clear_template=true 时把 template_id 置空;
|
||||
// account_id=0 显式解绑关联账号(null = 不动,见 CreateParams 注释)
|
||||
export type UpdateParams = Partial<CreateParams> & {
|
||||
clear_template?: boolean
|
||||
}
|
||||
|
||||
@@ -675,6 +675,14 @@ function selectProvider(provider: Provider) {
|
||||
if (provider === PROVIDER_ANTIGRAVITY && form.check_mode !== CHECK_MODE_QUOTA) {
|
||||
form.check_mode = CHECK_MODE_QUOTA
|
||||
}
|
||||
// 对称还原:从 antigravity 切走时撤掉强制 quota,否则编辑存量 antigravity
|
||||
// 监控换平台后仍停留在 quota(目标平台未必支持),update 会携带残留配置。
|
||||
// 同步清掉 quota 占位模型(loadFromMonitor 回填的 'quota'),否则切回
|
||||
// probe 后拿 'quota' 当探活模型(与离开 grok 清 DEFAULT_GROK_MODEL 同理)。
|
||||
if (previousProvider === PROVIDER_ANTIGRAVITY && form.check_mode === CHECK_MODE_QUOTA) {
|
||||
form.check_mode = CHECK_MODE_PROBE
|
||||
if (form.primary_model.trim() === 'quota') form.primary_model = ''
|
||||
}
|
||||
if (provider === PROVIDER_GROK) {
|
||||
if (!form.endpoint.trim()) form.endpoint = DEFAULT_GROK_ENDPOINT
|
||||
if (!form.primary_model.trim()) form.primary_model = DEFAULT_GROK_MODEL
|
||||
@@ -849,6 +857,9 @@ async function handleSubmit() {
|
||||
req.clear_template = true
|
||||
delete req.template_id
|
||||
}
|
||||
// account_id 同理:probe 模式不带账号,update 发 0 显式解绑存量关联
|
||||
// (后端 0=清空、null=不动)。仅 update——create 发 0 会落 &0 触发 FK 违约。
|
||||
if (!usesQuotaMode.value) req.account_id = 0
|
||||
await adminAPI.channelMonitor.update(target.id, req)
|
||||
appStore.showSuccess(t('admin.channelMonitor.updateSuccess'))
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-gray-900 dark:text-gray-100">{{ row.primary_model }}</span>
|
||||
<!-- 纯配额模式主模型是占位符 "quota"(数据源是账号不是模型),展示层替换为本地化标签 -->
|
||||
<span class="text-sm text-gray-900 dark:text-gray-100">{{ formatMonitorModel(row.primary_model) }}</span>
|
||||
<HelpTooltip>
|
||||
<template #trigger>
|
||||
<span
|
||||
@@ -13,7 +14,7 @@
|
||||
</template>
|
||||
<div class="space-y-2">
|
||||
<div class="text-xs font-semibold text-gray-100">
|
||||
{{ row.primary_model }}
|
||||
{{ formatMonitorModel(row.primary_model) }}
|
||||
<span
|
||||
class="ml-1 inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium"
|
||||
:class="statusBadgeClass(row.primary_status)"
|
||||
@@ -72,5 +73,5 @@ defineProps<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { statusLabel, statusBadgeClass, formatLatency } = useChannelMonitorFormat()
|
||||
const { statusLabel, statusBadgeClass, formatLatency, formatMonitorModel } = useChannelMonitorFormat()
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
class="flex items-center justify-between rounded-lg border border-gray-200 px-3 py-2 text-sm dark:border-dark-600"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ r.model }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ formatMonitorModel(r.model) }}</span>
|
||||
<span v-if="r.message" class="text-xs text-gray-500 dark:text-gray-400">{{ r.message }}</span>
|
||||
<MonitorQuotaView :snapshot="r.quota" class="mt-1" />
|
||||
</div>
|
||||
@@ -54,5 +54,5 @@ defineEmits<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { statusLabel, statusBadgeClass, formatLatency } = useChannelMonitorFormat()
|
||||
const { statusLabel, statusBadgeClass, formatLatency, formatMonitorModel } = useChannelMonitorFormat()
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { ChannelMonitor } from '@/api/admin/channelMonitor'
|
||||
import MonitorPrimaryModelCell from '../MonitorPrimaryModelCell.vue'
|
||||
|
||||
// 占位符 "quota" 是纯配额模式主模型的存储值(数据源是账号不是模型),
|
||||
// 展示层必须替换为本地化标签,不得作为假模型名透出。
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({ t: (key: string) => key }),
|
||||
}
|
||||
})
|
||||
|
||||
function makeRow(overrides: Partial<ChannelMonitor> = {}): ChannelMonitor {
|
||||
return {
|
||||
id: 1,
|
||||
name: 'kimi-coding',
|
||||
provider: 'kimi',
|
||||
api_mode: 'chat_completions',
|
||||
endpoint: '',
|
||||
api_key_masked: '',
|
||||
primary_model: 'quota',
|
||||
extra_models: [],
|
||||
group_name: '',
|
||||
enabled: true,
|
||||
interval_seconds: 60,
|
||||
jitter_seconds: 0,
|
||||
last_checked_at: null,
|
||||
created_by: 1,
|
||||
created_at: '2026-08-18T00:00:00Z',
|
||||
updated_at: '2026-08-18T00:00:00Z',
|
||||
primary_status: 'operational',
|
||||
primary_latency_ms: null,
|
||||
availability_7d: 100,
|
||||
extra_models_status: [],
|
||||
template_id: null,
|
||||
extra_headers: {},
|
||||
body_override_mode: 'off',
|
||||
body_override: null,
|
||||
check_mode: 'quota',
|
||||
account_id: 5,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function mountCell(row: ChannelMonitor) {
|
||||
return mount(MonitorPrimaryModelCell, {
|
||||
props: { row },
|
||||
global: {
|
||||
stubs: { MonitorQuotaView: true, HelpTooltip: false },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('MonitorPrimaryModelCell placeholder model display', () => {
|
||||
it('replaces the quota placeholder with the localized mode label', () => {
|
||||
const wrapper = mountCell(makeRow())
|
||||
expect(wrapper.text()).toContain('monitorCommon.checkMode.quota')
|
||||
})
|
||||
|
||||
it('keeps the real model for quota_probe and probe rows', () => {
|
||||
const wrapper = mountCell(makeRow({
|
||||
check_mode: 'quota_probe',
|
||||
primary_model: 'claude-sonnet-4-5',
|
||||
}))
|
||||
expect(wrapper.text()).toContain('claude-sonnet-4-5')
|
||||
expect(wrapper.text()).not.toContain('monitorCommon.checkMode.quota')
|
||||
})
|
||||
})
|
||||
@@ -30,7 +30,7 @@
|
||||
:key="m.model"
|
||||
class="border-b border-gray-100 dark:border-dark-800"
|
||||
>
|
||||
<td class="py-2 pr-3 font-medium text-gray-900 dark:text-gray-100">{{ m.model }}</td>
|
||||
<td class="py-2 pr-3 font-medium text-gray-900 dark:text-gray-100">{{ formatMonitorModel(m.model) }}</td>
|
||||
<td class="py-2 pr-3">
|
||||
<span
|
||||
class="inline-flex items-center rounded-full px-2 py-0.5 text-[11px]"
|
||||
@@ -83,7 +83,7 @@ defineEmits<{
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
const { statusLabel, statusBadgeClass, formatLatency, formatPercent } = useChannelMonitorFormat()
|
||||
const { statusLabel, statusBadgeClass, formatLatency, formatPercent, formatMonitorModel } = useChannelMonitorFormat()
|
||||
|
||||
const detail = ref<UserMonitorDetail | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -88,4 +88,16 @@ describe('MonitorCard quota snapshot visibility', () => {
|
||||
const wrapper = mountCard(makeItem())
|
||||
expect(wrapper.find('[data-testid="monitor-quota-view"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
// 占位符 "quota" 是主模型存储值,不得作为假模型名直接透出到用户端。
|
||||
it('shows the localized quota label instead of the raw placeholder model', () => {
|
||||
const wrapper = mountCard(makeItem())
|
||||
expect(wrapper.text()).toContain('monitorCommon.checkMode.quota')
|
||||
})
|
||||
|
||||
it('keeps the real model name for probe monitors', () => {
|
||||
const wrapper = mountCard(makeItem({ primary_model: 'claude-sonnet-4-5' }))
|
||||
expect(wrapper.text()).toContain('claude-sonnet-4-5')
|
||||
expect(wrapper.text()).not.toContain('monitorCommon.checkMode.quota')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,8 +23,9 @@
|
||||
>
|
||||
{{ providerLabel(item.provider) }}
|
||||
</span>
|
||||
<!-- 纯配额模式主模型是占位符 "quota",展示层替换为本地化「配额」标签 -->
|
||||
<span class="font-mono text-xs truncate text-gray-500 dark:text-gray-400">
|
||||
{{ item.primary_model }}
|
||||
{{ formatMonitorModel(item.primary_model) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="item.group_name"
|
||||
@@ -120,6 +121,7 @@ const {
|
||||
providerLabel,
|
||||
providerBadgeClass,
|
||||
formatLatency,
|
||||
formatMonitorModel,
|
||||
} = useChannelMonitorFormat()
|
||||
|
||||
const providerTintClass = computed(() =>
|
||||
|
||||
@@ -26,6 +26,9 @@ import {
|
||||
STATUS_DEGRADED,
|
||||
STATUS_FAILED,
|
||||
STATUS_ERROR,
|
||||
CHECK_MODE_PROBE,
|
||||
CHECK_MODE_QUOTA,
|
||||
CHECK_MODE_QUOTA_PROBE,
|
||||
} from '@/constants/channelMonitor'
|
||||
|
||||
const NEUTRAL_BADGE = 'bg-gray-100 text-gray-800 dark:bg-dark-700 dark:text-gray-300'
|
||||
@@ -76,6 +79,21 @@ export function useChannelMonitorFormat() {
|
||||
return m || '-'
|
||||
}
|
||||
|
||||
/**
|
||||
* Display label for a monitor's primary model. Pure-quota monitors carry the
|
||||
* literal placeholder "quota" (the probe target is an account, not a model),
|
||||
* which must not leak into the UI as a fake model name — render the
|
||||
* localized mode label instead. quota_probe keeps a real model name.
|
||||
*/
|
||||
const QUOTA_MODEL_PLACEHOLDER = 'quota'
|
||||
|
||||
function formatMonitorModel(model: string): string {
|
||||
if (model === QUOTA_MODEL_PLACEHOLDER) {
|
||||
return t('monitorCommon.checkMode.quota')
|
||||
}
|
||||
return model
|
||||
}
|
||||
|
||||
function providerBadgeClass(p: Provider | string): string {
|
||||
switch (p) {
|
||||
case PROVIDER_OPENAI:
|
||||
@@ -101,6 +119,22 @@ export function useChannelMonitorFormat() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tailwind class for the check-mode badge shown next to the provider badge
|
||||
* in the admin monitor list. Quota-bearing modes = blue (数据源是账号配额),
|
||||
* plain probe = neutral grey.
|
||||
*/
|
||||
function checkModeBadgeClass(m: CheckMode | string): string {
|
||||
switch (m) {
|
||||
case CHECK_MODE_QUOTA:
|
||||
case CHECK_MODE_QUOTA_PROBE:
|
||||
return 'bg-blue-100 text-blue-700 dark:bg-blue-500/15 dark:text-blue-300'
|
||||
case CHECK_MODE_PROBE:
|
||||
default:
|
||||
return NEUTRAL_BADGE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tailwind class for a provider radio-button-style picker (active/inactive state).
|
||||
* Reuses the same emerald/orange/sky palette as providerBadgeClass to keep
|
||||
@@ -181,7 +215,9 @@ export function useChannelMonitorFormat() {
|
||||
statusBadgeClass,
|
||||
providerLabel,
|
||||
checkModeLabel,
|
||||
formatMonitorModel,
|
||||
providerBadgeClass,
|
||||
checkModeBadgeClass,
|
||||
providerPickerClass,
|
||||
formatLatency,
|
||||
formatPercent,
|
||||
|
||||
@@ -78,6 +78,10 @@
|
||||
<span class="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium" :class="providerBadgeClass(row.provider)">
|
||||
{{ providerLabel(row.provider) }}
|
||||
</span>
|
||||
<!-- 三种检测模式并列展示,quota 系配额数据源与纯探活一眼可分 -->
|
||||
<span class="ml-1 inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium" :class="checkModeBadgeClass(row.check_mode)">
|
||||
{{ checkModeLabel(row.check_mode) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #cell-primary_model="{ row }">
|
||||
@@ -204,6 +208,8 @@ const adminMonitorTab = ref<'v2' | 'legacy'>(isChannelMonitorV1Mode() ? 'legacy'
|
||||
const {
|
||||
providerLabel,
|
||||
providerBadgeClass,
|
||||
checkModeLabel,
|
||||
checkModeBadgeClass,
|
||||
formatLatency,
|
||||
formatAvailability,
|
||||
} = useChannelMonitorFormat()
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { ChannelMonitor } from '@/api/admin/channelMonitor'
|
||||
import ChannelMonitorView from '@/views/admin/ChannelMonitorView.vue'
|
||||
|
||||
// P2-7(d):provider 徽章旁并列 check_mode 徽章,三种检测模式一眼可分
|
||||
// (quota 系 = 账号配额数据源,probe = 纯探活)。
|
||||
|
||||
const { listMonitors } = vi.hoisted(() => ({ listMonitors: vi.fn() }))
|
||||
|
||||
vi.mock('@/utils/featureFlags', () => ({
|
||||
isChannelMonitorV1Mode: () => true,
|
||||
isChannelMonitorV2Mode: () => false,
|
||||
getChannelMonitorMode: () => 'v1' as const,
|
||||
}))
|
||||
|
||||
vi.mock('@/features/channel-monitor-v2/MonitorSettingsPanel.vue', () => ({
|
||||
default: { name: 'MonitorSettingsPanel', template: '<div data-testid="v2-settings" />' },
|
||||
}))
|
||||
|
||||
vi.mock('@/api/admin', () => ({
|
||||
adminAPI: {
|
||||
channelMonitor: {
|
||||
list: listMonitors,
|
||||
duplicate: vi.fn(),
|
||||
update: vi.fn(),
|
||||
runNow: vi.fn(),
|
||||
del: vi.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/app', () => ({
|
||||
useAppStore: () => ({ showSuccess: vi.fn(), showError: vi.fn() }),
|
||||
}))
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({ t: (key: string) => key }),
|
||||
}
|
||||
})
|
||||
|
||||
const AppLayoutStub = defineComponent({
|
||||
template: '<main><slot /></main>',
|
||||
})
|
||||
|
||||
const TablePageLayoutStub = defineComponent({
|
||||
template: '<section><slot name="filters" /><slot name="table" /><slot name="pagination" /></section>',
|
||||
})
|
||||
|
||||
// 渲染 cell-provider slot,徽章断言基于它。
|
||||
const DataTableStub = defineComponent({
|
||||
props: {
|
||||
data: { type: Array, default: () => [] },
|
||||
columns: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
},
|
||||
template: '<div><div v-for="row in data" :key="row.id" class="provider-cell"><slot name="cell-provider" :row="row" /></div></div>',
|
||||
})
|
||||
|
||||
function makeMonitor(overrides: Partial<ChannelMonitor> = {}): ChannelMonitor {
|
||||
return {
|
||||
id: 42,
|
||||
name: 'primary',
|
||||
provider: 'openai',
|
||||
api_mode: 'chat_completions',
|
||||
endpoint: 'https://api.example.com',
|
||||
api_key_masked: 'sk-t***',
|
||||
primary_model: 'gpt-4o-mini',
|
||||
extra_models: [],
|
||||
group_name: '',
|
||||
enabled: true,
|
||||
interval_seconds: 60,
|
||||
jitter_seconds: 0,
|
||||
last_checked_at: null,
|
||||
created_by: 1,
|
||||
created_at: '2026-07-16T00:00:00Z',
|
||||
updated_at: '2026-07-16T00:00:00Z',
|
||||
primary_status: '',
|
||||
primary_latency_ms: null,
|
||||
availability_7d: 0,
|
||||
extra_models_status: [],
|
||||
template_id: null,
|
||||
extra_headers: {},
|
||||
body_override_mode: 'off',
|
||||
body_override: null,
|
||||
check_mode: 'probe',
|
||||
account_id: null,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function mountView() {
|
||||
return mount(ChannelMonitorView, {
|
||||
global: {
|
||||
stubs: {
|
||||
AppLayout: AppLayoutStub,
|
||||
TablePageLayout: TablePageLayoutStub,
|
||||
DataTable: DataTableStub,
|
||||
MonitorFiltersBar: true,
|
||||
Pagination: true,
|
||||
ConfirmDialog: true,
|
||||
EmptyState: true,
|
||||
HelpTooltip: true,
|
||||
Toggle: true,
|
||||
MonitorFormDialog: true,
|
||||
MonitorTemplateManagerDialog: true,
|
||||
MonitorRunResultDialog: true,
|
||||
MonitorPrimaryModelCell: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('ChannelMonitorView check-mode badge', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
listMonitors.mockReset()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['quota', 'monitorCommon.checkMode.quota'],
|
||||
['quota_probe', 'monitorCommon.checkMode.quota_probe'],
|
||||
['probe', 'monitorCommon.checkMode.probe'],
|
||||
] as const)('renders the %s badge next to the provider badge', async (mode, label) => {
|
||||
listMonitors.mockResolvedValue({
|
||||
items: [makeMonitor({ check_mode: mode })],
|
||||
total: 1,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
pages: 1,
|
||||
})
|
||||
const wrapper = mountView()
|
||||
await flushPromises()
|
||||
|
||||
const cell = wrapper.get('.provider-cell')
|
||||
expect(cell.text()).toContain(label)
|
||||
// provider 徽章仍在(不因并列展示被顶掉)。
|
||||
expect(cell.text()).toContain('monitorCommon.providers.openai')
|
||||
|
||||
const badges = cell.findAll('span')
|
||||
const modeBadge = badges.find(el => el.text() === label)
|
||||
expect(modeBadge).toBeDefined()
|
||||
// quota 系=蓝、probe=中性灰的配色区分。
|
||||
const cls = modeBadge!.attributes('class')
|
||||
if (mode === 'probe') {
|
||||
expect(cls).toContain('bg-gray-100')
|
||||
expect(cls).not.toContain('bg-blue-100')
|
||||
} else {
|
||||
expect(cls).toContain('bg-blue-100')
|
||||
expect(cls).not.toContain('bg-gray-100')
|
||||
}
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
@@ -308,4 +308,69 @@ describe('MonitorFormDialog linked account selector', () => {
|
||||
expect(accountTrigger(wrapper).text()).not.toContain('alpha')
|
||||
expect(accountTrigger(wrapper).text()).toContain('linkedAccountPlaceholder')
|
||||
})
|
||||
|
||||
// P2-7(b):antigravity 强制的 quota 在切走时要对称还原,占位模型一并清掉。
|
||||
it('restores probe mode and clears the quota placeholder when switching away from antigravity', async () => {
|
||||
accountsGetById.mockRejectedValue(new Error('gone'))
|
||||
const wrapper = mountDialog(makeMonitor({
|
||||
provider: 'antigravity',
|
||||
check_mode: 'quota',
|
||||
account_id: 7,
|
||||
endpoint: '',
|
||||
primary_model: 'quota',
|
||||
}))
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.get('[data-testid="monitor-provider-anthropic"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
// 占位模型已被清空:probe 模式必填校验拦下提交,而不是拿 'quota' 探活。
|
||||
await wrapper.get('#channel-monitor-form').trigger('submit')
|
||||
await flushPromises()
|
||||
expect(monitorUpdate).not.toHaveBeenCalled()
|
||||
|
||||
await wrapper.get('[data-testid="monitor-primary-model"]').setValue('claude-sonnet-4-5')
|
||||
await wrapper.get('#channel-monitor-form').trigger('submit')
|
||||
await flushPromises()
|
||||
expect(monitorUpdate).toHaveBeenCalledWith(42, expect.objectContaining({
|
||||
check_mode: 'probe',
|
||||
primary_model: 'claude-sonnet-4-5',
|
||||
}))
|
||||
})
|
||||
|
||||
// P2-7(c):update 切回 probe 显式发 account_id=0 解绑存量关联(null=不动)。
|
||||
it('unbinds the linked account with account_id=0 when a quota monitor switches back to probe', async () => {
|
||||
accountsGetById.mockRejectedValue(new Error('gone'))
|
||||
const wrapper = mountDialog(makeMonitor({
|
||||
provider: 'anthropic',
|
||||
check_mode: 'quota',
|
||||
account_id: 999,
|
||||
endpoint: '',
|
||||
primary_model: 'quota',
|
||||
}))
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.get('[data-testid="monitor-check-mode-probe"]').trigger('click')
|
||||
await wrapper.get('[data-testid="monitor-primary-model"]').setValue('claude-sonnet-4-5')
|
||||
await wrapper.get('#channel-monitor-form').trigger('submit')
|
||||
await flushPromises()
|
||||
|
||||
expect(monitorUpdate).toHaveBeenCalledWith(42, expect.objectContaining({ account_id: 0 }))
|
||||
})
|
||||
|
||||
// P2-7(c):create 绝不发 0(后端会把 0 存成 &0 触发外键违约),保持 null。
|
||||
it('keeps account_id null when creating a probe monitor', async () => {
|
||||
const wrapper = mountDialog()
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.findAll('input[type="text"]')[0].setValue('my monitor')
|
||||
await wrapper.get('[data-testid="monitor-primary-model"]').setValue('claude-sonnet-4-5')
|
||||
await wrapper.get('#channel-monitor-form').trigger('submit')
|
||||
await flushPromises()
|
||||
|
||||
expect(monitorCreate).toHaveBeenCalledWith(expect.objectContaining({
|
||||
check_mode: 'probe',
|
||||
account_id: null,
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user