Skip to main content
Glama
persistence.test.ts17.2 kB
import { promises as fsp } from 'fs'; import * as fs from 'fs'; import os from 'os'; import path from 'path'; import { MemoryManager } from '../memory-manager'; import { VerificationMemory } from '../verification-memory'; import { PathUtils } from '../utils/path-utils'; import { SimilarityEngine } from '../similarity-engine'; const sleep = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms)); const waitUntil = async (predicate: () => boolean | Promise<boolean>, timeoutMs = 500): Promise<void> => { const start = Date.now(); while (!(await predicate())) { if (Date.now() - start > timeoutMs) { throw new Error('Timed out waiting for condition'); } await sleep(10); } }; describe('Persistence hardening', () => { let tempDir: string; beforeEach(async () => { tempDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'smart-thinking-tests-')); jest.spyOn(PathUtils, 'getDataDirectory').mockReturnValue(tempDir); jest.spyOn(PathUtils, 'getTempDirectory').mockReturnValue(tempDir); }); afterEach(async () => { jest.restoreAllMocks(); VerificationMemory.resetInstance(); if (tempDir && fs.existsSync(tempDir)) { await fsp.rm(tempDir, { recursive: true, force: true }); } }); test('MemoryManager sanitizes legacy payloads and persists clean data', async () => { await fsp.mkdir(path.join(tempDir, 'memories'), { recursive: true }); const legacyMemoryPath = path.join(tempDir, 'memories', 'default.json'); const legacyKnowledgePath = path.join(tempDir, 'knowledge.json'); const legacyMemoryPayload = [ { id: 'legacy-1', content: 'Contenu hérité', tags: ['context'], timestamp: '2024-01-01T00:00:00.000Z', embedding: [0.12, 0.34], metadata: { sessionId: 'default', openaiTokens: 42, notes: 'préserver cette note' } } ]; const legacyKnowledgePayload = { 'legacy-entry': { description: 'connaissance héritée', cohereVector: [0.1, 0.2], nested: { openaiPrompt: 'should vanish', hint: 'keep me' } } }; await fsp.writeFile(legacyMemoryPath, JSON.stringify(legacyMemoryPayload, null, 2), 'utf8'); await fsp.writeFile(legacyKnowledgePath, JSON.stringify(legacyKnowledgePayload, null, 2), 'utf8'); const manager = new MemoryManager(); await waitUntil(() => Boolean(manager.getMemory('legacy-1'))); const loadedLegacy = manager.getMemory('legacy-1'); expect(loadedLegacy).toBeDefined(); expect(loadedLegacy?.metadata).toEqual({ sessionId: 'default', notes: 'préserver cette note' }); expect((loadedLegacy as Record<string, unknown>).embedding).toBeUndefined(); manager.setKnowledge('legacy-entry', { openaiPrompt: 'retiré', description: 'actualisé', nested: { cohereScore: 0.99, insight: 'préserver' } }); const newId = manager.addMemory('Nouvelle mémoire', ['analyse']); await (manager as unknown as { saveToStorage: () => Promise<void> }).saveToStorage(); const storedMemoriesRaw = await fsp.readFile(legacyMemoryPath, 'utf8'); const storedMemories = JSON.parse(storedMemoriesRaw) as Array<Record<string, unknown>>; expect(storedMemoriesRaw).not.toContain('embedding'); expect(storedMemoriesRaw).not.toContain('openai'); expect(storedMemories.find(item => item.id === newId)).toBeDefined(); for (const item of storedMemories) { expect(item).not.toHaveProperty('embedding'); const metadata = item.metadata as Record<string, unknown> | undefined; if (metadata) { expect(Object.keys(metadata)).not.toEqual(expect.arrayContaining(['openaiTokens'])); } } const storedKnowledgeRaw = await fsp.readFile(legacyKnowledgePath, 'utf8'); expect(storedKnowledgeRaw).not.toContain('openaiPrompt'); expect(storedKnowledgeRaw).not.toContain('cohere'); const storedKnowledge = JSON.parse(storedKnowledgeRaw); expect(storedKnowledge['legacy-entry']).toEqual({ description: 'actualisé', nested: { insight: 'préserver' } }); }); test('MemoryManager falls back to embedded samples when no persisted data exists', async () => { const manager = new MemoryManager(); await waitUntil(() => manager.getRecentMemories().length > 0); const fallbackMemories = manager.getRecentMemories(10); expect(fallbackMemories.length).toBeGreaterThan(0); fallbackMemories.forEach(memory => { expect(memory).not.toHaveProperty('embedding'); expect(memory.content.length).toBeGreaterThan(0); }); const knowledgeKeys = ['raisonnement-efficace', 'biais-cognitifs']; knowledgeKeys.forEach(key => { const knowledge = manager.getKnowledge(key); expect(knowledge).toBeDefined(); }); }); test('MemoryManager handles directory creation fallback gracefully', async () => { const tempFallback = path.join(os.tmpdir(), 'smart-thinking-temp-fallback'); jest.spyOn(PathUtils, 'getTempDirectory').mockReturnValue(tempFallback); const ensureSpy = jest .spyOn(PathUtils, 'ensureDirectoryExists') .mockImplementationOnce(async () => { throw new Error('permission denied'); }) .mockImplementation(async () => undefined); const manager = new MemoryManager(); await waitUntil(() => manager.getRecentMemories().length > 0); await (manager as unknown as { ensureDirectoriesExist: () => Promise<void> }).ensureDirectoriesExist(); expect(manager['dataDir']).toContain('smart-thinking-temp-fallback'); ensureSpy.mockRestore(); await fsp.rm(tempFallback, { recursive: true, force: true }); }); test('MemoryManager retrieval helpers leverage similarity engine and graph persistence', async () => { const managerWithoutEngine = new MemoryManager(); const fallbackId = managerWithoutEngine.addMemory('Analyse contextuelle avancée', ['analysis', 'context']); const fallbackMatches = await managerWithoutEngine.getRelevantMemories('analyse contextuelle'); expect(fallbackMatches[0].id).toBe(fallbackId); await (managerWithoutEngine as unknown as { saveToStorage: () => Promise<void> }).saveToStorage(); expect(await managerWithoutEngine.loadGraphState('inconnue')).toBeNull(); const engine = new SimilarityEngine(); const withEngine = new MemoryManager(engine); const nodeId = withEngine.addMemory('Graphe orienté pondéré', ['graph', 'analysis'], 'session-a'); const heuristicId = withEngine.addMemory('Optimisation heuristique locale', ['heuristic', 'analysis'], 'session-a'); withEngine.addMemory('Mémoire d\'archive', ['archive'], 'session-b'); const defaultId = withEngine.addMemory('Mise à jour globale', ['analysis']); const sessionARecent = withEngine.getRecentMemories(5, 'session-a'); expect(sessionARecent.map(item => item.id).sort()).toEqual([heuristicId, nodeId].sort()); const defaultRecent = withEngine.getRecentMemories(2); expect(defaultRecent.map(item => item.id)).toContain(defaultId); const analysisMatches = withEngine.getMemoriesByTag('analysis', 10, 'session-a'); expect(analysisMatches.map(item => item.id).sort()).toEqual([heuristicId, nodeId].sort()); const engineMatches = await withEngine.getRelevantMemories('optimisation heuristique du graphe', 2, 'session-a'); expect(engineMatches.length).toBeGreaterThan(0); expect(engineMatches.some(match => match.id === heuristicId || match.id === nodeId)).toBe(true); await withEngine.saveGraphState('session-a', JSON.stringify({ nodes: 3 })); const graphState = await withEngine.loadGraphState('session-a'); expect(graphState).toBe(JSON.stringify({ nodes: 3 })); withEngine.clear(); expect(withEngine.getRecentMemories().length).toBe(0); withEngine.addMemory('Session B mémoire', ['archive'], 'session-b'); expect(withEngine.getRecentMemories(5, 'session-b').length).toBe(1); await (withEngine as unknown as { saveToStorage: () => Promise<void> }).saveToStorage(); expect(await withEngine.loadGraphState('session-missing')).toBeNull(); class EmptySimilarityEngine extends SimilarityEngine { async findSimilarTexts(): Promise<Array<{ text: string; score: number }>> { return []; } } const fallbackEngine = new MemoryManager(new EmptySimilarityEngine()); fallbackEngine.addMemory('Analyse locale ciblée', ['analyse', 'locale'], 'session-c'); const fallbackEngineMatches = await fallbackEngine.getRelevantMemories('analyse locale approfondie', 2, 'session-c'); expect(fallbackEngineMatches.length).toBeGreaterThan(0); await (fallbackEngine as unknown as { saveToStorage: () => Promise<void> }).saveToStorage(); }); test('VerificationMemory migrates legacy entries and keeps persistence consistent', async () => { const verificationsPath = path.join(tempDir, 'verifications.json'); const legacyVerificationPayload = { version: 0, verifications: [ { id: 'verification-1', text: 'Legacy verification', status: 'verified', confidence: '0.95', sources: ['https://legacy.example'], timestamp: '2024-01-01T00:00:00.000Z', sessionId: 'legacy-session', expiresAt: '2024-02-01T00:00:00.000Z', embedding: [0.11, 0.22], openaiTrace: { tokens: 12 } } ] }; await fsp.writeFile(verificationsPath, JSON.stringify(legacyVerificationPayload, null, 2), 'utf8'); const verificationMemory = VerificationMemory.getInstance(); const legacyResult = await verificationMemory.findVerification('Legacy verification', 'legacy-session'); expect(legacyResult).not.toBeNull(); expect(legacyResult?.confidence).toBeCloseTo(0.95, 5); const sessionEntries = verificationMemory.getSessionVerifications('legacy-session'); expect(sessionEntries).toHaveLength(1); expect(sessionEntries[0].confidence).toBeCloseTo(0.95, 5); const duplicateId = await verificationMemory.addVerification( 'Legacy verification', 'contradicted', 0.1, ['https://dup.example'], 'legacy-session', 3600 ); expect(duplicateId).toBe('verification-1'); const freshId = await verificationMemory.addVerification( 'Fresh fact', 'verified', 0.82, ['https://fresh.example'], 'legacy-session', 3600 ); expect(freshId).toBeDefined(); await (verificationMemory as unknown as { persistToStorage: () => Promise<void> }).persistToStorage(); VerificationMemory.resetInstance(); const reloadedMemory = VerificationMemory.getInstance(); reloadedMemory.stopCleanupTasks(); await waitUntil( () => reloadedMemory.getSessionVerifications('legacy-session').length === 2, 1500 ); const reloadedEntries = reloadedMemory.getSessionVerifications('legacy-session'); reloadedEntries.forEach(entry => { expect(entry).not.toHaveProperty('embedding'); }); const updatedLegacy = reloadedEntries.find(item => item.id === 'verification-1'); expect(updatedLegacy).toBeDefined(); expect(updatedLegacy?.status).toBe('contradicted'); }); test('VerificationMemory similarity cache and session management', async () => { VerificationMemory.resetInstance(); const verificationMemory = VerificationMemory.getInstance(); verificationMemory.stopCleanupTasks(); verificationMemory.setSimilarityEngine(new SimilarityEngine()); const baseId = await verificationMemory.addVerification( 'Analyse prédictive avancée', 'verified', 0.88, ['https://analytics.example'], 'session-y', 3600 ); await verificationMemory.addVerification( 'Analyse descriptive de base', 'partially_verified', 0.6, ['https://descriptive.example'], 'session-y', 3600 ); const similarityHit = await verificationMemory.findVerification('Analyse prédictive avancée', 'session-y'); expect(similarityHit?.id).toBe(baseId); expect(await verificationMemory.findVerification('Entrée inconnue', 'session-y')).toBeNull(); expect(await verificationMemory.findVerification('Analyse prédictive avancée', 'session-inexistante')).toBeNull(); const similarityResults = await verificationMemory.searchSimilarVerifications('Analyse prédictive avancée', 'session-y', 5, 0.1); expect(similarityResults.some(result => result.id === baseId)).toBe(true); const statsWithCache = verificationMemory.getStats(); expect(statsWithCache.cacheSize).toBeGreaterThanOrEqual(0); (verificationMemory as unknown as { cleanSimilarityCache: () => void }).cleanSimilarityCache(); const statsWithoutCache = verificationMemory.getStats(); expect(statsWithoutCache.cacheSize).toBe(0); const paginated = verificationMemory.getSessionVerifications('session-y', 1, 1, 'partially_verified'); expect(paginated.length).toBe(0); verificationMemory.clearSession('session-y'); verificationMemory.clearSession('session-inexistante'); await (verificationMemory as unknown as { persistToStorage: () => Promise<void> }).persistToStorage(); expect(verificationMemory.getSessionVerifications('session-y').length).toBe(0); }); test('VerificationMemory text fallback and duplication without similarity engine', async () => { VerificationMemory.resetInstance(); const verificationMemory = VerificationMemory.getInstance(); verificationMemory.stopCleanupTasks(); await verificationMemory.addVerification( 'Texte original détaillé', 'verified', 0.7, ['https://original.example'], 'session-text', 3600 ); await verificationMemory.addVerification( 'Texte tres proche détaillé', 'verified', 0.65, ['https://close.example'], 'session-text', 3600 ); const fallbackMatch = await verificationMemory.findVerification('Texte très proche détaillé', 'session-text'); expect(fallbackMatch).not.toBeNull(); const duplicateId = await verificationMemory.addVerification( 'Texte très proche détaillé', 'verified', 0.5, ['https://duplicate.example'], 'session-text', 3600 ); expect(duplicateId).toBeDefined(); const withoutEngine = await verificationMemory.searchSimilarVerifications('Texte original détaillé', 'session-text'); expect(withoutEngine).toEqual([]); const paged = verificationMemory.getSessionVerifications('session-text', 0, 1); expect(paged.length).toBe(1); await (verificationMemory as unknown as { persistToStorage: () => Promise<void> }).persistToStorage(); verificationMemory.clearSession('session-text'); expect(verificationMemory.getSessionVerifications('session-text').length).toBe(0); }); test('VerificationMemory statistics and cleanup cover expiration workflow', async () => { VerificationMemory.resetInstance(); const verificationsPath = path.join(tempDir, 'verifications.json'); await fsp.rm(verificationsPath, { force: true }); const verificationMemory = VerificationMemory.getInstance(); verificationMemory.stopCleanupTasks(); verificationMemory.setSimilarityEngine(new SimilarityEngine()); await verificationMemory.addVerification( 'Analyse des données', 'verified', 0.9, ['https://data.example'], 'session-z', 100 ); await verificationMemory.addVerification( 'Résultats partiels', 'partially_verified', 0.6, ['https://partial.example'], 'session-z', 100 ); await verificationMemory.addVerification( 'Entrée expirante', 'unverified', 0.3, [], 'session-z', 1 ); await (verificationMemory as unknown as { persistToStorage: () => Promise<void> }).persistToStorage(); await sleep(5); const beforeCleanup = verificationMemory.getSessionVerifications('session-z').length; (verificationMemory as unknown as { cleanExpiredEntries: () => void }).cleanExpiredEntries(); const afterCleanup = verificationMemory.getSessionVerifications('session-z').length; expect(beforeCleanup).toBeGreaterThan(afterCleanup); const stats = verificationMemory.getStats(); expect(stats.totalEntries).toBeGreaterThanOrEqual(2); expect(stats.entriesByStatus['partially_verified']).toBeGreaterThanOrEqual(1); const similar = await verificationMemory.searchSimilarVerifications('Analyse des données', 'session-z'); expect(similar.length).toBeGreaterThanOrEqual(1); const duplicate = await verificationMemory.addVerification( 'Analyse des données', 'contradicted', 0.2, [], 'session-z', 50 ); expect(duplicate).toBeDefined(); const updated = verificationMemory.getSessionVerifications('session-z'); expect(updated.some(entry => entry.status === 'contradicted')).toBe(true); verificationMemory.clearAll(); expect(verificationMemory.getStats().totalEntries).toBe(0); }); });

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/Leghis/Smart-Thinking'

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