import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { Test, TestingModule } from "@nestjs/testing"; import { KnowledgeCacheService } from "./cache.service"; // Integration tests - require running Valkey instance // Skip in unit test runs, enable with: INTEGRATION_TESTS=true pnpm test describe.skipIf(!process.env.INTEGRATION_TESTS)("KnowledgeCacheService", () => { let service: KnowledgeCacheService; beforeEach(async () => { // Set environment variables for testing process.env.KNOWLEDGE_CACHE_ENABLED = "true"; process.env.KNOWLEDGE_CACHE_TTL = "300"; process.env.VALKEY_URL = "redis://localhost:6379"; const module: TestingModule = await Test.createTestingModule({ providers: [KnowledgeCacheService], }).compile(); service = module.get(KnowledgeCacheService); }); afterEach(async () => { // Clean up if (service && service.isEnabled()) { await service.onModuleDestroy(); } }); describe("Cache Enabled/Disabled", () => { it("should be enabled by default", () => { expect(service.isEnabled()).toBe(true); }); it("should be disabled when KNOWLEDGE_CACHE_ENABLED=false", async () => { process.env.KNOWLEDGE_CACHE_ENABLED = "false"; const module = await Test.createTestingModule({ providers: [KnowledgeCacheService], }).compile(); const disabledService = module.get(KnowledgeCacheService); expect(disabledService.isEnabled()).toBe(false); }); }); describe("Entry Caching", () => { const workspaceId = "test-workspace-id"; const slug = "test-entry"; const entryData = { id: "entry-id", workspaceId, slug, title: "Test Entry", content: "Test content", tags: [], }; it("should return null on cache miss", async () => { if (!service.isEnabled()) { return; // Skip if cache is disabled } await service.onModuleInit(); const result = await service.getEntry(workspaceId, slug); expect(result).toBeNull(); }); it("should cache and retrieve entry data", async () => { if (!service.isEnabled()) { return; } await service.onModuleInit(); // Set cache await service.setEntry(workspaceId, slug, entryData); // Get from cache const result = await service.getEntry(workspaceId, slug); expect(result).toEqual(entryData); }); it("should invalidate entry cache", async () => { if (!service.isEnabled()) { return; } await service.onModuleInit(); // Set cache await service.setEntry(workspaceId, slug, entryData); // Verify it's cached let result = await service.getEntry(workspaceId, slug); expect(result).toEqual(entryData); // Invalidate await service.invalidateEntry(workspaceId, slug); // Verify it's gone result = await service.getEntry(workspaceId, slug); expect(result).toBeNull(); }); }); describe("Search Caching", () => { const workspaceId = "test-workspace-id"; const query = "test search"; const filters = { status: "PUBLISHED", page: 1, limit: 20 }; const searchResults = { data: [], pagination: { page: 1, limit: 20, total: 0, totalPages: 0 }, query, }; it("should cache and retrieve search results", async () => { if (!service.isEnabled()) { return; } await service.onModuleInit(); // Set cache await service.setSearch(workspaceId, query, filters, searchResults); // Get from cache const result = await service.getSearch(workspaceId, query, filters); expect(result).toEqual(searchResults); }); it("should differentiate search results by filters", async () => { if (!service.isEnabled()) { return; } await service.onModuleInit(); const filters1 = { page: 1, limit: 20 }; const filters2 = { page: 2, limit: 20 }; const results1 = { ...searchResults, pagination: { ...searchResults.pagination, page: 1 } }; const results2 = { ...searchResults, pagination: { ...searchResults.pagination, page: 2 } }; await service.setSearch(workspaceId, query, filters1, results1); await service.setSearch(workspaceId, query, filters2, results2); const result1 = await service.getSearch(workspaceId, query, filters1); const result2 = await service.getSearch(workspaceId, query, filters2); expect(result1.pagination.page).toBe(1); expect(result2.pagination.page).toBe(2); }); it("should invalidate all search caches for workspace", async () => { if (!service.isEnabled()) { return; } await service.onModuleInit(); // Set multiple search caches await service.setSearch(workspaceId, "query1", {}, searchResults); await service.setSearch(workspaceId, "query2", {}, searchResults); // Invalidate all await service.invalidateSearches(workspaceId); // Verify both are gone const result1 = await service.getSearch(workspaceId, "query1", {}); const result2 = await service.getSearch(workspaceId, "query2", {}); expect(result1).toBeNull(); expect(result2).toBeNull(); }); }); describe("Graph Caching", () => { const workspaceId = "test-workspace-id"; const entryId = "entry-id"; const maxDepth = 2; const graphData = { centerNode: { id: entryId, slug: "test", title: "Test", tags: [], depth: 0 }, nodes: [], edges: [], stats: { totalNodes: 1, totalEdges: 0, maxDepth }, }; it("should cache and retrieve graph data", async () => { if (!service.isEnabled()) { return; } await service.onModuleInit(); // Set cache await service.setGraph(workspaceId, entryId, maxDepth, graphData); // Get from cache const result = await service.getGraph(workspaceId, entryId, maxDepth); expect(result).toEqual(graphData); }); it("should differentiate graphs by maxDepth", async () => { if (!service.isEnabled()) { return; } await service.onModuleInit(); const graph1 = { ...graphData, stats: { ...graphData.stats, maxDepth: 1 } }; const graph2 = { ...graphData, stats: { ...graphData.stats, maxDepth: 2 } }; await service.setGraph(workspaceId, entryId, 1, graph1); await service.setGraph(workspaceId, entryId, 2, graph2); const result1 = await service.getGraph(workspaceId, entryId, 1); const result2 = await service.getGraph(workspaceId, entryId, 2); expect(result1.stats.maxDepth).toBe(1); expect(result2.stats.maxDepth).toBe(2); }); it("should invalidate all graph caches for workspace", async () => { if (!service.isEnabled()) { return; } await service.onModuleInit(); // Set cache await service.setGraph(workspaceId, entryId, maxDepth, graphData); // Invalidate await service.invalidateGraphs(workspaceId); // Verify it's gone const result = await service.getGraph(workspaceId, entryId, maxDepth); expect(result).toBeNull(); }); }); describe("Cache Statistics", () => { it("should track hits and misses", async () => { if (!service.isEnabled()) { return; } await service.onModuleInit(); const workspaceId = "test-workspace-id"; const slug = "test-entry"; const entryData = { id: "1", slug, title: "Test" }; // Reset stats service.resetStats(); // Miss await service.getEntry(workspaceId, slug); let stats = service.getStats(); expect(stats.misses).toBe(1); expect(stats.hits).toBe(0); // Set await service.setEntry(workspaceId, slug, entryData); stats = service.getStats(); expect(stats.sets).toBe(1); // Hit await service.getEntry(workspaceId, slug); stats = service.getStats(); expect(stats.hits).toBe(1); expect(stats.hitRate).toBeCloseTo(0.5); // 1 hit, 1 miss = 50% }); it("should reset statistics", async () => { if (!service.isEnabled()) { return; } await service.onModuleInit(); const workspaceId = "test-workspace-id"; const slug = "test-entry"; await service.getEntry(workspaceId, slug); // miss service.resetStats(); const stats = service.getStats(); expect(stats.hits).toBe(0); expect(stats.misses).toBe(0); expect(stats.sets).toBe(0); expect(stats.deletes).toBe(0); expect(stats.hitRate).toBe(0); }); }); describe("Clear Workspace Cache", () => { it("should clear all caches for a workspace", async () => { if (!service.isEnabled()) { return; } await service.onModuleInit(); const workspaceId = "test-workspace-id"; // Set various caches await service.setEntry(workspaceId, "entry1", { id: "1" }); await service.setSearch(workspaceId, "query", {}, { data: [] }); await service.setGraph(workspaceId, "entry-id", 1, { nodes: [] }); // Clear all await service.clearWorkspaceCache(workspaceId); // Verify all are gone const entry = await service.getEntry(workspaceId, "entry1"); const search = await service.getSearch(workspaceId, "query", {}); const graph = await service.getGraph(workspaceId, "entry-id", 1); expect(entry).toBeNull(); expect(search).toBeNull(); expect(graph).toBeNull(); }); }); });