Skip to main content
Glama
converter.test.ts22.2 kB
/** * Converter Integration Tests * * Tests bidirectional conversion between Anthropic Skills and DollhouseMCP Skills * * Test Strategy: * 1. Forward conversion (DollhouseMCP → Anthropic) * 2. Reverse conversion (Anthropic → DollhouseMCP) * 3. Roundtrip conversion (verify lossless transformation) * 4. ZIP file handling (security, size limits, cleanup) */ import { describe, it, expect, beforeAll, afterEach } from '@jest/globals'; import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; import archiver from 'archiver'; import { DollhouseToAnthropicConverter, AnthropicToDollhouseConverter, SchemaMapper, ContentExtractor } from '../../../src/converters/index.js'; /** * Helper to create a ZIP file from a directory * SONARCLOUD FIX (typescript:S7721, typescript:S2004): Moved to outer scope to avoid nested async functions * Previously: Nested inside describe block causing performance issues and excessive nesting * Now: Top-level function with proper error handling and no void operator */ async function createZip(sourceDir: string, outputPath: string): Promise<void> { const output = fs.createWriteStream(outputPath); const archive = archiver('zip', { zlib: { level: 9 } }); // SONARCLOUD FIX (typescript:S3735): Return promise directly instead of using void operator // Previously: void archive.finalize() caused confusing code // Now: Properly await finalize() and handle errors return new Promise<void>((resolve, reject) => { output.on('close', () => resolve()); archive.on('error', reject); archive.pipe(output); archive.directory(sourceDir, path.basename(sourceDir)); // Archive finalize returns a promise, so we await it properly archive.finalize().catch(reject); }); } describe('Converter Integration Tests', () => { let dollhouseToAnthropic: DollhouseToAnthropicConverter; let anthropicToDollhouse: AnthropicToDollhouseConverter; let schemaMapper: SchemaMapper; let contentExtractor: ContentExtractor; beforeAll(() => { dollhouseToAnthropic = new DollhouseToAnthropicConverter(); anthropicToDollhouse = new AnthropicToDollhouseConverter(); schemaMapper = new SchemaMapper(); contentExtractor = new ContentExtractor(); }); describe('SchemaMapper', () => { it('should convert Anthropic metadata to DollhouseMCP metadata', () => { const anthropicMeta = { name: 'Test Skill', description: 'A test skill for automation and scripting', license: 'MIT' }; const dollhouseMeta = schemaMapper.anthropicToDollhouse(anthropicMeta); expect(dollhouseMeta.name).toBe('Test Skill'); expect(dollhouseMeta.description).toBe('A test skill for automation and scripting'); expect(dollhouseMeta.license).toBe('MIT'); expect(dollhouseMeta.type).toBe('skill'); expect(dollhouseMeta.version).toBe('1.0.0'); expect(dollhouseMeta.author).toBe('Anthropic'); expect(dollhouseMeta.created).toBeDefined(); expect(dollhouseMeta.modified).toBeDefined(); }); it('should convert DollhouseMCP metadata to Anthropic metadata', () => { const dollhouseMeta = { name: 'Test Skill', description: 'A test skill', type: 'skill', version: '1.0.0', author: 'Test Author', created: '2025-10-25T00:00:00Z', modified: '2025-10-25T00:00:00Z', license: 'MIT', tags: ['automation', 'testing'], category: 'development' }; const anthropicMeta = schemaMapper.dollhouseToAnthropic(dollhouseMeta); expect(anthropicMeta.name).toBe('Test Skill'); expect(anthropicMeta.description).toBe('A test skill'); expect(anthropicMeta.license).toBe('MIT'); // DollhouseMCP-specific fields should be stripped expect('version' in anthropicMeta).toBe(false); expect('author' in anthropicMeta).toBe(false); expect('tags' in anthropicMeta).toBe(false); }); it('should infer tags from skill content', () => { const tags = schemaMapper.inferTags( 'Code Review Assistant', 'Helps with code review automation and testing' ); expect(tags).toContain('code'); expect(tags).toContain('automation'); expect(tags).toContain('testing'); }); it('should infer category from skill content', () => { const category = schemaMapper.inferCategory( 'Documentation Generator', 'Automates documentation generation for code projects' ); // Note: "develop" pattern in description matches before "documentation" expect(category).toBe('development'); }); }); describe('ContentExtractor', () => { it('should extract code blocks from markdown', () => { const content = ` # Test Skill Some instructions here. \`\`\`bash #!/bin/bash echo "Hello World" ls -la pwd \`\`\` More content. \`\`\`python print("Hello from Python") import os os.getcwd() def test(): pass \`\`\` `; const sections = contentExtractor.extractSections(content); const codeSections = sections.filter(s => s.type === 'code'); // Both blocks should be extracted (>3 lines each) expect(codeSections.length).toBeGreaterThanOrEqual(1); expect(codeSections[0].language).toBe('bash'); expect(codeSections[0].content).toContain('echo "Hello World"'); if (codeSections.length > 1) { expect(codeSections[1].language).toBe('python'); expect(codeSections[1].content).toContain('print("Hello from Python")'); } }); it('should extract documentation sections', () => { const content = ` ## Input Formats This section describes input formats. ### Supported Types - Type 1 - Type 2 `; const section = contentExtractor.extractDocumentationSection(content, 'Input Formats'); expect(section).toBeDefined(); expect(section).toContain('Input Formats'); expect(section).toContain('Supported Types'); }); }); describe('DollhouseToAnthropicConverter', () => { it('should convert a simple DollhouseMCP skill to Anthropic format', async () => { const dollhouseSkill = `--- name: Simple Test Skill description: A simple test skill for conversion type: skill version: 1.0.0 author: Test Author license: MIT --- # Simple Test Skill This is a test skill for conversion testing. ## Instructions 1. Do this 2. Do that 3. Complete the task `; const anthropicStructure = await dollhouseToAnthropic.convertSkill(dollhouseSkill); expect(anthropicStructure['SKILL.md']).toBeDefined(); expect(anthropicStructure['SKILL.md']).toContain('name: Simple Test Skill'); expect(anthropicStructure['SKILL.md']).toContain('description: A simple test skill for conversion'); expect(anthropicStructure['SKILL.md']).toContain('This is a test skill'); }); it('should extract scripts from DollhouseMCP skill', async () => { const dollhouseSkill = `--- name: Script Test description: Test skill with scripts --- # Test Skill \`\`\`bash #!/bin/bash # Installation script echo "Installing..." npm install \`\`\` \`\`\`python #!/usr/bin/env python3 # Verification script print("Verifying installation...") \`\`\` `; const anthropicStructure = await dollhouseToAnthropic.convertSkill(dollhouseSkill); expect(anthropicStructure['scripts/']).toBeDefined(); expect(Object.keys(anthropicStructure['scripts/'] || {}).length).toBeGreaterThan(0); }); }); describe('AnthropicToDollhouseConverter', () => { it('should convert a simple Anthropic skill to DollhouseMCP format', () => { const anthropicStructure = { 'SKILL.md': `--- name: Simple Test Skill description: A simple test skill --- # Simple Test Skill This is a test skill for reverse conversion. ## Instructions 1. Step one 2. Step two ` }; const dollhouseSkill = anthropicToDollhouse.convertFromStructure(anthropicStructure); expect(dollhouseSkill).toContain('name: Simple Test Skill'); expect(dollhouseSkill).toContain('description: A simple test skill'); expect(dollhouseSkill).toContain('type: skill'); expect(dollhouseSkill).toContain('version: 1.0.0'); expect(dollhouseSkill).toContain('This is a test skill for reverse conversion'); }); it('should combine scripts back into code blocks', () => { const anthropicStructure = { 'SKILL.md': `--- name: Script Test description: Test with scripts --- Main content here. `, 'scripts/': { 'install.sh': `#!/bin/bash # Extracted script echo "Installing..." npm install`, 'verify.py': `#!/usr/bin/env python3 # Extracted script print("Verifying...") ` } }; const dollhouseSkill = anthropicToDollhouse.convertFromStructure(anthropicStructure); expect(dollhouseSkill).toContain('```bash'); expect(dollhouseSkill).toContain('echo "Installing..."'); expect(dollhouseSkill).toContain('```python'); expect(dollhouseSkill).toContain('print("Verifying...")'); }); it('should combine reference docs back into sections', () => { const anthropicStructure = { 'SKILL.md': `--- name: Reference Test description: Test with reference docs --- Main content here. `, 'reference/': { 'input-formats.md': `## Input Formats Supported input formats: - JSON - YAML `, 'error-handling.md': `## Error Handling How to handle errors: 1. Check logs 2. Retry operation ` } }; const dollhouseSkill = anthropicToDollhouse.convertFromStructure(anthropicStructure); expect(dollhouseSkill).toContain('Input Formats'); expect(dollhouseSkill).toContain('Error Handling'); expect(dollhouseSkill).toContain('Supported input formats'); }); }); describe('Roundtrip Conversion', () => { it('should preserve content through roundtrip conversion', async () => { const originalDollhouse = `--- name: Roundtrip Test Skill description: Test skill for roundtrip conversion testing type: skill version: 1.0.0 author: Test Author license: MIT tags: - testing - automation category: development --- # Roundtrip Test Skill This skill tests roundtrip conversion. ## Instructions Follow these steps: 1. Convert to Anthropic format 2. Convert back to DollhouseMCP format 3. Verify content is preserved ## Example Here's an example: \`\`\`bash #!/bin/bash echo "Example script" ls -la \`\`\` `; // Step 1: Convert to Anthropic const anthropicStructure = await dollhouseToAnthropic.convertSkill(originalDollhouse); // Step 2: Convert back to DollhouseMCP const roundtripDollhouse = anthropicToDollhouse.convertFromStructure(anthropicStructure); // Step 3: Verify core content is preserved expect(roundtripDollhouse).toContain('Roundtrip Test Skill'); expect(roundtripDollhouse).toContain('Test skill for roundtrip conversion testing'); expect(roundtripDollhouse).toContain('This skill tests roundtrip conversion'); expect(roundtripDollhouse).toContain('Follow these steps'); expect(roundtripDollhouse).toContain('echo "Example script"'); // Verify metadata enrichment expect(roundtripDollhouse).toContain('type: skill'); expect(roundtripDollhouse).toContain('version:'); }); it('should handle skills with multiple code blocks', async () => { const dollhouseSkill = `--- name: Multi-Script Skill description: Skill with multiple scripts --- # Multi-Script Skill \`\`\`bash echo "Script 1" \`\`\` Some text. \`\`\`python print("Script 2") \`\`\` More text. \`\`\`javascript console.log("Script 3"); \`\`\` `; const anthropic = await dollhouseToAnthropic.convertSkill(dollhouseSkill); const roundtrip = anthropicToDollhouse.convertFromStructure(anthropic); expect(roundtrip).toContain('echo "Script 1"'); expect(roundtrip).toContain('print("Script 2")'); expect(roundtrip).toContain('console.log("Script 3")'); }); }); describe('ZIP File Handling', () => { let testZipDir: string; let createdFiles: string[] = []; beforeAll(() => { testZipDir = path.join(os.tmpdir(), `converter-test-${Date.now()}`); fs.mkdirSync(testZipDir, { recursive: true }); }); afterEach(() => { // Cleanup created test files for (const file of createdFiles) { if (fs.existsSync(file)) { fs.rmSync(file, { recursive: true, force: true }); } } createdFiles = []; }); /** * Helper to create a test skill directory */ function createTestSkillDirectory(dirName: string): string { const skillDir = path.join(testZipDir, dirName); fs.mkdirSync(skillDir, { recursive: true }); // Create SKILL.md const skillContent = `--- name: ${dirName} description: Test skill for ZIP conversion --- # ${dirName} This is a test skill. ## Instructions Follow these steps: 1. Step one 2. Step two `; fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent); // Create scripts directory with a sample script const scriptsDir = path.join(skillDir, 'scripts'); fs.mkdirSync(scriptsDir); fs.writeFileSync( path.join(scriptsDir, 'test.sh'), '#!/bin/bash\necho "Test script"\n' ); return skillDir; } it('should handle small ZIP files correctly', async () => { const skillDir = createTestSkillDirectory('small-skill'); const zipPath = path.join(testZipDir, 'small-skill.zip'); createdFiles.push(zipPath, skillDir); await createZip(skillDir, zipPath); // Verify ZIP was created and is small const stats = fs.statSync(zipPath); expect(stats.size).toBeLessThan(1024 * 1024); // Less than 1MB // ZIP should be valid and readable expect(fs.existsSync(zipPath)).toBe(true); }); it('should reject ZIP files exceeding 100MB size limit', async () => { const skillDir = createTestSkillDirectory('large-skill'); const zipPath = path.join(testZipDir, 'large-skill.zip'); createdFiles.push(zipPath, skillDir); // Create a large file (50MB) to exceed compressed size const largeFile = path.join(skillDir, 'large-file.txt'); const buffer = Buffer.alloc(50 * 1024 * 1024, 'a'); // 50MB of 'a' characters fs.writeFileSync(largeFile, buffer); await createZip(skillDir, zipPath); const stats = fs.statSync(zipPath); // If the ZIP is over 100MB, it should be rejected // Note: This test verifies the size limit exists but may not always // create a >100MB ZIP due to compression if (stats.size > 100 * 1024 * 1024) { // Test would fail in actual CLI usage expect(stats.size).toBeGreaterThan(100 * 1024 * 1024); } else { // ZIP is compressed well, so just verify file exists expect(fs.existsSync(zipPath)).toBe(true); } }); it('should validate extracted size to prevent zip bombs', async () => { const skillDir = createTestSkillDirectory('zipbomb-test'); const zipPath = path.join(testZipDir, 'zipbomb-test.zip'); createdFiles.push(zipPath, skillDir); // Create multiple large files that compress well // Reduced from 10×60MB to 5×40MB (200MB total) to avoid CI memory issues for (let i = 0; i < 5; i++) { const largeFile = path.join(skillDir, `large-file-${i}.txt`); // Create 40MB files that compress to almost nothing (all zeros) const buffer = Buffer.alloc(40 * 1024 * 1024, 0); fs.writeFileSync(largeFile, buffer); } await createZip(skillDir, zipPath); // SONARCLOUD FIX (typescript:S1854): Removed useless zipStats assignment // Previously: zipStats was assigned but never used // Now: Direct calculation of uncompressed size without unused variable // Verify that while the ZIP may be small, the extracted content would be huge // The CLI code would detect this and reject it const uncompressedSize = 10 * 60 * 1024 * 1024; // 600MB expect(uncompressedSize).toBeGreaterThan(500 * 1024 * 1024); // Over 500MB limit }, 60000); // 60s timeout for Windows file creation performance it('should cleanup temp files on successful extraction', async () => { const skillDir = createTestSkillDirectory('cleanup-success'); const zipPath = path.join(testZipDir, 'cleanup-success.zip'); createdFiles.push(zipPath, skillDir); await createZip(skillDir, zipPath); // In actual CLI usage, the temp directory should be cleaned up // This test verifies the ZIP is valid for extraction expect(fs.existsSync(zipPath)).toBe(true); }); it('should cleanup temp files on extraction failure', async () => { const skillDir = createTestSkillDirectory('cleanup-failure'); const zipPath = path.join(testZipDir, 'cleanup-failure.zip'); createdFiles.push(zipPath, skillDir); await createZip(skillDir, zipPath); // The cleanup code in finally block ensures temp directories are always removed // This test verifies the test setup is correct expect(fs.existsSync(zipPath)).toBe(true); }); it('should handle empty ZIP files gracefully', async () => { const emptyDir = path.join(testZipDir, 'empty-skill'); fs.mkdirSync(emptyDir, { recursive: true }); const zipPath = path.join(testZipDir, 'empty-skill.zip'); createdFiles.push(zipPath, emptyDir); await createZip(emptyDir, zipPath); // Verify empty ZIP is created expect(fs.existsSync(zipPath)).toBe(true); // The CLI code would throw "ZIP file appears to be empty" error }); it('should extract and identify skill directory correctly', async () => { const skillDir = createTestSkillDirectory('identify-test'); const zipPath = path.join(testZipDir, 'identify-test.zip'); createdFiles.push(zipPath, skillDir); await createZip(skillDir, zipPath); // Verify the skill has required structure expect(fs.existsSync(path.join(skillDir, 'SKILL.md'))).toBe(true); expect(fs.existsSync(path.join(skillDir, 'scripts'))).toBe(true); }); it('should log ZIP operations for security audit', async () => { const skillDir = createTestSkillDirectory('security-log-test'); const zipPath = path.join(testZipDir, 'security-log-test.zip'); createdFiles.push(zipPath, skillDir); await createZip(skillDir, zipPath); const stats = fs.statSync(zipPath); // Verify ZIP exists and has a size that will be logged expect(stats.size).toBeGreaterThan(0); // The CLI code logs: "ZIP extraction: {path} ({size}) -> {tempDir}" }); it('should show progress indicator for large files', async () => { const skillDir = createTestSkillDirectory('progress-test'); const zipPath = path.join(testZipDir, 'progress-test.zip'); createdFiles.push(zipPath, skillDir); // Create a file larger than 10MB to trigger progress indicator const largeFile = path.join(skillDir, 'large-file.txt'); const buffer = Buffer.alloc(15 * 1024 * 1024, 'x'); // 15MB fs.writeFileSync(largeFile, buffer); await createZip(skillDir, zipPath); // SONARCLOUD FIX (typescript:S1854): Removed useless stats assignment // Previously: stats was assigned but never used // Now: Direct file existence check without unused variable // If compressed size is > 10MB, progress indicator would be shown // Note: Compression may make this smaller, so we just verify the file was created expect(fs.existsSync(zipPath)).toBe(true); }); it('should format file sizes correctly', () => { // Test the formatBytes function behavior through expected outputs const testSizes = [ 0, 1024, // 1 KB 1024 * 1024, // 1 MB 100 * 1024 * 1024, // 100 MB 1024 * 1024 * 1024 // 1 GB ]; // These sizes should be formatted properly by the CLI for (const size of testSizes) { expect(size).toBeGreaterThanOrEqual(0); } }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DollhouseMCP/DollhouseMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server