Files
flynn/src/skills/registrySource.test.ts
T
2026-02-16 00:10:41 -08:00

149 lines
4.0 KiB
TypeScript

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/);
});
});