model-select.spec.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. // @vitest-environment jsdom
  2. import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
  3. import { afterEach, describe, expect, it, vi } from 'vitest'
  4. import type { ModelTarget } from '@deepseek-ai/dsh-client-connection/client'
  5. import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
  6. import type { ComponentProps } from 'react'
  7. import type { ModelDirectoryState } from '../src/client/directory.ts'
  8. import { ModelSelect } from '../src/client/ModelSelect.tsx'
  9. import { zh } from '../src/client/locales.ts'
  10. import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
  11. // The seat's key domain is model ∪ common; the stub mirrors the real lookup
  12. // chain: package dictionary, then common vocabulary, then the key.
  13. const t: ComponentProps<typeof ModelSelect>['t'] = (key, params) => {
  14. const template = (zh as Record<string, string>)[key]
  15. ?? (commonZh as Record<string, string>)[key]
  16. ?? key
  17. return params === undefined
  18. ? template
  19. : template.replace(/\{(\w+)\}/g, (match, name: string) => name in params ? String(params[name]) : match)
  20. }
  21. const reasoning = {
  22. efforts: [
  23. { id: 'off', name: 'Off' },
  24. { id: 'high', name: 'High' },
  25. { id: 'max', name: 'Max', description: 'Largest budget' },
  26. ],
  27. defaultEffort: 'high',
  28. }
  29. function state(overrides: Partial<ModelDirectoryState> = {}): ModelDirectoryState {
  30. return {
  31. current: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
  32. groups: [{
  33. id: 'deepseek-official',
  34. name: 'DeepSeek',
  35. models: [{ id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash', reasoning }],
  36. }],
  37. failures: [],
  38. status: 'ready',
  39. error: null,
  40. ...overrides,
  41. }
  42. }
  43. afterEach(cleanup)
  44. describe('ModelSelect reasoning effort', () => {
  45. it('renders adapter metadata and submits the effort as part of the session target', async () => {
  46. const directory = createSnapshotStore<ModelDirectoryState>(state())
  47. const select = vi.fn(async (target: ModelTarget) => {
  48. directory.set(state({ current: target }))
  49. return true
  50. })
  51. render(<ModelSelect
  52. locked={false}
  53. available
  54. directory={directory}
  55. load={vi.fn()}
  56. select={select}
  57. t={t}
  58. />)
  59. const trigger = screen.getByRole('button', {
  60. name: '选择模型,当前 DeepSeek-V4-Flash,推理等级 High',
  61. })
  62. fireEvent.click(trigger)
  63. fireEvent.click(screen.getByRole('menuitem', { name: /推理等级/ }))
  64. expect(screen.getAllByRole('menuitemradio').map(item => item.textContent))
  65. .toEqual(['Off', 'High', 'MaxLargest budget'])
  66. fireEvent.click(screen.getByRole('menuitemradio', { name: /Max/ }))
  67. await waitFor(() => {
  68. expect(select).toHaveBeenCalledWith({
  69. provider: 'deepseek-official',
  70. model: 'deepseek-v4-flash',
  71. reasoningEffort: 'max',
  72. })
  73. expect(trigger.getAttribute('aria-label')).toBe('选择模型,当前 DeepSeek-V4-Flash,推理等级 Max')
  74. })
  75. })
  76. it('offers provider default only when the adapter does not configure a model default', () => {
  77. const directory = createSnapshotStore(state({
  78. groups: [{
  79. id: 'provider',
  80. name: 'Provider',
  81. models: [{
  82. id: 'model',
  83. name: 'Model',
  84. reasoning: { efforts: [{ id: 'standard', name: 'Standard' }] },
  85. }],
  86. }],
  87. current: { provider: 'provider', model: 'model' },
  88. }))
  89. render(<ModelSelect
  90. locked={false}
  91. available
  92. directory={directory}
  93. load={vi.fn()}
  94. select={vi.fn().mockResolvedValue(true)}
  95. t={t}
  96. />)
  97. fireEvent.click(screen.getByRole('button', {
  98. name: '选择模型,当前 Model,推理等级 Default',
  99. }))
  100. fireEvent.click(screen.getByRole('menuitem', { name: /推理等级/ }))
  101. expect(screen.getAllByRole('menuitemradio').map(item => item.textContent))
  102. .toEqual(['Default', 'Standard'])
  103. })
  104. it('prompts for a new selection when the current target is no longer advertised', () => {
  105. const directory = createSnapshotStore(state({
  106. current: { provider: 'deepseek-official', model: 'removed-model' },
  107. }))
  108. const select = vi.fn().mockResolvedValue(true)
  109. render(<ModelSelect
  110. locked={false}
  111. available
  112. directory={directory}
  113. load={vi.fn()}
  114. select={select}
  115. t={t}
  116. />)
  117. const trigger = screen.getByRole('button', { name: '选择模型' })
  118. expect(trigger.textContent).toContain('选择模型')
  119. fireEvent.click(trigger)
  120. expect(screen.queryByRole('menuitem', { name: /推理等级/ })).toBeNull()
  121. fireEvent.click(screen.getByRole('menuitem', { name: /模型/ }))
  122. expect(screen.queryByText('removed-model')).toBeNull()
  123. expect(screen.getByRole('menuitemradio', { name: 'DeepSeek-V4-Flash' })).toBeTruthy()
  124. })
  125. it('renders no Agent-bound control for an addressed subagent session', () => {
  126. const load = vi.fn()
  127. render(<ModelSelect
  128. locked={false}
  129. available={false}
  130. directory={createSnapshotStore(state())}
  131. load={load}
  132. select={vi.fn().mockResolvedValue(false)}
  133. t={t}
  134. />)
  135. expect(screen.queryByRole('button')).toBeNull()
  136. expect(load).not.toHaveBeenCalled()
  137. })
  138. })