Skip to main content
Glama

DollhouseMCP

by DollhouseMCP
real-github-integration.test.ts19.1 kB
/** * Real GitHub Integration Tests * These tests perform ACTUAL GitHub API operations - NO MOCKS! * * NOTE: These tests are skipped in CI environments to prevent conflicts * when multiple CI runs attempt to modify the same test repository simultaneously. * The tests should be run locally during development to verify GitHub integration. */ import { describe, it, expect, beforeAll, afterAll, afterEach } from '@jest/globals'; import { setupTestEnvironment, TestEnvironment, ERROR_CODES } from './setup-test-env.js'; import { GitHubTestClient } from '../utils/github-api-client.js'; import { createZiggyTestPersona, createTestPersona, createTestPersonaSet } from '../utils/test-persona-factory.js'; import { PortfolioRepoManager } from '../../src/portfolio/PortfolioRepoManager.js'; import { retryWithBackoff, retryIfRetryable } from './utils/retry.js'; // Skip the entire test suite in CI environments to prevent conflicts const describeOrSkip = process.env.CI ? describe.skip : describe; describeOrSkip('Real GitHub Portfolio Integration Tests', () => { let testEnv: TestEnvironment; let githubClient: GitHubTestClient; let portfolioManager: PortfolioRepoManager; let uploadedFiles: string[] = []; beforeAll(async () => { console.log('\n🚀 Starting real GitHub integration tests...\n'); // Setup and validate environment testEnv = await setupTestEnvironment(); // Skip tests if no token available if (testEnv.skipTests) { console.log('⏭️ Skipping GitHub integration tests - no token available'); return; } githubClient = new GitHubTestClient(testEnv); // Initialize portfolio manager with real token portfolioManager = new PortfolioRepoManager(); portfolioManager.setToken(testEnv.githubToken); console.log(`\n📋 Test Configuration:`); console.log(` Repository: ${testEnv.testRepo}`); console.log(` User: ${testEnv.githubUser}`); console.log(` Cleanup: ${testEnv.cleanupAfter ? 'Yes' : 'No'}`); console.log(` Branch: ${testEnv.testBranch}\n`); }, 60000); // Longer timeout for setup afterEach(async () => { // Track files for cleanup if (testEnv?.cleanupAfter && uploadedFiles.length > 0 && githubClient) { console.log(`\n🧹 Cleaning up ${uploadedFiles.length} test files...`); for (const file of uploadedFiles) { await githubClient.deleteFile(file); } uploadedFiles = []; } }); afterAll(async () => { console.log('\n✅ GitHub integration tests completed\n'); }); describe('Single Element Upload - Success Path', () => { it('should successfully upload a single persona to GitHub and verify it exists', async () => { // Skip test if no token available if (testEnv.skipTests) { console.log('⏭️ Test skipped - no GitHub token'); return; } console.log('\n▶️ Test: Upload single persona to GitHub'); // Step 1: Create test persona console.log(' 1️⃣ Creating test persona...'); const ziggyPersona = createZiggyTestPersona({ author: testEnv.githubUser, prefix: testEnv.personaPrefix }); // Step 2: Upload to GitHub via PortfolioRepoManager console.log(' 2️⃣ Uploading to GitHub...'); const uploadResult = await retryIfRetryable( async () => await portfolioManager.saveElement(ziggyPersona, true), { maxAttempts: 5, // Increased from 3 to handle aggressive CI concurrency onRetry: (attempt, error) => console.log(` ↻ Retry ${attempt} due to: ${error.message}`) } ); expect(uploadResult).toBeTruthy(); expect(uploadResult).toContain('github.com'); console.log(` ✅ Upload successful: ${uploadResult}`); // Step 3: Extract file path from result // Use the actual generateFileName method for consistency const fileName = PortfolioRepoManager.generateFileName(ziggyPersona.metadata.name || 'unnamed'); const filePath = `personas/${fileName}.md`; uploadedFiles.push(filePath); // Step 4: Verify file exists on GitHub by fetching it console.log(' 3️⃣ Verifying file exists on GitHub...'); // Use retry logic for eventual consistency const githubFile = await retryWithBackoff( async () => { const file = await githubClient.getFile(filePath); if (!file) { throw new Error(`File not found at ${filePath}`); } return file; }, { maxAttempts: 3, initialDelayMs: 1000, maxDelayMs: 5000, onRetry: (attempt, _error, delayMs) => { console.log(` ⏳ Retry ${attempt}/3: Waiting ${delayMs}ms for GitHub to process file...`); } } ); expect(githubFile).not.toBeNull(); expect(githubFile?.content).toBeTruthy(); console.log(` ✅ File verified at: ${filePath}`); // Step 5: Compare uploaded content with original console.log(' 4️⃣ Comparing content...'); expect(githubFile?.content).toContain('Test Ziggy'); expect(githubFile?.content).toContain('Quantum Leap'); console.log(' ✅ Content matches original'); // Step 6: Verify URL is accessible (not 404) console.log(' 5️⃣ Verifying URL is accessible...'); const urlAccessible = await githubClient.verifyUrl(uploadResult); expect(urlAccessible).toBe(true); console.log(' ✅ URL is accessible (not 404)'); console.log('\n✅ Single element upload test PASSED'); }, 60000); it('should handle GitHub response with null commit field correctly', async () => { // Skip if no GitHub token if (testEnv.skipTests) { console.log('⏭️ Skipping test - no GitHub token available'); return; } console.log('\n▶️ Test: Handle null commit in GitHub response'); // This tests the specific bug from the QA report // We'll upload a file and verify it works even if commit is null const testPersona = createTestPersona({ author: testEnv.githubUser, prefix: testEnv.personaPrefix, name: `${testEnv.personaPrefix}null-commit-test-${Date.now()}` }); console.log(' 1️⃣ Uploading test persona...'); const result = await retryIfRetryable( async () => await portfolioManager.saveElement(testPersona, true), { maxAttempts: 5, // Increased from 3 to handle aggressive CI concurrency onRetry: (attempt, error) => console.log(` ↻ Retry ${attempt} due to: ${error.message}`) } ); // Even with potential null commit, should return a valid URL expect(result).toBeTruthy(); expect(result).toContain('github.com'); expect(result).not.toContain('null'); expect(result).not.toContain('undefined'); // Use the actual generateFileName method from PortfolioRepoManager for consistency const fileName = PortfolioRepoManager.generateFileName(testPersona.metadata.name || 'unnamed'); const filePath = `personas/${fileName}.md`; uploadedFiles.push(filePath); console.log(` ✅ Handled response correctly: ${result}`); console.log(` 📁 Expected file path: ${filePath}`); // Verify file actually exists with proper retry logic // GitHub API may have eventual consistency, so we retry with exponential backoff const file = await retryWithBackoff( async () => { const fetchedFile = await githubClient.getFile(filePath); if (!fetchedFile) { throw new Error(`File not found at ${filePath}`); } return fetchedFile; }, { maxAttempts: 3, initialDelayMs: 1000, maxDelayMs: 5000, onRetry: (attempt, _error, delayMs) => { console.log(` ⏳ Retry ${attempt}/3: File not yet available, waiting ${delayMs}ms...`); } } ); expect(file).not.toBeNull(); expect(file.content).toBeTruthy(); console.log(' ✅ File exists on GitHub despite response variations'); }, 30000); }); describe('Error Code Validation', () => { it('should return PORTFOLIO_SYNC_001 for invalid token', async () => { // Skip if no GitHub token if (testEnv.skipTests) { console.log('⏭️ Skipping test - no GitHub token available'); return; } console.log('\n▶️ Test: Invalid token error (SYNC_001)'); const badManager = new PortfolioRepoManager(); badManager.setToken('ghp_invalid_token_xxx'); const testPersona = createTestPersona(); await expect(badManager.saveElement(testPersona, true)) .rejects .toThrow(/PORTFOLIO_SYNC_001|401|authentication|unauthorized/i); console.log(' ✅ Correctly returned PORTFOLIO_SYNC_001 for bad token'); }, 30000); it('should handle rate limit errors gracefully', async () => { // Skip if no GitHub token if (testEnv.skipTests) { console.log('⏭️ Skipping test - no GitHub token available'); return; } console.log('\n▶️ Test: Rate limit handling'); // Check current rate limit const rateLimit = await githubClient.getRateLimit(); console.log(` Current rate limit: ${rateLimit.remaining} remaining`); if (rateLimit.remaining < 10) { console.log(' ⚠️ Rate limit too low, skipping aggressive test'); return; } // This won't trigger actual rate limit but tests the handling const testPersona = createTestPersona({ author: testEnv.githubUser, prefix: testEnv.personaPrefix, name: `${testEnv.personaPrefix}rate-limit-test-${Date.now()}` }); const result = await retryIfRetryable( async () => await portfolioManager.saveElement(testPersona, true), { maxAttempts: 5, // Increased from 3 to handle aggressive CI concurrency onRetry: (attempt, error) => console.log(` ↻ Retry ${attempt} due to: ${error.message}`) } ); expect(result).toBeTruthy(); console.log(' ✅ Rate limit handling verified'); // Use the actual generateFileName method from PortfolioRepoManager for consistency const fileName = PortfolioRepoManager.generateFileName(testPersona.metadata.name || 'unnamed'); const filePath = `personas/${fileName}.md`; uploadedFiles.push(filePath); }, 30000); }); describe('Bulk Sync Prevention', () => { it('should upload ONLY the specified element, not all personas', async () => { // Skip if no GitHub token if (testEnv.skipTests) { console.log('⏭️ Skipping test - no GitHub token available'); return; } console.log('\n▶️ Test: Single upload does not trigger bulk sync'); // Create multiple personas const personas = createTestPersonaSet({ author: testEnv.githubUser, prefix: testEnv.personaPrefix }); console.log(` 1️⃣ Created ${personas.length} test personas (1 public, 2 private)`); // Upload only the public one const publicPersona = personas[0]; console.log(' 2️⃣ Uploading ONLY the public persona...'); const result = await retryIfRetryable( async () => await portfolioManager.saveElement(publicPersona, true), { maxAttempts: 5, // Increased from 3 to handle aggressive CI concurrency onRetry: (attempt, error) => console.log(` ↻ Retry ${attempt} due to: ${error.message}`) } ); expect(result).toBeTruthy(); // Use the actual generateFileName method for consistency const publicFileName = PortfolioRepoManager.generateFileName(publicPersona.metadata.name || 'unnamed'); const publicPath = `personas/${publicFileName}.md`; uploadedFiles.push(publicPath); // Verify ONLY the public persona was uploaded console.log(' 3️⃣ Verifying only ONE file was uploaded...'); // Check that private personas were NOT uploaded for (let i = 1; i < personas.length; i++) { const privatePersona = personas[i]; const privateFileName = PortfolioRepoManager.generateFileName(privatePersona.metadata.name || 'unnamed'); const privatePath = `personas/${privateFileName}.md`; const privateFile = await githubClient.getFile(privatePath); expect(privateFile).toBeNull(); console.log(` ✅ Private persona ${i} was NOT uploaded`); } // List all files in personas directory const allFiles = await githubClient.listFiles('personas'); const testFiles = allFiles.filter(f => testEnv.personaPrefix && f.includes(testEnv.personaPrefix)); console.log(` 📁 Test files in personas/: ${testFiles.length}`); console.log(' ✅ Confirmed: Only requested element was uploaded'); }, 60000); }); describe('URL Extraction Fallbacks', () => { it('should generate correct URLs with various response formats', async () => { // Skip if no GitHub token if (testEnv.skipTests) { console.log('⏭️ Skipping test - no GitHub token available'); return; } console.log('\n▶️ Test: URL extraction with fallbacks'); const testPersona = createTestPersona({ author: testEnv.githubUser, prefix: testEnv.personaPrefix, name: `${testEnv.personaPrefix}url-test-${Date.now()}` }); console.log(' 1️⃣ Uploading test persona...'); const result = await retryIfRetryable( async () => await portfolioManager.saveElement(testPersona, true), { maxAttempts: 5, // Increased from 3 to handle aggressive CI concurrency onRetry: (attempt, error) => console.log(` ↻ Retry ${attempt} due to: ${error.message}`) } ); // Verify URL format expect(result).toMatch(/https:\/\/github\.com\/.+/); expect(result).not.toContain('undefined'); expect(result).not.toContain('null'); console.log(` ✅ Generated valid URL: ${result}`); // Verify the URL actually works const urlWorks = await githubClient.verifyUrl(result); expect(urlWorks).toBe(true); console.log(' ✅ URL is accessible'); // Use the actual generateFileName method from PortfolioRepoManager for consistency const fileName = PortfolioRepoManager.generateFileName(testPersona.metadata.name || 'unnamed'); const filePath = `personas/${fileName}.md`; uploadedFiles.push(filePath); }, 30000); }); describe('Real User Flow Simulation', () => { it('should complete the exact flow a user would follow', async () => { // Skip if no GitHub token if (testEnv.skipTests) { console.log('⏭️ Skipping test - no GitHub token available'); return; } console.log('\n▶️ Test: Complete user flow simulation'); console.log(' Simulating: User wants to upload Ziggy persona to GitHub portfolio'); // Step 1: User has multiple personas locally console.log('\n 1️⃣ User has multiple personas in local portfolio:'); const localPersonas = [ { name: 'Ziggy', description: 'Quantum Leap AI', private: false }, { name: 'Work Assistant', description: 'Private work helper', private: true }, { name: 'Family Helper', description: 'Personal assistant', private: true } ]; localPersonas.forEach(p => { console.log(` - ${p.name} (${p.private ? 'private' : 'public'})`); }); // Step 2: User chooses to upload only Ziggy console.log('\n 2️⃣ User action: "Upload Ziggy to my GitHub portfolio"'); const ziggyPersona = createZiggyTestPersona({ author: testEnv.githubUser, prefix: testEnv.personaPrefix }); // Step 3: System uploads to GitHub console.log('\n 3️⃣ System uploading to GitHub...'); const startTime = Date.now(); const uploadUrl = await retryIfRetryable( async () => await portfolioManager.saveElement(ziggyPersona, true), { maxAttempts: 5, // Increased from 3 to handle aggressive CI concurrency onRetry: (attempt, error) => console.log(` ↻ Retry ${attempt} due to: ${error.message}`) } ); const uploadTime = Date.now() - startTime; expect(uploadUrl).toBeTruthy(); console.log(` ✅ Upload complete in ${uploadTime}ms`); console.log(` 📍 URL: ${uploadUrl}`); // Step 4: User wants to verify it's really there console.log('\n 4️⃣ User verification: "Is it really on GitHub?"'); // Use the actual generateFileName method for consistency const fileName = PortfolioRepoManager.generateFileName(ziggyPersona.metadata.name || 'unnamed'); const filePath = `personas/${fileName}.md`; uploadedFiles.push(filePath); // Verify with retry for eventual consistency const githubFile = await retryWithBackoff( async () => { const file = await githubClient.getFile(filePath); if (!file) { throw new Error(`File not found at ${filePath}`); } return file; }, { maxAttempts: 3, initialDelayMs: 1000, maxDelayMs: 5000, onRetry: (attempt, _error, delayMs) => { console.log(` ⏳ Retry ${attempt}/3: Waiting ${delayMs}ms for file to be available...`); } } ); expect(githubFile).not.toBeNull(); expect(githubFile?.content).toContain('Ziggy'); console.log(' ✅ Yes! File exists on GitHub'); // Step 5: User checks that private personas were NOT uploaded console.log('\n 5️⃣ User concern: "Did my private personas stay private?"'); const privateFiles = [ 'personas/work-assistant.md', 'personas/family-helper.md' ]; for (const privateFile of privateFiles) { const exists = await githubClient.getFile(privateFile); expect(exists).toBeNull(); } console.log(' ✅ Yes! Private personas were NOT uploaded'); // Step 6: User wants to share the URL console.log('\n 6️⃣ User action: "Share the GitHub URL with friends"'); const urlAccessible = await githubClient.verifyUrl(uploadUrl); expect(urlAccessible).toBe(true); console.log(` ✅ URL is shareable and working: ${uploadUrl}`); console.log('\n🎉 Complete user flow test PASSED!'); console.log(' The exact scenario from the QA report works correctly.\n'); }, 90000); }); });

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