Files
deepseek-harness/packages/client/ui-model/tests/model-select.client.spec.tsx
imccyu 7ad54e7791 refactor(client): name the compile face in every client test filename
A test file under packages/client now says which face it covers:
`*.client.spec.{ts,tsx}` and its `*.client.{ts,tsx}` helpers belong to the
Client aggregate, `*.host.spec.ts` to the host aggregate. The carrier's four
node-half specs take the Host suffix.

The two suffixes are mutually exclusive, so each aggregate excludes the
other's and both keep one broad test glob: `exclude` wins over `include`, and
`packages/client/**` no longer has to be excluded wholesale from the host
program with per-file `files` entries carved back out of it. A Host-face spec
that reaches only Host source therefore needs no cross-face project
reference, which the split-project rule rejects.

vitest still discovers every file through `**/*.spec.{ts,tsx}`.
2026-08-12 01:41:40 +08:00

185 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { ModelSelection } from '@deepseek-ai/dsh-api-remotes/client'
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
import type { ComponentProps } from 'react'
import type { ModelDirectoryState } from '../src/client/directory.ts'
import { ModelSelect } from '../src/client/ModelSelect.tsx'
import { zh } from '../src/client/locales.ts'
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
// The seat's key domain is model common; the stub mirrors the real lookup
// chain: package dictionary, then common vocabulary, then the key.
const t: ComponentProps<typeof ModelSelect>['t'] = (key, params) => {
const template = (zh as Record<string, string>)[key]
?? (commonZh as Record<string, string>)[key]
?? key
return params === undefined
? template
: template.replace(/\{(\w+)\}/g, (match, name: string) => name in params ? String(params[name]) : match)
}
const reasoning = {
efforts: [
{ id: 'off', name: 'Off' },
{ id: 'high', name: 'High' },
{ id: 'max', name: 'Max', description: 'Largest budget' },
],
defaultEffort: 'high',
}
function state(overrides: Partial<ModelDirectoryState> = {}): ModelDirectoryState {
return {
current: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
routable: true,
groups: [{
id: 'deepseek-official',
name: 'DeepSeek',
models: [{ id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash', reasoning }],
}],
failures: [],
status: 'ready',
error: null,
...overrides,
}
}
afterEach(cleanup)
describe('ModelSelect reasoning effort', () => {
it('renders adapter metadata and submits the effort as part of the session selection', async () => {
const directory = createSnapshotStore<ModelDirectoryState>(state())
const select = vi.fn(async (selection: ModelSelection) => {
directory.set(state({ current: selection }))
return true
})
render(<ModelSelect
locked={false}
available
directory={directory}
load={vi.fn()}
select={select}
t={t}
/>)
const trigger = screen.getByRole('button', {
name: '选择模型,当前 DeepSeek-V4-Flash推理等级 High',
})
fireEvent.click(trigger)
fireEvent.click(screen.getByRole('menuitem', { name: /推理等级/ }))
expect(screen.getAllByRole('menuitemradio').map(item => item.textContent))
.toEqual(['Off', 'High', 'MaxLargest budget'])
fireEvent.click(screen.getByRole('menuitemradio', { name: /Max/ }))
await waitFor(() => {
expect(select).toHaveBeenCalledWith({
provider: 'deepseek-official',
model: 'deepseek-v4-flash',
reasoningEffort: 'max',
})
expect(trigger.getAttribute('aria-label')).toBe('选择模型,当前 DeepSeek-V4-Flash推理等级 Max')
})
})
it('offers provider default only when the adapter does not configure a model default', () => {
const directory = createSnapshotStore(state({
groups: [{
id: 'provider',
name: 'Provider',
models: [{
id: 'model',
name: 'Model',
reasoning: { efforts: [{ id: 'standard', name: 'Standard' }] },
}],
}],
current: { provider: 'provider', model: 'model' },
}))
render(<ModelSelect
locked={false}
available
directory={directory}
load={vi.fn()}
select={vi.fn().mockResolvedValue(true)}
t={t}
/>)
fireEvent.click(screen.getByRole('button', {
name: '选择模型,当前 Model推理等级 Default',
}))
fireEvent.click(screen.getByRole('menuitem', { name: /推理等级/ }))
expect(screen.getAllByRole('menuitemradio').map(item => item.textContent))
.toEqual(['Default', 'Standard'])
})
it('prompts for a selection when the current model is no longer advertised', () => {
const directory = createSnapshotStore(state({
current: { provider: 'deepseek-official', model: 'removed-model' },
}))
const select = vi.fn().mockResolvedValue(true)
render(<ModelSelect
locked={false}
available
directory={directory}
load={vi.fn()}
select={select}
t={t}
/>)
const trigger = screen.getByRole('button', { name: '选择模型' })
expect(trigger.textContent).toContain('选择模型')
fireEvent.click(trigger)
expect(screen.queryByRole('menuitem', { name: /推理等级/ })).toBeNull()
fireEvent.click(screen.getByRole('menuitem', { name: /模型/ }))
expect(screen.queryByText('removed-model')).toBeNull()
expect(screen.getByRole('menuitemradio', { name: 'DeepSeek-V4-Flash' })).toBeTruthy()
})
it('announces a rejected selection as a transient toast and keeps the in-menu strip for loads', async () => {
const groups = [{
id: 'deepseek-official',
name: 'DeepSeek',
models: [
{ id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash', reasoning },
{ id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro' },
],
}]
const directory = createSnapshotStore<ModelDirectoryState>(state({ groups }))
const select = vi.fn(async () => {
directory.set(state({ groups, status: 'error', error: 'model-unavailable: session already contains images' }))
return false
})
render(<ModelSelect
locked={false}
available
directory={directory}
load={vi.fn()}
select={select}
t={t}
/>)
fireEvent.click(screen.getByRole('button', { name: /选择模型|当前/ }))
fireEvent.click(screen.getByRole('menuitem', { name: /模型/ }))
fireEvent.click(screen.getByRole('menuitemradio', { name: /DeepSeek-V4-Pro/ }))
const toast = await screen.findByRole('alert')
expect(toast.textContent).toContain('模型操作失败model-unavailable: session already contains images')
// The selection failure does not render the in-menu load strip (no Retry).
expect(screen.queryByRole('button', { name: '重试' })).toBeNull()
})
it('renders no Agent-bound control for an addressed subagent session', () => {
const load = vi.fn()
render(<ModelSelect
locked={false}
available={false}
directory={createSnapshotStore(state())}
load={load}
select={vi.fn().mockResolvedValue(false)}
t={t}
/>)
expect(screen.queryByRole('button')).toBeNull()
expect(load).not.toHaveBeenCalled()
})
})