Files
deepseek-harness/packages/client/ui-agent-preset/tests/section.spec.tsx
Yichen Jiang b77fb9036c refactor(agent-presets,web): copy-only preset authoring with a path to the files
The web YAML editor is gone. agentPreset.write (arbitrary composition
text) became agentPreset.copy { from, agentPreset, name? }: a host-side
whole-directory copy of ids the host resolves itself — symlinks
dereferenced, modes re-tightened to owner-only with owner-execute kept,
metadata rewritten to keep the source's description but never its name or
roster order. No composition text or path crosses the wire in either
authoring direction, and the entryListSchema/!!js concern dissolves with
assertComposition itself.

The settings section becomes: a read-only viewer over shipped
compositions, a copy dialog (id + optional display name) as the only
create entry, delete for custom rows, and a location action leading into
the preset's own files — agentPreset.openDocument { agentPreset } resolves
the directory host-side and opens it natively, or answers
{ opened: false, path } for the row to show as text where the deployment
has no desktop. agentPreset.list reports hasDocument beside authorable;
the gateway's nativeOpen config pins the capability where
canOpenNativePath platform detection would mislead. The privileged set is
now read/copy/openDocument/remove.

With files as the only composition editor, standing mounts grew
stamp-keyed generations: ensureStanding compares the composition file's
mtime+size and starts the next generation for later sessions, while every
joined session keeps the generation it runs on.

New keyless web lane (agent-preset-authoring, overlay pins
nativeOpen: false so goldens render one branch on every platform) drives
view/copy/reveal/delete end to end; the real-composition CLI e2e switches
to copy semantics.
2026-08-08 22:35:26 +08:00

357 lines
14 KiB
TypeScript

// @vitest-environment jsdom
/**
* The management section's rendering rules: which actions a row offers depends
* on its trust, a shipped composition opens in a read-only viewer, creation is
* a copy dialog that collects an id and an optional name, and the location
* action follows the host's desktop capability.
*/
import { cleanup, fireEvent, render, screen, waitFor, within } 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 { AgentPresetSection } from '../src/client/AgentPresetSection.tsx'
import type { AgentPresetSectionProps } from '../src/client/AgentPresetSection.tsx'
import type { AgentPresetSectionState, CopyDraft } from '../src/client/section-store.ts'
import { en } from '../src/client/locales.ts'
afterEach(cleanup)
const READY: AgentPresetSectionState = {
status: 'ready',
error: null,
authorable: true,
hasDocument: true,
rows: [
{ id: 'standard', trust: 'system', isDefault: true, name: '标准模式', description: '完整的编码 agent。' },
{ id: 'mine', trust: 'user', isDefault: false },
],
copy: null,
view: null,
pendingDelete: null,
deleting: false,
revealedPaths: {},
}
/**
* Render the section over a fixed snapshot, with every action a spy.
* @param state - the snapshot to render.
* @returns the spies, so a test can assert what a click reached.
*/
function renderSection(state: Partial<AgentPresetSectionState> = {}) {
const store = createSnapshotStore<AgentPresetSectionState>({ ...READY, ...state })
const actions = {
load: vi.fn(() => Promise.resolve()),
view: vi.fn(() => Promise.resolve()),
closeView: vi.fn(),
beginCopy: vi.fn(),
cancelCopy: vi.fn(),
setCopyId: vi.fn(),
setCopyName: vi.fn(),
confirmCopy: vi.fn(() => Promise.resolve()),
openLocation: vi.fn(() => Promise.resolve()),
confirmDelete: vi.fn(),
remove: vi.fn(() => Promise.resolve()),
makeDefault: vi.fn(() => Promise.resolve()),
}
const props = {
...actions,
useAgentPresetSection: bindSnapshotSelector(store),
t: (key: keyof typeof en) => en[key],
} as unknown as AgentPresetSectionProps
render(<AgentPresetSection {...props} />)
return actions
}
/** Locate a card by the id it prints, not by its display name. */
function rowFor(id: string): HTMLElement {
const key = screen.getAllByText(id).find(node => node.tagName === 'CODE')
const row = key?.closest('li') ?? null
/* v8 ignore next -- every rendered card prints its id */
if (row === null) throw new Error(`no card for ${id}`)
return row
}
describe('the preset list', () => {
it('reads the roster once when it first renders', async () => {
const actions = renderSection()
await waitFor(() => { expect(actions.load).toHaveBeenCalledTimes(1) })
})
it('shows the published name and description, falling back to the id', () => {
renderSection()
// The name is what a picker reads; the id stays visible as the key the
// composition and the session header actually carry.
expect(screen.getByText('标准模式')).toBeTruthy()
expect(screen.getByText('完整的编码 agent。')).toBeTruthy()
const mine = rowFor('mine')
expect(within(mine).getAllByText('mine').length).toBeGreaterThan(0)
expect(within(mine).getByText(en.noDescription)).toBeTruthy()
})
it('marks trust and the one in use, and offers no "set default" on it', () => {
renderSection()
const standard = rowFor('standard')
expect(within(standard).getByText(en.builtIn)).toBeTruthy()
expect(within(standard).getByText(en.inUse)).toBeTruthy()
expect(within(standard).queryByText(en.setDefault)).toBeNull()
expect(within(rowFor('mine')).getByText(en.userTrust)).toBeTruthy()
})
it('separates built-in presets from custom ones', () => {
renderSection()
// Two different things: one set ships with the deployment and is
// read-only, the other is the user's own.
expect(screen.getByRole('heading', { name: en.builtInGroup })).toBeTruthy()
expect(screen.getByRole('heading', { name: en.customGroup })).toBeTruthy()
})
it('shows no group heading for a set nobody has', () => {
renderSection({ rows: [{ id: 'standard', trust: 'system', isDefault: true }] })
expect(screen.queryByRole('heading', { name: en.customGroup })).toBeNull()
})
it('leads with the guidance that creation starts from a copy', () => {
renderSection()
// The page has no create button: the intro is what tells a first-time
// reader that duplicating a built-in preset IS the way to make one.
expect(screen.getByText(new RegExp(en.copyHint))).toBeTruthy()
})
it('picks a preset by clicking its card, and the one in use is inert', () => {
const actions = renderSection()
const inUse = within(rowFor('standard')).getByRole('button', { name: `${en.inUse}: 标准模式` })
expect(inUse).toHaveProperty('disabled', true)
fireEvent.click(inUse)
// Clicking the card IS the choice; the preset already in use cannot be
// re-picked, so the click reaches nothing.
expect(actions.makeDefault).not.toHaveBeenCalled()
})
it('offers View on a shipped row and the location on a custom one', () => {
renderSection()
// A shipped preset is the composition a copy starts from — reading it is
// the point. A custom preset is edited in its files, so its row leads
// there instead; there is no editor for either.
const standard = rowFor('standard')
expect(within(standard).getByRole('button', { name: `${en.view}: 标准模式` })).toBeTruthy()
expect(within(standard).queryByRole('button', { name: `${en.openLocation}: 标准模式` })).toBeNull()
const mine = rowFor('mine')
expect(within(mine).getByRole('button', { name: `${en.openLocation}: mine` })).toBeTruthy()
expect(within(mine).queryByRole('button', { name: `${en.view}: mine` })).toBeNull()
})
it('offers Delete only for a locally authored preset', () => {
renderSection()
expect(within(rowFor('mine')).getByRole('button', { name: `${en.delete}: mine` })).toBeTruthy()
expect(within(rowFor('standard')).queryByRole('button', { name: `${en.delete}: 标准模式` })).toBeNull()
})
it('disables duplication when nothing is writable, and says why', () => {
renderSection({ authorable: false })
const duplicate = within(rowFor('standard')).getByRole('button', { name: `${en.duplicate}: 标准模式` })
expect(duplicate).toHaveProperty('disabled', true)
expect(duplicate.getAttribute('data-tip')).toBe(en.duplicateUnavailable)
})
it('labels the location by what it will do without a desktop', () => {
renderSection({ hasDocument: false })
expect(within(rowFor('mine')).getByRole('button', { name: `${en.showLocation}: mine` })).toBeTruthy()
})
it('shows a revealed directory on its row', () => {
renderSection({ revealedPaths: { mine: '/home/user/.dsh/.agent-presets/mine' } })
const mine = rowFor('mine')
expect(within(mine).getByText('/home/user/.dsh/.agent-presets/mine')).toBeTruthy()
expect(within(mine).getByText(en.revealedPathLabel)).toBeTruthy()
// The reveal belongs to its row alone.
expect(within(rowFor('standard')).queryByText(en.revealedPathLabel)).toBeNull()
})
it('routes the row actions to the controller', () => {
const actions = renderSection()
// The card body is the control that picks a preset.
fireEvent.click(within(rowFor('mine')).getByRole('button', { name: `${en.setDefault}: mine` }))
fireEvent.click(within(rowFor('mine')).getByRole('button', { name: `${en.openLocation}: mine` }))
fireEvent.click(within(rowFor('mine')).getByRole('button', { name: `${en.duplicate}: mine` }))
fireEvent.click(within(rowFor('standard')).getByRole('button', { name: `${en.view}: 标准模式` }))
expect(actions.makeDefault).toHaveBeenCalledWith('mine')
expect(actions.openLocation).toHaveBeenCalledWith('mine')
expect(actions.beginCopy).toHaveBeenCalledWith('mine')
expect(actions.view).toHaveBeenCalledWith('standard')
})
it('shows a page-level failure without hiding the list', () => {
renderSection({ error: 'settings are read-only' })
expect(screen.getByRole('alert').textContent).toBe('settings are read-only')
expect(rowFor('mine')).toBeTruthy()
})
it('renders nothing when the deployment composes no presets', () => {
const { container } = render(<AgentPresetSection {...({
useAgentPresetSection: bindSnapshotSelector(
createSnapshotStore<AgentPresetSectionState>({ ...READY, status: 'unavailable', rows: [] })),
t: (key: keyof typeof en) => en[key],
load: vi.fn(() => Promise.resolve()),
} as unknown as AgentPresetSectionProps)} />)
expect(container.firstChild).toBeNull()
})
it('offers a retry when the roster could not be read', () => {
const actions = renderSection({ status: 'error', error: 'roster unavailable' })
expect(screen.getByRole('alert').textContent).toContain('roster unavailable')
fireEvent.click(screen.getByText(en.retry))
expect(actions.load).toHaveBeenCalledTimes(2)
})
})
describe('the copy dialog', () => {
const draft: CopyDraft = {
from: 'standard', fromTitle: '标准模式', id: '', name: '', saving: false, error: null,
}
it('names its source and collects only an id and a display name', () => {
const actions = renderSection({ copy: draft })
const dialog = screen.getByRole('dialog')
expect(dialog.getAttribute('aria-label')).toBe(`${en.copyTitle} · ${en.copyOf} 标准模式`)
expect(within(dialog).getByText(en.copyIntro)).toBeTruthy()
fireEvent.change(within(dialog).getByPlaceholderText(en.presetIdPlaceholder), { target: { value: 'my-agent' } })
fireEvent.change(within(dialog).getByPlaceholderText(en.displayNamePlaceholder), { target: { value: '我的模式' } })
expect(actions.setCopyId).toHaveBeenCalledWith('my-agent')
expect(actions.setCopyName).toHaveBeenCalledWith('我的模式')
// Nothing else is collected: the description and the composition are
// edited in the preset's own files.
expect(within(dialog).queryByRole('textbox', { name: /description/i })).toBeNull()
})
it('creates and cancels through the controller', () => {
const actions = renderSection({ copy: { ...draft, id: 'my-agent' } })
const dialog = screen.getByRole('dialog')
fireEvent.click(within(dialog).getByText(en.create))
fireEvent.click(within(dialog).getByText(en.cancel))
expect(actions.confirmCopy).toHaveBeenCalledTimes(1)
expect(actions.cancelCopy).toHaveBeenCalledTimes(1)
})
it('blocks a copy the host would refuse, and says why', () => {
const actions = renderSection({ copy: { ...draft, id: 'Upper Case' } })
const dialog = screen.getByRole('dialog')
expect(within(dialog).getByRole('alert').textContent).toBe(en.idInvalid)
fireEvent.click(within(dialog).getByText(en.create))
// Disabled rather than round-tripping: the id is a directory name and the
// rule is the host's own.
expect(actions.confirmCopy).not.toHaveBeenCalled()
})
it('shows the host\'s refusal instead of the local blocker', () => {
renderSection({ copy: { ...draft, id: 'my-agent', error: 'already exists' } })
expect(within(screen.getByRole('dialog')).getByRole('alert').textContent).toBe('already exists')
})
it('reports a copy in flight and blocks a second click', () => {
const actions = renderSection({ copy: { ...draft, id: 'my-agent', saving: true } })
fireEvent.click(within(screen.getByRole('dialog')).getByText(en.creating))
expect(actions.confirmCopy).not.toHaveBeenCalled()
})
it('dismisses on Escape', () => {
const actions = renderSection({ copy: draft })
fireEvent.keyDown(document, { key: 'Escape' })
expect(actions.cancelCopy).toHaveBeenCalledTimes(1)
})
})
describe('the read-only viewer', () => {
it('shows the composition text under the preset\'s name', () => {
renderSection({ view: { id: 'standard', title: '标准模式', content: '- id: tool-bash\n' } })
const dialog = screen.getByRole('dialog')
expect(dialog.getAttribute('aria-label')).toBe(`${en.view} · 标准模式`)
expect(within(dialog).getByText(en.composition)).toBeTruthy()
expect(within(dialog).getByText(/tool-bash/).textContent).toBe('- id: tool-bash\n')
})
it('closes through the controller', () => {
const actions = renderSection({ view: { id: 'standard', title: '标准模式', content: '- id: x\n' } })
fireEvent.click(within(screen.getByRole('dialog')).getByText(en.close))
expect(actions.closeView).toHaveBeenCalledTimes(1)
})
it('dismisses on Escape', () => {
const actions = renderSection({ view: { id: 'standard', title: '标准模式', content: '- id: x\n' } })
fireEvent.keyDown(document, { key: 'Escape' })
expect(actions.closeView).toHaveBeenCalledTimes(1)
})
})
describe('deleting a preset', () => {
it('asks before deleting', () => {
const actions = renderSection()
fireEvent.click(within(rowFor('mine')).getByRole('button', { name: `${en.delete}: mine` }))
expect(actions.confirmDelete).toHaveBeenCalledWith('mine')
})
it('confirms and dismisses through the controller', () => {
const actions = renderSection({ pendingDelete: 'mine' })
const dialog = screen.getByRole('dialog')
fireEvent.click(within(dialog).getByText(en.deleteConfirm))
fireEvent.click(within(dialog).getByText(en.cancel))
expect(actions.remove).toHaveBeenCalledTimes(1)
expect(actions.confirmDelete).toHaveBeenLastCalledWith(null)
})
it('dismisses the confirmation on Escape', () => {
const actions = renderSection({ pendingDelete: 'mine' })
fireEvent.keyDown(document, { key: 'Escape' })
expect(actions.confirmDelete).toHaveBeenCalledWith(null)
})
it('reports a delete in flight', () => {
const actions = renderSection({ pendingDelete: 'mine', deleting: true })
fireEvent.click(within(screen.getByRole('dialog')).getByText(en.deleting))
expect(actions.remove).not.toHaveBeenCalled()
})
})