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-/) */ function tarball(sha: string, skills: Record): 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 = { [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']) }) })