/**
* 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);
});
});
});