Skip to main content
Glama

DollhouseMCP

by DollhouseMCP
migrate-test-metadata.test.tsβ€’24.2 kB
/** * Tests for the migration script: migrate-test-metadata.ts * Tests dry-run mode, rollback functionality, and edge cases */ import { jest } from '@jest/globals'; import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; // Note: Due to module path restrictions, we import functions individually // import { addTestMetadata, removeTestMetadata, isTestFile } from '../../../../scripts/migrate-test-metadata.js'; // For now, we'll test the functions by copying their implementations // This is a temporary approach until the migration script is modularized /** * Determines the test suite category based on file path */ function determineTestSuite(filePath: string): string { if (filePath.includes('test/fixtures')) return 'test-fixtures'; if (filePath.includes('test-elements')) return 'roundtrip-testing'; if (filePath.includes('data/')) return 'bundled-test-data'; if (filePath.includes('test/__tests__')) return 'integration-testing'; return 'unit-testing'; } /** * Determines the purpose of the test file based on filename and content */ function determineTestPurpose(filePath: string, content: string): string { const filename = path.basename(filePath, '.md'); // Check filename patterns if (filename.includes('sample-')) return 'Test fixture for workflow validation'; if (filename.includes('edge-case')) return 'Edge case testing for robustness validation'; if (filename.includes('invalid')) return 'Invalid data testing for error handling'; if (filename.includes('roundtrip')) return 'End-to-end roundtrip workflow testing'; // Check content patterns if (content.includes('test persona') || content.includes('test fixture')) { return 'Test persona for automated testing'; } if (content.includes('edge case') || content.includes('unicode') || content.includes('special character')) { return 'Edge case validation testing'; } if (content.includes('roundtrip') || content.includes('workflow')) { return 'Workflow integration testing'; } // Check element type from content if (content.includes('type: persona') || content.includes('Type: persona')) { return 'Test persona for behavior validation'; } if (content.includes('type: skill') || content.includes('Type: skill')) { return 'Test skill for capability validation'; } if (content.includes('type: agent') || content.includes('Type: agent')) { return 'Test agent for autonomous behavior validation'; } if (content.includes('type: template') || content.includes('Type: template')) { return 'Test template for formatting validation'; } if (content.includes('type: ensemble') || content.includes('Type: ensemble')) { return 'Test ensemble for orchestration validation'; } if (content.includes('type: memory') || content.includes('Type: memory')) { return 'Test memory for state persistence validation'; } return 'General test data for DollhouseMCP system validation'; } function addTestMetadata(content: string, filePath: string): string { const suite = determineTestSuite(filePath); const purpose = determineTestPurpose(filePath, content); // Extract relative path (simulate path.relative behavior for tests) const relativePath = filePath.replace(/^\/+/, ''); const testMetadataBlock = [ '_dollhouseMCPTest: true', '_testMetadata:', ` suite: "${suite}"`, ` purpose: "${purpose}"`, ` created: "2025-08-20"`, ' version: "1.0.0"', ` migrated: "${new Date().toISOString()}"`, ` originalPath: "${relativePath}"` ].join('\n'); const lines = content.split('\n'); if (lines[0] === '---') { const endIndex = lines.findIndex((line, index) => index > 0 && line === '---'); if (endIndex > 0) { lines.splice(endIndex, 0, testMetadataBlock); return lines.join('\n'); } } return `---\n${testMetadataBlock}\n---\n\n${content}`; } function removeTestMetadata(content: string): string { // Remove _dollhouseMCPTest and _testMetadata lines const lines = content.split('\n'); const filtered = lines.filter(line => { const trimmed = line.trim(); return !trimmed.startsWith('_dollhouseMCPTest:') && !trimmed.startsWith('_testMetadata:') && !line.match(/^\s+(suite|purpose|created|version|migrated|originalPath):/); }); return filtered.join('\n'); } function isTestFile(filePath: string, content: string): boolean { const relativePath = filePath; // Check if it's in a test directory if (relativePath.includes('test/') || relativePath.includes('data/') || relativePath.includes('test-elements/')) { return true; } // Check filename patterns const filename = path.basename(filePath); const TEST_FILE_PATTERNS = [ /^sample-/i, /^test-/i, /edge-case/i, /invalid-element/i, /roundtrip.*test/i, ]; if (TEST_FILE_PATTERNS.some(pattern => pattern.test(filename))) { return true; } // Check content patterns (case-insensitive) const lowerContent = content.toLowerCase(); if (lowerContent.includes('test') && ( lowerContent.includes('validation') || lowerContent.includes('testing') || lowerContent.includes('edge case') || lowerContent.includes('fixture') )) { return true; } return false; } describe('Migration Script Tests', () => { let tempDir: string; let originalArgv: string[]; beforeEach(async () => { tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'migration-test-')); originalArgv = process.argv; }); afterEach(async () => { await fs.rm(tempDir, { recursive: true, force: true }); process.argv = originalArgv; }); describe('addTestMetadata', () => { it('should add metadata to file with existing frontmatter', () => { const content = `--- name: Test File version: 1.0.0 type: persona --- # Test Content This is test content.`; const result = addTestMetadata(content, '/path/to/test-file.md'); expect(result).toContain('_dollhouseMCPTest: true'); expect(result).toContain('_testMetadata:'); // Fix: test-file.md with type: persona should be detected as "Test persona for behavior validation" and unit-testing suite expect(result).toContain('suite: "unit-testing"'); expect(result).toContain('purpose: "Test persona for behavior validation"'); expect(result).toContain('originalPath: "path/to/test-file.md"'); expect(result).toContain('name: Test File'); expect(result).toContain('# Test Content'); }); it('should create frontmatter for file without existing metadata', () => { const content = `# Test Content This is a test file without frontmatter.`; const result = addTestMetadata(content, '/test/fixtures/sample-test.md'); // Fix: replaced non-existent toStartWith with toMatch using regex expect(result).toMatch(/^---\n/); expect(result).toContain('_dollhouseMCPTest: true'); expect(result).toContain('_testMetadata:'); expect(result).toContain('# Test Content'); expect(result).toContain('This is a test file without frontmatter.'); }); it('should determine correct test suite based on path', () => { const testCases = [ { path: '/test/fixtures/sample.md', expectedSuite: 'test-fixtures' }, { path: '/data/personas/test.md', expectedSuite: 'bundled-test-data' }, { path: '/test-elements/roundtrip.md', expectedSuite: 'roundtrip-testing' }, { path: '/test/__tests__/integration.md', expectedSuite: 'integration-testing' }, { path: '/other/test.md', expectedSuite: 'unit-testing' } ]; for (const testCase of testCases) { const content = '# Test'; const result = addTestMetadata(content, testCase.path); expect(result).toContain(`suite: "${testCase.expectedSuite}"`); } }); it('should determine correct purpose based on filename and content', () => { const testCases = [ { file: 'sample-persona.md', content: '# Sample', expectedPurpose: 'Test fixture for workflow validation' }, { file: 'edge-case-test.md', content: '# Edge Case', expectedPurpose: 'Edge case testing for robustness validation' }, { file: 'invalid-element.md', content: '# Invalid', expectedPurpose: 'Invalid data testing for error handling' }, { file: 'test-persona.md', content: 'type: persona\n# Test Persona', expectedPurpose: 'Test persona for behavior validation' }, { file: 'skill-test.md', content: 'type: skill\n# Test Skill', expectedPurpose: 'Test skill for capability validation' } ]; for (const testCase of testCases) { const result = addTestMetadata(testCase.content, `/test/${testCase.file}`); expect(result).toContain(`purpose: "${testCase.expectedPurpose}"`); } }); it('should include timestamp and version information', () => { const content = '# Test'; const result = addTestMetadata(content, '/test/file.md'); expect(result).toContain('created: "'); expect(result).toContain('version: "1.0.0"'); expect(result).toContain('migrated: "'); // Check date format (YYYY-MM-DD) const createdMatch = result.match(/created: "(\d{4}-\d{2}-\d{2})"/); expect(createdMatch).toBeTruthy(); expect(new Date(createdMatch![1])).toBeInstanceOf(Date); // Check ISO timestamp format const migratedMatch = result.match(/migrated: "([^"]+)"/); expect(migratedMatch).toBeTruthy(); expect(new Date(migratedMatch![1])).toBeInstanceOf(Date); }); }); describe('removeTestMetadata', () => { it('should remove test metadata while preserving other fields', () => { const content = `--- name: Test File version: 1.0.0 type: persona _dollhouseMCPTest: true _testMetadata: suite: "test-fixtures" purpose: "Test file" created: "2025-08-20" version: "1.0.0" migrated: "2025-08-20T12:00:00Z" originalPath: "test/file.md" description: This is a test file --- # Test Content`; const result = removeTestMetadata(content); expect(result).not.toContain('_dollhouseMCPTest'); expect(result).not.toContain('_testMetadata'); expect(result).not.toContain('suite:'); expect(result).not.toContain('purpose:'); expect(result).not.toContain('created:'); expect(result).not.toContain('migrated:'); expect(result).not.toContain('originalPath:'); // Should preserve other fields expect(result).toContain('name: Test File'); expect(result).toContain('version: 1.0.0'); expect(result).toContain('type: persona'); expect(result).toContain('description: This is a test file'); expect(result).toContain('# Test Content'); }); it('should handle files without test metadata gracefully', () => { const content = `--- name: Regular File version: 1.0.0 --- # Regular Content`; const result = removeTestMetadata(content); expect(result).toEqual(content); }); it('should handle malformed metadata fields', () => { const content = `--- name: Test File _dollhouseMCPTest: true _testMetadata: malformed line without colon suite: "test" purpose: "testing" invalid_dollhouseMCPTest: false _testMetadata_other: value --- # Content`; const result = removeTestMetadata(content); expect(result).not.toContain('_dollhouseMCPTest: true'); expect(result).not.toContain('_testMetadata:'); expect(result).not.toContain('suite: "test"'); expect(result).not.toContain('purpose: "testing"'); // Should preserve non-matching lines expect(result).toContain('name: Test File'); expect(result).toContain('invalid_dollhouseMCPTest: false'); expect(result).toContain('_testMetadata_other: value'); }); }); describe('isTestFile', () => { it('should identify test files by directory path', () => { const testCases = [ { path: '/project/test/fixtures/file.md', content: '# Content', expected: true }, { path: '/project/data/personas/file.md', content: '# Content', expected: true }, { path: '/project/test-elements/file.md', content: '# Content', expected: true }, { path: '/project/src/regular/file.md', content: '# Content', expected: false }, ]; for (const testCase of testCases) { const result = isTestFile(testCase.path, testCase.content); expect(result).toBe(testCase.expected); } }); it('should identify test files by filename patterns', () => { const testCases = [ { path: '/project/sample-persona.md', content: '# Content', expected: true }, { path: '/project/test-skill.md', content: '# Content', expected: true }, { path: '/project/edge-case-test.md', content: '# Content', expected: true }, { path: '/project/invalid-element.md', content: '# Content', expected: true }, { path: '/project/roundtrip-test.md', content: '# Content', expected: true }, { path: '/project/regular-file.md', content: '# Content', expected: false }, ]; for (const testCase of testCases) { const result = isTestFile(testCase.path, testCase.content); expect(result).toBe(testCase.expected); } }); it('should identify test files by content patterns', () => { const testCases = [ { path: '/project/file.md', content: 'This is a test validation file for edge case testing.', expected: true }, { path: '/project/file.md', content: 'This file is used for testing purposes and validation.', expected: true }, { path: '/project/file.md', content: 'Test fixture for system validation.', expected: true }, { path: '/project/file.md', content: 'Regular content without test indicators.', expected: false }, { path: '/project/file.md', content: 'This document tests your understanding.', // "tests" but not validation context expected: false }, ]; for (const testCase of testCases) { const result = isTestFile(testCase.path, testCase.content); expect(result).toBe(testCase.expected); } }); }); describe('Migration Integration', () => { beforeEach(async () => { // Create test directory structure const testDirs = ['test/fixtures', 'data/personas', 'test-elements']; for (const dir of testDirs) { await fs.mkdir(path.join(tempDir, dir), { recursive: true }); } }); it('should perform dry-run migration without modifying files', async () => { // Create test files const testFiles = [ { path: 'test/fixtures/sample-persona.md', content: `--- name: Sample Persona type: persona --- # Sample Persona` }, { path: 'data/personas/test-data.md', content: `--- name: Test Data --- # Test Data` } ]; for (const file of testFiles) { const fullPath = path.join(tempDir, file.path); await fs.writeFile(fullPath, file.content); } // Mock command line arguments for dry run process.argv = ['node', 'migrate-test-metadata.ts', '--dry-run']; // Change working directory context for the migration const originalCwd = process.cwd(); const mockCwd = jest.spyOn(process, 'cwd').mockReturnValue(tempDir); try { // Note: We can't easily test the full migrate() function here because it's designed // to work with the project structure. Instead, we test the individual functions. // Verify files haven't been modified for (const file of testFiles) { const fullPath = path.join(tempDir, file.path); const content = await fs.readFile(fullPath, 'utf-8'); expect(content).toEqual(file.content); expect(content).not.toContain('_dollhouseMCPTest'); } } finally { mockCwd.mockRestore(); } }); it('should handle migration with mixed file types', async () => { const mixedFiles = [ { path: 'test-file-with-metadata.md', content: `--- name: Already Migrated _dollhouseMCPTest: true _testMetadata: suite: "existing" --- # Already migrated`, shouldMigrate: false }, { path: 'test-file-without-metadata.md', content: `--- name: Needs Migration type: persona --- # Needs migration`, shouldMigrate: true }, { path: 'non-test-file.md', content: `--- name: Regular File type: persona --- # Regular content`, shouldMigrate: false } ]; for (const file of mixedFiles) { // Place non-test files in a regular directory, test files in test/fixtures const dir = file.path === 'non-test-file.md' ? 'src' : 'test/fixtures'; const fullPath = path.join(tempDir, dir, file.path); await fs.mkdir(path.dirname(fullPath), { recursive: true }); await fs.writeFile(fullPath, file.content); } // Test migration logic on each file for (const file of mixedFiles) { const dir = file.path === 'non-test-file.md' ? 'src' : 'test/fixtures'; const fullPath = path.join(tempDir, dir, file.path); const content = await fs.readFile(fullPath, 'utf-8'); const hasMetadata = content.includes('_dollhouseMCPTest'); const isTest = isTestFile(fullPath, content); if (file.shouldMigrate) { expect(hasMetadata).toBe(false); expect(isTest).toBe(true); // Test migration const migratedContent = addTestMetadata(content, fullPath); expect(migratedContent).toContain('_dollhouseMCPTest: true'); } else if (hasMetadata) { expect(hasMetadata).toBe(true); expect(isTest).toBe(true); } else { expect(isTest).toBe(false); } } }); it('should handle migration rollback correctly', async () => { const testFile = path.join(tempDir, 'test/fixtures/rollback-test.md'); const originalContent = `--- name: Rollback Test type: persona version: 1.0.0 --- # Rollback Test Content`; await fs.writeFile(testFile, originalContent); // Step 1: Add metadata (simulate migration) let content = await fs.readFile(testFile, 'utf-8'); const migratedContent = addTestMetadata(content, testFile); await fs.writeFile(testFile, migratedContent); // Verify metadata was added content = await fs.readFile(testFile, 'utf-8'); expect(content).toContain('_dollhouseMCPTest: true'); expect(content).toContain('_testMetadata:'); // Step 2: Remove metadata (simulate rollback) const rolledBackContent = removeTestMetadata(content); await fs.writeFile(testFile, rolledBackContent); // Verify metadata was removed const finalContent = await fs.readFile(testFile, 'utf-8'); expect(finalContent).not.toContain('_dollhouseMCPTest'); expect(finalContent).not.toContain('_testMetadata'); expect(finalContent).toContain('name: Rollback Test'); expect(finalContent).toContain('type: persona'); expect(finalContent).toContain('# Rollback Test Content'); }); it('should handle edge cases during migration', async () => { const edgeCases = [ { name: 'empty-file.md', content: '', expectError: false }, { name: 'only-frontmatter.md', content: `--- name: Only Frontmatter ---`, expectError: false }, { name: 'malformed-yaml.md', content: `--- name: Malformed invalid: {broken yaml --- # Content`, expectError: false }, { name: 'unicode-content.md', content: `--- name: "Unicode Test πŸš€" description: "Γ‰mojis and spΓ«cial chΓ€racteαΉ›s" --- # Unicode Content δΈ­ζ–‡`, expectError: false } ]; for (const testCase of edgeCases) { const filePath = path.join(tempDir, 'test/fixtures', testCase.name); await fs.writeFile(filePath, testCase.content); // Test that migration functions don't throw errors try { const isTest = isTestFile(filePath, testCase.content); if (isTest) { const migratedContent = addTestMetadata(testCase.content, filePath); expect(typeof migratedContent).toBe('string'); const rolledBackContent = removeTestMetadata(migratedContent); expect(typeof rolledBackContent).toBe('string'); } } catch (error) { if (!testCase.expectError) { throw new Error(`Unexpected error for ${testCase.name}: ${error}`); } } } }); it('should preserve file permissions and timestamps', async () => { const testFile = path.join(tempDir, 'test/fixtures/permissions-test.md'); const content = `--- name: Permissions Test --- # Test Content`; await fs.writeFile(testFile, content); // Get original stats const originalStats = await fs.stat(testFile); // Wait a moment to ensure timestamp difference await new Promise(resolve => setTimeout(resolve, 10)); // Simulate migration const migratedContent = addTestMetadata(content, testFile); await fs.writeFile(testFile, migratedContent); // Check that file was actually modified (content changed) const newContent = await fs.readFile(testFile, 'utf-8'); expect(newContent).not.toEqual(content); expect(newContent).toContain('_dollhouseMCPTest'); // Get new stats const newStats = await fs.stat(testFile); // Permissions should be preserved (or at least reasonable) expect(newStats.mode).toBeDefined(); expect(newStats.uid).toBe(originalStats.uid); expect(newStats.gid).toBe(originalStats.gid); }); }); describe('Error Handling', () => { it('should handle files that cannot be read', async () => { const nonExistentFile = path.join(tempDir, 'does-not-exist.md'); // isTestFile should handle non-existent files gracefully expect(() => isTestFile(nonExistentFile, '')).not.toThrow(); const result = isTestFile(nonExistentFile, ''); expect(typeof result).toBe('boolean'); }); it('should handle extremely large frontmatter', async () => { const largeFrontmatter = 'x'.repeat(10000); const content = `--- name: Large Frontmatter Test description: "${largeFrontmatter}" --- # Content`; // Should not throw errors expect(() => addTestMetadata(content, '/test/large.md')).not.toThrow(); expect(() => removeTestMetadata(content)).not.toThrow(); const result = addTestMetadata(content, '/test/large.md'); expect(result).toContain('_dollhouseMCPTest: true'); }); it('should handle special characters in file paths', async () => { const specialPaths = [ '/test/files with spaces/test-file.md', '/test/files-with-hyphens/test-file.md', '/test/files_with_underscores/test-file.md', '/test/files.with.dots/test-file.md', '/test/files(with)parentheses/test-file.md' ]; const content = '# Test content'; for (const specialPath of specialPaths) { expect(() => isTestFile(specialPath, content)).not.toThrow(); expect(() => addTestMetadata(content, specialPath)).not.toThrow(); const result = addTestMetadata(content, specialPath); expect(result).toContain('_dollhouseMCPTest: true'); } }); }); });

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