Files
deepseek-harness/packages/preset/agent-presets/tests/authoring.spec.ts
Yichen Jiang 7281615d44 feat(agent-presets): give a preset a name and a description
A picker showed directory names, so the settings page could only ever list
`standard` / `core-web` / `cordis` and hope the reader knew what they meant.
A preset may now publish display text in an optional `preset.yml` beside
its composition, and the section renders cards — name, description, and the
one in use — instead of rows.

The file carries display text ONLY. `id` is the directory name and `trust`
comes from the root a preset was discovered under, so neither is writable
there: otherwise a locally authored preset could name itself into the
shipped set. It is a separate file because a composition is a top-level list
of plugin rows — YAML cannot carry sibling keys beside it, and a fake
metadata row would hand the Loader something to load.

Every read failure degrades to no metadata; absent, malformed, wrongly
typed, and blank all mean the same thing and the picker falls back to the
id. Presentation is not capability: a preset whose name is broken still
mounts.

The editor gained name and description fields above the YAML, and clearing
both removes the file rather than storing a blank name.
2026-08-07 00:41:50 +08:00

218 lines
8.5 KiB
TypeScript

/**
* Authoring a preset writes a composition into the deployment's `user` root.
* The id is a directory name, so its pattern is a containment boundary rather
* than a style rule; the shipped `.system` set stays read-only.
*/
import { mkdtemp, mkdir, readFile, writeFile } from 'node:fs/promises'
import { existsSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import Include from '@cordisjs/plugin-include'
import { beforeEach, describe, expect, it } from 'vitest'
import AgentPresets, {
COMPOSITION_FILE, METADATA_FILE, assertComposition,
} from '@deepseek-ai/dsh-agent-presets'
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), 'fixtures')
const VALID = '- id: tool-alpha\n name: ../../plugins/contribute.js\n config:\n tool: alpha\n'
let ctx: Context
let userRoot: string
beforeEach(async () => {
userRoot = await mkdtemp(join(tmpdir(), 'dsh-preset-authoring-'))
ctx = new Context()
ctx.baseUrl = pathToFileURL(FIXTURES).href + '/'
await ctx.plugin(Loader)
ctx.loader.builtins.include = Include
await ctx.plugin(AgentPresets, {
default: 'standard',
roots: [
{ path: join(FIXTURES, 'system'), trust: 'system' as const },
{ path: userRoot, trust: 'user' as const },
],
})
})
describe('authoring a preset', () => {
it('creates one in the user root and lists it', async () => {
await ctx.agentPresets.write('mine', VALID)
expect(await readFile(join(userRoot, 'mine', COMPOSITION_FILE), 'utf8')).toBe(VALID)
const listed = await ctx.agentPresets.list()
expect(listed.find(preset => preset.id === 'mine')?.trust).toBe('user')
})
it('reads back what it stored', async () => {
await ctx.agentPresets.write('mine', VALID)
expect(await ctx.agentPresets.read('mine')).toBe(VALID)
})
it('replaces an existing local preset', async () => {
await ctx.agentPresets.write('mine', VALID)
const next = '- id: tool-beta\n name: ../../plugins/contribute.js\n config:\n tool: beta\n'
await ctx.agentPresets.write('mine', next)
expect(await ctx.agentPresets.read('mine')).toBe(next)
})
it('refuses an id that could escape the preset root', async () => {
for (const id of ['../escape', 'a/b', '/abs', '..', 'Upper']) {
await expect(ctx.agentPresets.write(id, VALID)).rejects.toThrow(/must match/)
}
// Nothing was created for any of them.
expect(existsSync(join(userRoot, 'escape'))).toBe(false)
})
it('refuses text that is not a top-level entry list', async () => {
await expect(ctx.agentPresets.write('bad', 'tools: [a, b]\n'))
.rejects.toThrow(/top-level list of plugin rows/)
await expect(ctx.agentPresets.write('bad', '- id: x\n name: [unclosed\n'))
.rejects.toThrow(/not a valid entry list/)
expect(existsSync(join(userRoot, 'bad'))).toBe(false)
})
it('accepts a composition using the `!!js` dialect the include reads', () => {
// A preset legitimately carries expressions; rejecting them would make
// the editor refuse compositions the loader accepts.
expect(() => { assertComposition('- id: x\n name: y\n config:\n cwd: !!js process.cwd()\n') })
.not.toThrow()
})
it('refuses to overwrite a preset that ships with the deployment', async () => {
await expect(ctx.agentPresets.write('standard', VALID))
.rejects.toThrow(/ships with the deployment/)
expect(await ctx.agentPresets.read('standard')).not.toBe(VALID)
})
})
describe('display metadata beside a composition', () => {
it('stores the name and description the author supplied', async () => {
await ctx.agentPresets.write('mine', VALID, { name: '我的模式', description: '只做检索。' })
expect(await readFile(join(userRoot, 'mine', METADATA_FILE), 'utf8'))
.toContain('name: 我的模式')
const listed = (await ctx.agentPresets.list()).find(preset => preset.id === 'mine')
expect(listed).toMatchObject({ name: '我的模式', description: '只做检索。' })
})
it('removes the file when both fields are cleared', async () => {
await ctx.agentPresets.write('mine', VALID, { name: '我的模式' })
await ctx.agentPresets.write('mine', VALID, {})
// An empty metadata document would read as an intentional blank name;
// absence is what "this preset publishes no display text" looks like.
expect(existsSync(join(userRoot, 'mine', METADATA_FILE))).toBe(false)
expect((await ctx.agentPresets.list()).find(preset => preset.id === 'mine')?.name).toBeUndefined()
})
it('keeps a composition mountable when its metadata is unreadable', async () => {
await ctx.agentPresets.write('mine', VALID)
await writeFile(join(userRoot, 'mine', METADATA_FILE), 'name: [unclosed\n')
// Presentation is not capability: discovery still yields the preset.
const listed = (await ctx.agentPresets.list()).find(preset => preset.id === 'mine')
expect(listed?.name).toBeUndefined()
expect(await ctx.agentPresets.resolve('mine')).toMatchObject({ id: 'mine' })
})
})
describe('deleting a preset', () => {
it('removes a locally authored one', async () => {
await ctx.agentPresets.write('mine', VALID)
await ctx.agentPresets.remove('mine')
expect(existsSync(join(userRoot, 'mine'))).toBe(false)
expect((await ctx.agentPresets.list()).some(preset => preset.id === 'mine')).toBe(false)
})
it('refuses to delete a shipped one', async () => {
await expect(ctx.agentPresets.remove('standard'))
.rejects.toThrow(/ships with the deployment/)
})
it('reports an unknown id rather than silently succeeding', async () => {
await expect(ctx.agentPresets.remove('never-existed')).rejects.toThrow(/not found/)
})
})
describe('a deployment with more than one user root', () => {
it('refuses to delete a preset the writable root does not own', async () => {
const second = await mkdtemp(join(tmpdir(), 'dsh-preset-second-'))
await mkdir(join(second, 'elsewhere'), { recursive: true })
await writeFile(join(second, 'elsewhere', COMPOSITION_FILE), VALID)
const layered = new Context()
layered.baseUrl = pathToFileURL(FIXTURES).href + '/'
await layered.plugin(Loader)
layered.loader.builtins.include = Include
await layered.plugin(AgentPresets, {
default: 'standard',
roots: [
{ path: userRoot, trust: 'user' as const },
{ path: second, trust: 'user' as const },
],
})
// Writes go to the first user root, so a preset discovered from a later
// one is `user` trust yet outside what deletion is allowed to touch —
// `rm -r` on a directory this root does not own is the failure to avoid.
await expect(layered.agentPresets.remove('elsewhere'))
.rejects.toThrow(/does not live under the writable preset root/)
expect(existsSync(join(second, 'elsewhere'))).toBe(true)
})
})
describe('a deployment with no writable root', () => {
it('says authoring is unavailable rather than guessing a directory', async () => {
const readOnly = new Context()
readOnly.baseUrl = pathToFileURL(FIXTURES).href + '/'
await readOnly.plugin(Loader)
readOnly.loader.builtins.include = Include
await readOnly.plugin(AgentPresets, {
default: 'standard',
roots: [{ path: join(FIXTURES, 'system'), trust: 'system' as const }],
})
expect(readOnly.agentPresets.authorable).toBe(false)
await expect(readOnly.agentPresets.write('mine', VALID))
.rejects.toThrow(/no user-writable preset root/)
})
})
describe('a user root that does not exist yet', () => {
it('is created by the first save', async () => {
const absent = join(await mkdtemp(join(tmpdir(), 'dsh-preset-absent-')), 'nested', 'preset')
const fresh = new Context()
fresh.baseUrl = pathToFileURL(FIXTURES).href + '/'
await fresh.plugin(Loader)
fresh.loader.builtins.include = Include
await fresh.plugin(AgentPresets, {
default: 'mine',
roots: [{ path: absent, trust: 'user' as const }],
})
await fresh.agentPresets.write('mine', VALID)
expect(await readFile(join(absent, 'mine', COMPOSITION_FILE), 'utf8')).toBe(VALID)
})
})
describe('a stray file beside the preset directories', () => {
it('does not become a preset', async () => {
await mkdir(join(userRoot, 'not-a-preset'), { recursive: true })
await writeFile(join(userRoot, 'not-a-preset', 'README.txt'), 'nope\n')
expect((await ctx.agentPresets.list()).some(preset => preset.id === 'not-a-preset')).toBe(false)
})
})