Files
deepseek-harness/packages/client/ui-agent-preset/tests/components.spec.tsx
Yichen Jiang 0ab035c748 fix(web): name the preset in General settings, and say 预设 in Chinese
The preference row rendered `option.id` while every other surface — the
new-session chip, the session header label, the preset cards — renders the
metadata name. So the same roster read `标准模式` on one screen and `standard`
on the next, and the id is addressing, not a label. A preset that names itself
nothing still falls back to its id, which is then all there is to say about it.

The row's Chinese copy also still ended on the English word: the section is
`Agent 预设`, so the sentence about a running session keeping its composition
says 预设 too.
2026-08-07 03:01:30 +08:00

278 lines
11 KiB
TypeScript

// @vitest-environment jsdom
/**
* The three conversation-adjacent surfaces: the General-settings row naming the
* default for later sessions, the new-session chip naming the next one's, and
* the session header's read-only label. The split is the host's rule — a
* session's history is produced under its preset's tools, so the choice is
* only ever offered before one starts.
*/
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
import { AgentPresetLabel } from '../src/client/AgentPresetLabel.tsx'
import type { AgentPresetLabelProps } from '../src/client/AgentPresetLabel.tsx'
import { AgentPresetRow } from '../src/client/AgentPresetRow.tsx'
import type { AgentPresetRowProps } from '../src/client/AgentPresetRow.tsx'
import { AgentPresetSeat } from '../src/client/AgentPresetSeat.tsx'
import type { AgentPresetSeatProps } from '../src/client/AgentPresetSeat.tsx'
import type { AgentPresetSettingsState } from '../src/client/settings-store.ts'
import type { AgentPresetSeatState } from '../src/client/seat-store.ts'
import { en } from '../src/client/locales.ts'
afterEach(cleanup)
const ROW_READY: AgentPresetSettingsState = {
status: 'ready',
error: null,
writable: true,
currentValue: 'standard',
// `mine` deliberately names itself nothing: the row must fall back to the
// id for a preset whose author wrote no metadata.
options: [{ id: 'standard', trust: 'system', name: '标准模式' }, { id: 'mine', trust: 'user' }],
}
const SEAT_READY: AgentPresetSeatState = {
current: 'standard',
options: [
{ id: 'standard', trust: 'system', name: '标准模式', description: '完整的编码 agent。' },
{ id: 'mine', trust: 'user' },
],
busy: false,
error: null,
}
function renderRow(state: Partial<AgentPresetSettingsState> = {}) {
const store = createSnapshotStore<AgentPresetSettingsState>({ ...ROW_READY, ...state })
const actions = { load: vi.fn(() => Promise.resolve()), select: vi.fn(() => Promise.resolve()) }
render(<AgentPresetRow {...({
...actions,
useAgentPreset: bindSnapshotSelector(store),
t: (key: keyof typeof en) => en[key],
} as unknown as AgentPresetRowProps)} />)
return actions
}
function renderSeat(state: Partial<AgentPresetSeatState> = {}) {
const store = createSnapshotStore<AgentPresetSeatState>({ ...SEAT_READY, ...state })
const actions = { load: vi.fn(() => Promise.resolve()), select: vi.fn(() => Promise.resolve()) }
render(<AgentPresetSeat {...({
...actions,
useAgentPresetSeat: bindSnapshotSelector(store),
t: (key: keyof typeof en) => en[key],
} as unknown as AgentPresetSeatProps)} />)
return actions
}
function renderLabel(
summary: { blank: boolean; agentPreset?: string } | undefined,
roster: Partial<AgentPresetSettingsState> = {},
) {
// The chip and the label read the same roster, metadata included.
const store = createSnapshotStore<AgentPresetSettingsState>({
...ROW_READY, options: SEAT_READY.options, ...roster,
})
const sessions = createSnapshotStore({ byId: summary === undefined ? {} : { s1: summary } })
const load = vi.fn(() => Promise.resolve())
const view = render(<AgentPresetLabel {...({
load,
sessionId: 's1',
useSessions: bindSnapshotSelector(sessions),
useAgentPresets: bindSnapshotSelector(store),
t: (key: keyof typeof en) => en[key],
} as unknown as AgentPresetLabelProps)} />)
return { load, view }
}
describe('the General-settings row', () => {
it('reads the roster once and shows the current default', async () => {
const actions = renderRow()
await waitFor(() => { expect(actions.load).toHaveBeenCalledTimes(1) })
expect(screen.getByRole('button').textContent).toContain('标准模式')
})
it('marks a locally authored option as local', () => {
renderRow()
fireEvent.click(screen.getByRole('button'))
// A local preset is exactly as privileged as the plugins it names, so the
// list says which rows are local rather than presenting all as vetted.
expect(screen.getByText(`mine · ${en.userTrust}`)).toBeTruthy()
// The shipped one carries no marker; only local rows are called out.
expect(screen.getAllByText('标准模式')).toHaveLength(2)
})
it('writes the picked preset and closes the menu', () => {
const actions = renderRow()
fireEvent.click(screen.getByRole('button'))
fireEvent.click(screen.getByText(`mine · ${en.userTrust}`))
expect(actions.select).toHaveBeenCalledWith('mine')
expect(screen.getByRole('button').getAttribute('aria-expanded')).toBe('false')
})
it('closes on an outside dismissal', () => {
renderRow()
fireEvent.click(screen.getByRole('button'))
fireEvent.keyDown(document, { key: 'Escape' })
expect(screen.getByRole('button').getAttribute('aria-expanded')).toBe('false')
})
it('says it is loading before the roster answers', () => {
renderRow({ status: 'loading', currentValue: '' })
expect(screen.getByRole('button').textContent).toContain(en.loading)
expect(screen.getByRole('button')).toHaveProperty('disabled', true)
})
it('shows a failure in place of the description', () => {
renderRow({ error: 'roster unavailable' })
expect(screen.getByRole('alert').textContent).toBe('roster unavailable')
})
it('renders nothing when the deployment composes no presets', () => {
const { container } = render(<AgentPresetRow {...({
load: vi.fn(() => Promise.resolve()),
select: vi.fn(() => Promise.resolve()),
useAgentPreset: bindSnapshotSelector(
createSnapshotStore<AgentPresetSettingsState>({ ...ROW_READY, status: 'unavailable', options: [] })),
t: (key: keyof typeof en) => en[key],
} as unknown as AgentPresetRowProps)} />)
expect(container.firstChild).toBeNull()
})
it('closes and locks the menu when the settings turn read-only', () => {
const store = createSnapshotStore<AgentPresetSettingsState>(ROW_READY)
render(<AgentPresetRow {...({
load: vi.fn(() => Promise.resolve()),
select: vi.fn(() => Promise.resolve()),
useAgentPreset: bindSnapshotSelector(store),
t: (key: keyof typeof en) => en[key],
} as unknown as AgentPresetRowProps)} />)
fireEvent.click(screen.getByRole('button'))
act(() => { store.set({ ...ROW_READY, writable: false }) })
expect(screen.getByRole('button').getAttribute('aria-expanded')).toBe('false')
expect(screen.getByRole('button')).toHaveProperty('disabled', true)
})
})
describe('the new-session chip', () => {
it('reads the roster once and shows the staged preset by name', async () => {
const actions = renderSeat()
await waitFor(() => { expect(actions.load).toHaveBeenCalledTimes(1) })
expect(screen.getByRole('button').textContent).toContain('标准模式')
expect(screen.getByRole('button').getAttribute('title')).toBe(en.seatHint)
})
it('offers each preset with what it is for', () => {
renderSeat()
fireEvent.click(screen.getByRole('button'))
// The id alone never said what a preset does; the description is the
// whole reason a preset can publish metadata at all.
expect(screen.getByText('完整的编码 agent。')).toBeTruthy()
// A preset that published none still reads as a row, with its id standing
// in for the name.
expect(screen.getByText(en.noDescription)).toBeTruthy()
expect(screen.getByText('mine')).toBeTruthy()
})
it('falls back to the id when the staged preset published no name', () => {
renderSeat({ current: 'mine' })
expect(screen.getByRole('button').textContent).toContain('mine')
})
it('stages the picked preset and closes the menu', () => {
const actions = renderSeat()
fireEvent.click(screen.getByRole('button'))
fireEvent.click(screen.getByText('mine'))
expect(actions.select).toHaveBeenCalledWith('mine')
expect(screen.getByRole('button').getAttribute('aria-expanded')).toBe('false')
})
it('disables the trigger while a switch is in flight', () => {
renderSeat({ busy: true })
expect(screen.getByRole('button')).toHaveProperty('disabled', true)
})
it('shows a refused switch on the trigger', () => {
renderSeat({ error: 'session has already started' })
expect(screen.getByRole('button').getAttribute('title')).toBe('session has already started')
})
it('renders nothing before the roster arrives or when there is none', () => {
const empty = renderSeat({ options: [] })
expect(empty).toBeTruthy()
expect(screen.queryByRole('button')).toBeNull()
cleanup()
renderSeat({ current: '' })
expect(screen.queryByRole('button')).toBeNull()
})
it('closes on an outside dismissal', () => {
renderSeat()
fireEvent.click(screen.getByRole('button'))
fireEvent.keyDown(document, { key: 'Escape' })
expect(screen.getByRole('button').getAttribute('aria-expanded')).toBe('false')
})
})
describe('the session-header label', () => {
it('names the preset the session runs, and never offers a switch', async () => {
const { load } = renderLabel({ blank: false, agentPreset: 'standard' })
await waitFor(() => { expect(load).toHaveBeenCalledTimes(1) })
// A control here would promise a switch the host refuses outright.
expect(screen.queryByRole('button')).toBeNull()
expect(screen.getByTitle('完整的编码 agent。').textContent).toBe('标准模式')
})
it('falls back to the id, and to the generic hint, when metadata is absent', () => {
renderLabel({ blank: true, agentPreset: 'mine' })
expect(screen.getByTitle(en.headerHint).textContent).toBe('mine')
})
it('shows the id until the roster resolves it', () => {
renderLabel({ blank: false, agentPreset: 'standard' }, { options: [] })
// The session's own summary is the authority on which preset it runs; the
// roster only supplies the display name, and its arrival is a later frame.
expect(screen.getByTitle(en.headerHint).textContent).toBe('standard')
})
it('renders nothing, and reads no roster, when the session records no preset', async () => {
const absent = renderLabel({ blank: true })
expect(absent.view.container.firstChild).toBeNull()
cleanup()
// A session the list has not caught up to is the same answer: a deployment
// that composes no presets must not pay for a roster read per header.
const unknown = renderLabel(undefined)
expect(unknown.view.container.firstChild).toBeNull()
await act(async () => { await Promise.resolve() })
expect(absent.load).not.toHaveBeenCalled()
expect(unknown.load).not.toHaveBeenCalled()
})
})