| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107 |
- import { execFileSync } from 'node:child_process'
- import fs from 'node:fs'
- import os from 'node:os'
- import path from 'node:path'
- import { describe, expect, it } from 'vitest'
- import { skillDisplayId, skillsToDisable } from '../src/providers/codex/skills'
- import { CodexSkillService } from '../src/services/codex-skills'
- const SHA1 = 'a'.repeat(40)
- const SHA2 = 'b'.repeat(40)
- /** 构造一个与 GitHub codeload 格式相同的仓库快照(顶层目录 skills-<sha>/) */
- function tarball(sha: string, skills: Record<string, string>): Buffer {
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'repo-'))
- const top = path.join(root, `skills-${sha}`)
- for (const [name, license] of Object.entries(skills)) {
- const d = path.join(top, 'skills/.curated', name)
- fs.mkdirSync(d, { recursive: true })
- fs.writeFileSync(path.join(d, 'SKILL.md'), `---\nname: "${name}"\ndescription: "${name} skill"\n---\nbody\n`)
- fs.writeFileSync(path.join(d, 'LICENSE.txt'), license)
- }
- fs.mkdirSync(path.join(top, 'skills/.system/imagegen'), { recursive: true })
- fs.writeFileSync(path.join(top, 'README.md'), 'x')
- const out = path.join(root, 'repo.tar.gz')
- execFileSync('tar', ['-czf', out, '-C', root, `skills-${sha}`])
- return fs.readFileSync(out)
- }
- describe('CodexSkillService(openai/skills)', () => {
- const base = fs.mkdtempSync(path.join(os.tmpdir(), 'cxskills-'))
- const roots = { catalog: path.join(base, 'catalog'), skills: path.join(base, 'codex-home/skills') }
- let sha = SHA1
- const repos: Record<string, Buffer> = {
- [SHA1]: tarball(SHA1, { pdf: 'Apache License\nVersion 2.0', secret: 'PROPRIETARY AND CONFIDENTIAL. All rights reserved.', figma: 'Use of these Figma skills is governed by the Figma Developer Terms' }),
- [SHA2]: tarball(SHA2, { pdf: 'Apache License\nVersion 2.0', screenshot: 'MIT License\nPermission is hereby granted, free of charge' }),
- }
- const audits: string[] = []
- const fetcher = async (url: string) => {
- if (url.endsWith('/commits/main')) return new Response(JSON.stringify({ sha }))
- const s = url.split('/').pop()!
- return repos[s] ? new Response(repos[s]) : new Response('nf', { status: 404 })
- }
- const svc = new CodexSkillService(s => audits.push(s), fetcher, roots)
- it('获取目录、识别许可证;安装后写入标记,专有许可拒绝安装', async () => {
- expect(svc.catalog().sha).toBeNull()
- const cat = await svc.refresh()
- expect(cat.sha).toBe(SHA1)
- expect(cat.skills.map(s => [s.name, s.license, s.licenseName])).toEqual([['figma', 'restricted', '第三方条款'], ['pdf', 'open', 'Apache-2.0'], ['secret', 'restricted', '专有许可']])
- expect(() => svc.install('secret')).toThrow(/专有许可/)
- expect(() => svc.install('figma')).toThrow(/第三方条款/)
- const after = svc.install('pdf')
- expect(after.skills.find(s => s.name === 'pdf')).toMatchObject({ installed: true, installedSha: SHA1, updatable: false })
- expect(fs.existsSync(path.join(roots.skills, 'pdf/SKILL.md'))).toBe(true)
- const marker = JSON.parse(fs.readFileSync(path.join(roots.skills, 'pdf/.agent-server.json'), 'utf8'))
- expect(marker).toMatchObject({ source: 'openai/skills', sha: SHA1, license: 'Apache-2.0' })
- expect(svc.installedNames()).toEqual(['pdf'])
- })
- it('目录更新后可以升级已安装技能;只能卸载自己安装的技能', async () => {
- sha = SHA2
- const cat = await svc.refresh()
- expect(cat.skills.find(s => s.name === 'pdf')).toMatchObject({ installed: true, updatable: true })
- expect(svc.install('pdf').skills.find(s => s.name === 'pdf')).toMatchObject({ installedSha: SHA2, updatable: false })
- fs.mkdirSync(path.join(roots.skills, 'mine'), { recursive: true })
- fs.writeFileSync(path.join(roots.skills, 'mine/SKILL.md'), '---\nname: mine\n---\n')
- expect(svc.catalog().unmanaged).toEqual(['mine'])
- expect(() => svc.uninstall('mine')).toThrow()
- expect(() => svc.install('mine')).toThrow()
- svc.uninstall('pdf')
- expect(fs.existsSync(path.join(roots.skills, 'pdf'))).toBe(false)
- expect(audits.some(a => a.includes('安装 Codex 官方技能 pdf'))).toBe(true)
- })
- it('拒绝不安全的名称', () => {
- expect(() => svc.install('../etc')).toThrow(/格式不正确/)
- })
- })
- describe('Codex 技能 ID 与禁用规则', () => {
- const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cxhome-'))
- fs.mkdirSync(path.join(home, 'skills/pdf'), { recursive: true })
- fs.writeFileSync(path.join(home, 'skills/pdf/.agent-server.json'), '{}')
- fs.mkdirSync(path.join(home, 'skills/mine'), { recursive: true })
- const all = [
- { name: 'imagegen', scope: 'system' as const, path: '/sys/imagegen/SKILL.md' },
- { name: 'skill-installer', scope: 'system' as const, path: '/sys/skill-installer/SKILL.md' },
- { name: 'pdf', scope: 'user' as const, path: path.join(home, 'skills/pdf/SKILL.md') },
- { name: 'mine', scope: 'user' as const, path: path.join(home, 'skills/mine/SKILL.md') },
- { name: 'agent-skills:report-conventions', scope: 'user' as const, path: '/srv/skills/report-conventions/SKILL.md' },
- ].map(s => ({ ...s, description: '', enabled: true, pluginId: null }))
- it('展示 ID', () => {
- expect(all.map(s => skillDisplayId(s, home))).toEqual([
- 'codex-system:imagegen', 'codex-system:skill-installer', 'openai-skills:pdf', 'codex-user:mine', 'agent-skills:report-conventions',
- ])
- })
- it('all 只禁用始终禁用的技能;列表模式禁用未选中的技能', () => {
- expect(skillsToDisable(all, 'all', home, 'agent-skills').map(d => d.path)).toEqual(['/sys/skill-installer/SKILL.md'])
- const off = skillsToDisable(all, ['openai-skills:pdf', 'report-conventions'], home, 'agent-skills').map(d => path.basename(path.dirname(d.path)))
- expect(off).toEqual(['imagegen', 'skill-installer', 'mine'])
- })
- })
|