import { mkdtempSync, rmSync, writeFileSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; import { describe, expect, it } from 'vitest'; import { loadSkillRegistryCatalog, parseSkillRegistryCatalog } from './registrySource.js'; describe('parseSkillRegistryCatalog', () => { it('parses a valid registry catalog', () => { const catalog = parseSkillRegistryCatalog({ skills: [ { id: 'todoist', name: 'Todoist', version: '1.2.3', source: 'https://example.com/skills/todoist.git', summary: 'Task manager integration', publisher: 'acme', homepage: 'https://example.com/todoist', }, ], }); expect(catalog.skills).toHaveLength(1); expect(catalog.skills[0]?.id).toBe('todoist'); expect(catalog.skills[0]?.publisher).toBe('acme'); }); it('rejects missing required fields', () => { expect(() => parseSkillRegistryCatalog({ skills: [ { id: 'missing-summary', name: 'Broken', version: '0.1.0', source: 'https://example.com/skills/broken.git', }, ], }), ).toThrow(/summary/); }); it('rejects invalid ids', () => { expect(() => parseSkillRegistryCatalog({ skills: [ { id: 'Not Valid!', name: 'Broken', version: '0.1.0', source: 'https://example.com/skills/broken.git', summary: 'Broken', }, ], }), ).toThrow(/id/); }); }); describe('loadSkillRegistryCatalog', () => { it('loads catalog from a local file path', async () => { const dir = mkdtempSync(join(tmpdir(), 'flynn-registry-test-')); const filePath = join(dir, 'registry.json'); writeFileSync( filePath, JSON.stringify({ skills: [ { id: 'notes', name: 'Notes', version: '0.0.1', source: './skills/notes', summary: 'Local notes skill', }, ], }), 'utf8', ); try { const catalog = await loadSkillRegistryCatalog({ type: 'file', path: filePath }); expect(catalog.skills).toHaveLength(1); expect(catalog.skills[0]?.source).toBe('./skills/notes'); } finally { rmSync(dir, { recursive: true, force: true }); } }); it('loads catalog from HTTPS URL with injected fetch', async () => { const catalog = await loadSkillRegistryCatalog( { type: 'url', url: 'https://registry.example.com/catalog.json' }, { fetchImpl: async () => ({ ok: true, status: 200, json: async () => ({ skills: [ { id: 'calendar', name: 'Calendar', version: '2.0.0', source: 'https://example.com/skills/calendar.git', summary: 'Calendar integration', }, ], }), }) as Response, }, ); expect(catalog.skills).toHaveLength(1); expect(catalog.skills[0]?.id).toBe('calendar'); }); it('rejects non-HTTPS URL sources', async () => { await expect( loadSkillRegistryCatalog( { type: 'url', url: 'http://registry.example.com/catalog.json' }, { fetchImpl: async () => ({ ok: true, status: 200, json: async () => ({ skills: [] }), }) as Response, }, ), ).rejects.toThrow(/https/); }); it('propagates HTTP fetch failures with status', async () => { await expect( loadSkillRegistryCatalog( { type: 'url', url: 'https://registry.example.com/catalog.json' }, { fetchImpl: async () => ({ ok: false, status: 502, json: async () => ({}), }) as Response, }, ), ).rejects.toThrow(/502/); }); });