Files
deepseek-harness/packages/client/ui-agent-preset/tests/components.spec.tsx
Yichen Jiang 6dfc568ec2 feat(web): author agent presets from a settings page
A composition is a file, but "edit it on the filesystem" is not a browser
affordance. The roster gains `read`/`write`/`remove` beside `select`, and
the browser gains a settings section over them: the presets as rows, one
composition open in a YAML editor at a time, and per-row default, duplicate,
and delete.

All four authoring methods are loopback-pinned. A composition names the
plugins a session runs, so reading one is reconnaissance, writing one is
arbitrary capability, and selecting one can move a session onto a preset
that edits the live runtime. `agentPreset.list` deliberately stays ordinary
and now reports `authorable`, so a surface knows whether creating is
possible at all rather than offering a button whose save always fails.

Authoring starts by duplicating: a shipped preset opens read-only because
the deployment's copy is what a broken local one is compared against. Ids
are contained before they become directory names, and the text is parsed
with the loader's own schema, so a save cannot leave a file no session
could load.

Fixes a defect the real-composition test found: a preset written under the
user's home could never mount, because the loader resolves a row against the
composition's own directory and Node's `node_modules` walk from there never
reaches the installed harness. The mount now records the host base and sends
bare specifiers there, leaving relative paths resolving from the preset.

Also closes the coverage the earlier surfaces in this stack shipped without —
the General row, the composer seat, and the plugin halves now have tests.
2026-08-07 00:41:50 +08:00

233 lines
8.8 KiB
TypeScript

// @vitest-environment jsdom
/**
* The two picker surfaces: the General-settings row that names the default for
* later sessions, and the composer seat that names this one's — the seat stops
* being a control once the conversation starts, because the history from there
* on was produced under the preset's tools.
*/
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 { 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',
options: [{ id: 'standard', trust: 'system' }, { id: 'mine', trust: 'user' }],
}
const SEAT_READY: AgentPresetSeatState = {
current: 'standard',
options: [{ id: 'standard', trust: 'system' }, { id: 'mine', trust: 'user' }],
switchable: true,
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> = {}, locked = false) {
const store = createSnapshotStore<AgentPresetSeatState>({ ...SEAT_READY, ...state })
const actions = { load: vi.fn(() => Promise.resolve()), select: vi.fn(() => Promise.resolve()) }
render(<AgentPresetSeat {...({
...actions,
locked,
useAgentPresetSeat: bindSnapshotSelector(store),
t: (key: keyof typeof en) => en[key],
} as unknown as AgentPresetSeatProps)} />)
return actions
}
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('standard')
})
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('standard')).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 composer seat', () => {
it('reads the roster once and offers the session\'s preset', async () => {
const actions = renderSeat()
await waitFor(() => { expect(actions.load).toHaveBeenCalledTimes(1) })
expect(screen.getByRole('button').textContent).toContain('standard')
expect(screen.getByRole('button').getAttribute('title')).toBe(en.seatHint)
})
it('switches the session and closes the menu', () => {
const actions = renderSeat()
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('becomes a plain label once the conversation has started', () => {
renderSeat({ switchable: false })
// Not a disabled menu: that would suggest the preset could still change.
expect(screen.queryByRole('button')).toBeNull()
expect(screen.getByTitle(en.lockedHint).textContent).toBe('standard')
})
it('closes an open menu the moment the session stops being blank', () => {
const store = createSnapshotStore<AgentPresetSeatState>(SEAT_READY)
render(<AgentPresetSeat {...({
load: vi.fn(() => Promise.resolve()),
select: vi.fn(() => Promise.resolve()),
locked: false,
useAgentPresetSeat: bindSnapshotSelector(store),
t: (key: keyof typeof en) => en[key],
} as unknown as AgentPresetSeatProps)} />)
fireEvent.click(screen.getByRole('button'))
act(() => { store.set({ ...SEAT_READY, switchable: false }) })
expect(screen.getByTitle(en.lockedHint)).toBeTruthy()
})
it('disables the trigger while the composer or a switch is busy', () => {
renderSeat({ busy: true })
expect(screen.getByRole('button')).toHaveProperty('disabled', true)
cleanup()
renderSeat({}, true)
expect(screen.getByRole('button')).toHaveProperty('disabled', true)
})
it('shows a failed 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 = render(<AgentPresetSeat {...({
load: vi.fn(() => Promise.resolve()),
select: vi.fn(() => Promise.resolve()),
locked: false,
useAgentPresetSeat: bindSnapshotSelector(
createSnapshotStore<AgentPresetSeatState>({ ...SEAT_READY, options: [] })),
t: (key: keyof typeof en) => en[key],
} as unknown as AgentPresetSeatProps)} />)
expect(empty.container.firstChild).toBeNull()
cleanup()
const unresolved = render(<AgentPresetSeat {...({
load: vi.fn(() => Promise.resolve()),
select: vi.fn(() => Promise.resolve()),
locked: false,
useAgentPresetSeat: bindSnapshotSelector(
createSnapshotStore<AgentPresetSeatState>({ ...SEAT_READY, current: '' })),
t: (key: keyof typeof en) => en[key],
} as unknown as AgentPresetSeatProps)} />)
expect(unresolved.container.firstChild).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')
})
})