plugins.test.ts 3.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. import fs from 'node:fs'
  2. import os from 'node:os'
  3. import path from 'node:path'
  4. import { beforeAll, describe, expect, it } from 'vitest'
  5. import { resolveSkills } from '../src/providers/claude/skills'
  6. import { AuditService } from '../src/services/catalog'
  7. import { PluginService } from '../src/services/plugins'
  8. import { SettingsService } from '../src/services/settings'
  9. import { createDb, migrateDb } from '../src/db'
  10. describe('resolveSkills', () => {
  11. const plugins = [
  12. { name: 'agent-skills', path: '/a', skills: ['agent-skills:report-conventions'] },
  13. { name: 'document-skills', path: '/b', skills: ['document-skills:xlsx', 'document-skills:docx'] },
  14. ]
  15. it('all 选中全部技能并加载全部插件', () => {
  16. const r = resolveSkills('all', plugins)
  17. expect(r.skills).toHaveLength(3)
  18. expect(r.plugins.map(p => p.path)).toEqual(['/a', '/b'])
  19. })
  20. it('不带前缀的名称指本地技能;只加载被用到的插件', () => {
  21. const r = resolveSkills(['report-conventions'], plugins)
  22. expect(r.skills).toEqual(['agent-skills:report-conventions'])
  23. expect(r.plugins.map(p => p.path)).toEqual(['/a'])
  24. })
  25. it('忽略不存在的技能;空列表不加载任何技能', () => {
  26. expect(resolveSkills(['document-skills:pptx'], plugins).skills).toEqual([])
  27. expect(resolveSkills([], plugins)).toEqual({ plugins: [], skills: [] })
  28. })
  29. })
  30. // 用本地测试市场驱动真实的 `claude plugin` 命令(不联网、不涉及官方技能文件)
  31. describe('PluginService(真实 CLI)', () => {
  32. const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'plugins-'))
  33. const mkt = path.join(tmp, 'mkt')
  34. let svc: PluginService
  35. beforeAll(() => {
  36. process.env.CLAUDE_CONFIG_DIR = path.join(tmp, 'cfg')
  37. fs.mkdirSync(path.join(mkt, '.claude-plugin'), { recursive: true })
  38. fs.mkdirSync(path.join(mkt, 'demo/.claude-plugin'), { recursive: true })
  39. fs.mkdirSync(path.join(mkt, 'demo/skills/hello'), { recursive: true })
  40. fs.writeFileSync(path.join(mkt, '.claude-plugin/marketplace.json'), JSON.stringify({ name: 'test-mkt', owner: { name: 't' }, plugins: [{ name: 'demo', source: './demo', description: '测试插件' }] }))
  41. fs.writeFileSync(path.join(mkt, 'demo/.claude-plugin/plugin.json'), JSON.stringify({ name: 'demo', version: '1.0.0' }))
  42. fs.writeFileSync(path.join(mkt, 'demo/skills/hello/SKILL.md'), '---\nname: hello\ndescription: "打个招呼"\n---\nhi\n')
  43. fs.writeFileSync(path.join(mkt, 'demo/skills/hello/LICENSE.txt'), '© 2025 Anthropic, PBC. All rights reserved.\nUse is governed by your agreement with Anthropic.')
  44. const db = createDb(path.join(tmp, 'db.sqlite'))
  45. migrateDb(db)
  46. svc = new PluginService(new SettingsService(db), new AuditService(db))
  47. })
  48. it('添加市场 → 安装 → 列出技能与许可 → 停用 → 卸载', async () => {
  49. await svc.act({ action: 'marketplace-add', source: mkt })
  50. let s = (await svc.act({ action: 'install', plugin: 'demo@test-mkt' })).state
  51. const p = s.installed.find(x => x.id === 'demo@test-mkt')!
  52. expect(p.enabled).toBe(true)
  53. expect(p.license.kind).toBe('restricted')
  54. expect(p.skills).toEqual([expect.objectContaining({ id: 'demo:hello', description: '打个招呼', license: 'restricted' })])
  55. expect((await svc.loadable()).map(l => l.name)).toEqual(['demo'])
  56. s = (await svc.act({ action: 'disable', plugin: 'demo@test-mkt' })).state
  57. expect(s.installed.find(x => x.id === 'demo@test-mkt')!.enabled).toBe(false)
  58. expect(await svc.loadable()).toEqual([])
  59. s = (await svc.act({ action: 'uninstall', plugin: 'demo@test-mkt' })).state
  60. expect(s.installed).toEqual([])
  61. }, 120_000)
  62. it('失败的操作返回错误信息', async () => {
  63. await expect(svc.act({ action: 'install', plugin: 'nope@nowhere' })).rejects.toThrow(/not found/)
  64. }, 60_000)
  65. it('拒绝可疑参数', async () => {
  66. await expect(svc.act({ action: 'install', plugin: 'x; rm -rf /' })).rejects.toThrow(/格式不正确/)
  67. })
  68. })