import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { Test, TestingModule } from "@nestjs/testing"; import { LlmManagerService } from "./llm-manager.service"; import { PrismaService } from "../prisma/prisma.service"; import type { LlmProviderInstance } from "@prisma/client"; import type { LlmProviderInterface, LlmProviderHealthStatus, } from "./providers/llm-provider.interface"; // Mock the OllamaProvider module before importing LlmManagerService vi.mock("./providers/ollama.provider", () => { class MockOllamaProvider { readonly name = "Ollama"; readonly type = "ollama" as const; private config: { endpoint: string; timeout?: number }; constructor(config: { endpoint: string; timeout?: number }) { this.config = config; } async initialize(): Promise { // No-op for testing } async checkHealth() { return { healthy: true, provider: "ollama", endpoint: this.config.endpoint, }; } async listModels(): Promise { return ["model1", "model2"]; } async chat() { return { model: "test", message: { role: "assistant", content: "Mock response" }, done: true, }; } async *chatStream() { yield { model: "test", message: { role: "assistant", content: "Mock stream" }, done: true, }; } async embed() { return { model: "test", embeddings: [[0.1, 0.2, 0.3]], }; } getConfig() { return { ...this.config }; } } return { OllamaProvider: MockOllamaProvider, }; }); /** * Mock provider for testing purposes */ class MockProvider implements LlmProviderInterface { readonly name = "MockProvider"; readonly type = "ollama" as const; constructor(private config: { endpoint: string }) {} async initialize(): Promise { // No-op for testing } async checkHealth(): Promise { return { healthy: true, provider: "ollama", endpoint: this.config.endpoint, }; } async listModels(): Promise { return ["model1", "model2"]; } // eslint-disable-next-line @typescript-eslint/no-explicit-any async chat(request: any): Promise { return { model: request.model, message: { role: "assistant", content: "Mock response" }, done: true, }; } // eslint-disable-next-line @typescript-eslint/no-explicit-any async *chatStream(request: any): AsyncGenerator { yield { model: request.model, message: { role: "assistant", content: "Mock stream" }, done: true, }; } // eslint-disable-next-line @typescript-eslint/no-explicit-any async embed(request: any): Promise { return { model: request.model, embeddings: [[0.1, 0.2, 0.3]], }; } getConfig(): { endpoint: string } { return { ...this.config }; } } /** * Unhealthy mock provider for testing error scenarios */ class UnhealthyMockProvider extends MockProvider { async checkHealth(): Promise { return { healthy: false, provider: "ollama", endpoint: this.getConfig().endpoint, error: "Connection failed", }; } } describe("LlmManagerService", () => { let service: LlmManagerService; let prisma: PrismaService; const mockProviderInstance: LlmProviderInstance = { id: "550e8400-e29b-41d4-a716-446655440000", providerType: "ollama", displayName: "Test Ollama", userId: null, config: { endpoint: "http://localhost:11434", timeout: 30000 }, isDefault: true, isEnabled: true, createdAt: new Date(), updatedAt: new Date(), }; const mockUserProviderInstance: LlmProviderInstance = { id: "550e8400-e29b-41d4-a716-446655440001", providerType: "ollama", displayName: "User Ollama", userId: "user-123", config: { endpoint: "http://user-ollama:11434", timeout: 30000 }, isDefault: false, isEnabled: true, createdAt: new Date(), updatedAt: new Date(), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ LlmManagerService, { provide: PrismaService, useValue: { llmProviderInstance: { findMany: vi.fn(), findUnique: vi.fn(), findFirst: vi.fn(), }, }, }, ], }).compile(); service = module.get(LlmManagerService); prisma = module.get(PrismaService); }); afterEach(() => { vi.clearAllMocks(); }); describe("initialization", () => { it("should be defined", () => { expect(service).toBeDefined(); }); it("should load enabled providers from database on module init", async () => { const findManySpy = vi .spyOn(prisma.llmProviderInstance, "findMany") .mockResolvedValue([mockProviderInstance]); await service.onModuleInit(); expect(findManySpy).toHaveBeenCalledWith({ where: { isEnabled: true }, }); }); it("should handle empty database gracefully", async () => { vi.spyOn(prisma.llmProviderInstance, "findMany").mockResolvedValue([]); await service.onModuleInit(); const providers = await service.getAllProviders(); expect(providers).toHaveLength(0); }); it("should skip disabled providers during initialization", async () => { // Mock the database to return only enabled providers (empty array since all are disabled) vi.spyOn(prisma.llmProviderInstance, "findMany").mockResolvedValue([]); await service.onModuleInit(); const providers = await service.getAllProviders(); expect(providers).toHaveLength(0); }); it("should initialize all loaded providers", async () => { vi.spyOn(prisma.llmProviderInstance, "findMany").mockResolvedValue([mockProviderInstance]); await service.onModuleInit(); const provider = await service.getProviderById(mockProviderInstance.id); expect(provider).toBeDefined(); }); }); describe("registerProvider", () => { it("should register a new provider instance", async () => { await service.registerProvider(mockProviderInstance); const provider = await service.getProviderById(mockProviderInstance.id); expect(provider).toBeDefined(); expect(provider?.name).toBe("Ollama"); }); it("should update an existing provider instance", async () => { await service.registerProvider(mockProviderInstance); const updatedInstance = { ...mockProviderInstance, config: { endpoint: "http://new-endpoint:11434", timeout: 60000 }, }; await service.registerProvider(updatedInstance); const provider = await service.getProviderById(mockProviderInstance.id); expect(provider?.getConfig().endpoint).toBe("http://new-endpoint:11434"); }); it("should throw error for unknown provider type", async () => { const invalidInstance = { ...mockProviderInstance, providerType: "unknown", }; await expect( service.registerProvider(invalidInstance as LlmProviderInstance) ).rejects.toThrow("Unknown provider type: unknown"); }); it("should initialize provider after registration", async () => { // Provider initialization is tested implicitly by successful registration // and the ability to interact with the provider afterwards await service.registerProvider(mockProviderInstance); const provider = await service.getProviderById(mockProviderInstance.id); expect(provider).toBeDefined(); expect(provider.name).toBe("Ollama"); }); }); describe("unregisterProvider", () => { it("should remove a provider from the registry", async () => { await service.registerProvider(mockProviderInstance); await service.unregisterProvider(mockProviderInstance.id); await expect(service.getProviderById(mockProviderInstance.id)).rejects.toThrow( `Provider with ID ${mockProviderInstance.id} not found` ); }); it("should not throw error when unregistering non-existent provider", async () => { await expect(service.unregisterProvider("non-existent-id")).resolves.not.toThrow(); }); }); describe("getProviderById", () => { it("should return provider by ID", async () => { await service.registerProvider(mockProviderInstance); const provider = await service.getProviderById(mockProviderInstance.id); expect(provider).toBeDefined(); expect(provider?.name).toBe("Ollama"); }); it("should throw error when provider not found", async () => { await expect(service.getProviderById("non-existent-id")).rejects.toThrow( "Provider with ID non-existent-id not found" ); }); }); describe("getAllProviders", () => { it("should return all active providers", async () => { await service.registerProvider(mockProviderInstance); await service.registerProvider(mockUserProviderInstance); const providers = await service.getAllProviders(); expect(providers).toHaveLength(2); }); it("should return empty array when no providers registered", async () => { const providers = await service.getAllProviders(); expect(providers).toHaveLength(0); }); it("should return provider IDs and names", async () => { await service.registerProvider(mockProviderInstance); const providers = await service.getAllProviders(); expect(providers[0]).toHaveProperty("id"); expect(providers[0]).toHaveProperty("name"); expect(providers[0]).toHaveProperty("provider"); }); }); describe("getDefaultProvider", () => { it("should return the default system-level provider", async () => { await service.registerProvider(mockProviderInstance); const provider = await service.getDefaultProvider(); expect(provider).toBeDefined(); expect(provider?.name).toBe("Ollama"); }); it("should throw error when no default provider exists", async () => { await expect(service.getDefaultProvider()).rejects.toThrow("No default provider configured"); }); it("should prefer system-level default over user-level", async () => { const userDefault = { ...mockUserProviderInstance, isDefault: true }; await service.registerProvider(mockProviderInstance); await service.registerProvider(userDefault); const provider = await service.getDefaultProvider(); expect(provider?.getConfig().endpoint).toBe("http://localhost:11434"); }); }); describe("getUserProvider", () => { it("should return user-specific provider", async () => { await service.registerProvider(mockUserProviderInstance); const provider = await service.getUserProvider("user-123"); expect(provider).toBeDefined(); expect(provider?.getConfig().endpoint).toBe("http://user-ollama:11434"); }); it("should prioritize user-level provider over system default", async () => { await service.registerProvider(mockProviderInstance); await service.registerProvider(mockUserProviderInstance); const provider = await service.getUserProvider("user-123"); expect(provider?.getConfig().endpoint).toBe("http://user-ollama:11434"); }); it("should fall back to default provider when user has no specific provider", async () => { await service.registerProvider(mockProviderInstance); const provider = await service.getUserProvider("user-456"); expect(provider?.getConfig().endpoint).toBe("http://localhost:11434"); }); it("should throw error when no provider available for user", async () => { await expect(service.getUserProvider("user-123")).rejects.toThrow( "No provider available for user user-123" ); }); }); describe("checkAllProvidersHealth", () => { it("should return health status for all providers", async () => { await service.registerProvider(mockProviderInstance); const healthStatuses = await service.checkAllProvidersHealth(); expect(healthStatuses).toHaveLength(1); expect(healthStatuses[0]).toHaveProperty("id"); expect(healthStatuses[0]).toHaveProperty("healthy"); expect(healthStatuses[0].healthy).toBe(true); }); it("should handle individual provider health check failures", async () => { // Create a mock instance that will use UnhealthyMockProvider const unhealthyInstance = { ...mockProviderInstance, id: "unhealthy-id" }; // Register the unhealthy provider await service.registerProvider(unhealthyInstance); const healthStatuses = await service.checkAllProvidersHealth(); expect(healthStatuses).toHaveLength(1); // The health status depends on the actual implementation expect(healthStatuses[0]).toHaveProperty("healthy"); }); it("should return empty array when no providers registered", async () => { const healthStatuses = await service.checkAllProvidersHealth(); expect(healthStatuses).toHaveLength(0); }); }); describe("reloadFromDatabase", () => { it("should reload all enabled providers from database", async () => { const findManySpy = vi .spyOn(prisma.llmProviderInstance, "findMany") .mockResolvedValue([mockProviderInstance]); await service.reloadFromDatabase(); expect(findManySpy).toHaveBeenCalledWith({ where: { isEnabled: true }, }); }); it("should clear existing providers before reloading", async () => { await service.registerProvider(mockProviderInstance); vi.spyOn(prisma.llmProviderInstance, "findMany").mockResolvedValue([]); await service.reloadFromDatabase(); const providers = await service.getAllProviders(); expect(providers).toHaveLength(0); }); it("should update providers with latest database state", async () => { vi.spyOn(prisma.llmProviderInstance, "findMany").mockResolvedValue([mockProviderInstance]); await service.reloadFromDatabase(); const updatedInstance = { ...mockProviderInstance, config: { endpoint: "http://updated:11434", timeout: 60000 }, }; vi.spyOn(prisma.llmProviderInstance, "findMany").mockResolvedValue([updatedInstance]); await service.reloadFromDatabase(); const provider = await service.getProviderById(mockProviderInstance.id); expect(provider?.getConfig().endpoint).toBe("http://updated:11434"); }); }); describe("error handling", () => { it("should handle database connection failures gracefully", async () => { vi.spyOn(prisma.llmProviderInstance, "findMany").mockRejectedValue( new Error("Database connection failed") ); await expect(service.onModuleInit()).rejects.toThrow( "Failed to load providers from database" ); }); it("should handle provider initialization failures", async () => { // Test with an unknown provider type to trigger initialization failure const invalidInstance = { ...mockProviderInstance, providerType: "invalid-type", }; await expect( service.registerProvider(invalidInstance as LlmProviderInstance) ).rejects.toThrow("Unknown provider type: invalid-type"); }); }); });