Skip to main content
Glama
test-container-manager.test.ts33 kB
/** * TestContainerManager Unit Tests * * Tests for the TestContainerManager that orchestrates container lifecycle. * These tests mock all dependencies to test the orchestration logic * without requiring Docker to be installed. * * @module __tests__/unit/containers/test-container-manager.test * * _Requirements: 1.1, 1.2, 1.3, 1.5, 2.1, 2.2, 2.3, 2.4_ */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { TestContainerManager } from "../../../containers/test-container-manager"; import type { ContainerManagerConfig, DockerAvailability, IContainerLogger, IDockerComposeWrapper, IEnvironmentConfigurator, IPortAllocator, ServiceStatus, TestEnvironmentConfig, } from "../../../containers/types"; describe("TestContainerManager", () => { // Mock dependencies let mockComposeWrapper: IDockerComposeWrapper; let mockPortAllocator: IPortAllocator; let mockEnvConfigurator: IEnvironmentConfigurator; let mockLogger: IContainerLogger; // Default test configuration const defaultConfig: Partial<ContainerManagerConfig> = { autoStart: true, startupTimeout: 60, keepRunning: false, preserveData: false, isCI: false, composeFile: "docker-compose.test.yml", projectName: "thoughtmcp-test", postgres: { defaultPort: 5433, portRangeStart: 5434, portRangeEnd: 5500, serviceName: "postgres-test", }, ollama: { defaultPort: 11435, portRangeStart: 11436, portRangeEnd: 11500, serviceName: "ollama-test", embeddingModel: "nomic-embed-text", modelPullTimeout: 300, }, }; // Default mock environment config const defaultEnvConfig: TestEnvironmentConfig = { databaseUrl: "postgresql://thoughtmcp_test:test_password@localhost:5433/thoughtmcp_test", dbHost: "localhost", dbPort: 5433, dbName: "thoughtmcp_test", dbUser: "thoughtmcp_test", dbPassword: "test_password", ollamaHost: "http://localhost:11435", ollamaPort: 11435, }; beforeEach(() => { // Create mock implementations mockComposeWrapper = { isAvailable: vi.fn(), up: vi.fn(), down: vi.fn(), ps: vi.fn(), isServiceHealthy: vi.fn(), logs: vi.fn(), exec: vi.fn(), }; mockPortAllocator = { isPortAvailable: vi.fn(), findAvailablePort: vi.fn(), reservePort: vi.fn(), releasePort: vi.fn(), }; mockEnvConfigurator = { configureFromServices: vi.fn(), configurePostgres: vi.fn(), configureOllama: vi.fn(), getConfiguration: vi.fn().mockReturnValue(defaultEnvConfig), reset: vi.fn(), }; mockLogger = { logStarting: vi.fn(), logHealthCheck: vi.fn(), logReady: vi.fn(), logError: vi.fn(), logStopped: vi.fn(), }; }); afterEach(() => { vi.clearAllMocks(); }); // Helper to create manager with default mocks function createManager(config?: Partial<ContainerManagerConfig>): TestContainerManager { return new TestContainerManager( mockComposeWrapper, mockPortAllocator, mockEnvConfigurator, mockLogger, { ...defaultConfig, ...config } ); } // Helper to mock Docker available function mockDockerAvailable(): void { (mockComposeWrapper.isAvailable as ReturnType<typeof vi.fn>).mockResolvedValue({ available: true, dockerVersion: "24.0.0", composeVersion: "2.20.0", } as DockerAvailability); } // Helper to mock Docker unavailable function mockDockerUnavailable(error: string, suggestion?: string): void { (mockComposeWrapper.isAvailable as ReturnType<typeof vi.fn>).mockResolvedValue({ available: false, error, suggestion, } as DockerAvailability); } // Helper to mock healthy services function mockHealthyServices(): void { const services: ServiceStatus[] = [ { name: "postgres-test", status: "running", health: "healthy", ports: [{ internal: 5432, external: 5433, protocol: "tcp" }], }, { name: "ollama-test", status: "running", health: "healthy", ports: [{ internal: 11434, external: 11435, protocol: "tcp" }], }, ]; (mockComposeWrapper.ps as ReturnType<typeof vi.fn>).mockResolvedValue(services); (mockComposeWrapper.isServiceHealthy as ReturnType<typeof vi.fn>).mockResolvedValue(true); // Mock embedding model already available (mockComposeWrapper.exec as ReturnType<typeof vi.fn>).mockResolvedValue({ exitCode: 0, stdout: "NAME ID SIZE MODIFIED\nnomic-embed-text:latest abc123 274 MB 2 days ago", stderr: "", }); } // Helper to mock no running services function mockNoRunningServices(): void { (mockComposeWrapper.ps as ReturnType<typeof vi.fn>).mockResolvedValue([]); // Mock embedding model check - model already available (default behavior) (mockComposeWrapper.exec as ReturnType<typeof vi.fn>).mockResolvedValue({ exitCode: 0, stdout: "NAME ID SIZE MODIFIED\nnomic-embed-text:latest abc123 274 MB 2 days ago", stderr: "", }); } // Helper to mock ports available function mockPortsAvailable(): void { (mockPortAllocator.isPortAvailable as ReturnType<typeof vi.fn>).mockResolvedValue(true); } describe("startContainers", () => { /** * Requirement 1.1: WHEN the test suite starts THEN the Test Container Manager * SHALL check if required containers are already running */ it("should check for existing running containers", async () => { mockDockerAvailable(); mockHealthyServices(); mockPortsAvailable(); const manager = createManager(); await manager.startContainers(); expect(mockComposeWrapper.ps).toHaveBeenCalledWith("docker-compose.test.yml"); }); /** * Requirement 1.5: WHEN containers are already running on expected ports * THEN the Test Container Manager SHALL reuse the existing containers */ it("should reuse existing healthy containers", async () => { mockDockerAvailable(); mockHealthyServices(); const manager = createManager(); const containers = await manager.startContainers(); // Should not call up since containers are already running expect(mockComposeWrapper.up).not.toHaveBeenCalled(); // Should configure environment from existing services expect(mockEnvConfigurator.configureFromServices).toHaveBeenCalled(); // Should return container info with startedByManager = false expect(containers).toHaveLength(2); expect(containers[0].startedByManager).toBe(false); expect(containers[1].startedByManager).toBe(false); }); /** * Requirement 1.2: WHEN required containers are not running * THEN the Test Container Manager SHALL start the containers using Docker */ it("should start containers when none are running", async () => { mockDockerAvailable(); mockNoRunningServices(); mockPortsAvailable(); (mockComposeWrapper.up as ReturnType<typeof vi.fn>).mockResolvedValue(undefined); // After up, return healthy services (mockComposeWrapper.ps as ReturnType<typeof vi.fn>) .mockResolvedValueOnce([]) // First call - no services .mockResolvedValueOnce([ // After up { name: "postgres-test", status: "running", health: "healthy", ports: [{ internal: 5432, external: 5433, protocol: "tcp" }], }, { name: "ollama-test", status: "running", health: "healthy", ports: [{ internal: 11434, external: 11435, protocol: "tcp" }], }, ]); (mockComposeWrapper.isServiceHealthy as ReturnType<typeof vi.fn>).mockResolvedValue(true); const manager = createManager(); const containers = await manager.startContainers(); expect(mockComposeWrapper.up).toHaveBeenCalledWith( "docker-compose.test.yml", expect.objectContaining({ detach: true, wait: true, timeout: 60, }) ); expect(containers).toHaveLength(2); expect(containers[0].startedByManager).toBe(true); }); /** * Requirement 1.3: WHEN containers are started THEN the Test Container Manager * SHALL wait for health checks to pass before allowing tests to proceed */ it("should wait for health checks via docker compose --wait", async () => { mockDockerAvailable(); mockNoRunningServices(); mockPortsAvailable(); (mockComposeWrapper.up as ReturnType<typeof vi.fn>).mockResolvedValue(undefined); (mockComposeWrapper.ps as ReturnType<typeof vi.fn>) .mockResolvedValueOnce([]) .mockResolvedValueOnce([ { name: "postgres-test", status: "running", health: "healthy", ports: [{ internal: 5432, external: 5433, protocol: "tcp" }], }, { name: "ollama-test", status: "running", health: "healthy", ports: [{ internal: 11434, external: 11435, protocol: "tcp" }], }, ]); (mockComposeWrapper.isServiceHealthy as ReturnType<typeof vi.fn>).mockResolvedValue(true); const manager = createManager(); await manager.startContainers(); expect(mockComposeWrapper.up).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ wait: true }) ); }); it("should throw error when health checks fail", async () => { mockDockerAvailable(); mockNoRunningServices(); mockPortsAvailable(); (mockComposeWrapper.up as ReturnType<typeof vi.fn>).mockResolvedValue(undefined); (mockComposeWrapper.ps as ReturnType<typeof vi.fn>) .mockResolvedValueOnce([]) .mockResolvedValueOnce([ { name: "postgres-test", status: "running", health: "unhealthy", ports: [], }, ]); (mockComposeWrapper.isServiceHealthy as ReturnType<typeof vi.fn>).mockResolvedValue(false); const manager = createManager(); await expect(manager.startContainers()).rejects.toThrow("Health checks failed"); expect(mockLogger.logError).toHaveBeenCalled(); }); it("should skip startup when AUTO_START_CONTAINERS is false", async () => { const manager = createManager({ autoStart: false }); const containers = await manager.startContainers(); expect(mockComposeWrapper.isAvailable).not.toHaveBeenCalled(); expect(mockComposeWrapper.up).not.toHaveBeenCalled(); expect(containers).toEqual([]); }); it("should throw error when Docker is unavailable", async () => { mockDockerUnavailable("Docker is not installed", "Install Docker Desktop"); const manager = createManager(); await expect(manager.startContainers()).rejects.toThrow("Docker is not installed"); expect(mockLogger.logError).toHaveBeenCalledWith( "docker", "Docker is not installed", "Install Docker Desktop" ); }); it("should allocate dynamic ports when defaults are occupied", async () => { mockDockerAvailable(); mockNoRunningServices(); // First port check fails (occupied), second succeeds (mockPortAllocator.isPortAvailable as ReturnType<typeof vi.fn>) .mockResolvedValueOnce(false) // postgres default port occupied .mockResolvedValueOnce(true); // ollama default port available (mockPortAllocator.findAvailablePort as ReturnType<typeof vi.fn>).mockResolvedValue(5434); (mockComposeWrapper.up as ReturnType<typeof vi.fn>).mockResolvedValue(undefined); (mockComposeWrapper.ps as ReturnType<typeof vi.fn>) .mockResolvedValueOnce([]) .mockResolvedValueOnce([ { name: "postgres-test", status: "running", health: "healthy", ports: [{ internal: 5432, external: 5434, protocol: "tcp" }], }, { name: "ollama-test", status: "running", health: "healthy", ports: [{ internal: 11434, external: 11435, protocol: "tcp" }], }, ]); (mockComposeWrapper.isServiceHealthy as ReturnType<typeof vi.fn>).mockResolvedValue(true); const manager = createManager(); await manager.startContainers(); expect(mockPortAllocator.findAvailablePort).toHaveBeenCalledWith(5434, 5500); expect(mockPortAllocator.reservePort).toHaveBeenCalledWith(5434); expect(mockComposeWrapper.up).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ env: expect.objectContaining({ TEST_DB_PORT: "5434" }), }) ); }); it("should log ready status with connection details", async () => { mockDockerAvailable(); mockNoRunningServices(); mockPortsAvailable(); (mockComposeWrapper.up as ReturnType<typeof vi.fn>).mockResolvedValue(undefined); (mockComposeWrapper.ps as ReturnType<typeof vi.fn>) .mockResolvedValueOnce([]) .mockResolvedValueOnce([ { name: "postgres-test", status: "running", health: "healthy", ports: [{ internal: 5432, external: 5433, protocol: "tcp" }], }, { name: "ollama-test", status: "running", health: "healthy", ports: [{ internal: 11434, external: 11435, protocol: "tcp" }], }, ]); (mockComposeWrapper.isServiceHealthy as ReturnType<typeof vi.fn>).mockResolvedValue(true); const manager = createManager(); await manager.startContainers(); expect(mockLogger.logReady).toHaveBeenCalledWith( "postgres", expect.stringContaining("localhost:5433") ); expect(mockLogger.logReady).toHaveBeenCalledWith( "ollama", expect.stringContaining("http://localhost:11435") ); }); it("should check for embedding model and pull if not available", async () => { mockDockerAvailable(); mockNoRunningServices(); mockPortsAvailable(); (mockComposeWrapper.up as ReturnType<typeof vi.fn>).mockResolvedValue(undefined); // First exec call: model not found, second: pull succeeds (mockComposeWrapper.exec as ReturnType<typeof vi.fn>) .mockResolvedValueOnce({ exitCode: 0, stdout: "NAME ID SIZE MODIFIED\n", stderr: "", }) .mockResolvedValueOnce({ exitCode: 0, stdout: "pulling manifest\npulling abc123\nverifying sha256\nwriting manifest\nsuccess", stderr: "", }); (mockComposeWrapper.ps as ReturnType<typeof vi.fn>) .mockResolvedValueOnce([]) .mockResolvedValueOnce([ { name: "postgres-test", status: "running", health: "healthy", ports: [{ internal: 5432, external: 5433, protocol: "tcp" }], }, { name: "ollama-test", status: "running", health: "healthy", ports: [{ internal: 11434, external: 11435, protocol: "tcp" }], }, ]); (mockComposeWrapper.isServiceHealthy as ReturnType<typeof vi.fn>).mockResolvedValue(true); const manager = createManager(); await manager.startContainers(); // Should have called exec twice: once to list, once to pull expect(mockComposeWrapper.exec).toHaveBeenCalledTimes(2); expect(mockComposeWrapper.exec).toHaveBeenCalledWith( "docker-compose.test.yml", "ollama-test", ["ollama", "list"] ); expect(mockComposeWrapper.exec).toHaveBeenCalledWith( "docker-compose.test.yml", "ollama-test", ["ollama", "pull", "nomic-embed-text"] ); expect(mockLogger.logHealthCheck).toHaveBeenCalledWith( "ollama", expect.stringContaining("Pulling embedding model") ); }); it("should skip model pull if model already available", async () => { mockDockerAvailable(); mockHealthyServices(); const manager = createManager(); await manager.startContainers(); // Should have called exec only once to check model availability expect(mockComposeWrapper.exec).toHaveBeenCalledTimes(1); expect(mockComposeWrapper.exec).toHaveBeenCalledWith( "docker-compose.test.yml", "ollama-test", ["ollama", "list"] ); expect(mockLogger.logHealthCheck).toHaveBeenCalledWith( "ollama", expect.stringContaining("already available") ); }); it("should throw error if model pull fails", async () => { mockDockerAvailable(); mockNoRunningServices(); mockPortsAvailable(); (mockComposeWrapper.up as ReturnType<typeof vi.fn>).mockResolvedValue(undefined); // First exec call: model not found, second: pull fails (mockComposeWrapper.exec as ReturnType<typeof vi.fn>) .mockResolvedValueOnce({ exitCode: 0, stdout: "NAME ID SIZE MODIFIED\n", stderr: "", }) .mockResolvedValueOnce({ exitCode: 1, stdout: "", stderr: "Error: pull model manifest: file does not exist", }); (mockComposeWrapper.ps as ReturnType<typeof vi.fn>) .mockResolvedValueOnce([]) .mockResolvedValueOnce([ { name: "postgres-test", status: "running", health: "healthy", ports: [{ internal: 5432, external: 5433, protocol: "tcp" }], }, { name: "ollama-test", status: "running", health: "healthy", ports: [{ internal: 11434, external: 11435, protocol: "tcp" }], }, ]); (mockComposeWrapper.isServiceHealthy as ReturnType<typeof vi.fn>).mockResolvedValue(true); const manager = createManager(); await expect(manager.startContainers()).rejects.toThrow( "Failed to pull embedding model nomic-embed-text" ); expect(mockLogger.logError).toHaveBeenCalledWith( "ollama", expect.stringContaining("Failed to pull model"), expect.any(String) ); }); }); describe("stopContainers", () => { /** * Requirement 2.1: WHEN all tests complete successfully * THEN the Test Container Manager SHALL stop containers that it started */ it("should stop containers that were started by manager", async () => { mockDockerAvailable(); mockNoRunningServices(); mockPortsAvailable(); (mockComposeWrapper.up as ReturnType<typeof vi.fn>).mockResolvedValue(undefined); (mockComposeWrapper.down as ReturnType<typeof vi.fn>).mockResolvedValue(undefined); (mockComposeWrapper.ps as ReturnType<typeof vi.fn>) .mockResolvedValueOnce([]) .mockResolvedValueOnce([ { name: "postgres-test", status: "running", health: "healthy", ports: [{ internal: 5432, external: 5433, protocol: "tcp" }], }, { name: "ollama-test", status: "running", health: "healthy", ports: [{ internal: 11434, external: 11435, protocol: "tcp" }], }, ]); (mockComposeWrapper.isServiceHealthy as ReturnType<typeof vi.fn>).mockResolvedValue(true); const manager = createManager(); await manager.startContainers(); await manager.stopContainers(); expect(mockComposeWrapper.down).toHaveBeenCalledWith( "docker-compose.test.yml", expect.objectContaining({ volumes: true }) ); }); /** * Requirement 2.4: WHEN containers were pre-existing (not started by Test Container Manager) * THEN the Test Container Manager SHALL leave those containers running */ it("should not stop pre-existing containers", async () => { mockDockerAvailable(); mockHealthyServices(); const manager = createManager(); await manager.startContainers(); // Reuses existing await manager.stopContainers(); expect(mockComposeWrapper.down).not.toHaveBeenCalled(); expect(mockLogger.logStopped).toHaveBeenCalledWith( expect.stringContaining("none started by manager") ); }); /** * Requirement 6.3: WHEN the environment variable KEEP_CONTAINERS_RUNNING is set to "true" * THEN the Test Container Manager SHALL not stop containers after tests complete */ it("should not stop containers when KEEP_CONTAINERS_RUNNING is true", async () => { mockDockerAvailable(); mockNoRunningServices(); mockPortsAvailable(); (mockComposeWrapper.up as ReturnType<typeof vi.fn>).mockResolvedValue(undefined); (mockComposeWrapper.ps as ReturnType<typeof vi.fn>) .mockResolvedValueOnce([]) .mockResolvedValueOnce([ { name: "postgres-test", status: "running", health: "healthy", ports: [{ internal: 5432, external: 5433, protocol: "tcp" }], }, { name: "ollama-test", status: "running", health: "healthy", ports: [{ internal: 11434, external: 11435, protocol: "tcp" }], }, ]); (mockComposeWrapper.isServiceHealthy as ReturnType<typeof vi.fn>).mockResolvedValue(true); const manager = createManager({ keepRunning: true }); await manager.startContainers(); await manager.stopContainers(); expect(mockComposeWrapper.down).not.toHaveBeenCalled(); expect(mockLogger.logStopped).toHaveBeenCalledWith(expect.stringContaining("kept running")); }); /** * Requirement 2.3: WHEN the Test Container Manager stops containers * THEN the Test Container Manager SHALL remove the containers to free resources */ it("should remove volumes when preserveData is false", async () => { mockDockerAvailable(); mockNoRunningServices(); mockPortsAvailable(); (mockComposeWrapper.up as ReturnType<typeof vi.fn>).mockResolvedValue(undefined); (mockComposeWrapper.down as ReturnType<typeof vi.fn>).mockResolvedValue(undefined); (mockComposeWrapper.ps as ReturnType<typeof vi.fn>) .mockResolvedValueOnce([]) .mockResolvedValueOnce([ { name: "postgres-test", status: "running", health: "healthy", ports: [{ internal: 5432, external: 5433, protocol: "tcp" }], }, { name: "ollama-test", status: "running", health: "healthy", ports: [{ internal: 11434, external: 11435, protocol: "tcp" }], }, ]); (mockComposeWrapper.isServiceHealthy as ReturnType<typeof vi.fn>).mockResolvedValue(true); const manager = createManager({ preserveData: false }); await manager.startContainers(); await manager.stopContainers(); expect(mockComposeWrapper.down).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ volumes: true }) ); }); it("should preserve volumes when preserveData is true", async () => { mockDockerAvailable(); mockNoRunningServices(); mockPortsAvailable(); (mockComposeWrapper.up as ReturnType<typeof vi.fn>).mockResolvedValue(undefined); (mockComposeWrapper.down as ReturnType<typeof vi.fn>).mockResolvedValue(undefined); (mockComposeWrapper.ps as ReturnType<typeof vi.fn>) .mockResolvedValueOnce([]) .mockResolvedValueOnce([ { name: "postgres-test", status: "running", health: "healthy", ports: [{ internal: 5432, external: 5433, protocol: "tcp" }], }, { name: "ollama-test", status: "running", health: "healthy", ports: [{ internal: 11434, external: 11435, protocol: "tcp" }], }, ]); (mockComposeWrapper.isServiceHealthy as ReturnType<typeof vi.fn>).mockResolvedValue(true); const manager = createManager({ preserveData: true }); await manager.startContainers(); await manager.stopContainers(); expect(mockComposeWrapper.down).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ volumes: false }) ); }); it("should release allocated ports on cleanup", async () => { mockDockerAvailable(); mockNoRunningServices(); (mockPortAllocator.isPortAvailable as ReturnType<typeof vi.fn>) .mockResolvedValueOnce(false) .mockResolvedValueOnce(false); (mockPortAllocator.findAvailablePort as ReturnType<typeof vi.fn>) .mockResolvedValueOnce(5434) .mockResolvedValueOnce(11436); (mockComposeWrapper.up as ReturnType<typeof vi.fn>).mockResolvedValue(undefined); (mockComposeWrapper.down as ReturnType<typeof vi.fn>).mockResolvedValue(undefined); (mockComposeWrapper.ps as ReturnType<typeof vi.fn>) .mockResolvedValueOnce([]) .mockResolvedValueOnce([ { name: "postgres-test", status: "running", health: "healthy", ports: [{ internal: 5432, external: 5434, protocol: "tcp" }], }, { name: "ollama-test", status: "running", health: "healthy", ports: [{ internal: 11434, external: 11436, protocol: "tcp" }], }, ]); (mockComposeWrapper.isServiceHealthy as ReturnType<typeof vi.fn>).mockResolvedValue(true); const manager = createManager(); await manager.startContainers(); await manager.stopContainers(); expect(mockPortAllocator.releasePort).toHaveBeenCalledWith(5434); expect(mockPortAllocator.releasePort).toHaveBeenCalledWith(11436); }); it("should reset environment configuration on cleanup", async () => { mockDockerAvailable(); mockNoRunningServices(); mockPortsAvailable(); (mockComposeWrapper.up as ReturnType<typeof vi.fn>).mockResolvedValue(undefined); (mockComposeWrapper.down as ReturnType<typeof vi.fn>).mockResolvedValue(undefined); (mockComposeWrapper.ps as ReturnType<typeof vi.fn>) .mockResolvedValueOnce([]) .mockResolvedValueOnce([ { name: "postgres-test", status: "running", health: "healthy", ports: [{ internal: 5432, external: 5433, protocol: "tcp" }], }, { name: "ollama-test", status: "running", health: "healthy", ports: [{ internal: 11434, external: 11435, protocol: "tcp" }], }, ]); (mockComposeWrapper.isServiceHealthy as ReturnType<typeof vi.fn>).mockResolvedValue(true); const manager = createManager(); await manager.startContainers(); await manager.stopContainers(); expect(mockEnvConfigurator.reset).toHaveBeenCalled(); }); /** * Requirement 2.2: WHEN tests fail or are interrupted * THEN the Test Container Manager SHALL still stop containers that it started */ it("should handle cleanup errors gracefully", async () => { mockDockerAvailable(); mockNoRunningServices(); mockPortsAvailable(); (mockComposeWrapper.up as ReturnType<typeof vi.fn>).mockResolvedValue(undefined); (mockComposeWrapper.down as ReturnType<typeof vi.fn>).mockRejectedValue( new Error("Cleanup failed") ); (mockComposeWrapper.ps as ReturnType<typeof vi.fn>) .mockResolvedValueOnce([]) .mockResolvedValueOnce([ { name: "postgres-test", status: "running", health: "healthy", ports: [{ internal: 5432, external: 5433, protocol: "tcp" }], }, { name: "ollama-test", status: "running", health: "healthy", ports: [{ internal: 11434, external: 11435, protocol: "tcp" }], }, ]); (mockComposeWrapper.isServiceHealthy as ReturnType<typeof vi.fn>).mockResolvedValue(true); const manager = createManager(); await manager.startContainers(); // Should not throw, cleanup is best-effort await expect(manager.stopContainers()).resolves.not.toThrow(); expect(mockLogger.logError).toHaveBeenCalledWith( "containers", expect.stringContaining("Failed to stop") ); }); }); describe("getStatus", () => { it("should return null containers when not initialized", () => { const manager = createManager(); const status = manager.getStatus(); expect(status.postgres).toBeNull(); expect(status.ollama).toBeNull(); expect(status.isReady).toBe(false); expect(status.startTime).toBeNull(); }); it("should return container info when initialized", async () => { mockDockerAvailable(); mockHealthyServices(); const manager = createManager(); await manager.startContainers(); const status = manager.getStatus(); expect(status.postgres).not.toBeNull(); expect(status.postgres?.service).toBe("postgres-test"); expect(status.postgres?.port).toBe(5433); expect(status.ollama).not.toBeNull(); expect(status.ollama?.service).toBe("ollama-test"); expect(status.isReady).toBe(true); expect(status.startTime).toBeInstanceOf(Date); }); it("should indicate startedByManager correctly", async () => { mockDockerAvailable(); mockHealthyServices(); const manager = createManager(); await manager.startContainers(); // Reuses existing const status = manager.getStatus(); expect(status.postgres?.startedByManager).toBe(false); expect(status.ollama?.startedByManager).toBe(false); }); }); describe("isHealthy", () => { it("should return false when not initialized", async () => { const manager = createManager(); const healthy = await manager.isHealthy(); expect(healthy).toBe(false); }); it("should return true when all services are healthy", async () => { mockDockerAvailable(); mockHealthyServices(); const manager = createManager(); await manager.startContainers(); (mockComposeWrapper.isServiceHealthy as ReturnType<typeof vi.fn>).mockResolvedValue(true); const healthy = await manager.isHealthy(); expect(healthy).toBe(true); }); it("should return false when any service is unhealthy", async () => { mockDockerAvailable(); mockHealthyServices(); const manager = createManager(); await manager.startContainers(); (mockComposeWrapper.isServiceHealthy as ReturnType<typeof vi.fn>) .mockResolvedValueOnce(true) // postgres healthy .mockResolvedValueOnce(false); // ollama unhealthy const healthy = await manager.isHealthy(); expect(healthy).toBe(false); }); }); describe("getConfig and getState", () => { it("should return current configuration", () => { const manager = createManager({ startupTimeout: 120 }); const config = manager.getConfig(); expect(config.startupTimeout).toBe(120); expect(config.composeFile).toBe("docker-compose.test.yml"); }); it("should return current state", async () => { mockDockerAvailable(); mockNoRunningServices(); mockPortsAvailable(); (mockComposeWrapper.up as ReturnType<typeof vi.fn>).mockResolvedValue(undefined); (mockComposeWrapper.ps as ReturnType<typeof vi.fn>) .mockResolvedValueOnce([]) .mockResolvedValueOnce([ { name: "postgres-test", status: "running", health: "healthy", ports: [{ internal: 5432, external: 5433, protocol: "tcp" }], }, { name: "ollama-test", status: "running", health: "healthy", ports: [{ internal: 11434, external: 11435, protocol: "tcp" }], }, ]); (mockComposeWrapper.isServiceHealthy as ReturnType<typeof vi.fn>).mockResolvedValue(true); const manager = createManager(); await manager.startContainers(); const state = manager.getState(); expect(state.isInitialized).toBe(true); expect(state.servicesStartedByManager).toContain("postgres-test"); expect(state.servicesStartedByManager).toContain("ollama-test"); }); }); });

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