import { describe, expect, it } from 'vitest' import { mkdir, readFile, symlink, writeFile } from 'node:fs/promises' import { dirname, join } from 'node:path' import { tmpdir } from 'node:os' import { Context } from 'cordis' import SkillService from '@deepseek-ai/dsh-skill' import { FileSystem, FsVersion, type FsEditOutcome, type FsEditRequest, type FsInfo, type FsTarget, type FsWriteOutcome } from '@deepseek-ai/dsh-fs' async function tempDir(name: string): Promise { return await import('node:fs/promises').then(fs => fs.mkdtemp(join(tmpdir(), `dsh-${name}-`))) } async function writeSkill(root: string, name: string, description: string, body = 'Use the skill.'): Promise { const dir = join(root, name) await mkdir(dir, { recursive: true }) await writeFile(join(dir, 'SKILL.md'), `---\nname: ${name}\ndescription: ${description}\n---\n\n${body}\n`) } async function writeFlatSkill(root: string, name: string, description: string, body = 'Flat body.'): Promise { await mkdir(root, { recursive: true }) await writeFile(join(root, `${name}.md`), `---\nname: ${name}\ndescription: ${description}\n---\n\n${body}\n`) } class TestFileSystem extends FileSystem { override async resolve(path: string): Promise { return { targetKey: path as never, displayPath: path } } override async stat(target: FsTarget): Promise { try { const fs = await import('node:fs/promises') const info = await fs.stat(target.displayPath) return { version: FsVersion(String(info.mtimeMs)), type: info.isFile() ? 'file' : info.isDirectory() ? 'directory' : 'other', size: info.size, } } catch { return undefined } } override async readText(target: FsTarget): Promise { const text = await readFile(target.displayPath, 'utf8') if (text.includes('\uFFFD')) throw new Error('not text') return text } override async streamText(_target: FsTarget): Promise> { throw new Error('not needed in skill tests') } override async listDir(): Promise { throw new Error('not needed in skill tests') } override async writeText(target: FsTarget, content: string): Promise { await mkdir(dirname(target.displayPath), { recursive: true }) await writeFile(target.displayPath, content) return { operation: 'create', version: FsVersion('test'), before: null, after: content } } override async editText(_target: FsTarget, _request: FsEditRequest): Promise { throw new Error('not needed in skill tests') } } describe('SkillService', () => { it('discovers project, user, agents, and system skill roots in priority order', async () => { const home = await tempDir('skill-home') const agentsHome = await tempDir('agents-home') const project = await tempDir('skill-project') await mkdir(join(project, '.git'), { recursive: true }) await writeSkill(join(home, '.dsh/skills/.system'), 'same', 'system skill') await writeSkill(join(agentsHome, '.agents/skills'), 'same', 'user agents skill') await writeSkill(join(home, '.dsh/skills'), 'same', 'user dsh skill') await writeSkill(join(project, '.agents/skills'), 'same', 'project agents skill') await writeSkill(join(project, '.dsh/skills'), 'same', 'project dsh skill') await writeSkill(join(home, '.dsh/skills/.system'), 'system-only', 'system only') const ctx = new Context() await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(agentsHome, '.agents'), installSystemSkills: false }) const skills = await ctx.skills.list({ cwd: join(project, 'src') }) expect(skills.map(skill => [skill.name, skill.description])).toEqual([ ['same', 'project dsh skill'], ['system-only', 'system only'], ]) expect(skills.find(skill => skill.name === 'same')?.source).toBe('project-dsh') }) it('sorts the final model-visible list by skill name after priority conflict resolution', async () => { const home = await tempDir('skill-sorted-home') const agentsHome = await tempDir('skill-sorted-agents') const project = await tempDir('skill-sorted-project') await mkdir(join(project, '.git'), { recursive: true }) await writeSkill(join(project, '.dsh/skills'), 'z-project', 'Project skill') await writeSkill(join(home, '.dsh/skills'), 'm-user', 'User skill') await writeSkill(join(home, '.dsh/skills/.system'), 'a-system', 'System skill') await writeSkill(join(home, '.dsh/skills/.system'), 'm-user', 'Shadowed system skill') const ctx = new Context() await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(agentsHome, '.agents'), installSystemSkills: false }) expect((await ctx.skills.list({ cwd: project })).map(skill => [skill.name, skill.description])).toEqual([ ['a-system', 'System skill'], ['m-user', 'User skill'], ['z-project', 'Project skill'], ]) }) it('gives project skills priority over runtime skills while runtime overrides user and system skills', async () => { const home = await tempDir('skill-runtime-priority') const project = await tempDir('skill-runtime-project') await mkdir(join(project, '.git'), { recursive: true }) await writeSkill(join(project, '.dsh/skills'), 'project-name', 'Project wins') await writeSkill(join(home, '.dsh/skills'), 'runtime-name', 'User loses') await writeSkill(join(home, '.dsh/skills/.system'), 'runtime-name', 'System loses') const ctx = new Context() await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false }) ctx.skills.register({ name: 'project-name', description: 'Runtime loses to project', content: 'Runtime body.', directory: 'memory://project-name', source: 'runtime', }) ctx.skills.register({ name: 'runtime-name', description: 'Runtime wins', content: 'Runtime body.', directory: 'memory://runtime-name', source: 'runtime', }) expect((await ctx.skills.get('project-name', { cwd: project }))?.description).toBe('Project wins') expect((await ctx.skills.get('runtime-name', { cwd: project }))?.description).toBe('Runtime wins') }) it('does not scan .system twice through the user dsh root', async () => { const home = await tempDir('skill-system') await writeSkill(join(home, '.dsh/skills/.system'), 'builtin', 'builtin skill') const ctx = new Context() await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false }) expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['builtin']) }) it('parses flat skills and filters invalid or model-disabled skills from listing', async () => { const home = await tempDir('skill-flat') await writeFlatSkill(join(home, '.dsh/skills'), 'flat-skill', 'flat description', 'Flat instructions.') await writeFile(join(home, '.dsh/skills/bad.md'), '---\nname: Bad_Name\ndescription: bad\n---\n\nbad') await writeFile(join(home, '.dsh/skills/missing-description.md'), '---\nname: missing-description\n---\n\nbad') await writeFile(join(home, '.dsh/skills/no-frontmatter.md'), 'No frontmatter.') await writeFile(join(home, '.dsh/skills/plain-markdown.md'), '# Notes\nNot a skill.') await writeFile(join(home, '.dsh/skills/open-frontmatter.md'), '---\nname: open-frontmatter') await writeFile(join(home, '.dsh/skills/non-object.md'), '---\n[]\n---\n\nbad') await writeFile(join(home, '.dsh/skills/no-trailing-body.md'), '---\nname: no-trailing-body\ndescription: No trailing body\n---') await writeFile(join(home, '.dsh/skills/notes.txt'), 'ignored') await mkdir(join(home, '.dsh/skills/not-a-skill'), { recursive: true }) await writeSkill(join(home, '.dsh/skills'), 'hidden-skill', 'hidden description', 'Hidden.') await writeFile(join(home, '.dsh/skills/hidden-skill/SKILL.md'), '---\nname: hidden-skill\ndescription: hidden description\ndisableModelInvocation: true\n---\n\nHidden.\n') const ctx = new Context() await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false }) expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['flat-skill', 'no-trailing-body']) expect((await ctx.skills.get('hidden-skill'))?.content).toContain('Hidden.') expect(await ctx.skills.get('Bad_Name')).toBeUndefined() }) it('supports CRLF frontmatter and ignores delimiter-looking text inside YAML values', async () => { const home = await tempDir('skill-frontmatter-crlf') const root = join(home, '.dsh/skills') await mkdir(root, { recursive: true }) await writeFile(join(root, 'crlf-skill.md'), [ '---', 'name: crlf-skill', 'description: CRLF skill', 'metadata:', ' marker: "----"', '---', '', 'CRLF body.', ].join('\r\n')) await writeFile(join(root, 'block-skill.md'), [ '---', 'name: block-skill', 'description: |', ' Includes a ---- marker that is not a delimiter.', '---', '', 'Block body.', ].join('\n')) const ctx = new Context() await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false }) expect((await ctx.skills.get('crlf-skill'))?.content).toBe('CRLF body.') expect((await ctx.skills.get('crlf-skill'))?.metadata).toEqual({ marker: '----' }) expect((await ctx.skills.get('block-skill'))?.description).toBe('Includes a ---- marker that is not a delimiter.\n') expect((await ctx.skills.get('block-skill'))?.content).toBe('Block body.') }) it('skips invalid YAML skill files without poisoning discovery cache', async () => { const home = await tempDir('skill-invalid-yaml') const root = join(home, '.dsh/skills') await writeSkill(root, 'good-skill', 'Good skill') await writeFile(join(root, 'bad-yaml.md'), '---\nname: bad-yaml\ndescription: [unclosed\n---\n\nBad body.\n') const ctx = new Context() await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false }) expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['good-skill']) await writeFile(join(root, 'bad-yaml.md'), '---\nname: fixed-skill\ndescription: Fixed skill\n---\n\nFixed body.\n') expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['good-skill']) const dispose = ctx.skills.register({ name: 'runtime-skill', description: 'Runtime skill', content: 'Runtime body.', directory: 'memory://runtime', source: 'runtime', }) expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['fixed-skill', 'good-skill', 'runtime-skill']) dispose() }) it('does not cache a rejected discovery promise', async () => { const home = await tempDir('skill-rejected-cache') const ctx = new Context() await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false }) const internals = ctx.skills as unknown as { collectFresh(roots: unknown): Promise } const original = internals.collectFresh.bind(ctx.skills) let fail = true internals.collectFresh = async (roots: unknown) => { if (fail) throw new Error('transient discovery failure') return await original(roots) } await expect(ctx.skills.list()).rejects.toThrow('transient discovery failure') fail = false await writeSkill(join(home, '.dsh/skills'), 'late-good', 'Late good') await expect(ctx.skills.list()).resolves.toMatchObject([{ name: 'late-good' }]) }) it('discovers symlinked skill directories and flat files', async () => { const home = await tempDir('skill-symlink-home') const external = await tempDir('skill-symlink-external') await writeSkill(external, 'linked-dir', 'Linked directory') await writeFlatSkill(external, 'linked-flat', 'Linked flat') await mkdir(join(home, '.dsh/skills'), { recursive: true }) await symlink(join(external, 'linked-dir'), join(home, '.dsh/skills/linked-dir')) await symlink(join(external, 'linked-flat.md'), join(home, '.dsh/skills/linked-flat.md')) await symlink(join(external, 'missing'), join(home, '.dsh/skills/broken-link')) await symlink('/dev/null', join(home, '.dsh/skills/device-link')) const ctx = new Context() await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false }) expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['linked-dir', 'linked-flat']) }) it('honors prompt and cache bounds from config', async () => { const home = await tempDir('skill-config-bounds') const firstProject = await tempDir('skill-config-first') const secondProject = await tempDir('skill-config-second') await mkdir(join(firstProject, '.git'), { recursive: true }) await mkdir(join(secondProject, '.git'), { recursive: true }) await writeSkill(join(firstProject, '.dsh/skills'), 'first-skill', 'abcdefghij') await writeSkill(join(secondProject, '.dsh/skills'), 'second-skill', 'Second') const ctx = new Context() await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false, promptFieldMaxLength: 6, collectCacheMaxEntries: 1, }) expect(await ctx.skills.renderModelListing({ cwd: firstProject })).toContain('description: abc...') expect((await ctx.skills.list({ cwd: firstProject })).map(skill => skill.name)).toEqual(['first-skill']) await writeSkill(join(firstProject, '.dsh/skills'), 'late-first', 'Late first') expect((await ctx.skills.list({ cwd: firstProject })).map(skill => skill.name)).toEqual(['first-skill']) await ctx.skills.list({ cwd: secondProject }) expect((await ctx.skills.list({ cwd: firstProject })).map(skill => skill.name)).toEqual(['first-skill', 'late-first']) }) it('rejects invalid positive-integer config caps', async () => { const home = await tempDir('skill-invalid-config') const ctx = new Context() await expect(ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false, promptFieldMaxLength: 0, })).rejects.toThrow('promptFieldMaxLength') await expect(ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false, collectCacheMaxEntries: 1.5, })).rejects.toThrow('collectCacheMaxEntries') }) it('renders no model listing when no model-invocable skills exist', async () => { const home = await tempDir('skill-empty-listing') const ctx = new Context() await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false }) expect(await ctx.skills.renderModelListing()).toBe('') const request = { model: 'm', messages: [], system: 'base' } const result = await ctx.waterfall('agent/request', { session: { header: { cwd: home } }, } as never, 1, 1, request, () => Promise.resolve(request)) expect(result.system).toBe('base') }) it('supports default home root resolution without installing system skills', async () => { const previousDshHome = process.env.DSH_HOME const envHome = await tempDir('skill-env-home') try { process.env.DSH_HOME = join(envHome, '.dsh') await new Context().plugin(SkillService, { installSystemSkills: false }) delete process.env.DSH_HOME await new Context().plugin(SkillService, { installSystemSkills: false }) } finally { if (previousDshHome === undefined) { delete process.env.DSH_HOME } else { process.env.DSH_HOME = previousDshHome } } }) it('keeps constructor defaults when schema preprocessing is not involved', async () => { const home = await tempDir('skill-constructor-defaults') const service = new SkillService(new Context(), { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), }) expect((await service.list()).map(skill => skill.name)).toEqual(['dsh-plugin-creator', 'dsh-skill-creator']) }) it('installs system skills into the DSH home without overwriting existing files', async () => { const home = await tempDir('skill-install') const existing = join(home, '.dsh/skills/.system/dsh-plugin-creator/SKILL.md') await mkdir(join(home, '.dsh/skills/.system/dsh-plugin-creator'), { recursive: true }) await writeFile(existing, '---\nname: dsh-plugin-creator\ndescription: Custom system skill\n---\n\nCustom body.\n') const ctx = new Context() await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents') }) expect((await ctx.skills.list()).map(skill => [skill.name, skill.description])).toEqual([ ['dsh-plugin-creator', 'Custom system skill'], ['dsh-skill-creator', 'Create or update DeepSeek Harness SKILL.md instructions.'], ]) expect(await readFile(existing, 'utf8')).toContain('Custom body.') expect(await readFile(join(home, '.dsh/skills/.system/dsh-skill-creator/SKILL.md'), 'utf8')).toContain('dsh-skill-creator') }) it('uses the filesystem service when installing bundled system skills', async () => { const home = await tempDir('skill-install-fs') const existing = join(home, '.dsh/skills/.system/dsh-plugin-creator/SKILL.md') await mkdir(join(home, '.dsh/skills/.system/dsh-plugin-creator'), { recursive: true }) await writeFile(existing, '---\nname: dsh-plugin-creator\ndescription: Existing system skill\n---\n\nExisting body.\n') const ctx = new Context() await ctx.plugin(TestFileSystem) await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents') }) expect((await ctx.skills.list()).map(skill => [skill.name, skill.description])).toEqual([ ['dsh-plugin-creator', 'Existing system skill'], ['dsh-skill-creator', 'Create or update DeepSeek Harness SKILL.md instructions.'], ]) expect(await readFile(existing, 'utf8')).toContain('Existing body.') expect(await readFile(join(home, '.dsh/skills/.system/dsh-skill-creator/SKILL.md'), 'utf8')).toContain('dsh-skill-creator') }) it('renders bundled system skill files with and without routing metadata', async () => { const home = await tempDir('skill-install-render') const ctx = new Context() await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents') }) await ctx.skills.list() expect(await readFile(join(home, '.dsh/skills/.system/dsh-plugin-creator/SKILL.md'), 'utf8')).not.toContain('whenToUse:') expect(await readFile(join(home, '.dsh/skills/.system/dsh-skill-creator/SKILL.md'), 'utf8')).toContain('whenToUse:') }) it('uses the filesystem service for skill file reads when it is available', async () => { const home = await tempDir('skill-read-fs') const root = join(home, '.dsh/skills') await writeFlatSkill(root, 'text-skill', 'Text skill', 'Text body.') await mkdir(join(root, 'empty-dir'), { recursive: true }) await mkdir(join(root, 'directory-skill/SKILL.md'), { recursive: true }) await writeFile(join(root, 'binary-skill.md'), Buffer.concat([ Buffer.from('---\nname: binary-skill\ndescription: Binary skill\n---\n\n'), Buffer.from([0xff]), Buffer.from('\n'), ])) const ctx = new Context() await ctx.plugin(TestFileSystem) await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false }) expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['text-skill']) expect(await ctx.skills.get('binary-skill')).toBeUndefined() }) it('degrades when bundled system skill installation fails', async () => { const home = await tempDir('skill-install-fail') await writeFile(join(home, '.dsh'), 'not a directory') await writeSkill(join(home, '.agents/skills'), 'fallback-skill', 'Fallback skill') const ctx = new Context() await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents') }) expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['fallback-skill']) }) it('memoizes disk discovery until runtime skill registrations change', async () => { const home = await tempDir('skill-cache') await writeSkill(join(home, '.dsh/skills'), 'initial-skill', 'Initial skill') const ctx = new Context() await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false }) expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['initial-skill']) await writeSkill(join(home, '.dsh/skills'), 'late-skill', 'Late skill') expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['initial-skill']) const dispose = ctx.skills.register({ name: 'runtime-skill', description: 'runtime', content: 'Runtime body.', directory: 'memory://runtime', source: 'runtime', }) expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['initial-skill', 'late-skill', 'runtime-skill']) dispose() expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['initial-skill', 'late-skill']) }) it('includes extra roots, optional metadata, and explicit false disable flags', async () => { const home = await tempDir('skill-extra') const extra = await tempDir('skill-extra-root') await writeFile(join(extra, 'extra-skill.md'), [ '---', 'name: extra-skill', 'description: Extra skill', 'whenToUse: For extra-root tests', 'disableModelInvocation: false', 'metadata:', ' owner: tests', '---', '', 'Extra body.', ].join('\n')) const ctx = new Context() await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), extraRoots: [extra], installSystemSkills: false, }) expect(await ctx.skills.list()).toEqual([{ name: 'extra-skill', description: 'Extra skill', whenToUse: 'For extra-root tests', disableModelInvocation: false, directory: extra, source: 'extra', }]) expect((await ctx.skills.get('extra-skill'))?.metadata).toEqual({ owner: 'tests' }) expect(await ctx.skills.renderModelListing()).toContain('whenToUse: For extra-root tests') }) it('bounds prompt listing fields without changing stored skill content', async () => { const home = await tempDir('skill-prompt-bounds') const longDescription = 'a'.repeat(600) await writeSkill(join(home, '.dsh/skills'), 'long-skill', longDescription, 'Full body.') const ctx = new Context() await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false }) const listing = await ctx.skills.renderModelListing() expect(listing).toContain(`${'a'.repeat(497)}...`) expect(listing).not.toContain('a'.repeat(600)) expect((await ctx.skills.get('long-skill'))?.description).toBe(longDescription) }) it('escapes prompt listing text fields without changing stored skill content', async () => { const home = await tempDir('skill-prompt-escape') const root = join(home, '.dsh/skills') await mkdir(root, { recursive: true }) await writeFile(join(root, 'escaped-skill.md'), [ '---', 'name: escaped-skill', 'description: Use safely', 'whenToUse: Handle & marker', '---', 'Full body.', ].join('\n')) const ctx = new Context() await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false }) const listing = await ctx.skills.renderModelListing() expect(listing).toContain('description: Use </available_skills><oops> safely') expect(listing).toContain('whenToUse: Handle <tag> & marker') expect(listing).not.toContain('description: Use safely') expect((await ctx.skills.get('escaped-skill'))?.description).toBe('Use safely') }) it('adds skill guidance through the agent/request waterfall without including bodies', async () => { const home = await tempDir('skill-guidance') await writeSkill(join(home, '.dsh/skills'), 'research-helper', 'Research helper', 'Long body that must not be listed.') const ctx = new Context() await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false }) const request = await ctx.waterfall('agent/request', { session: { header: { cwd: home } }, } as never, 1, 1, { model: 'm', messages: [], system: 'base' }, () => Promise.resolve({ model: 'm', messages: [], system: 'base' })) expect(request.system ?? '').toContain('## Skills\n') expect(request.system ?? '').toContain('research-helper') expect(request.system ?? '').toContain('source="project-dsh"') expect(request.system ?? '').not.toContain(home) expect(request.system ?? '').not.toContain('Long body') expect((request.system ?? '').match(/## Skills/g)).toHaveLength(1) const sameObject = { model: 'm', messages: [], system: 'base' } const sameObjectResult = await ctx.waterfall('agent/request', { session: { header: { cwd: home } }, } as never, 1, 1, sameObject, () => Promise.resolve(sameObject)) expect(sameObjectResult.system).toContain('## Skills') const requestWithoutBase = await ctx.waterfall('agent/request', { session: { header: { cwd: home } }, } as never, 1, 1, { model: 'm', messages: [] }, () => Promise.resolve({ model: 'm', messages: [] })) expect(requestWithoutBase.system).toContain('## Skills') const copyCtx = new Context() await copyCtx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false }) copyCtx.on('agent/request', async (_agent, _turn, _step, requestToCopy) => ({ ...requestToCopy })) const copiedRequest = await copyCtx.waterfall('agent/request', { session: { header: { cwd: home } }, } as never, 1, 1, { model: 'm', messages: [], system: 'base' }, () => Promise.resolve({ model: 'm', messages: [], system: 'base' })) expect((copiedRequest.system ?? '').match(/## Skills/g)).toHaveLength(1) }) it('cleans up runtime registered skills when the contributing fiber is disposed', async () => { const ctx = new Context() const home = await tempDir('skill-runtime') await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false }) const fiber = await ctx.plugin(Object.assign((inner: Context) => { inner.skills.register({ name: 'runtime-skill', description: 'runtime', content: 'Runtime body.', directory: 'memory://runtime', source: 'runtime', }) }, { inject: ['skills'] })) expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['runtime-skill']) await fiber.dispose() expect(await ctx.skills.list()).toEqual([]) }) it('bounds discovery cache entries across many project roots', async () => { const home = await tempDir('skill-cache-bound-home') const projects = await Promise.all(Array.from({ length: 129 }, async (_, index) => { const project = await tempDir(`skill-cache-bound-project-${index}`) await mkdir(join(project, '.git'), { recursive: true }) await writeSkill(join(project, '.dsh/skills'), `project-${index}`, `Project ${index}`) return project })) const ctx = new Context() await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false }) const firstProject = projects[0] if (firstProject === undefined) throw new Error('expected at least one project') expect((await ctx.skills.list({ cwd: firstProject })).map(skill => skill.name)).toEqual(['project-0']) await writeSkill(join(firstProject, '.dsh/skills'), 'late-project-0', 'Late project 0') expect((await ctx.skills.list({ cwd: firstProject })).map(skill => skill.name)).toEqual(['project-0']) for (const project of projects.slice(1)) { await ctx.skills.list({ cwd: project }) } expect((await ctx.skills.list({ cwd: firstProject })).map(skill => skill.name)).toEqual(['late-project-0', 'project-0']) }) it('removes runtime registered skills when the returned disposer is called', async () => { const home = await tempDir('skill-runtime-disposer') const ctx = new Context() await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false }) const dispose = ctx.skills.register({ name: 'manual-dispose', description: 'manual', content: 'Manual body.', directory: 'memory://manual', source: 'runtime', }) expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['manual-dispose']) dispose() expect(await ctx.skills.list()).toEqual([]) }) it('rejects invalid runtime skill registrations', async () => { const home = await tempDir('skill-runtime-invalid') const ctx = new Context() await ctx.plugin(SkillService, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false }) expect(() => ctx.skills.register({ name: 'Bad_Name', description: 'bad', content: 'bad', directory: 'memory://bad', source: 'runtime', })).toThrow('invalid skill name') expect(() => ctx.skills.register({ name: 'empty-description', description: '', content: 'bad', directory: 'memory://bad', source: 'runtime', })).toThrow('requires a description') }) })