|
|
@@ -10,18 +10,18 @@ import { createDb, migrateDb } from '../src/db'
|
|
|
|
|
|
describe('resolveSkills', () => {
|
|
|
const plugins = [
|
|
|
- { name: 'agent-skills', path: '/a', skills: ['agent-skills:report-conventions'] },
|
|
|
- { name: 'document-skills', path: '/b', skills: ['document-skills:xlsx', 'document-skills:docx'] },
|
|
|
+ { name: 'agent-skills', path: '/a', skills: [{ id: 'agent-skills:report-conventions', loadId: 'agent-skills:report-conventions' }] },
|
|
|
+ // 官方仓库按路径加载时,CLI 以目录名作为前缀
|
|
|
+ { name: 'dbd4588f9e10', path: '/b', skills: [{ id: 'document-skills:xlsx', loadId: 'dbd4588f9e10:xlsx' }, { id: 'document-skills:docx', loadId: 'dbd4588f9e10:docx' }] },
|
|
|
]
|
|
|
- it('all 选中全部技能并加载全部插件', () => {
|
|
|
+ it('all 选中全部技能并加载全部插件,输出 CLI 使用的 ID', () => {
|
|
|
const r = resolveSkills('all', plugins)
|
|
|
- expect(r.skills).toHaveLength(3)
|
|
|
+ expect(r.skills).toEqual(['agent-skills:report-conventions', 'dbd4588f9e10:xlsx', 'dbd4588f9e10:docx'])
|
|
|
expect(r.plugins.map(p => p.path)).toEqual(['/a', '/b'])
|
|
|
})
|
|
|
- it('不带前缀的名称指本地技能;只加载被用到的插件', () => {
|
|
|
- const r = resolveSkills(['report-conventions'], plugins)
|
|
|
- expect(r.skills).toEqual(['agent-skills:report-conventions'])
|
|
|
- expect(r.plugins.map(p => p.path)).toEqual(['/a'])
|
|
|
+ it('按展示 ID 选择;不带前缀的名称指本地技能;只加载被用到的插件', () => {
|
|
|
+ expect(resolveSkills(['report-conventions'], plugins)).toEqual({ plugins: [{ type: 'local', path: '/a' }], skills: ['agent-skills:report-conventions'] })
|
|
|
+ expect(resolveSkills(['document-skills:xlsx'], plugins)).toEqual({ plugins: [{ type: 'local', path: '/b' }], skills: ['dbd4588f9e10:xlsx'] })
|
|
|
})
|
|
|
it('忽略不存在的技能;空列表不加载任何技能', () => {
|
|
|
expect(resolveSkills(['document-skills:pptx'], plugins).skills).toEqual([])
|
|
|
@@ -43,7 +43,7 @@ describe('PluginService(真实 CLI)', () => {
|
|
|
fs.writeFileSync(path.join(mkt, '.claude-plugin/marketplace.json'), JSON.stringify({ name: 'test-mkt', owner: { name: 't' }, plugins: [{ name: 'demo', source: './demo', description: '测试插件' }] }))
|
|
|
fs.writeFileSync(path.join(mkt, 'demo/.claude-plugin/plugin.json'), JSON.stringify({ name: 'demo', version: '1.0.0' }))
|
|
|
fs.writeFileSync(path.join(mkt, 'demo/skills/hello/SKILL.md'), '---\nname: hello\ndescription: "打个招呼"\n---\nhi\n')
|
|
|
- fs.writeFileSync(path.join(mkt, 'demo/skills/hello/LICENSE.txt'), '© 2025 Anthropic, PBC. All rights reserved.\nUse is governed by your agreement with Anthropic.')
|
|
|
+ fs.writeFileSync(path.join(mkt, 'demo/skills/hello/LICENSE.txt'), 'LICENSE: Use is governed by your agreement with\nAnthropic regarding use of the services.')
|
|
|
const db = createDb(path.join(tmp, 'db.sqlite'))
|
|
|
migrateDb(db)
|
|
|
svc = new PluginService(new SettingsService(db), new AuditService(db))
|
|
|
@@ -57,6 +57,7 @@ describe('PluginService(真实 CLI)', () => {
|
|
|
expect(p.license.kind).toBe('restricted')
|
|
|
expect(p.skills).toEqual([expect.objectContaining({ id: 'demo:hello', description: '打个招呼', license: 'restricted' })])
|
|
|
expect((await svc.loadable()).map(l => l.name)).toEqual(['demo'])
|
|
|
+ expect((await svc.loadable())[0]!.skills).toEqual([{ id: 'demo:hello', loadId: 'demo:hello' }])
|
|
|
|
|
|
s = (await svc.act({ action: 'disable', plugin: 'demo@test-mkt' })).state
|
|
|
expect(s.installed.find(x => x.id === 'demo@test-mkt')!.enabled).toBe(false)
|
|
|
@@ -66,6 +67,30 @@ describe('PluginService(真实 CLI)', () => {
|
|
|
expect(s.installed).toEqual([])
|
|
|
}, 120_000)
|
|
|
|
|
|
+ it('官方仓库式布局:多个插件共用目录、没有 plugin.json、只声明部分技能', async () => {
|
|
|
+ const repo = path.join(tmp, 'repo')
|
|
|
+ for (const k of ['xlsx', 'docx', 'art']) {
|
|
|
+ fs.mkdirSync(path.join(repo, 'skills', k), { recursive: true })
|
|
|
+ fs.writeFileSync(path.join(repo, 'skills', k, 'SKILL.md'), `---\nname: ${k}\ndescription: ${k}\n---\n`)
|
|
|
+ }
|
|
|
+ fs.mkdirSync(path.join(repo, '.claude-plugin'), { recursive: true })
|
|
|
+ fs.writeFileSync(path.join(repo, '.claude-plugin/marketplace.json'), JSON.stringify({
|
|
|
+ name: 'repo-mkt', owner: { name: 't' },
|
|
|
+ plugins: [
|
|
|
+ { name: 'doc-skills', source: './', strict: false, skills: ['./skills/xlsx', './skills/docx'] },
|
|
|
+ { name: 'example', source: './', strict: false, skills: ['./skills/art'] },
|
|
|
+ ],
|
|
|
+ }))
|
|
|
+ await svc.act({ action: 'marketplace-add', source: repo })
|
|
|
+ const s = (await svc.act({ action: 'install', plugin: 'doc-skills@repo-mkt' })).state
|
|
|
+ const p = s.installed.find(x => x.id === 'doc-skills@repo-mkt')!
|
|
|
+ expect(p.skills.map(k => k.id)).toEqual(['doc-skills:docx', 'doc-skills:xlsx'])
|
|
|
+ const l = (await svc.loadable()).find(x => x.path === p.installPath)!
|
|
|
+ expect(l.name).toBe(path.basename(p.installPath))
|
|
|
+ expect(l.skills.map(k => k.loadId)).toEqual([`${l.name}:docx`, `${l.name}:xlsx`])
|
|
|
+ await svc.act({ action: 'uninstall', plugin: 'doc-skills@repo-mkt' })
|
|
|
+ }, 120_000)
|
|
|
+
|
|
|
it('失败的操作返回错误信息', async () => {
|
|
|
await expect(svc.act({ action: 'install', plugin: 'nope@nowhere' })).rejects.toThrow(/not found/)
|
|
|
}, 60_000)
|