edit-save.spec.ts 5.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. import { defineComponent } from 'vue'
  2. import { shallowMount, flushPromises } from '@vue/test-utils'
  3. import { beforeEach, describe, expect, it, vi } from 'vitest'
  4. import type { Account } from '@/types'
  5. const mocks = vi.hoisted(() => ({ update: vi.fn(), apply: vi.fn(), preview: vi.fn(), trafficGet: vi.fn(), trafficSave: vi.fn() }))
  6. vi.mock('@/stores/app', () => ({ useAppStore: () => ({ showError: vi.fn(), showSuccess: vi.fn(), showInfo: vi.fn() }) }))
  7. vi.mock('@/stores/auth', () => ({ useAuthStore: () => ({ isSimpleMode: true }) }))
  8. vi.mock('@/api/admin', () => ({ adminAPI: { accounts: { update: mocks.update, applyAntiDegrade: mocks.apply, previewAntiDegrade: mocks.preview, checkMixedChannelRisk: vi.fn().mockResolvedValue({ has_risk: false }) }, settings: { getWebSearchEmulationConfig: vi.fn().mockResolvedValue({ enabled: false, providers: [] }), getSettings: vi.fn().mockResolvedValue({}) }, tlsFingerprintProfiles: { list: vi.fn().mockResolvedValue([]) } } }))
  9. vi.mock('@/api/admin/accounts', () => ({ getAntigravityDefaultModelMapping: vi.fn() }))
  10. vi.mock('@/api/admin/accountTraffic', async () => ({ ...await vi.importActual('@/api/admin/accountTraffic'), accountTrafficAPI: { get: mocks.trafficGet, save: mocks.trafficSave } }))
  11. vi.mock('vue-i18n', async () => ({ ...await vi.importActual('vue-i18n'), useI18n: () => ({ t: (key: string) => key }) }))
  12. import EditAccountModal from '../../src/components/account/EditAccountModal.vue'
  13. import AccountTrafficControls from '../../src/components/account/AccountTrafficControls.vue'
  14. import { defaultTrafficPolicy } from '../../src/api/admin/accountTraffic'
  15. const dialog = defineComponent({ props: ['show'], template: '<div v-if="show"><slot/><slot name="footer"/></div>' })
  16. const account = (extra: Record<string, unknown> = {}) => ({ id: 42, name: 'Review fixture', platform: 'openai', type: 'oauth', credentials: {}, extra, proxy_id: null, concurrency: 128, priority: 1, rate_multiplier: 1, status: 'active', group_ids: [], expires_at: null, auto_pause_on_expired: false, created_at: '2026-09-15T00:00:00Z', updated_at: '2026-09-15T01:00:00Z' } as Account)
  17. function modal(value = account()) { return shallowMount(EditAccountModal, { props: { show: true, account: value, groups: [], proxies: [] }, global: { stubs: { BaseDialog: dialog, AccountTrafficControls: false } } }) }
  18. async function openTraffic(w: ReturnType<typeof modal>) { const d = w.get('details[data-testid=account-traffic-controls]'); (d.element as HTMLDetailsElement).open = true; await d.trigger('toggle'); await flushPromises() }
  19. beforeEach(() => {
  20. vi.clearAllMocks()
  21. mocks.update.mockImplementation(async (_id, payload) => ({ ...account(payload.extra), ...payload }))
  22. mocks.trafficGet.mockResolvedValue({ policy: defaultTrafficPolicy(), state_available: true, hard_limit: 128, state: null })
  23. mocks.trafficSave.mockImplementation(async (_id, policy) => ({ policy, hard_limit: 128, state_available: true }))
  24. mocks.preview.mockResolvedValue({ account_id: 42, eligible: true, enabled: false, changes: [] })
  25. mocks.apply.mockResolvedValue(account())
  26. })
  27. describe('account save review: expected invariants', () => {
  28. it('fresh same-account props must replace older independently saved traffic settings on reopen', async () => {
  29. const old = { ...defaultTrafficPolicy(), strict_rpm_enabled: true }
  30. const fresh = { ...defaultTrafficPolicy(), strict_rpm_enabled: false }
  31. const w = modal(account({ account_traffic_control: old }))
  32. w.findComponent(AccountTrafficControls).vm.$emit('saved', old)
  33. await w.setProps({ show: false })
  34. await w.setProps({ account: { ...account({ account_traffic_control: fresh }), updated_at: '2026-09-15T02:00:00Z' }, show: true })
  35. await w.get('#edit-account-form').trigger('submit'); await flushPromises()
  36. const sent = mocks.update.mock.calls.at(-1)![1]
  37. w.unmount(); expect(sent.extra.account_traffic_control.strict_rpm_enabled).toBe(false)
  38. })
  39. it('ordinary save must not close and discard an unsaved traffic draft', async () => {
  40. const w = modal(); await openTraffic(w)
  41. await w.get('[data-testid=strict-rpm-toggle]').setValue(true)
  42. await w.get('#edit-account-form').trigger('submit'); await flushPromises()
  43. const saved = mocks.trafficSave.mock.calls.length > 0 || mocks.update.mock.calls.some(([, payload]) => payload.extra?.account_traffic_control?.strict_rpm_enabled)
  44. const discarded = !saved && (w.emitted('close')?.length ?? 0) > 0
  45. w.unmount(); expect(discarded).toBe(false)
  46. })
  47. it('ordinary save must wait for an in-flight independent traffic save', async () => {
  48. let resolve!: (value: unknown) => void
  49. mocks.trafficSave.mockReturnValue(new Promise(r => { resolve = r }))
  50. const w = modal(); await openTraffic(w)
  51. await w.get('[data-testid=strict-rpm-toggle]').setValue(true)
  52. await w.findAll('button').find(b => b.text() === '保存流量控制')!.trigger('click')
  53. await w.get('#edit-account-form').trigger('submit'); await flushPromises()
  54. const concurrent = mocks.update.mock.calls.length > 0
  55. resolve({ policy: { ...defaultTrafficPolicy(), strict_rpm_enabled: true }, state_available: true, hard_limit: 128 }); await flushPromises()
  56. w.unmount(); expect(concurrent).toBe(false)
  57. })
  58. it('switching identity should not reset an unsaved concurrency edit', async () => {
  59. const w = modal()
  60. const input = w.findAll('input[type=number]').find(i => (i.element as HTMLInputElement).value === '128')!
  61. await input.setValue(256)
  62. await w.get('[data-testid=anti-degrade-mode-1]').trigger('click'); await flushPromises()
  63. await w.get('[data-testid=anti-degrade-confirm]').trigger('click'); await flushPromises()
  64. const number = (input.element as HTMLInputElement).value
  65. w.unmount(); expect(number).toBe('256')
  66. })
  67. })