Files
deepseek-harness/packages/preset/agent-presets/tests/authoring.spec.ts
Yichen Jiang b77fb9036c refactor(agent-presets,web): copy-only preset authoring with a path to the files
The web YAML editor is gone. agentPreset.write (arbitrary composition
text) became agentPreset.copy { from, agentPreset, name? }: a host-side
whole-directory copy of ids the host resolves itself — symlinks
dereferenced, modes re-tightened to owner-only with owner-execute kept,
metadata rewritten to keep the source's description but never its name or
roster order. No composition text or path crosses the wire in either
authoring direction, and the entryListSchema/!!js concern dissolves with
assertComposition itself.

The settings section becomes: a read-only viewer over shipped
compositions, a copy dialog (id + optional display name) as the only
create entry, delete for custom rows, and a location action leading into
the preset's own files — agentPreset.openDocument { agentPreset } resolves
the directory host-side and opens it natively, or answers
{ opened: false, path } for the row to show as text where the deployment
has no desktop. agentPreset.list reports hasDocument beside authorable;
the gateway's nativeOpen config pins the capability where
canOpenNativePath platform detection would mislead. The privileged set is
now read/copy/openDocument/remove.

With files as the only composition editor, standing mounts grew
stamp-keyed generations: ensureStanding compares the composition file's
mtime+size and starts the next generation for later sessions, while every
joined session keeps the generation it runs on.

New keyless web lane (agent-preset-authoring, overlay pins
nativeOpen: false so goldens render one branch on every platform) drives
view/copy/reveal/delete end to end; the real-composition CLI e2e switches
to copy semantics.
2026-08-08 22:35:26 +08:00

269 lines
11 KiB
TypeScript

/**
* Authoring a preset copies an existing one's directory into the deployment's
* `user` root — copy is the only authoring write, so no caller ever supplies
* composition text. 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 { chmod, mkdtemp, mkdir, readFile, stat, 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, copyComposition, METADATA_FILE,
} 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
/** Hand-craft a preset directory (tests cannot author text through the service). */
async function seedPreset(
root: string, id: string, options: { composition?: string; metadata?: string; extras?: Record<string, string> } = {},
): Promise<void> {
await mkdir(join(root, id), { recursive: true })
await writeFile(join(root, id, COMPOSITION_FILE), options.composition ?? VALID)
if (options.metadata !== undefined) {
await writeFile(join(root, id, METADATA_FILE), options.metadata)
}
for (const [name, content] of Object.entries(options.extras ?? {})) {
await mkdir(dirname(join(root, id, name)), { recursive: true })
await writeFile(join(root, id, name), content)
}
}
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('copying a preset', () => {
it('copies a shipped preset into the user root and lists it', async () => {
await ctx.agentPresets.copy('standard', 'mine')
expect(await readFile(join(userRoot, 'mine', COMPOSITION_FILE), 'utf8'))
.toBe(await ctx.agentPresets.read('standard'))
const listed = await ctx.agentPresets.list()
expect(listed.find(preset => preset.id === 'mine')?.trust).toBe('user')
})
it('copies the whole directory, execute bits kept and group/other stripped', async () => {
await seedPreset(userRoot, 'source', {
extras: { 'skills/demo/SKILL.md': '# demo\n', 'skills/demo/run.sh': '#!/bin/sh\n' },
})
await chmod(join(userRoot, 'source', 'skills', 'demo', 'run.sh'), 0o755)
await ctx.agentPresets.copy('source', 'mine')
expect(await readFile(join(userRoot, 'mine', 'skills', 'demo', 'SKILL.md'), 'utf8')).toBe('# demo\n')
// A preset may ship runnable helpers; the copy keeps them runnable for the
// owner while withdrawing the world-readability of the install.
expect((await stat(join(userRoot, 'mine', 'skills', 'demo', 'run.sh'))).mode & 0o777).toBe(0o700)
expect((await stat(join(userRoot, 'mine', 'skills', 'demo', 'SKILL.md'))).mode & 0o777).toBe(0o600)
expect((await stat(join(userRoot, 'mine'))).mode & 0o777).toBe(0o700)
})
it('keeps the source description but never its name or order', async () => {
await seedPreset(userRoot, 'source', { metadata: 'name: 源模式\ndescription: 只做检索。\norder: 1\n' })
await ctx.agentPresets.copy('source', 'mine')
// Two rows presenting identically is how a roster stops being a chooser,
// and the shipped set's declared order is not the copy's to claim.
const metadata = await readFile(join(userRoot, 'mine', METADATA_FILE), 'utf8')
expect(metadata).toContain('description: 只做检索。')
expect(metadata).not.toContain('name:')
expect(metadata).not.toContain('order:')
expect((await ctx.agentPresets.list()).find(preset => preset.id === 'mine'))
.toMatchObject({ description: '只做检索。' })
})
it('stores the display name the author supplied', async () => {
await ctx.agentPresets.copy('standard', 'mine', '我的模式')
expect(await readFile(join(userRoot, 'mine', METADATA_FILE), 'utf8')).toContain('name: 我的模式')
expect((await ctx.agentPresets.list()).find(preset => preset.id === 'mine'))
.toMatchObject({ name: '我的模式' })
})
it('publishes no metadata file when there is nothing to publish', async () => {
await seedPreset(userRoot, 'source')
await ctx.agentPresets.copy('source', 'mine')
// 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('refuses an id that could escape the preset root', async () => {
for (const id of ['../escape', 'a/b', '/abs', '..', 'Upper']) {
await expect(ctx.agentPresets.copy('standard', id)).rejects.toThrow(/must match/)
}
// Nothing was created for any of them.
expect(existsSync(join(userRoot, 'escape'))).toBe(false)
})
it('refuses an id the roster already supplies, shipped ones included', async () => {
await ctx.agentPresets.copy('standard', 'mine')
await expect(ctx.agentPresets.copy('standard', 'mine')).rejects.toThrow(/already exists/)
// A user directory named like a shipped preset would be shadowed by it.
await expect(ctx.agentPresets.copy('standard', 'minimal')).rejects.toThrow(/already exists/)
})
it('refuses a directory that occupies the name without being a preset', async () => {
await mkdir(join(userRoot, 'occupied'), { recursive: true })
await writeFile(join(userRoot, 'occupied', 'README.txt'), 'nope\n')
// Discovery does not list it (no composition file), so only the disk
// check can refuse it with a readable error instead of a filesystem code.
await expect(ctx.agentPresets.copy('standard', 'occupied')).rejects.toThrow(/already exists/)
expect(await readFile(join(userRoot, 'occupied', 'README.txt'), 'utf8')).toBe('nope\n')
})
it('reports an unknown source rather than creating anything', async () => {
await expect(ctx.agentPresets.copy('never-existed', 'mine')).rejects.toThrow(/not found/)
expect(existsSync(join(userRoot, 'mine'))).toBe(false)
})
it('leaves nothing behind when the copy itself fails', async () => {
const source = {
id: 'gone',
trust: 'user' as const,
path: join(userRoot, 'gone', COMPOSITION_FILE),
}
// The source vanished between resolve and copy: the half-made target is
// rolled back rather than left invisible to discovery.
await expect(copyComposition(
[{ path: userRoot, trust: 'user' as const }], source, 'mine',
)).rejects.toThrow()
expect(existsSync(join(userRoot, 'mine'))).toBe(false)
})
})
describe('deleting a preset', () => {
it('removes a locally authored one', async () => {
await ctx.agentPresets.copy('standard', 'mine')
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 seedPreset(second, 'elsewhere')
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.copy('standard', 'mine'))
.rejects.toThrow(/no user-writable preset root/)
})
})
describe('a user root that does not exist yet', () => {
it('is created by the first copy', 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: 'standard',
roots: [
{ path: join(FIXTURES, 'system'), trust: 'system' as const },
{ path: absent, trust: 'user' as const },
],
})
await fresh.agentPresets.copy('standard', 'mine')
expect(await readFile(join(absent, 'mine', COMPOSITION_FILE), 'utf8'))
.toBe(await fresh.agentPresets.read('standard'))
})
})
describe('display metadata beside a composition', () => {
it('keeps a composition mountable when its metadata is unreadable', async () => {
await ctx.agentPresets.copy('standard', 'mine')
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('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)
})
})