Skip to main content
Glama
e2e-setup.ts14.2 kB
/** * E2E Global Setup: End-to-End Tests Container Management * * This setup file is specifically for E2E tests that: * - REQUIRE real PostgreSQL for database operations * - REQUIRE real Ollama for embedding operations * - Test complete user workflows with real dependencies * * Responsibilities: * - Start PostgreSQL container using docker-compose.test.yml * - Wait for PostgreSQL to be healthy (accept connections) * - Start Ollama container using docker-compose.test.yml * - Wait for Ollama to be healthy (model loaded and responding) * - Run database migrations to prepare schema * - Export connection details for tests to use * - Return teardown function with closure access to container state * * Requirements: 13.9, 13.10, 13.11 * - 13.9: E2E test setup automatically spins up PostgreSQL and Ollama containers * - 13.10: E2E test teardown automatically spins down containers * - 13.11: E2E container setup fails with clear error message if setup fails * * @module __tests__/setup/e2e-setup */ import { existsSync, readFileSync } from "fs"; import { resolve } from "path"; import { DockerComposeWrapper } from "../../containers/index.js"; import { DatabaseConnectionManager } from "../../database/connection-manager.js"; import { SchemaMigrationSystem } from "../../database/schema-migration.js"; /** * Loads environment variables from a .env file. */ function loadEnvFile(envPath: string): void { if (!existsSync(envPath)) { return; } const envContent = readFileSync(envPath, "utf-8"); envContent.split("\n").forEach((line) => { const trimmed = line.trim(); if (trimmed && !trimmed.startsWith("#")) { const [key, ...valueParts] = trimmed.split("="); const value = valueParts.join("="); if (key && value && !process.env[key]) { process.env[key] = value; } } }); } /** * Sets default environment variables for PostgreSQL. */ function setPostgresEnvironment(): void { process.env.DATABASE_URL = process.env.DATABASE_URL ?? "postgresql://thoughtmcp_test:test_password@localhost:5433/thoughtmcp_test"; process.env.DB_HOST = process.env.DB_HOST ?? "localhost"; process.env.DB_PORT = process.env.DB_PORT ?? "5433"; process.env.DB_NAME = process.env.DB_NAME ?? "thoughtmcp_test"; process.env.DB_USER = process.env.DB_USER ?? "thoughtmcp_test"; process.env.DB_PASSWORD = process.env.DB_PASSWORD ?? "test_password"; process.env.DB_POOL_SIZE = process.env.DB_POOL_SIZE ?? "5"; } /** * Sets Ollama environment variables for real embedding operations. */ function setOllamaEnvironment(): void { process.env.OLLAMA_HOST = process.env.OLLAMA_HOST ?? "http://localhost:11435"; process.env.EMBEDDING_MODEL = process.env.EMBEDDING_MODEL ?? "nomic-embed-text"; process.env.EMBEDDING_DIMENSION = process.env.EMBEDDING_DIMENSION ?? "768"; process.env.USE_MOCK_EMBEDDINGS = "false"; } /** * Checks if PostgreSQL container is already running. */ async function isPostgresRunning( composeWrapper: DockerComposeWrapper, composeFile: string ): Promise<boolean> { try { const services = await composeWrapper.ps(composeFile); const postgresService = services.find( (s) => s.name === "postgres-test" || s.name.includes("postgres") ); return postgresService?.status === "running"; } catch { return false; } } /** * Checks if Ollama container is already running. */ async function isOllamaRunning( composeWrapper: DockerComposeWrapper, composeFile: string ): Promise<boolean> { try { const services = await composeWrapper.ps(composeFile); const ollamaService = services.find( (s) => s.name === "ollama-test" || s.name.includes("ollama") ); return ollamaService?.status === "running"; } catch { return false; } } /** * Starts both PostgreSQL and Ollama containers using Docker Compose. */ async function startAllContainers( composeWrapper: DockerComposeWrapper, composeFile: string ): Promise<void> { console.log(" Starting PostgreSQL and Ollama containers..."); await composeWrapper.up(composeFile, { detach: true, wait: true, timeout: 120, }); } /** * Starts only PostgreSQL container using Docker Compose. */ async function startPostgresContainer( composeWrapper: DockerComposeWrapper, composeFile: string ): Promise<void> { console.log(" Starting PostgreSQL container..."); await composeWrapper.up(composeFile, { detach: true, wait: true, timeout: 60, env: { COMPOSE_PROFILES: "postgres" }, }); } /** * Starts only Ollama container using Docker Compose. */ async function startOllamaContainer( composeWrapper: DockerComposeWrapper, composeFile: string ): Promise<void> { console.log(" Starting Ollama container..."); await composeWrapper.up(composeFile, { detach: true, wait: true, timeout: 120, env: { COMPOSE_PROFILES: "ollama" }, }); } /** * Waits for PostgreSQL to be ready to accept connections. */ async function waitForPostgresReady(maxAttempts = 30, intervalMs = 1000): Promise<void> { console.log(" Waiting for PostgreSQL to be ready..."); const dbManager = new DatabaseConnectionManager({ host: process.env.DB_HOST ?? "localhost", port: parseInt(process.env.DB_PORT ?? "5433", 10), database: process.env.DB_NAME ?? "thoughtmcp_test", user: process.env.DB_USER ?? "thoughtmcp_test", password: process.env.DB_PASSWORD ?? "test_password", poolSize: 1, }); for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { await dbManager.connect(); await dbManager.disconnect(); console.log(` ✅ PostgreSQL ready after ${attempt} attempt(s)`); return; } catch { if (attempt === maxAttempts) { throw new Error(`PostgreSQL not ready after ${maxAttempts} attempts`); } await new Promise((resolve) => setTimeout(resolve, intervalMs)); } } } /** * Waits for Ollama to be ready and the model to be available. */ async function waitForOllamaReady(maxAttempts = 60, intervalMs = 2000): Promise<void> { console.log(" Waiting for Ollama to be ready..."); const ollamaHost = process.env.OLLAMA_HOST ?? "http://localhost:11435"; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { const response = await fetch(`${ollamaHost}/api/tags`); if (response.ok) { console.log(` ✅ Ollama API ready after ${attempt} attempt(s)`); return; } } catch { // Ollama not ready yet } if (attempt === maxAttempts) { throw new Error(`Ollama not ready after ${maxAttempts} attempts`); } await new Promise((resolve) => setTimeout(resolve, intervalMs)); } } /** * Ensures the embedding model is available in Ollama. */ async function ensureModelReady(): Promise<void> { const ollamaHost = process.env.OLLAMA_HOST ?? "http://localhost:11435"; const modelName = process.env.EMBEDDING_MODEL ?? "nomic-embed-text"; console.log(` Checking if model '${modelName}' is available...`); try { const tagsResponse = await fetch(`${ollamaHost}/api/tags`); if (!tagsResponse.ok) { throw new Error(`Failed to get model list: ${tagsResponse.statusText}`); } const tagsData = (await tagsResponse.json()) as { models?: Array<{ name: string }> }; const models = tagsData.models ?? []; const modelExists = models.some( (m) => m.name === modelName || m.name.startsWith(`${modelName}:`) ); if (modelExists) { console.log(` ✅ Model '${modelName}' is available`); return; } console.log(` Model '${modelName}' not found, pulling...`); const pullResponse = await fetch(`${ollamaHost}/api/pull`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: modelName }), }); if (!pullResponse.ok) { throw new Error(`Failed to pull model: ${pullResponse.statusText}`); } const reader = pullResponse.body?.getReader(); if (reader) { while (true) { const { done } = await reader.read(); if (done) break; } } console.log(` ✅ Model '${modelName}' pulled successfully`); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.warn(` ⚠️ Could not verify model availability: ${errorMessage}`); } } /** * Runs database migrations. */ async function runMigrations(): Promise<void> { console.log(" Running database migrations..."); const dbManager = new DatabaseConnectionManager({ host: process.env.DB_HOST ?? "localhost", port: parseInt(process.env.DB_PORT ?? "5433", 10), database: process.env.DB_NAME ?? "thoughtmcp_test", user: process.env.DB_USER ?? "thoughtmcp_test", password: process.env.DB_PASSWORD ?? "test_password", poolSize: parseInt(process.env.DB_POOL_SIZE ?? "5", 10), }); try { await dbManager.connect(); const migrationSystem = new SchemaMigrationSystem(dbManager); await migrationSystem.runMigrations(); console.log(" ✅ Database migrations completed"); } finally { await dbManager.disconnect(); } } /** * E2E global setup function. * * Returns a teardown function with closure access to container state, * eliminating the need for file-based state persistence. */ export default async function e2eSetup(): Promise<() => Promise<void>> { console.log("🚀 Starting E2E Test Setup (Real PostgreSQL + Real Ollama)..."); // Set test environment process.env.NODE_ENV = "test"; process.env.TEST_BUCKET = "e2e"; process.env.LOG_LEVEL = process.env.LOG_LEVEL ?? "ERROR"; // Load .env.test file if it exists loadEnvFile(resolve(process.cwd(), ".env.test")); // Set environment variables setPostgresEnvironment(); setOllamaEnvironment(); console.log("✅ Test environment configured"); console.log(` Database: ${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`); console.log(` Ollama: ${process.env.OLLAMA_HOST}`); console.log(` Model: ${process.env.EMBEDDING_MODEL}`); // Check if container management should be skipped (e.g., in CI with pre-started services) const skipContainerManagement = process.env.SKIP_CONTAINER_MANAGEMENT === "true"; // Track which containers we start (used by teardown closure) let postgresStartedBySetup = false; let ollamaStartedBySetup = false; if (skipContainerManagement) { console.log(" ℹ️ Skipping container management (SKIP_CONTAINER_MANAGEMENT=true)"); console.log(" ℹ️ Using externally managed PostgreSQL and Ollama services"); } else { // Initialize container management const composeWrapper = new DockerComposeWrapper(); const composeFile = process.env.TEST_COMPOSE_FILE ?? "docker-compose.test.yml"; // Check if containers are already running const postgresAlreadyRunning = await isPostgresRunning(composeWrapper, composeFile); const ollamaAlreadyRunning = await isOllamaRunning(composeWrapper, composeFile); if (postgresAlreadyRunning) { console.log(" ℹ️ PostgreSQL container already running - reusing"); } if (ollamaAlreadyRunning) { console.log(" ℹ️ Ollama container already running - reusing"); } // Start containers as needed try { if (!postgresAlreadyRunning && !ollamaAlreadyRunning) { await startAllContainers(composeWrapper, composeFile); postgresStartedBySetup = true; ollamaStartedBySetup = true; console.log(" ✅ Both containers started"); } else if (!postgresAlreadyRunning) { await startPostgresContainer(composeWrapper, composeFile); postgresStartedBySetup = true; console.log(" ✅ PostgreSQL container started"); } else if (!ollamaAlreadyRunning) { await startOllamaContainer(composeWrapper, composeFile); ollamaStartedBySetup = true; console.log(" ✅ Ollama container started"); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error("❌ E2E container setup failed:", errorMessage); console.error(" Please ensure Docker is running and try again."); console.error(" You can also start containers manually:"); console.error(" docker compose -f docker-compose.test.yml up -d"); throw new Error(`E2E container setup failed: ${errorMessage}`); } } // Wait for services and run migrations await waitForPostgresReady(); await runMigrations(); await waitForOllamaReady(); await ensureModelReady(); console.log("✅ E2E setup complete"); console.log(" PostgreSQL: ready"); console.log(" Ollama: ready"); // Return teardown function with closure access to container state return async () => { console.log("\n🧹 Starting E2E Test Teardown..."); if (skipContainerManagement) { console.log(" Skipping container teardown (SKIP_CONTAINER_MANAGEMENT=true)"); console.log("✅ E2E teardown complete\n"); return; } if (process.env.KEEP_CONTAINERS_RUNNING === "true") { console.log(" Keeping containers running (KEEP_CONTAINERS_RUNNING=true)"); console.log("✅ E2E teardown complete\n"); return; } if (!postgresStartedBySetup) { console.log(" PostgreSQL was already running - leaving it running"); } if (!ollamaStartedBySetup) { console.log(" Ollama was already running - leaving it running"); } if (postgresStartedBySetup || ollamaStartedBySetup) { const composeWrapper = new DockerComposeWrapper(); const composeFile = process.env.TEST_COMPOSE_FILE ?? "docker-compose.test.yml"; try { console.log(" Stopping containers..."); await composeWrapper.down(composeFile, { volumes: true, timeout: 15 }); console.log(" ✅ Containers stopped and volumes removed"); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(` ⚠️ Failed to stop containers: ${errorMessage}`); } } console.log("✅ E2E teardown complete\n"); }; }

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/keyurgolani/ThoughtMcp'

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