Skip to main content
Glama
server-startup-autoload.test.ts18.9 kB
/** * Integration tests for Server Startup Auto-Load Memories Feature * Issue #1430: Auto-load baseline memories on server startup * * These tests verify the full startup flow, including: * - Loading auto-load memories during server initialization * - Config-based control of auto-load feature * - Graceful handling of loading failures * - Correct logging of loaded memories */ import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from '@jest/globals'; import { ServerStartup } from '../../../src/server/startup.js'; import { ConfigManager } from '../../../src/config/ConfigManager.js'; import { PortfolioManager } from '../../../src/portfolio/PortfolioManager.js'; import * as fs from 'node:fs/promises'; import * as path from 'node:path'; import * as os from 'node:os'; describe('Server Startup - Auto-Load Memories Integration', () => { let testDir: string; let portfolioDir: string; let memoriesDir: string; let configDir: string; let originalPortfolioDir: string | undefined; let originalConfigDir: string | undefined; let serverStartup: ServerStartup; beforeAll(async () => { // Create unique test directories testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'server-startup-autoload-test-')); portfolioDir = path.join(testDir, 'portfolio'); memoriesDir = path.join(portfolioDir, 'memories'); configDir = path.join(testDir, '.config'); // Save original environment originalPortfolioDir = process.env.DOLLHOUSE_PORTFOLIO_DIR; originalConfigDir = process.env.TEST_CONFIG_DIR; // Set test environment process.env.NODE_ENV = 'test'; process.env.DOLLHOUSE_PORTFOLIO_DIR = portfolioDir; process.env.TEST_CONFIG_DIR = configDir; // Create directory structure await fs.mkdir(memoriesDir, { recursive: true }); await fs.mkdir(configDir, { recursive: true }); // Reset singletons (PortfolioManager as any).instance = null; ConfigManager.resetForTesting(); }); afterAll(async () => { // Restore original environment if (originalPortfolioDir === undefined) { delete process.env.DOLLHOUSE_PORTFOLIO_DIR; } else { process.env.DOLLHOUSE_PORTFOLIO_DIR = originalPortfolioDir; } if (originalConfigDir === undefined) { delete process.env.TEST_CONFIG_DIR; } else { process.env.TEST_CONFIG_DIR = originalConfigDir; } // Clean up test directory await fs.rm(testDir, { recursive: true, force: true }); // Reset singletons (PortfolioManager as any).instance = null; ConfigManager.resetForTesting(); }); beforeEach(async () => { // Clear memories directory between tests // Note: No try-catch needed - if directory doesn't exist, readdir will throw // but we handle that by recreating the directory below try { const entries = await fs.readdir(memoriesDir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(memoriesDir, entry.name); if (entry.isDirectory()) { // force: true handles non-existent directories without throwing await fs.rm(fullPath, { recursive: true, force: true }); } else { await fs.unlink(fullPath); } } } catch { // Directory doesn't exist - expected scenario during cleanup, no action needed } // Ensure memories directory exists await fs.mkdir(memoriesDir, { recursive: true }); // Reset config directory // force: true handles non-existent directories without throwing await fs.rm(configDir, { recursive: true, force: true }); await fs.mkdir(configDir, { recursive: true }); // Reset singletons for each test (PortfolioManager as any).instance = null; ConfigManager.resetForTesting(); // Create new ServerStartup instance serverStartup = new ServerStartup(); }); afterEach(() => { // Clean up singletons (PortfolioManager as any).instance = null; ConfigManager.resetForTesting(); }); describe('Auto-Load Memories on Startup', () => { it('should load auto-load memories during server initialization', async () => { // Create an auto-load memory const autoLoadMemory = `--- name: baseline-knowledge type: memory description: Baseline knowledge for DollhouseMCP version: 1.0.0 autoLoad: true priority: 1 --- # Baseline Knowledge This memory should be auto-loaded on server startup. `; await fs.writeFile(path.join(memoriesDir, 'baseline-knowledge.yaml'), autoLoadMemory); // Initialize server (which should load auto-load memories) await serverStartup.initialize({ skipMigration: true }); // Test passes if no errors thrown - memories are loaded during initialization // The actual loading is verified in unit tests, this tests the integration flow expect(true).toBe(true); }); it('should load multiple auto-load memories in priority order', async () => { // Create multiple auto-load memories with different priorities const highPriority = `--- name: high-priority-memory type: memory description: High priority memory version: 1.0.0 autoLoad: true priority: 1 --- High priority content `; const lowPriority = `--- name: low-priority-memory type: memory description: Low priority memory version: 1.0.0 autoLoad: true priority: 100 --- Low priority content `; await fs.writeFile(path.join(memoriesDir, 'high-priority.yaml'), highPriority); await fs.writeFile(path.join(memoriesDir, 'low-priority.yaml'), lowPriority); // Initialize server await serverStartup.initialize({ skipMigration: true }); // Test passes if no errors thrown expect(true).toBe(true); }); it('should not fail startup when no auto-load memories exist', async () => { // Create a regular memory (no autoLoad flag) const regularMemory = `--- name: regular-memory type: memory description: Regular memory version: 1.0.0 --- Regular content `; await fs.writeFile(path.join(memoriesDir, 'regular.yaml'), regularMemory); // Initialize server - should not throw await expect(serverStartup.initialize({ skipMigration: true })).resolves.not.toThrow(); }); it('should handle empty memories directory gracefully', async () => { // Initialize server with empty memories directory await expect(serverStartup.initialize({ skipMigration: true })).resolves.not.toThrow(); }); it('should work with date-organized memories', async () => { // Create date folder const dateFolder = path.join(memoriesDir, '2025-10-30'); await fs.mkdir(dateFolder, { recursive: true }); const datedMemory = `--- name: dated-autoload type: memory description: Auto-load memory in date folder version: 1.0.0 autoLoad: true priority: 5 --- Dated auto-load content `; await fs.writeFile(path.join(dateFolder, 'dated-autoload.yaml'), datedMemory); // Initialize server await serverStartup.initialize({ skipMigration: true }); // Test passes if no errors thrown expect(true).toBe(true); }); }); describe('Config-Based Control', () => { it('should respect config when auto-load is disabled', async () => { // Create config with auto-load disabled const config = { version: '1.0.0', user: { username: null, email: null, display_name: null }, github: { portfolio: { repository_url: null, repository_name: 'dollhouse-portfolio', default_branch: 'main', auto_create: true }, auth: { use_oauth: true, token_source: 'environment' as const } }, sync: { enabled: false, individual: { require_confirmation: true, show_diff_before_sync: true, track_versions: true, keep_history: 10 }, bulk: { upload_enabled: false, download_enabled: false, require_preview: true, respect_local_only: true }, privacy: { scan_for_secrets: true, scan_for_pii: true, warn_on_sensitive: true, excluded_patterns: [] } }, collection: { auto_submit: false, require_review: true, add_attribution: true }, autoLoad: { enabled: false, // Auto-load disabled maxTokenBudget: 5000, memories: [] }, elements: { auto_activate: {}, default_element_dir: portfolioDir, enhanced_index: { enabled: true, limits: { maxTriggersPerElement: 50, maxTriggerLength: 50, maxKeywordsToCheck: 100 }, telemetry: { enabled: false, sampleRate: 0.1, metricsInterval: 60000 } } }, display: { persona_indicators: { enabled: true, style: 'minimal' as const, include_emoji: true }, verbose_logging: false, show_progress: true }, wizard: { completed: false, dismissed: false } }; // Write config file const configPath = path.join(configDir, 'config.yml'); const yaml = await import('js-yaml'); await fs.writeFile(configPath, yaml.dump(config)); // Create an auto-load memory const autoLoadMemory = `--- name: should-not-load type: memory description: Should not load when disabled version: 1.0.0 autoLoad: true priority: 1 --- This should not be loaded `; await fs.writeFile(path.join(memoriesDir, 'should-not-load.yaml'), autoLoadMemory); // Initialize server - should not throw even when auto-load is disabled await expect(serverStartup.initialize({ skipMigration: true })).resolves.not.toThrow(); }); it('should load memories when auto-load is enabled (default)', async () => { // Create an auto-load memory (default config has autoLoad.enabled = true) const autoLoadMemory = `--- name: should-load type: memory description: Should load when enabled version: 1.0.0 autoLoad: true priority: 1 --- This should be loaded `; await fs.writeFile(path.join(memoriesDir, 'should-load.yaml'), autoLoadMemory); // Initialize server with default config await expect(serverStartup.initialize({ skipMigration: true })).resolves.not.toThrow(); }); }); describe('Error Handling', () => { it('should continue startup gracefully if memory loading fails', async () => { // Create a corrupted memory file const corruptedMemory = `--- name: corrupted description: This has invalid YAML autoLoad: [unclosed array --- Invalid content `; await fs.writeFile(path.join(memoriesDir, 'corrupted.yaml'), corruptedMemory); // Initialize server - should not throw even with corrupted memory await expect(serverStartup.initialize({ skipMigration: true })).resolves.not.toThrow(); }); it('should continue startup if memories directory does not exist', async () => { // Remove memories directory await fs.rm(memoriesDir, { recursive: true, force: true }); // Initialize server - should not throw await expect(serverStartup.initialize({ skipMigration: true })).resolves.not.toThrow(); }); it('should handle permission errors gracefully', async () => { // Create a memory file const memoryPath = path.join(memoriesDir, 'test-memory.yaml'); const memory = `--- name: test-memory type: memory description: Test memory version: 1.0.0 autoLoad: true --- Test content `; await fs.writeFile(memoryPath, memory); // Make file unreadable (may not work on all systems) try { await fs.chmod(memoryPath, 0o000); } catch { // Skip test if chmod fails (e.g., on Windows) return; } // Initialize server - should not throw await expect(serverStartup.initialize({ skipMigration: true })).resolves.not.toThrow(); // SECURITY: Safe - test cleanup restoring standard permissions (rw-r--r--) // Context: Permission test creates unreadable file (0o000) to test error handling, // then restores normal permissions (0o644) so temp directory cleanup succeeds. // This is test-only code in isolated temp directory, not production. // SonarCloud: Reviewed and approved - necessary for test cleanup await fs.chmod(memoryPath, 0o644); // NOSONAR - Test cleanup in isolated temp directory }); it('should handle corrupted config gracefully', async () => { // Write invalid config const configPath = path.join(configDir, 'config.yml'); await fs.writeFile(configPath, 'invalid: yaml: content: [[['); // Create an auto-load memory const memory = `--- name: test type: memory description: Test version: 1.0.0 autoLoad: true --- Content `; await fs.writeFile(path.join(memoriesDir, 'test.yaml'), memory); // Initialize server - should use default config and not throw await expect(serverStartup.initialize({ skipMigration: true })).resolves.not.toThrow(); }); }); describe('Integration with Other Startup Tasks', () => { it('should run auto-load after portfolio initialization', async () => { // Create auto-load memory const memory = `--- name: integration-test type: memory description: Integration test memory version: 1.0.0 autoLoad: true priority: 1 --- Integration test content `; await fs.writeFile(path.join(memoriesDir, 'integration-test.yaml'), memory); // Initialize server (includes portfolio setup + auto-load) await serverStartup.initialize({ skipMigration: true }); // Verify portfolio exists const portfolioManager = PortfolioManager.getInstance(); const portfolioExists = await portfolioManager.exists(); expect(portfolioExists).toBe(true); // Test passes if no errors thrown expect(true).toBe(true); }); it('should skip migration when skipMigration is true', async () => { // Create auto-load memory const memory = `--- name: skip-migration-test type: memory description: Skip migration test version: 1.0.0 autoLoad: true --- Content `; await fs.writeFile(path.join(memoriesDir, 'skip-migration-test.yaml'), memory); // Initialize with skipMigration (typical for tests) await serverStartup.initialize({ skipMigration: true }); // Test passes if no errors thrown expect(true).toBe(true); }); it('should handle concurrent initialization gracefully', async () => { // Create auto-load memory const memory = `--- name: concurrent-test type: memory description: Concurrent test version: 1.0.0 autoLoad: true --- Content `; await fs.writeFile(path.join(memoriesDir, 'concurrent-test.yaml'), memory); // Try to initialize multiple times concurrently const promises = [ serverStartup.initialize({ skipMigration: true }), serverStartup.initialize({ skipMigration: true }), serverStartup.initialize({ skipMigration: true }) ]; // All should complete without throwing await expect(Promise.all(promises)).resolves.not.toThrow(); }); }); describe('Memory Priority and Ordering', () => { it('should respect priority order during loading', async () => { // Create memories with different priorities const memories = [ { name: 'priority-10', priority: 10 }, { name: 'priority-1', priority: 1 }, { name: 'priority-50', priority: 50 }, { name: 'priority-5', priority: 5 } ]; for (const mem of memories) { const content = `--- name: ${mem.name} type: memory description: Memory with priority ${mem.priority} version: 1.0.0 autoLoad: true priority: ${mem.priority} --- Content for ${mem.name} `; await fs.writeFile(path.join(memoriesDir, `${mem.name}.yaml`), content); } // Initialize server await serverStartup.initialize({ skipMigration: true }); // Test passes if no errors thrown - priority ordering is handled internally expect(true).toBe(true); }); it('should handle memories without priority (default to 999)', async () => { // Create memories with and without priority const withPriority = `--- name: with-priority type: memory description: With priority version: 1.0.0 autoLoad: true priority: 10 --- With priority content `; const withoutPriority = `--- name: without-priority type: memory description: Without priority version: 1.0.0 autoLoad: true --- Without priority content `; await fs.writeFile(path.join(memoriesDir, 'with-priority.yaml'), withPriority); await fs.writeFile(path.join(memoriesDir, 'without-priority.yaml'), withoutPriority); // Initialize server await serverStartup.initialize({ skipMigration: true }); // Test passes if no errors thrown expect(true).toBe(true); }); }); describe('Performance and Scalability', () => { it('should handle large number of auto-load memories efficiently', async () => { // Create 20 auto-load memories const createPromises = []; for (let i = 1; i <= 20; i++) { const content = `--- name: memory-${i} type: memory description: Auto-load memory ${i} version: 1.0.0 autoLoad: true priority: ${i} --- Content for memory ${i} `; createPromises.push( fs.writeFile(path.join(memoriesDir, `memory-${i}.yaml`), content) ); } await Promise.all(createPromises); // Initialize server - should complete in reasonable time const startTime = Date.now(); await serverStartup.initialize({ skipMigration: true }); const duration = Date.now() - startTime; // Should complete within 10 seconds even with 20 memories expect(duration).toBeLessThan(10000); }); it('should handle memories in multiple date folders', async () => { // Create multiple date folders with auto-load memories const dates = ['2025-10-28', '2025-10-29', '2025-10-30']; for (const date of dates) { const dateFolder = path.join(memoriesDir, date); await fs.mkdir(dateFolder, { recursive: true }); const content = `--- name: memory-${date} type: memory description: Memory from ${date} version: 1.0.0 autoLoad: true priority: 1 --- Content from ${date} `; await fs.writeFile(path.join(dateFolder, `memory-${date}.yaml`), content); } // Initialize server await serverStartup.initialize({ skipMigration: true }); // Test passes if no errors thrown expect(true).toBe(true); }); }); });

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