Skip to main content
Glama
DocumentManagementService.test.ts43.5 kB
import path from "node:path"; import { createFsFromVolume, vol } from "memfs"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { LibraryNotFoundInStoreError, VersionNotFoundInStoreError } from "./errors"; vi.mock("node:fs", () => ({ default: createFsFromVolume(vol), existsSync: vi.fn(vol.existsSync), })); vi.mock("../utils/paths", () => ({ getProjectRoot: vi.fn(() => "/docs-mcp-server"), })); // Mock env-paths using mockImplementation const mockEnvPaths = { data: "/mock/env/path/data" }; const mockEnvPathsFn = vi.fn().mockReturnValue(mockEnvPaths); // Keep the spy/implementation separate vi.mock("env-paths", () => ({ // Mock with a placeholder function initially default: vi.fn(), })); import envPaths from "env-paths"; // Assign the actual implementation to the mocked function vi.mocked(envPaths).mockImplementation(mockEnvPathsFn); // Define the instance methods mock const mockStore = { initialize: vi.fn(), shutdown: vi.fn(), queryUniqueVersions: vi.fn(), checkDocumentExists: vi.fn(), queryLibraryVersions: vi.fn().mockResolvedValue(new Map<string, any[]>()), addDocuments: vi.fn(), deletePages: vi.fn(), // Status tracking methods updateVersionStatus: vi.fn(), updateVersionProgress: vi.fn(), getVersionsByStatus: vi.fn(), // Scraper options methods storeScraperOptions: vi.fn(), getScraperOptions: vi.fn(), findVersionsBySourceUrl: vi.fn(), resolveVersionId: vi.fn(), // Library management methods getLibrary: vi.fn(), deleteLibrary: vi.fn(), }; // Mock the DocumentStore module vi.mock("./DocumentStore", () => { // Create the mock constructor *inside* the factory function const MockDocumentStore = vi.fn(() => mockStore); return { DocumentStore: MockDocumentStore }; }); import { EventBusService } from "../events"; import { getProjectRoot } from "../utils/paths"; // Import the mocked constructor AFTER vi.mock import { DocumentManagementService } from "./DocumentManagementService"; // Mock DocumentRetrieverService (keep existing structure) const mockRetriever = { search: vi.fn(), }; vi.mock("./DocumentRetrieverService", () => ({ DocumentRetrieverService: vi.fn().mockImplementation(() => mockRetriever), })); // Mock DocumentManagementClient for factory tests const mockClientInitialize = vi.fn().mockResolvedValue(undefined); const MockDocumentManagementClient = vi .fn() .mockImplementation((_url: string) => ({ initialize: mockClientInitialize })); vi.mock("./DocumentManagementClient", () => ({ DocumentManagementClient: MockDocumentManagementClient, })); // --- END MOCKS --- describe("DocumentManagementService", () => { let docService: DocumentManagementService; // For general tests const projectRoot = getProjectRoot(); // Define expected paths consistently using the calculated actual root // Note: getProjectRoot() called here will now run *after* fs is mocked, // so it needs the dummy package.json created in beforeEach. const _expectedOldDbPath = path.join(projectRoot, ".store", "documents.db"); const _expectedStandardDbPath = path.join(mockEnvPaths.data, "documents.db"); beforeEach(() => { vi.clearAllMocks(); vol.reset(); // Reset memfs // --- Create dummy package.json in memfs for getProjectRoot() --- // Ensure the calculated project root directory exists in memfs vol.mkdirSync(projectRoot, { recursive: true }); // Create a dummy package.json file there vol.writeFileSync(path.join(projectRoot, "package.json"), "{}"); // ------------------------------------------------------------- // Ensure envPaths mock is reset/set for general tests mockEnvPathsFn.mockReturnValue(mockEnvPaths); // Set OPENAI_API_KEY for tests to enable default embedding behavior process.env.OPENAI_API_KEY = "test-api-key"; // Initialize the main service instance used by most tests // This will now use memfs for its internal fs calls const eventBus = new EventBusService(); docService = new DocumentManagementService("/test/store/path", eventBus); }); afterEach(async () => { // Shutdown the main service instance await docService?.shutdown(); }); // --- getActiveEmbeddingConfig Tests --- describe("getActiveEmbeddingConfig", () => { it("should delegate to the underlying store", async () => { const mockConfig = { provider: "openai" as const, model: "text-embedding-3-small", dimensions: 1536, modelSpec: "openai:text-embedding-3-small", }; (mockStore as any).getActiveEmbeddingConfig = vi.fn().mockReturnValue(mockConfig); await docService.initialize(); const config = docService.getActiveEmbeddingConfig(); expect((mockStore as any).getActiveEmbeddingConfig).toHaveBeenCalled(); expect(config).toEqual(mockConfig); }); it("should return null when store returns null", async () => { (mockStore as any).getActiveEmbeddingConfig = vi.fn().mockReturnValue(null); await docService.initialize(); const config = docService.getActiveEmbeddingConfig(); expect((mockStore as any).getActiveEmbeddingConfig).toHaveBeenCalled(); expect(config).toBeNull(); }); }); // --- Pipeline Configuration Tests --- describe("Pipeline Configuration", () => { beforeEach(() => { vol.reset(); // Reset memfs volume before each test vi.clearAllMocks(); // Clear other mocks }); it("should accept pipeline configuration and pass it to factory", () => { const pipelineConfig = { chunkSizes: { preferred: 800, max: 1600, }, }; const eventBus = new EventBusService(); const service = new DocumentManagementService( "/test/path", eventBus, null, pipelineConfig, ); expect(service).toBeInstanceOf(DocumentManagementService); // Test passes if no errors are thrown during construction }); it("should work without pipeline configuration", () => { const eventBus = new EventBusService(); const service = new DocumentManagementService("/test/path", eventBus); expect(service).toBeInstanceOf(DocumentManagementService); // Test passes if no errors are thrown during construction }); it("should work with only embedding config provided", () => { const eventBus = new EventBusService(); const service = new DocumentManagementService("/test/path", eventBus, null); expect(service).toBeInstanceOf(DocumentManagementService); }); it("should work with both embedding and pipeline config", () => { const pipelineConfig = { chunkSizes: { preferred: 500 } }; const eventBus = new EventBusService(); const service = new DocumentManagementService( "/test/path", eventBus, null, pipelineConfig, ); expect(service).toBeInstanceOf(DocumentManagementService); }); }); // --- ensureVersion tests --- describe("ensureVersion", () => { it("creates library and version when both absent", async () => { mockStore.resolveVersionId.mockResolvedValue(10); const id = await docService.ensureVersion({ library: "React", version: "18.2.0" }); expect(id).toBe(10); // ensure normalize to lowercase expect(mockStore.resolveVersionId).toHaveBeenCalledWith("react", "18.2.0"); }); it("handles unversioned refs (empty version string)", async () => { mockStore.resolveVersionId.mockResolvedValue(20); const id = await docService.ensureVersion({ library: "Lodash", version: "" }); expect(id).toBe(20); expect(mockStore.resolveVersionId).toHaveBeenCalledWith("lodash", ""); }); it("trims whitespace and normalizes version", async () => { mockStore.resolveVersionId.mockResolvedValue(30); const id = await docService.ensureVersion({ library: " Express ", version: " ", }); expect(id).toBe(30); expect(mockStore.resolveVersionId).toHaveBeenCalledWith("express", ""); }); it("reuses single unversioned version across multiple ensureVersion calls (regression)", async () => { // simulate same returned id each time mockStore.resolveVersionId .mockResolvedValueOnce(10) .mockResolvedValueOnce(10) .mockResolvedValueOnce(10); const a = await docService.ensureVersion({ library: "TestLib", version: "" }); const b = await docService.ensureVersion({ library: "TestLib", version: "" }); const c = await docService.ensureVersion({ library: "TestLib", version: "" }); expect(a).toBe(10); expect(b).toBe(10); expect(c).toBe(10); expect(mockStore.resolveVersionId).toHaveBeenCalledTimes(3); }); }); // --- Factory function behavior tests --- describe("DocumentManagement factory functions", () => { beforeEach(() => { vi.clearAllMocks(); }); it("createDocumentManagement() returns initialized local service by default", async () => { const initSpy = vi.spyOn(DocumentManagementService.prototype, "initialize"); const { createDocumentManagement } = await import("./index"); const eventBus = new EventBusService(); const dm = await createDocumentManagement({ storePath: "/test/path", eventBus }); expect(initSpy).toHaveBeenCalledTimes(1); expect(dm).toBeInstanceOf(DocumentManagementService); // Should not construct remote client when no serverUrl is provided expect(MockDocumentManagementClient).not.toHaveBeenCalled(); }); it("createDocumentManagement({serverUrl}) returns initialized remote client", async () => { const { createDocumentManagement } = await import("./index"); const url = "http://localhost:8080"; const eventBus = new EventBusService(); const dm = await createDocumentManagement({ serverUrl: url, eventBus }); expect(MockDocumentManagementClient).toHaveBeenCalledWith(url); expect(mockClientInitialize).toHaveBeenCalledTimes(1); // Not a local service instance expect(dm).not.toBeInstanceOf(DocumentManagementService); }); it("createLocalDocumentManagement() returns initialized local service", async () => { const initSpy = vi.spyOn(DocumentManagementService.prototype, "initialize"); const { createLocalDocumentManagement } = await import("./index"); const eventBus = new EventBusService(); const dm = await createLocalDocumentManagement("/test/path", eventBus); expect(initSpy).toHaveBeenCalledTimes(1); expect(dm).toBeInstanceOf(DocumentManagementService); // Should never touch remote client in local helper expect(MockDocumentManagementClient).not.toHaveBeenCalled(); }); }); // --- END: Constructor Path Logic Tests --- // --- Existing Tests (Rely on global docService and mocks) --- // Grouped existing tests for clarity describe("Initialization and Shutdown", () => { it("should initialize correctly", async () => { // Uses global docService initialized in beforeEach await docService.initialize(); expect(mockStore.initialize).toHaveBeenCalled(); }); it("should shutdown correctly", async () => { // Uses global docService initialized in beforeEach await docService.shutdown(); expect(mockStore.shutdown).toHaveBeenCalled(); }); }); describe("Core Functionality", () => { // Uses global docService initialized in beforeEach it("should handle empty store existence check", async () => { mockStore.checkDocumentExists.mockResolvedValue(false); // Use mockStoreInstance const exists = await docService.exists("test-lib", "1.0.0"); expect(exists).toBe(false); expect(mockStore.checkDocumentExists).toHaveBeenCalledWith("test-lib", "1.0.0"); }); it("should remove all documents for a specific library and version", async () => { const library = "test-lib"; const version = "1.0.0"; await docService.removeAllDocuments(library, version); expect(mockStore.deletePages).toHaveBeenCalledWith(library, version); // Fix: Use mockStoreInstance }); it("should handle removing documents with null/undefined/empty version", async () => { const library = "test-lib"; await docService.removeAllDocuments(library, null); expect(mockStore.deletePages).toHaveBeenCalledWith(library, ""); // Fix: Use mockStoreInstance await docService.removeAllDocuments(library, undefined); expect(mockStore.deletePages).toHaveBeenCalledWith(library, ""); // Fix: Use mockStoreInstance await docService.removeAllDocuments(library, ""); expect(mockStore.deletePages).toHaveBeenCalledWith(library, ""); // Fix: Use mockStoreInstance }); describe("listVersions", () => { it("should return an empty array if the library has no documents", async () => { mockStore.queryUniqueVersions.mockResolvedValue([]); // Fix: Use mockStoreInstance const versions = await docService.listVersions("nonexistent-lib"); expect(versions).toEqual([]); }); it("should return an array versions sorted descending (latest first)", async () => { const library = "test-lib"; mockStore.queryUniqueVersions.mockResolvedValue(["1.0.0", "1.1.0", "1.2.0"]); // Fix: Use mockStoreInstance const versions = await docService.listVersions(library); expect(versions).toEqual(["1.2.0", "1.1.0", "1.0.0"]); expect(mockStore.queryUniqueVersions).toHaveBeenCalledWith(library); // Fix: Use mockStoreInstance }); it("should filter out empty string and non-semver versions, sorted descending", async () => { const library = "test-lib"; mockStore.queryUniqueVersions.mockResolvedValue([ // Fix: Use mockStoreInstance "1.0.0", "", "invalid-version", "2.0.0-beta", // Valid semver, should be included "2.0.0", ]); const versions = await docService.listVersions(library); expect(versions).toEqual(["2.0.0", "2.0.0-beta", "1.0.0"]); expect(mockStore.queryUniqueVersions).toHaveBeenCalledWith(library); // Fix: Use mockStoreInstance }); }); describe("findBestVersion", () => { const library = "test-lib"; beforeEach(() => { // Reset mocks for checkDocumentExists for each test mockStore.checkDocumentExists.mockResolvedValue(false); // Fix: Use mockStoreInstance }); it("should return best match and hasUnversioned=false when only semver exists", async () => { mockStore.queryUniqueVersions.mockResolvedValue(["1.0.0", "1.1.0", "2.0.0"]); // Fix: Use mockStoreInstance mockStore.checkDocumentExists.mockResolvedValue(false); // No unversioned // Fix: Use mockStoreInstance const result = await docService.findBestVersion(library, "1.5.0"); expect(result).toEqual({ bestMatch: "1.1.0", hasUnversioned: false }); expect(mockStore.queryUniqueVersions).toHaveBeenCalledWith(library); // Fix: Use mockStoreInstance expect(mockStore.checkDocumentExists).toHaveBeenCalledWith(library, ""); // Fix: Use mockStoreInstance }); it("should return latest match and hasUnversioned=false for 'latest'", async () => { mockStore.queryUniqueVersions.mockResolvedValue(["1.0.0", "2.0.0", "3.0.0"]); // Fix: Use mockStoreInstance mockStore.checkDocumentExists.mockResolvedValue(false); // Fix: Use mockStoreInstance const latestResult = await docService.findBestVersion(library, "latest"); expect(latestResult).toEqual({ bestMatch: "3.0.0", hasUnversioned: false }); const defaultResult = await docService.findBestVersion(library); // No target version expect(defaultResult).toEqual({ bestMatch: "3.0.0", hasUnversioned: false }); }); it("should return best match and hasUnversioned=true when both exist", async () => { mockStore.queryUniqueVersions.mockResolvedValue(["1.0.0", "1.1.0"]); // Fix: Use mockStoreInstance mockStore.checkDocumentExists.mockResolvedValue(true); // Unversioned exists // Fix: Use mockStoreInstance const result = await docService.findBestVersion(library, "1.0.x"); expect(result).toEqual({ bestMatch: "1.0.0", hasUnversioned: true }); }); it("should return latest match and hasUnversioned=true when both exist (latest)", async () => { mockStore.queryUniqueVersions.mockResolvedValue(["1.0.0", "2.0.0"]); // Fix: Use mockStoreInstance mockStore.checkDocumentExists.mockResolvedValue(true); // Fix: Use mockStoreInstance const result = await docService.findBestVersion(library); expect(result).toEqual({ bestMatch: "2.0.0", hasUnversioned: true }); }); it("should return null bestMatch and hasUnversioned=true when only unversioned exists", async () => { mockStore.queryUniqueVersions.mockResolvedValue([""]); // listVersions filters this out // Fix: Use mockStoreInstance mockStore.checkDocumentExists.mockResolvedValue(true); // Unversioned exists // Fix: Use mockStoreInstance const result = await docService.findBestVersion(library); expect(result).toEqual({ bestMatch: null, hasUnversioned: true }); const resultSpecific = await docService.findBestVersion(library, "1.0.0"); expect(resultSpecific).toEqual({ bestMatch: null, hasUnversioned: true }); }); it("should return fallback match and hasUnversioned=true when target is higher but unversioned exists", async () => { // Renamed test for clarity mockStore.queryUniqueVersions.mockResolvedValue(["1.0.0", "1.1.0"]); // Fix: Use mockStoreInstance mockStore.checkDocumentExists.mockResolvedValue(true); // Unversioned exists // Fix: Use mockStoreInstance const result = await docService.findBestVersion(library, "3.0.0"); // Target higher than available // Expect fallback to latest available (1.1.0) because a version was requested expect(result).toEqual({ bestMatch: "1.1.0", hasUnversioned: true }); // Corrected expectation }); it("should return fallback match and hasUnversioned=false when target is higher and only semver exists", async () => { // New test for specific corner case mockStore.queryUniqueVersions.mockResolvedValue(["1.0.0", "1.1.0"]); // Fix: Use mockStoreInstance mockStore.checkDocumentExists.mockResolvedValue(false); // No unversioned // Fix: Use mockStoreInstance const result = await docService.findBestVersion(library, "3.0.0"); // Target higher than available // Expect fallback to latest available (1.1.0) expect(result).toEqual({ bestMatch: "1.1.0", hasUnversioned: false }); }); it("should throw LibraryNotFoundInStoreError when no versions (semver or unversioned) exist", async () => { mockStore.queryUniqueVersions.mockResolvedValue([]); // No semver // Fix: Use mockStoreInstance mockStore.checkDocumentExists.mockResolvedValue(false); // No unversioned // Fix: Use mockStoreInstance await expect(docService.findBestVersion(library, "1.0.0")).rejects.toThrow( LibraryNotFoundInStoreError, ); await expect(docService.findBestVersion(library)).rejects.toThrow( LibraryNotFoundInStoreError, ); // Check error details const error = (await docService .findBestVersion(library) .catch((e) => e)) as LibraryNotFoundInStoreError; expect(error).toBeInstanceOf(LibraryNotFoundInStoreError); expect(error.library).toBe(library); expect(error.similarLibraries).toEqual([]); // No similar libraries in this mock setup }); it("should not throw for invalid target version format if unversioned exists", async () => { mockStore.queryUniqueVersions.mockResolvedValue(["1.0.0"]); // Has semver // Fix: Use mockStoreInstance mockStore.checkDocumentExists.mockResolvedValue(true); // Has unversioned // Fix: Use mockStoreInstance // Invalid format, but unversioned exists, so should return null match const result = await docService.findBestVersion(library, "invalid-format"); expect(result).toEqual({ bestMatch: null, hasUnversioned: true }); }); it("should throw VersionNotFoundInStoreError for invalid target version format if only semver exists", async () => { mockStore.queryUniqueVersions.mockResolvedValue(["1.0.0"]); // Has semver // Fix: Use mockStoreInstance mockStore.checkDocumentExists.mockResolvedValue(false); // No unversioned // Fix: Use mockStoreInstance // Invalid format, no unversioned fallback -> throw await expect( docService.findBestVersion(library, "invalid-format"), ).rejects.toThrow(VersionNotFoundInStoreError); }); }); describe("listLibraries", () => { it("should list libraries with enriched version metadata", async () => { const mockLibraryMap = new Map([ [ "lib1", [ { version: "1.0.0", versionId: 101, status: "completed", progressPages: 10, progressMaxPages: 10, sourceUrl: null, documentCount: 10, uniqueUrlCount: 5, indexedAt: "2024-01-01T00:00:00.000Z", }, { version: "1.1.0", versionId: 102, status: "completed", progressPages: 15, progressMaxPages: 15, sourceUrl: null, documentCount: 15, uniqueUrlCount: 7, indexedAt: "2024-02-01T00:00:00.000Z", }, ], ], [ "lib2", [ { version: "2.0.0", versionId: 201, status: "completed", progressPages: 20, progressMaxPages: 20, sourceUrl: null, documentCount: 20, uniqueUrlCount: 10, indexedAt: "2024-03-01T00:00:00.000Z", }, ], ], [ "unversioned-only", [ { version: "", versionId: 300, status: "completed", progressPages: 1, progressMaxPages: 1, sourceUrl: null, documentCount: 1, uniqueUrlCount: 1, indexedAt: "2024-04-01T00:00:00.000Z", }, ], ], [ "mixed-versions", [ { version: "", versionId: 400, status: "completed", progressPages: 2, progressMaxPages: 2, sourceUrl: null, documentCount: 2, uniqueUrlCount: 1, indexedAt: "2024-04-03T00:00:00.000Z", }, { version: "1.0.0", versionId: 401, status: "completed", progressPages: 5, progressMaxPages: 5, sourceUrl: null, documentCount: 5, uniqueUrlCount: 2, indexedAt: "2024-04-02T00:00:00.000Z", }, ], ], ] as any); mockStore.queryLibraryVersions.mockResolvedValue(mockLibraryMap as any); const result = await docService.listLibraries(); expect( result.map((r) => ({ library: r.library, versions: r.versions.map((v) => ({ ref: v.ref, status: v.status, counts: v.counts, indexedAt: v.indexedAt, })), })), ).toEqual([ { library: "lib1", versions: [ { ref: { library: "lib1", version: "1.0.0" }, status: "completed", counts: { documents: 10, uniqueUrls: 5 }, indexedAt: "2024-01-01T00:00:00.000Z", }, { ref: { library: "lib1", version: "1.1.0" }, status: "completed", counts: { documents: 15, uniqueUrls: 7 }, indexedAt: "2024-02-01T00:00:00.000Z", }, ], }, { library: "lib2", versions: [ { ref: { library: "lib2", version: "2.0.0" }, status: "completed", counts: { documents: 20, uniqueUrls: 10 }, indexedAt: "2024-03-01T00:00:00.000Z", }, ], }, { library: "unversioned-only", versions: [ { ref: { library: "unversioned-only", version: "" }, status: "completed", counts: { documents: 1, uniqueUrls: 1 }, indexedAt: "2024-04-01T00:00:00.000Z", }, ], }, { library: "mixed-versions", versions: [ { ref: { library: "mixed-versions", version: "" }, status: "completed", counts: { documents: 2, uniqueUrls: 1 }, indexedAt: "2024-04-03T00:00:00.000Z", }, { ref: { library: "mixed-versions", version: "1.0.0" }, status: "completed", counts: { documents: 5, uniqueUrls: 2 }, indexedAt: "2024-04-02T00:00:00.000Z", }, ], }, ]); expect(mockStore.queryLibraryVersions).toHaveBeenCalledTimes(1); }); it("should return an empty array if there are no libraries", async () => { // Mock returns an empty map of the correct type mockStore.queryLibraryVersions.mockResolvedValue( new Map< string, Array<{ version: string; documentCount: number; uniqueUrlCount: number; indexedAt: string | null; }> >(), ); const result = await docService.listLibraries(); expect(result).toEqual([]); expect(mockStore.queryLibraryVersions).toHaveBeenCalledTimes(1); }); // Test case where store returns a library that only had an unversioned entry // (which is now included, not filtered by the store) it("should correctly handle libraries with only unversioned entries", async () => { const mockLibraryMap = new Map([ [ "lib-unversioned", [ { version: "", versionId: 999, status: "completed", progressPages: 0, progressMaxPages: 0, sourceUrl: null, documentCount: 3, uniqueUrlCount: 2, indexedAt: "2024-04-04T00:00:00.000Z", }, ], ], ] as any); mockStore.queryLibraryVersions.mockResolvedValue(mockLibraryMap as any); const result = await docService.listLibraries(); expect(result).toEqual([ { library: "lib-unversioned", versions: [ { id: 999, ref: { library: "lib-unversioned", version: "" }, status: "completed", counts: { documents: 3, uniqueUrls: 2 }, indexedAt: "2024-04-04T00:00:00.000Z", sourceUrl: undefined, }, ], }, ]); expect(result[0].versions[0].progress).toBeUndefined(); expect(mockStore.queryLibraryVersions).toHaveBeenCalledTimes(1); }); }); // Tests for handling optional version parameter (null/undefined/"") describe("Optional Version Handling", () => { const library = "opt-lib"; const query = "optional"; it("exists should normalize version to empty string", async () => { await docService.exists(library, null); expect(mockStore.checkDocumentExists).toHaveBeenCalledWith(library, ""); await docService.exists(library, undefined); expect(mockStore.checkDocumentExists).toHaveBeenCalledWith(library, ""); await docService.exists(library, ""); expect(mockStore.checkDocumentExists).toHaveBeenCalledWith(library, ""); }); it("searchStore should normalize version to empty string", async () => { // Call without explicit limit, should use default limit of 5 await docService.searchStore(library, null, query); expect(mockRetriever.search).toHaveBeenCalledWith(library, "", query, 5); // Call with explicit limit await docService.searchStore(library, undefined, query, 7); expect(mockRetriever.search).toHaveBeenCalledWith(library, "", query, 7); // Call with another explicit limit await docService.searchStore(library, "", query, 10); expect(mockRetriever.search).toHaveBeenCalledWith(library, "", query, 10); }); }); describe("validateLibraryExists", () => { const library = "test-lib"; const existingLibraries = [ { library: "test-lib", versions: [{ version: "1.0.0", indexed: true }] }, { library: "another-lib", versions: [{ version: "2.0.0", indexed: true }] }, { library: "react", versions: [] }, ]; it("should resolve successfully if versioned documents exist", async () => { mockStore.getLibrary.mockResolvedValue({ id: 1, name: library.toLowerCase() }); await expect(docService.validateLibraryExists(library)).resolves.toBeUndefined(); expect(mockStore.getLibrary).toHaveBeenCalledWith(library); }); it("should resolve successfully if only unversioned documents exist", async () => { mockStore.getLibrary.mockResolvedValue({ id: 1, name: library.toLowerCase() }); await expect(docService.validateLibraryExists(library)).resolves.toBeUndefined(); expect(mockStore.getLibrary).toHaveBeenCalledWith(library); }); it("should throw LibraryNotFoundInStoreError if library does not exist (no suggestions)", async () => { const nonExistentLibrary = "non-existent-lib"; mockStore.getLibrary.mockResolvedValue(null); mockStore.queryLibraryVersions.mockResolvedValue(new Map()); await expect( docService.validateLibraryExists(nonExistentLibrary), ).rejects.toThrow(LibraryNotFoundInStoreError); const error = (await docService .validateLibraryExists(nonExistentLibrary) .catch((e) => e)) as LibraryNotFoundInStoreError; expect(error).toBeInstanceOf(LibraryNotFoundInStoreError); expect(error.library).toBe(nonExistentLibrary); expect(error.similarLibraries).toEqual([]); expect(mockStore.queryLibraryVersions).toHaveBeenCalled(); }); it("should throw LibraryNotFoundInStoreError with suggestions if library does not exist", async () => { const misspelledLibrary = "reac"; mockStore.getLibrary.mockResolvedValue(null); const mockLibraryMap = new Map< string, Array<{ version: string; documentCount: number; uniqueUrlCount: number; indexedAt: string | null; }> >( existingLibraries.map((l) => [ l.library, l.versions.map((v) => ({ version: v.version, documentCount: 0, uniqueUrlCount: 0, indexedAt: null, })), ]), ); mockStore.queryLibraryVersions.mockResolvedValue(mockLibraryMap); await expect(docService.validateLibraryExists(misspelledLibrary)).rejects.toThrow( LibraryNotFoundInStoreError, ); const error = (await docService .validateLibraryExists(misspelledLibrary) .catch((e) => e)) as LibraryNotFoundInStoreError; expect(error).toBeInstanceOf(LibraryNotFoundInStoreError); expect(error.library).toBe(misspelledLibrary); expect(error.similarLibraries).toEqual(["react"]); expect(mockStore.queryLibraryVersions).toHaveBeenCalled(); }); it("should handle case insensitivity", async () => { const libraryUpper = "TEST-LIB"; mockStore.getLibrary.mockResolvedValue({ id: 1, name: libraryUpper.toLowerCase(), }); await expect( docService.validateLibraryExists(libraryUpper), ).resolves.toBeUndefined(); expect(mockStore.getLibrary).toHaveBeenCalledWith(libraryUpper); }); }); describe("Pipeline Integration Methods", () => { it("should delegate status tracking to store", async () => { const versionId = 123; const status = "queued"; const errorMessage = "Test error"; // Test updateVersionStatus await docService.updateVersionStatus(versionId, status as any, errorMessage); expect(mockStore.updateVersionStatus).toHaveBeenCalledWith( versionId, status, errorMessage, ); // Test updateVersionProgress await docService.updateVersionProgress(versionId, 5, 10); expect(mockStore.updateVersionProgress).toHaveBeenCalledWith(versionId, 5, 10); // Test getVersionsByStatus mockStore.getVersionsByStatus.mockResolvedValue([]); await docService.getVersionsByStatus(["queued"] as any); expect(mockStore.getVersionsByStatus).toHaveBeenCalledWith(["queued"]); // Test getVersionsByStatus (legacy running replacement) mockStore.getVersionsByStatus.mockResolvedValue([]); await docService.getVersionsByStatus(["running"] as any); expect(mockStore.getVersionsByStatus).toHaveBeenCalledWith(["running"]); // Test getVersionsByStatus (legacy active replacement) mockStore.getVersionsByStatus.mockResolvedValue([]); await docService.getVersionsByStatus(["queued", "running", "updating"] as any); expect(mockStore.getVersionsByStatus).toHaveBeenCalledWith([ "queued", "running", "updating", ]); }); it("should delegate scraper options storage to store", async () => { const versionId = 456; const scraperOptions = { url: "https://example.com", library: "testlib", version: "1.0.0", maxDepth: 3, maxPages: 100, }; // Test storeScraperOptions await docService.storeScraperOptions(versionId, scraperOptions); expect(mockStore.storeScraperOptions).toHaveBeenCalledWith( versionId, scraperOptions, ); // Test getScraperOptions mockStore.getScraperOptions.mockResolvedValue(null); await docService.getScraperOptions(versionId); expect(mockStore.getScraperOptions).toHaveBeenCalledWith(versionId); // Test findVersionsBySourceUrl const sourceUrl = "https://docs.example.com"; mockStore.findVersionsBySourceUrl.mockResolvedValue([]); await docService.findVersionsBySourceUrl(sourceUrl); expect(mockStore.findVersionsBySourceUrl).toHaveBeenCalledWith(sourceUrl); }); it("should ensure library and version creation", async () => { const library = "NewLib"; const version = "2.0.0"; const expectedVersionId = 789; // Mock the store method mockStore.resolveVersionId.mockResolvedValue(expectedVersionId); const result = await docService.ensureLibraryAndVersion(library, version); // Should normalize library name to lowercase and version expect(mockStore.resolveVersionId).toHaveBeenCalledWith("newlib", "2.0.0"); expect(result).toBe(expectedVersionId); }); it("should handle version normalization in scraper methods", async () => { const versionId = 999; mockStore.getScraperOptions .mockResolvedValueOnce({ sourceUrl: "https://a", options: {} as any }) .mockResolvedValueOnce({ sourceUrl: "https://b", options: {} as any }); const result1 = await docService.getScraperOptions(versionId); const result2 = await docService.getScraperOptions(versionId); expect(result1?.sourceUrl).toEqual("https://a"); expect(result2?.sourceUrl).toEqual("https://b"); expect(mockStore.getScraperOptions).toHaveBeenCalledTimes(2); }); }); describe("getScraperOptions (service wrapper)", () => { it("should return stored object then null on subsequent call (combined happy/null path)", async () => { const versionId = 42; const stored = { sourceUrl: "https://docs.example.com", options: { maxDepth: 5 }, }; mockStore.getScraperOptions .mockResolvedValueOnce(stored) .mockResolvedValueOnce(null); const first = await docService.getScraperOptions(versionId); const second = await docService.getScraperOptions(versionId); expect(first).toEqual(stored); expect(second).toBeNull(); expect(mockStore.getScraperOptions).toHaveBeenNthCalledWith(1, versionId); expect(mockStore.getScraperOptions).toHaveBeenNthCalledWith(2, versionId); }); }); describe("listLibraries (enriched summaries)", () => { it("returns empty array when no libraries", async () => { mockStore.queryLibraryVersions.mockResolvedValue(new Map()); mockStore.getVersionsByStatus.mockResolvedValue([]); const result = await docService.listLibraries(); expect(result).toEqual([]); }); it("passes through multiple statuses and progress fields for enriched rows", async () => { const enrichedMap = new Map<string, any[]>([ [ "libStatus", [ { version: "1.0.0", versionId: 11, status: "completed", progressPages: 10, progressMaxPages: 10, sourceUrl: "https://ex/libStatus/1.0.0", documentCount: 50, uniqueUrlCount: 45, indexedAt: "2024-02-01T00:00:00.000Z", }, { version: "1.1.0", versionId: 12, status: "failed", progressPages: 3, progressMaxPages: 8, sourceUrl: "https://ex/libStatus/1.1.0", documentCount: 12, uniqueUrlCount: 10, indexedAt: null, }, { version: "2.0.0", versionId: 13, status: "cancelled", progressPages: 5, progressMaxPages: 20, sourceUrl: null, documentCount: 0, uniqueUrlCount: 0, indexedAt: null, }, { version: "", versionId: 14, status: "not_indexed", progressPages: 0, progressMaxPages: 0, sourceUrl: null, documentCount: 0, uniqueUrlCount: 0, indexedAt: null, }, ], ], ]); mockStore.queryLibraryVersions.mockResolvedValue(enrichedMap); const result = await docService.listLibraries(); const lib = result.find((r) => r.library === "libStatus"); expect(lib).toBeTruthy(); const byVer = Object.fromEntries( lib!.versions.map((v) => [v.ref.version || "__unver__", v]), ); expect(byVer["1.0.0"]).toMatchObject({ status: "completed", // progress omitted for completed }); expect(byVer["1.1.0"]).toMatchObject({ status: "failed", progress: { pages: 3, maxPages: 8 }, }); expect(byVer["2.0.0"]).toMatchObject({ status: "cancelled", progress: { pages: 5, maxPages: 20 }, }); expect(byVer.__unver__).toMatchObject({ status: "not_indexed", progress: { pages: 0, maxPages: 0 }, }); // Explicitly ensure progress is undefined for completed version expect(byVer["1.0.0"].progress).toBeUndefined(); }); it("omits progress for completed versions but includes for active ones", async () => { const enrichedMap = new Map<string, any[]>([ [ "libActive", [ { version: "1.0.0", versionId: 21, status: "completed", progressPages: 5, progressMaxPages: 5, sourceUrl: null, documentCount: 10, uniqueUrlCount: 9, indexedAt: "2024-05-01T00:00:00.000Z", }, { version: "1.1.0", versionId: 22, status: "running", progressPages: 2, progressMaxPages: 10, sourceUrl: null, documentCount: 4, uniqueUrlCount: 4, indexedAt: null, }, { version: "1.2.0", versionId: 23, status: "queued", progressPages: 0, progressMaxPages: 10, sourceUrl: null, documentCount: 0, uniqueUrlCount: 0, indexedAt: null, }, ], ], ]); mockStore.queryLibraryVersions.mockResolvedValue(enrichedMap); const result = await docService.listLibraries(); const lib = result.find((r) => r.library === "libActive"); expect(lib).toBeTruthy(); const byVer = Object.fromEntries(lib!.versions.map((v) => [v.ref.version, v])); expect(byVer["1.0.0"].progress).toBeUndefined(); expect(byVer["1.1.0"].progress).toEqual({ pages: 2, maxPages: 10 }); expect(byVer["1.2.0"].progress).toEqual({ pages: 0, maxPages: 10 }); }); }); describe("cleanup", () => { it("should shutdown without errors", async () => { const eventBus = new EventBusService(); const service = new DocumentManagementService("/test/path", eventBus, { provider: "openai", model: "text-embedding-ada-002", dimensions: 1536, modelSpec: "openai:text-embedding-ada-002", }); // Should complete shutdown without errors await expect(service.shutdown()).resolves.not.toThrow(); expect(mockStore.shutdown).toHaveBeenCalledOnce(); }); }); }); // Closing brace for describe("Core Functionality", ...) }); // Closing brace for the main describe block

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/arabold/docs-mcp-server'

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