mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
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.
218 lines
8.5 KiB
TypeScript
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)
|
|
})
|
|
})
|