memory.test.ts
1 import { describe, it, beforeEach, afterEach } from 'node:test' 2 import assert from 'node:assert/strict' 3 import fs from 'fs' 4 import path from 'path' 5 import { 6 normalizeLinkedMemoryIds, 7 normalizeMemoryLookupLimits, 8 resolveLookupRequest, 9 traverseLinkedMemoryGraph, 10 type MemoryLookupLimits, 11 type LinkedMemoryNode, 12 } from '../src/lib/server/memory/memory-graph' 13 14 // Use a test-specific database path 15 const TEST_DB_DIR = path.join(process.cwd(), 'data', 'test-memory') 16 const TEST_IMAGES_DIR = path.join(TEST_DB_DIR, 'memory-images') 17 18 function setupTestDb() { 19 // Clean up any existing test database 20 if (fs.existsSync(TEST_DB_DIR)) { 21 fs.rmSync(TEST_DB_DIR, { recursive: true, force: true }) 22 } 23 fs.mkdirSync(TEST_DB_DIR, { recursive: true }) 24 fs.mkdirSync(TEST_IMAGES_DIR, { recursive: true }) 25 } 26 27 function cleanupTestDb() { 28 if (fs.existsSync(TEST_DB_DIR)) { 29 fs.rmSync(TEST_DB_DIR, { recursive: true, force: true }) 30 } 31 } 32 33 describe('Memory System', () => { 34 beforeEach(() => { 35 setupTestDb() 36 }) 37 38 afterEach(() => { 39 cleanupTestDb() 40 }) 41 42 describe('normalizeLinkedMemoryIds', () => { 43 it('filters out empty strings and self-references', () => { 44 assert.deepEqual(normalizeLinkedMemoryIds(['a', '', 'b', ' ', 'a', 'c'], 'self'), ['a', 'b', 'c']) 45 }) 46 47 it('returns empty array for non-array input', () => { 48 assert.deepEqual(normalizeLinkedMemoryIds(null), []) 49 assert.deepEqual(normalizeLinkedMemoryIds(undefined), []) 50 assert.deepEqual(normalizeLinkedMemoryIds('not-an-array'), []) 51 }) 52 53 it('deduplicates ids', () => { 54 assert.deepEqual(normalizeLinkedMemoryIds(['a', 'b', 'a', 'a', 'c'], undefined), ['a', 'b', 'c']) 55 }) 56 }) 57 58 describe('normalizeMemoryLookupLimits', () => { 59 it('returns defaults for empty settings', () => { 60 const limits = normalizeMemoryLookupLimits({}) 61 assert.equal(limits.maxDepth, 3) 62 assert.equal(limits.maxPerLookup, 20) 63 assert.equal(limits.maxLinkedExpansion, 60) 64 }) 65 66 it('clamps values to valid ranges', () => { 67 const limits = normalizeMemoryLookupLimits({ 68 memoryReferenceDepth: 100, 69 maxMemoriesPerLookup: 1000, 70 maxLinkedMemoriesExpanded: 5000, 71 }) 72 assert.equal(limits.maxDepth, 12) // max 73 assert.equal(limits.maxPerLookup, 200) // max 74 assert.equal(limits.maxLinkedExpansion, 1000) // max 75 }) 76 77 it('clamps zeros to minimums', () => { 78 const limits = normalizeMemoryLookupLimits({ 79 memoryReferenceDepth: 0, 80 maxMemoriesPerLookup: 0, 81 maxLinkedMemoriesExpanded: 0, 82 }) 83 assert.equal(limits.maxDepth, 0) 84 assert.equal(limits.maxPerLookup, 1) // min 85 assert.equal(limits.maxLinkedExpansion, 0) 86 }) 87 88 it('uses legacy setting names as fallback', () => { 89 const limits = normalizeMemoryLookupLimits({ 90 memoryMaxDepth: 5, 91 memoryMaxPerLookup: 50, 92 }) 93 assert.equal(limits.maxDepth, 5) 94 assert.equal(limits.maxPerLookup, 50) 95 }) 96 }) 97 98 describe('resolveLookupRequest', () => { 99 const defaults: MemoryLookupLimits = { 100 maxDepth: 3, 101 maxPerLookup: 20, 102 maxLinkedExpansion: 60, 103 } 104 105 it('uses defaults when request is empty', () => { 106 const result = resolveLookupRequest(defaults, {}) 107 assert.deepEqual(result, defaults) 108 }) 109 110 it('overrides with request values', () => { 111 const result = resolveLookupRequest(defaults, { depth: 2, limit: 10, linkedLimit: 30 }) 112 assert.equal(result.maxDepth, 2) 113 assert.equal(result.maxPerLookup, 10) 114 assert.equal(result.maxLinkedExpansion, 30) 115 }) 116 117 it('caps at defaults maxima', () => { 118 const result = resolveLookupRequest(defaults, { depth: 100, limit: 1000, linkedLimit: 5000 }) 119 assert.equal(result.maxDepth, 3) // capped at default 120 assert.equal(result.maxPerLookup, 20) // capped at default 121 assert.equal(result.maxLinkedExpansion, 60) // capped at default 122 }) 123 }) 124 125 describe('traverseLinkedMemoryGraph', () => { 126 const fetchByIds = (ids: string[]): LinkedMemoryNode[] => { 127 return ids.map((id) => ({ 128 id, 129 linkedMemoryIds: id === 'a' ? ['b', 'c'] : id === 'b' ? ['d'] : [], 130 })) 131 } 132 133 it('returns empty for empty seeds', () => { 134 const result = traverseLinkedMemoryGraph([], { maxDepth: 3, maxPerLookup: 20, maxLinkedExpansion: 60 }, fetchByIds) 135 assert.deepEqual(result.entries, []) 136 assert.equal(result.truncated, false) 137 assert.equal(result.expandedLinkedCount, 0) 138 }) 139 140 it('traverses linked nodes by depth', () => { 141 // a -> [b, c], b -> [d] 142 const seeds = [{ id: 'a', linkedMemoryIds: ['b', 'c'] }] 143 const result = traverseLinkedMemoryGraph(seeds, { maxDepth: 2, maxPerLookup: 20, maxLinkedExpansion: 60 }, fetchByIds) 144 const ids = result.entries.map((n) => n.id) 145 assert.ok(ids.includes('a')) 146 assert.ok(ids.includes('b')) 147 assert.ok(ids.includes('c')) 148 assert.ok(ids.includes('d')) // depth 2 149 assert.equal(result.expandedLinkedCount, 3) // b, c, d 150 }) 151 152 it('respects maxDepth', () => { 153 // a -> [b], b -> [c], c -> [d] (if fetch returned that) 154 const limitedFetch = (ids: string[]): LinkedMemoryNode[] => { 155 const map: Record<string, string[]> = { a: ['b'], b: ['c'], c: ['d'], d: [] } 156 return ids.map((id) => ({ id, linkedMemoryIds: map[id] || [] })) 157 } 158 const seeds = [{ id: 'a', linkedMemoryIds: ['b'] }] 159 const result = traverseLinkedMemoryGraph(seeds, { maxDepth: 1, maxPerLookup: 20, maxLinkedExpansion: 60 }, limitedFetch) 160 const ids = result.entries.map((n) => n.id) 161 assert.ok(ids.includes('a')) 162 assert.ok(ids.includes('b')) 163 assert.ok(!ids.includes('c')) // depth 1 stops before c 164 }) 165 166 it('respects maxPerLookup', () => { 167 const seeds = [{ id: 'a', linkedMemoryIds: ['b', 'c', 'd', 'e'] }] 168 const result = traverseLinkedMemoryGraph(seeds, { maxDepth: 3, maxPerLookup: 3, maxLinkedExpansion: 60 }, fetchByIds) 169 assert.equal(result.entries.length, 3) 170 assert.equal(result.truncated, true) 171 }) 172 173 it('respects maxLinkedExpansion', () => { 174 const seeds = [{ id: 'a', linkedMemoryIds: ['b', 'c', 'd', 'e', 'f'] }] 175 const result = traverseLinkedMemoryGraph(seeds, { maxDepth: 3, maxPerLookup: 20, maxLinkedExpansion: 2 }, fetchByIds) 176 assert.equal(result.expandedLinkedCount, 2) 177 assert.equal(result.truncated, true) 178 }) 179 180 it('handles circular links gracefully', () => { 181 // a -> [b], b -> [a] (circular) 182 const circularFetch = (ids: string[]): LinkedMemoryNode[] => { 183 const map: Record<string, string[]> = { a: ['b'], b: ['a'] } 184 return ids.map((id) => ({ id, linkedMemoryIds: map[id] || [] })) 185 } 186 const seeds = [{ id: 'a', linkedMemoryIds: ['b'] }] 187 const result = traverseLinkedMemoryGraph(seeds, { maxDepth: 10, maxPerLookup: 100, maxLinkedExpansion: 100 }, circularFetch) 188 assert.equal(result.entries.length, 2) // just a and b 189 assert.equal(result.truncated, false) 190 }) 191 }) 192 }) 193 194 describe('Memory Database', () => { 195 beforeEach(() => { 196 setupTestDb() 197 }) 198 199 afterEach(() => { 200 cleanupTestDb() 201 }) 202 203 // Note: These tests require mocking the DB path or using a test-specific path 204 // The actual getMemoryDb() uses a singleton, so these are integration-style tests 205 206 describe('Reference normalization', () => { 207 it('converts legacy filePaths to references', () => { 208 const legacyPaths = [ 209 { path: '/src/lib/x.ts', contextSnippet: 'buggy function', kind: 'file' as const, timestamp: Date.now() }, 210 { path: '/src', kind: 'folder' as const, timestamp: Date.now() }, 211 ] 212 // This would be tested via the actual memory-db.ts normalizeReferences helper 213 // For now, verify the structure is expected 214 assert.equal(legacyPaths[0].path, '/src/lib/x.ts') 215 assert.equal(legacyPaths[0].kind, 'file') 216 }) 217 }) 218 219 describe('Image storage', () => { 220 it('rejects images over 10MB', async () => { 221 // This would require creating a large temp file 222 // Skipped for unit test - integration test needed 223 }) 224 225 it('compresses images to 1024px max dimension', async () => { 226 // This would require sharp and a real image file 227 // Skipped for unit test - integration test needed 228 }) 229 }) 230 })