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:
@@ -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 })
|
||||
|
||||
|
||||
@@ -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。' },
|
||||
|
||||
Reference in New Issue
Block a user