Files
deepseek-harness/packages/preset/agent-presets/tests/authoring.spec.ts
Yichen Jiang 6dfc568ec2 feat(web): author agent presets from a settings page
A composition is a file, but "edit it on the filesystem" is not a browser
affordance. The roster gains `read`/`write`/`remove` beside `select`, and
the browser gains a settings section over them: the presets as rows, one
composition open in a YAML editor at a time, and per-row default, duplicate,
and delete.

All four authoring methods are loopback-pinned. A composition names the
plugins a session runs, so reading one is reconnaissance, writing one is
arbitrary capability, and selecting one can move a session onto a preset
that edits the live runtime. `agentPreset.list` deliberately stays ordinary
and now reports `authorable`, so a surface knows whether creating is
possible at all rather than offering a button whose save always fails.

Authoring starts by duplicating: a shipped preset opens read-only because
the deployment's copy is what a broken local one is compared against. Ids
are contained before they become directory names, and the text is parsed
with the loader's own schema, so a save cannot leave a file no session
could load.

Fixes a defect the real-composition test found: a preset written under the
user's home could never mount, because the loader resolves a row against the
composition's own directory and Node's `node_modules` walk from there never
reaches the installed harness. The mount now records the host base and sends
bare specifiers there, leaving relative paths resolving from the preset.

Also closes the coverage the earlier surfaces in this stack shipped without —
the General row, the composer seat, and the plugin halves now have tests.
2026-08-07 00:41:50 +08:00

184 lines
6.9 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, 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('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)
})
})