Skip to main content
Glama
source-priority-helpers.ts11.1 kB
/** * Test helpers for element source priority integration tests * * Provides utilities for: * - Creating test elements in different sources (local, GitHub, collection) * - Mocking GitHub and collection APIs * - Configuring source priority settings * - Simulating source failures * - Test environment setup and cleanup */ import * as fs from 'node:fs/promises'; import * as path from 'node:path'; import { ElementType } from '../../../../src/portfolio/types.js'; import { ElementSource, SourcePriorityConfig, DEFAULT_SOURCE_PRIORITY } from '../../../../src/config/sourcePriority.js'; import { createTempDir } from './file-utils.js'; /** * Test element data structure */ export interface TestElement { name: string; description: string; version: string; elementType: ElementType; content?: string; author?: string; category?: string; tags?: string[]; } /** * Create element content in markdown format with frontmatter */ export function createElementContent(element: TestElement): string { const { name, description, version, content = 'Test content', author = 'Test Author', category, tags } = element; const frontmatter: any = { name, description, version, author, }; if (category) { frontmatter.category = category; } if (tags && tags.length > 0) { frontmatter.tags = tags; } const frontmatterYaml = Object.entries(frontmatter) .map(([key, value]) => { if (Array.isArray(value)) { const items = value.map(v => ` - ${v}`).join('\n'); return `${key}:\n${items}`; } return `${key}: ${value}`; }) .join('\n'); return `--- ${frontmatterYaml} --- ${content}`; } /** * Generate filename from element name */ export function elementNameToFilename(name: string, elementType: ElementType): string { const baseName = name.toLowerCase().replaceAll(/\s+/g, '-').replaceAll(/[^a-z0-9-]/g, ''); const extension = '.md'; return `${baseName}${extension}`; } /** * Get element type directory name */ export function getElementTypeDir(elementType: ElementType): string { return elementType; } /** * Create a test element in local portfolio */ export async function createLocalElement( portfolioDir: string, element: TestElement ): Promise<string> { const typeDir = path.join(portfolioDir, getElementTypeDir(element.elementType)); await fs.mkdir(typeDir, { recursive: true }); const filename = elementNameToFilename(element.name, element.elementType); const filePath = path.join(typeDir, filename); const content = createElementContent(element); await fs.writeFile(filePath, content, 'utf-8'); return filePath; } /** * Create multiple local elements */ export async function createLocalElements( portfolioDir: string, elements: TestElement[] ): Promise<string[]> { return Promise.all(elements.map(el => createLocalElement(portfolioDir, el))); } /** * Check if element exists in local portfolio */ export async function localElementExists( portfolioDir: string, name: string, elementType: ElementType ): Promise<boolean> { const filename = elementNameToFilename(name, elementType); const filePath = path.join(portfolioDir, getElementTypeDir(elementType), filename); try { await fs.access(filePath); return true; } catch { return false; } } /** * Get element content from local portfolio */ export async function getLocalElementContent( portfolioDir: string, name: string, elementType: ElementType ): Promise<string | null> { const filename = elementNameToFilename(name, elementType); const filePath = path.join(portfolioDir, getElementTypeDir(elementType), filename); try { return await fs.readFile(filePath, 'utf-8'); } catch { return null; } } /** * Delete element from local portfolio */ export async function deleteLocalElement( portfolioDir: string, name: string, elementType: ElementType ): Promise<void> { const filename = elementNameToFilename(name, elementType); const filePath = path.join(portfolioDir, getElementTypeDir(elementType), filename); try { await fs.unlink(filePath); } catch { // File doesn't exist, that's fine } } /** * Mock SHA counter for deterministic test data * FIX: Replaced Math.random() with counter to prevent weak cryptography warning (SonarCloud S2245) * Safe for test mocks - not used for security purposes */ let mockShaCounter = 0; /** * Generate deterministic mock SHA for testing * @returns Mock SHA string with incrementing counter */ function generateMockSha(prefix: string = 'mock-sha'): string { return `${prefix}-${++mockShaCounter}`; } /** * Mock GitHub API response for element content */ export interface MockGitHubElement { name: string; path: string; content: string; sha: string; size: number; type: 'file'; download_url: string; html_url: string; } /** * Create mock GitHub element response */ export function createMockGitHubElement(element: TestElement): MockGitHubElement { const content = createElementContent(element); const encodedContent = Buffer.from(content).toString('base64'); const filename = elementNameToFilename(element.name, element.elementType); const elementPath = `${getElementTypeDir(element.elementType)}/${filename}`; return { name: filename, path: elementPath, content: encodedContent, sha: generateMockSha('mock-sha'), size: content.length, type: 'file', download_url: `https://raw.githubusercontent.com/test/portfolio/main/${elementPath}`, html_url: `https://github.com/test/portfolio/blob/main/${elementPath}` }; } /** * Mock collection API response for element */ export interface MockCollectionElement { path: string; name: string; sha: string; size: number; url: string; html_url: string; git_url: string; download_url: string; type: 'file'; content?: string; encoding?: 'base64'; } /** * Create mock collection element response */ export function createMockCollectionElement(element: TestElement, collectionPath: string): MockCollectionElement { const content = createElementContent(element); const encodedContent = Buffer.from(content).toString('base64'); return { path: collectionPath, name: elementNameToFilename(element.name, element.elementType), sha: generateMockSha('mock-collection-sha'), size: content.length, url: `https://api.github.com/repos/DollhouseMCP/collection/contents/${collectionPath}`, html_url: `https://github.com/DollhouseMCP/collection/blob/main/${collectionPath}`, git_url: `https://api.github.com/repos/DollhouseMCP/collection/git/blobs/mock-sha`, download_url: `https://raw.githubusercontent.com/DollhouseMCP/collection/main/${collectionPath}`, type: 'file', content: encodedContent, encoding: 'base64' }; } /** * Setup test environment with temporary directories */ export async function setupSourcePriorityTestEnv(): Promise<{ portfolioDir: string; tempDir: string; cleanup: () => Promise<void>; }> { const tempDir = await createTempDir('source-priority-test'); const portfolioDir = path.join(tempDir, 'portfolio'); await fs.mkdir(portfolioDir, { recursive: true }); // Create element type directories for (const type of ['personas', 'skills', 'templates', 'agents', 'memories', 'ensembles']) { await fs.mkdir(path.join(portfolioDir, type), { recursive: true }); } const cleanup = async () => { try { await fs.rm(tempDir, { recursive: true, force: true }); } catch (error) { // Cleanup failed, but don't fail the test console.warn('Failed to cleanup test directory:', error); } }; return { portfolioDir, tempDir, cleanup }; } /** * Configure source priority for testing */ export function setTestSourcePriority(config: Partial<SourcePriorityConfig>): void { const fullConfig: SourcePriorityConfig = { ...DEFAULT_SOURCE_PRIORITY, ...config }; // Set environment variable for testing process.env.SOURCE_PRIORITY = JSON.stringify(fullConfig); } /** * Reset source priority to defaults */ export function resetSourcePriority(): void { delete process.env.SOURCE_PRIORITY; } /** * Create test source priority config */ export function createTestSourcePriorityConfig( priority: ElementSource[], options: { stopOnFirst?: boolean; checkAllForUpdates?: boolean; fallbackOnError?: boolean; } = {} ): SourcePriorityConfig { return { priority, stopOnFirst: options.stopOnFirst ?? true, checkAllForUpdates: options.checkAllForUpdates ?? false, fallbackOnError: options.fallbackOnError ?? true }; } /** * Simulate network failure for testing fallback behavior */ export class NetworkFailureSimulator { private failedSources: Set<ElementSource> = new Set(); /** * Mark a source as failing */ failSource(source: ElementSource): void { this.failedSources.add(source); } /** * Mark a source as working */ unfailSource(source: ElementSource): void { this.failedSources.delete(source); } /** * Check if a source should fail */ shouldFail(source: ElementSource): boolean { return this.failedSources.has(source); } /** * Reset all failures */ reset(): void { this.failedSources.clear(); } } /** * Wait for async operation with timeout */ export async function waitFor( condition: () => boolean | Promise<boolean>, timeout: number = 5000, interval: number = 100 ): Promise<void> { const startTime = Date.now(); while (Date.now() - startTime < timeout) { const result = await condition(); if (result) { return; } await new Promise(resolve => setTimeout(resolve, interval)); } throw new Error(`Condition not met within ${timeout}ms`); } /** * Sample test elements for common test scenarios */ export const TEST_ELEMENTS = { persona1: { name: 'Test Creative Writer', description: 'A creative writing assistant for testing', version: '1.0.0', elementType: 'personas' as ElementType, content: 'You are a creative writing assistant. Help users with writing tasks.', category: 'creative', tags: ['writing', 'creative', 'test'] }, persona2: { name: 'Test Technical Expert', description: 'A technical expert for testing', version: '1.0.0', elementType: 'personas' as ElementType, content: 'You are a technical expert. Help users with technical problems.', category: 'professional', tags: ['technical', 'expert', 'test'] }, skill1: { name: 'Test Code Review', description: 'A code review skill for testing', version: '1.0.0', elementType: 'skills' as ElementType, content: 'Perform comprehensive code reviews following best practices.', tags: ['code-review', 'testing', 'quality'] }, skill2: { name: 'Test Documentation', description: 'A documentation skill for testing', version: '1.0.0', elementType: 'skills' as ElementType, content: 'Create comprehensive documentation for code projects.', tags: ['documentation', 'testing', 'writing'] } };

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/mcp-server'

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