将 vansour/sub2api#1555 的 OpenAI compact 能力建模手工移植到当前 main:账号 级 compact 状态/auto-force_on-force_off 模式、compact-only 模型映射、调度器 tier 分层(已支持 > 未知 > 已知不支持)、管理后台 compact 主动探测,以及对应 i18n/状态徽章。普通 /responses 流量行为不变,无数据库迁移。
220 lines
5.4 KiB
TypeScript
220 lines
5.4 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import { defineComponent } from 'vue'
|
|
import { mount } from '@vue/test-utils'
|
|
|
|
const { updateAccountMock, checkMixedChannelRiskMock } = vi.hoisted(() => ({
|
|
updateAccountMock: vi.fn(),
|
|
checkMixedChannelRiskMock: vi.fn()
|
|
}))
|
|
|
|
vi.mock('@/stores/app', () => ({
|
|
useAppStore: () => ({
|
|
showError: vi.fn(),
|
|
showSuccess: vi.fn(),
|
|
showInfo: vi.fn()
|
|
})
|
|
}))
|
|
|
|
vi.mock('@/stores/auth', () => ({
|
|
useAuthStore: () => ({
|
|
isSimpleMode: true
|
|
})
|
|
}))
|
|
|
|
vi.mock('@/api/admin', () => ({
|
|
adminAPI: {
|
|
accounts: {
|
|
update: updateAccountMock,
|
|
checkMixedChannelRisk: checkMixedChannelRiskMock
|
|
},
|
|
settings: {
|
|
getWebSearchEmulationConfig: vi.fn().mockResolvedValue({ enabled: false, providers: [] }),
|
|
getSettings: vi.fn().mockResolvedValue({})
|
|
},
|
|
tlsFingerprintProfiles: {
|
|
list: vi.fn().mockResolvedValue([])
|
|
}
|
|
}
|
|
}))
|
|
|
|
vi.mock('@/api/admin/accounts', () => ({
|
|
getAntigravityDefaultModelMapping: 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
|
|
})
|
|
}
|
|
})
|
|
|
|
import EditAccountModal from '../EditAccountModal.vue'
|
|
|
|
const BaseDialogStub = defineComponent({
|
|
name: 'BaseDialog',
|
|
props: {
|
|
show: {
|
|
type: Boolean,
|
|
default: false
|
|
}
|
|
},
|
|
template: '<div v-if="show"><slot /><slot name="footer" /></div>'
|
|
})
|
|
|
|
const ModelWhitelistSelectorStub = defineComponent({
|
|
name: 'ModelWhitelistSelector',
|
|
props: {
|
|
modelValue: {
|
|
type: Array,
|
|
default: () => []
|
|
}
|
|
},
|
|
emits: ['update:modelValue'],
|
|
template: `
|
|
<div>
|
|
<button
|
|
type="button"
|
|
data-testid="rewrite-to-snapshot"
|
|
@click="$emit('update:modelValue', ['gpt-5.2-2025-12-11'])"
|
|
>
|
|
rewrite
|
|
</button>
|
|
<span data-testid="model-whitelist-value">
|
|
{{ Array.isArray(modelValue) ? modelValue.join(',') : '' }}
|
|
</span>
|
|
</div>
|
|
`
|
|
})
|
|
|
|
const SelectStub = defineComponent({
|
|
name: 'SelectStub',
|
|
props: {
|
|
modelValue: {
|
|
type: [String, Number, Boolean, null],
|
|
default: ''
|
|
},
|
|
options: {
|
|
type: Array,
|
|
default: () => []
|
|
}
|
|
},
|
|
emits: ['update:modelValue'],
|
|
template: `
|
|
<select
|
|
v-bind="$attrs"
|
|
:value="modelValue"
|
|
@change="$emit('update:modelValue', $event.target.value)"
|
|
>
|
|
<option v-for="option in options" :key="option.value" :value="option.value">
|
|
{{ option.label }}
|
|
</option>
|
|
</select>
|
|
`
|
|
})
|
|
|
|
function buildAccount() {
|
|
return {
|
|
id: 1,
|
|
name: 'OpenAI Key',
|
|
notes: '',
|
|
platform: 'openai',
|
|
type: 'apikey',
|
|
credentials: {
|
|
api_key: 'sk-test',
|
|
base_url: 'https://api.openai.com',
|
|
model_mapping: {
|
|
'gpt-5.2': 'gpt-5.2'
|
|
}
|
|
},
|
|
extra: {},
|
|
proxy_id: null,
|
|
concurrency: 1,
|
|
priority: 1,
|
|
rate_multiplier: 1,
|
|
status: 'active',
|
|
group_ids: [],
|
|
expires_at: null,
|
|
auto_pause_on_expired: false
|
|
} as any
|
|
}
|
|
|
|
function mountModal(account = buildAccount()) {
|
|
return mount(EditAccountModal, {
|
|
props: {
|
|
show: true,
|
|
account,
|
|
proxies: [],
|
|
groups: []
|
|
},
|
|
global: {
|
|
stubs: {
|
|
BaseDialog: BaseDialogStub,
|
|
Select: SelectStub,
|
|
Icon: true,
|
|
ProxySelector: true,
|
|
GroupSelector: true,
|
|
ModelWhitelistSelector: ModelWhitelistSelectorStub
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
describe('EditAccountModal', () => {
|
|
it('reopening the same account rehydrates the OpenAI whitelist from props', async () => {
|
|
const account = buildAccount()
|
|
updateAccountMock.mockReset()
|
|
checkMixedChannelRiskMock.mockReset()
|
|
checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false })
|
|
updateAccountMock.mockResolvedValue(account)
|
|
|
|
const wrapper = mountModal(account)
|
|
|
|
expect(wrapper.get('[data-testid="model-whitelist-value"]').text()).toBe('gpt-5.2')
|
|
|
|
await wrapper.get('[data-testid="rewrite-to-snapshot"]').trigger('click')
|
|
expect(wrapper.get('[data-testid="model-whitelist-value"]').text()).toBe('gpt-5.2-2025-12-11')
|
|
|
|
await wrapper.setProps({ show: false })
|
|
await wrapper.setProps({ show: true })
|
|
|
|
expect(wrapper.get('[data-testid="model-whitelist-value"]').text()).toBe('gpt-5.2')
|
|
|
|
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
|
|
|
|
expect(updateAccountMock).toHaveBeenCalledTimes(1)
|
|
expect(updateAccountMock.mock.calls[0]?.[1]?.credentials?.model_mapping).toEqual({
|
|
'gpt-5.2': 'gpt-5.2'
|
|
})
|
|
})
|
|
|
|
it('submits OpenAI compact mode and compact-only model mapping', async () => {
|
|
const account = buildAccount()
|
|
account.extra = {
|
|
openai_compact_mode: 'force_on'
|
|
}
|
|
account.credentials = {
|
|
...account.credentials,
|
|
compact_model_mapping: {
|
|
'gpt-5.4': 'gpt-5.4-openai-compact'
|
|
}
|
|
}
|
|
updateAccountMock.mockReset()
|
|
checkMixedChannelRiskMock.mockReset()
|
|
checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false })
|
|
updateAccountMock.mockResolvedValue(account)
|
|
|
|
const wrapper = mountModal(account)
|
|
|
|
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
|
|
|
|
expect(updateAccountMock).toHaveBeenCalledTimes(1)
|
|
expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.openai_compact_mode).toBe('force_on')
|
|
expect(updateAccountMock.mock.calls[0]?.[1]?.credentials?.compact_model_mapping).toEqual({
|
|
'gpt-5.4': 'gpt-5.4-openai-compact'
|
|
})
|
|
})
|
|
})
|