208 lines
6.1 KiB
TypeScript
208 lines
6.1 KiB
TypeScript
|
|
import { describe, it, expect } from 'vitest';
|
||
|
|
import {
|
||
|
|
ProfileRegistry,
|
||
|
|
defaultRegistry,
|
||
|
|
profileTemplateSchema,
|
||
|
|
validateTemplate,
|
||
|
|
getMissingEnvVars,
|
||
|
|
instantiateProfile,
|
||
|
|
generateMcpJsonEntry,
|
||
|
|
filesystemTemplate,
|
||
|
|
githubTemplate,
|
||
|
|
postgresTemplate,
|
||
|
|
slackTemplate,
|
||
|
|
memoryTemplate,
|
||
|
|
fetchTemplate,
|
||
|
|
} from '../src/profiles/index.js';
|
||
|
|
|
||
|
|
const allTemplates = [
|
||
|
|
filesystemTemplate,
|
||
|
|
githubTemplate,
|
||
|
|
postgresTemplate,
|
||
|
|
slackTemplate,
|
||
|
|
memoryTemplate,
|
||
|
|
fetchTemplate,
|
||
|
|
];
|
||
|
|
|
||
|
|
describe('ProfileTemplate schema', () => {
|
||
|
|
it.each(allTemplates)('validates $id template', (template) => {
|
||
|
|
const result = profileTemplateSchema.safeParse(template);
|
||
|
|
expect(result.success).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('rejects template with missing required fields', () => {
|
||
|
|
const result = profileTemplateSchema.safeParse({ id: 'x' });
|
||
|
|
expect(result.success).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('rejects template with invalid id format', () => {
|
||
|
|
const result = profileTemplateSchema.safeParse({
|
||
|
|
...filesystemTemplate,
|
||
|
|
id: 'Invalid ID!',
|
||
|
|
});
|
||
|
|
expect(result.success).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('rejects template with invalid category', () => {
|
||
|
|
const result = profileTemplateSchema.safeParse({
|
||
|
|
...filesystemTemplate,
|
||
|
|
category: 'nonexistent',
|
||
|
|
});
|
||
|
|
expect(result.success).toBe(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('ProfileRegistry', () => {
|
||
|
|
it('default registry contains all builtin templates', () => {
|
||
|
|
const ids = defaultRegistry.getAll().map((t) => t.id);
|
||
|
|
expect(ids).toContain('filesystem');
|
||
|
|
expect(ids).toContain('github');
|
||
|
|
expect(ids).toContain('postgres');
|
||
|
|
expect(ids).toContain('slack');
|
||
|
|
expect(ids).toContain('memory');
|
||
|
|
expect(ids).toContain('fetch');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('has no duplicate IDs', () => {
|
||
|
|
const ids = defaultRegistry.getAll().map((t) => t.id);
|
||
|
|
expect(new Set(ids).size).toBe(ids.length);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('getById returns correct template', () => {
|
||
|
|
expect(defaultRegistry.getById('github')).toBe(githubTemplate);
|
||
|
|
expect(defaultRegistry.getById('nonexistent')).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('getByCategory filters correctly', () => {
|
||
|
|
const integrations = defaultRegistry.getByCategory('integration');
|
||
|
|
expect(integrations.map((t) => t.id)).toEqual(
|
||
|
|
expect.arrayContaining(['github', 'slack']),
|
||
|
|
);
|
||
|
|
for (const t of integrations) {
|
||
|
|
expect(t.category).toBe('integration');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('getCategories returns unique categories', () => {
|
||
|
|
const cats = defaultRegistry.getCategories();
|
||
|
|
expect(cats.length).toBeGreaterThan(0);
|
||
|
|
expect(new Set(cats).size).toBe(cats.length);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('search finds by name', () => {
|
||
|
|
const results = defaultRegistry.search('git');
|
||
|
|
expect(results.some((t) => t.id === 'github')).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('search finds by description', () => {
|
||
|
|
const results = defaultRegistry.search('knowledge graph');
|
||
|
|
expect(results.some((t) => t.id === 'memory')).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('search returns empty for no match', () => {
|
||
|
|
expect(defaultRegistry.search('zzzznotfound')).toEqual([]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('register adds a custom template', () => {
|
||
|
|
const registry = new ProfileRegistry([]);
|
||
|
|
registry.register(filesystemTemplate);
|
||
|
|
expect(registry.has('filesystem')).toBe(true);
|
||
|
|
expect(registry.getAll()).toHaveLength(1);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('validateTemplate', () => {
|
||
|
|
it('returns success for valid template', () => {
|
||
|
|
const result = validateTemplate(filesystemTemplate);
|
||
|
|
expect(result.success).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns errors for invalid template', () => {
|
||
|
|
const result = validateTemplate({ id: '' });
|
||
|
|
expect(result.success).toBe(false);
|
||
|
|
if (!result.success) {
|
||
|
|
expect(result.errors.length).toBeGreaterThan(0);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('getMissingEnvVars', () => {
|
||
|
|
it('returns empty for template with no required env vars', () => {
|
||
|
|
expect(getMissingEnvVars(filesystemTemplate, {})).toEqual([]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns missing vars for github template', () => {
|
||
|
|
const missing = getMissingEnvVars(githubTemplate, {});
|
||
|
|
expect(missing).toContain('GITHUB_PERSONAL_ACCESS_TOKEN');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns empty when all vars provided', () => {
|
||
|
|
const missing = getMissingEnvVars(githubTemplate, {
|
||
|
|
GITHUB_PERSONAL_ACCESS_TOKEN: 'ghp_xxx',
|
||
|
|
});
|
||
|
|
expect(missing).toEqual([]);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('instantiateProfile', () => {
|
||
|
|
it('creates profile from template without env vars', () => {
|
||
|
|
const profile = instantiateProfile(filesystemTemplate, {});
|
||
|
|
expect(profile.name).toBe('filesystem');
|
||
|
|
expect(profile.templateId).toBe('filesystem');
|
||
|
|
expect(profile.command).toBe('npx');
|
||
|
|
expect(profile.args).toEqual(['-y', '@modelcontextprotocol/server-filesystem']);
|
||
|
|
expect(profile.env).toEqual({});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('creates profile with env vars', () => {
|
||
|
|
const profile = instantiateProfile(githubTemplate, {
|
||
|
|
GITHUB_PERSONAL_ACCESS_TOKEN: 'ghp_test123',
|
||
|
|
});
|
||
|
|
expect(profile.env.GITHUB_PERSONAL_ACCESS_TOKEN).toBe('ghp_test123');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('throws on missing required env vars', () => {
|
||
|
|
expect(() => instantiateProfile(githubTemplate, {})).toThrow(
|
||
|
|
'Missing required environment variables',
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('includes optional env vars when provided', () => {
|
||
|
|
const profile = instantiateProfile(filesystemTemplate, {
|
||
|
|
SOME_OPTIONAL: 'value',
|
||
|
|
});
|
||
|
|
// Optional vars not in template are not included
|
||
|
|
expect(profile.env).toEqual({});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('generateMcpJsonEntry', () => {
|
||
|
|
it('generates valid .mcp.json entry', () => {
|
||
|
|
const profile = instantiateProfile(githubTemplate, {
|
||
|
|
GITHUB_PERSONAL_ACCESS_TOKEN: 'ghp_abc',
|
||
|
|
});
|
||
|
|
const entry = generateMcpJsonEntry(profile);
|
||
|
|
expect(entry).toEqual({
|
||
|
|
github: {
|
||
|
|
command: 'npx',
|
||
|
|
args: ['-y', '@modelcontextprotocol/server-github'],
|
||
|
|
env: {
|
||
|
|
GITHUB_PERSONAL_ACCESS_TOKEN: 'ghp_abc',
|
||
|
|
},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('generates entry with empty env for no-env template', () => {
|
||
|
|
const profile = instantiateProfile(memoryTemplate, {});
|
||
|
|
const entry = generateMcpJsonEntry(profile);
|
||
|
|
expect(entry).toEqual({
|
||
|
|
memory: {
|
||
|
|
command: 'npx',
|
||
|
|
args: ['-y', '@modelcontextprotocol/server-memory'],
|
||
|
|
env: {},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|