server-initialization.test.js•11.9 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { LettaServer } from '../../core/server.js';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import axios from 'axios';
import { createLogger } from '../../core/logger.js';
// Mock dependencies
vi.mock('@modelcontextprotocol/sdk/server/index.js');
vi.mock('axios');
vi.mock('../../core/logger.js');
describe('LettaServer Initialization (LMP-82)', () => {
let originalEnv;
let mockLogger;
let mockMCPServer;
let mockAxiosInstance;
beforeEach(() => {
// Save original env vars
originalEnv = {
LETTA_BASE_URL: process.env.LETTA_BASE_URL,
LETTA_PASSWORD: process.env.LETTA_PASSWORD,
};
// Set up mock logger
mockLogger = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
child: vi.fn().mockReturnThis(),
};
createLogger.mockReturnValue(mockLogger);
// Set up mock MCP server
mockMCPServer = {
setRequestHandler: vi.fn(),
onerror: null,
connect: vi.fn(),
close: vi.fn(),
};
Server.mockImplementation(() => mockMCPServer);
// Set up mock axios instance
mockAxiosInstance = {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
patch: vi.fn(),
delete: vi.fn(),
request: vi.fn(),
};
axios.create.mockReturnValue(mockAxiosInstance);
});
afterEach(() => {
// Restore original env vars
process.env.LETTA_BASE_URL = originalEnv.LETTA_BASE_URL;
process.env.LETTA_PASSWORD = originalEnv.LETTA_PASSWORD;
// Clear all mocks
vi.clearAllMocks();
});
describe('Environment Variable Validation', () => {
it('should throw error when LETTA_BASE_URL is not set', () => {
delete process.env.LETTA_BASE_URL;
process.env.LETTA_PASSWORD = 'test-password';
expect(() => {
new LettaServer();
}).toThrow('Missing required environment variable: LETTA_BASE_URL');
});
it('should throw error when LETTA_BASE_URL is empty string', () => {
process.env.LETTA_BASE_URL = '';
process.env.LETTA_PASSWORD = 'test-password';
expect(() => {
new LettaServer();
}).toThrow('Missing required environment variable: LETTA_BASE_URL');
});
it('should not throw error when LETTA_PASSWORD is not set', () => {
process.env.LETTA_BASE_URL = 'https://test.letta.com';
delete process.env.LETTA_PASSWORD;
expect(() => {
new LettaServer();
}).not.toThrow();
});
it('should use empty string for password when not set', () => {
process.env.LETTA_BASE_URL = 'https://test.letta.com';
delete process.env.LETTA_PASSWORD;
const server = new LettaServer();
expect(server.password).toBe('');
});
});
describe('MCP Server Initialization', () => {
beforeEach(() => {
process.env.LETTA_BASE_URL = 'https://test.letta.com';
process.env.LETTA_PASSWORD = 'test-password';
});
it('should create MCP server with correct configuration', () => {
new LettaServer();
expect(Server).toHaveBeenCalledWith(
{
name: 'letta-server',
version: '0.1.0',
},
{
capabilities: {
tools: {
listChanged: true,
},
prompts: {
listChanged: true,
},
resources: {
subscribe: true,
listChanged: true,
},
},
},
);
});
it('should set error handler on MCP server', () => {
new LettaServer();
const testError = new Error('Test error');
// Trigger the error handler
mockMCPServer.onerror(testError);
expect(mockLogger.error).toHaveBeenCalledWith('MCP Error', { error: testError });
});
it('should store reference to MCP server instance', () => {
const server = new LettaServer();
expect(server.server).toBe(mockMCPServer);
});
});
describe('Logger Initialization', () => {
beforeEach(() => {
process.env.LETTA_BASE_URL = 'https://test.letta.com';
process.env.LETTA_PASSWORD = 'test-password';
});
it('should create logger with correct module name', () => {
new LettaServer();
expect(createLogger).toHaveBeenCalledWith('LettaServer');
});
it('should store logger reference', () => {
const server = new LettaServer();
expect(server.logger).toBe(mockLogger);
});
});
describe('API Client Configuration', () => {
it('should append /v1 to base URL if not present', () => {
process.env.LETTA_BASE_URL = 'https://test.letta.com';
process.env.LETTA_PASSWORD = 'test-password';
const server = new LettaServer();
expect(axios.create).toHaveBeenCalledWith({
baseURL: 'https://test.letta.com/v1',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
expect(server.apiBase).toBe('https://test.letta.com/v1');
});
it('should not duplicate /v1 if already present', () => {
process.env.LETTA_BASE_URL = 'https://test.letta.com/v1';
process.env.LETTA_PASSWORD = 'test-password';
new LettaServer();
expect(axios.create).toHaveBeenCalledWith({
baseURL: 'https://test.letta.com/v1/v1',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
});
it('should create axios instance with correct default headers', () => {
process.env.LETTA_BASE_URL = 'https://test.letta.com';
process.env.LETTA_PASSWORD = 'test-password';
new LettaServer();
expect(axios.create).toHaveBeenCalledWith(
expect.objectContaining({
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
}),
);
});
it('should store axios instance reference', () => {
process.env.LETTA_BASE_URL = 'https://test.letta.com';
process.env.LETTA_PASSWORD = 'test-password';
const server = new LettaServer();
expect(server.api).toBe(mockAxiosInstance);
});
});
describe('Property Initialization', () => {
beforeEach(() => {
process.env.LETTA_BASE_URL = 'https://test.letta.com';
process.env.LETTA_PASSWORD = 'test-password';
});
it('should initialize all required properties', () => {
const server = new LettaServer();
expect(server).toHaveProperty('logger');
expect(server).toHaveProperty('server');
expect(server).toHaveProperty('apiBase');
expect(server).toHaveProperty('password');
expect(server).toHaveProperty('api');
});
it('should store password from environment', () => {
const server = new LettaServer();
expect(server.password).toBe('test-password');
});
it('should handle special characters in password', () => {
process.env.LETTA_PASSWORD = 'p@$$w0rd!#$%^&*()';
const server = new LettaServer();
expect(server.password).toBe('p@$$w0rd!#$%^&*()');
});
});
describe('Edge Cases', () => {
it('should handle malformed URLs gracefully', () => {
process.env.LETTA_BASE_URL = 'not-a-valid-url';
process.env.LETTA_PASSWORD = 'test-password';
// Should not throw during construction
expect(() => {
new LettaServer();
}).not.toThrow();
// axios.create should still be called with the malformed URL
expect(axios.create).toHaveBeenCalledWith(
expect.objectContaining({
baseURL: 'not-a-valid-url/v1',
}),
);
});
it('should handle very long passwords', () => {
const longPassword = 'a'.repeat(1000);
process.env.LETTA_BASE_URL = 'https://test.letta.com';
process.env.LETTA_PASSWORD = longPassword;
const server = new LettaServer();
expect(server.password).toBe(longPassword);
});
it('should handle URLs with trailing slashes', () => {
process.env.LETTA_BASE_URL = 'https://test.letta.com/';
process.env.LETTA_PASSWORD = 'test-password';
const server = new LettaServer();
expect(server.apiBase).toBe('https://test.letta.com//v1');
});
});
describe('Multiple Instance Creation', () => {
beforeEach(() => {
process.env.LETTA_BASE_URL = 'https://test.letta.com';
process.env.LETTA_PASSWORD = 'test-password';
});
it('should allow creating multiple server instances', () => {
// Reset mocks to return different instances
const mockMCPServer2 = {
setRequestHandler: vi.fn(),
onerror: null,
connect: vi.fn(),
close: vi.fn(),
};
const mockAxiosInstance2 = {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
patch: vi.fn(),
delete: vi.fn(),
request: vi.fn(),
};
const mockLogger2 = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
child: vi.fn().mockReturnThis(),
};
Server.mockImplementationOnce(() => mockMCPServer);
Server.mockImplementationOnce(() => mockMCPServer2);
axios.create.mockReturnValueOnce(mockAxiosInstance);
axios.create.mockReturnValueOnce(mockAxiosInstance2);
createLogger.mockReturnValueOnce(mockLogger);
createLogger.mockReturnValueOnce(mockLogger2);
const server1 = new LettaServer();
const server2 = new LettaServer();
expect(server1).not.toBe(server2);
expect(server1.server).toBe(mockMCPServer);
expect(server2.server).toBe(mockMCPServer2);
expect(server1.api).toBe(mockAxiosInstance);
expect(server2.api).toBe(mockAxiosInstance2);
expect(server1.logger).toBe(mockLogger);
expect(server2.logger).toBe(mockLogger2);
});
it('should create separate MCP server instances', () => {
new LettaServer();
new LettaServer();
expect(Server).toHaveBeenCalledTimes(2);
});
it('should create separate axios instances', () => {
new LettaServer();
new LettaServer();
expect(axios.create).toHaveBeenCalledTimes(2);
});
});
});