fix(agent-presets,web): broken presets are roster rows, not gaps

A hand-damaged preset was silent until the worst moment. An unparsable
composition listed as an ordinary selectable row and failed only at the
next session start — set as default, every new session failed. A
directory whose composition file was deleted vanished from the roster
while still occupying its id: copy answered "delete the existing preset
first" while remove answered "not found", a dead end.

Discovery now owns health: every id-shaped directory is a roster slot,
broken when its composition is missing or unloadable, checked with the
loader's own entryListSchema dialect (!!js included) so health never
rejects what the loader accepts. `broken` rides AgentPreset, the
agentPreset.list entry, and the UI row; mount/recompose/standingKeyFor
refuse broken up front with the discovery-reported reason, while
resolve/read/remove still answer. The section renders marked red cards —
unselectable, uncopyable, deletable, location kept on custom rows — and
both pickers drop broken rows entirely.

The cordis preset's persona now forbids editing the shipped install
(corrupting cordis would disable the mode itself) and points authoring
at $DSH_HOME/.agent-presets; its skill teaches preset.yml metadata, the
copy-first workflow, the one-escalation sandbox reality, and honest
verification. Exercised live: asked to edit the shipped composition the
composed agent refuses citing both rules; asked for real presets (simple
and complex) it lands them under the user root with one approved
escalation each and self-checks with the loader dialect.
This commit is contained in:
Yichen Jiang
2026-08-09 02:17:56 +08:00
parent bf31797e95
commit c56f23ae1f
36 changed files with 720 additions and 87 deletions

View File

@@ -172,6 +172,43 @@ describe('the preset list', () => {
expect(duplicate.getAttribute('data-tip')).toBe(en.duplicateUnavailable)
})
it('marks a broken custom preset: unselectable, uncopyable, still deletable', () => {
const actions = renderSection({
rows: [
{ id: 'standard', trust: 'system', isDefault: true },
{ id: 'ghost', trust: 'user', isDefault: false, name: '幽灵预设', broken: 'the composition file agent.cordis.yml is missing' },
],
})
const ghost = rowFor('ghost')
// The reason is on the card, and the body cannot pick what cannot mount.
expect(within(ghost).getByText(en.brokenBadge)).toBeTruthy()
expect(within(ghost).getByRole('alert').textContent).toContain('is missing')
const body = within(ghost).getByRole('button', { name: `${en.brokenBadge}: 幽灵预设` })
expect(body).toHaveProperty('disabled', true)
fireEvent.click(body)
expect(actions.makeDefault).not.toHaveBeenCalled()
// Copying a broken preset would only mint another broken one; deleting
// and the location remain — the files are where it gets fixed.
const duplicate = within(ghost).getByRole('button', { name: `${en.duplicate}: 幽灵预设` })
expect(duplicate).toHaveProperty('disabled', true)
expect(duplicate.getAttribute('data-tip')).toBe(en.brokenNoCopy)
expect(within(ghost).getByRole('button', { name: `${en.delete}: 幽灵预设` })).toBeTruthy()
expect(within(ghost).getByRole('button', { name: `${en.openLocation}: 幽灵预设` })).toBeTruthy()
})
it('withholds the viewer on a broken shipped preset', () => {
renderSection({
rows: [{ id: 'standard', trust: 'system', isDefault: false, name: '标准模式', broken: 'the composition is not valid YAML' }],
})
// There is no readable composition to offer; the reason on the card is
// the whole story a shipped row can tell.
const standard = rowFor('standard')
expect(within(standard).queryByRole('button', { name: `${en.view}: 标准模式` })).toBeNull()
expect(within(standard).getByRole('alert').textContent).toContain('not valid YAML')
})
it('labels the location by what it will do without a desktop', () => {
renderSection({ hasDocument: false })

View File

@@ -90,6 +90,20 @@ describe('the agent-preset settings controller', () => {
])
})
it('offers no broken preset: the pickers choose the NEXT session\'s composition', async () => {
const controller = new AgentPresetSettingsController(fakeApi([
{ id: 'standard', trust: 'system', isDefault: true },
{ id: 'damaged', trust: 'user', isDefault: false, broken: 'the composition is not valid YAML' },
] as never))
await controller.load()
// A broken preset cannot compose a session; listing it here would defer
// that discovery to a failed session start. The management section shows
// (and deletes) it from its own store instead.
expect(controller.store.getSnapshot().options.map(option => option.id)).toEqual(['standard'])
})
it('carries the display metadata a preset published', async () => {
const controller = new AgentPresetSettingsController(fakeApi([
{ id: 'standard', trust: 'system', isDefault: true, name: '标准模式', description: '完整的编码 agent。' },