diff --git a/src/services/memory/index.js b/src/services/memory/index.js index 986bbbf..cf7375f 100644 --- a/src/services/memory/index.js +++ b/src/services/memory/index.js @@ -198,10 +198,15 @@ export async function ingestEvents(events, options = {}) { * @returns {Promise} Sorted list of relevant memories. */ export async function getRelevantMemories(params) { - const { repository = memoryRepository, ...queryParams } = params; + const { + repository = memoryRepository, + ranker = filterAndRankMemories, + cache: cacheProvider = cache, + ...queryParams + } = params; const telemetry = buildTelemetryEmitter(params); const cacheKey = JSON.stringify(queryParams); - const cached = cache.get(cacheKey); + const cached = cacheProvider.get(cacheKey); if (cached) { telemetry('memory:retrieved', { agent_id: queryParams.agent_id, @@ -220,9 +225,9 @@ export async function getRelevantMemories(params) { ? await repository.listMemories(queryParams) : [...memoryStore.values()]; - const ranked = await filterAndRankMemories(source, queryParams); + const ranked = await ranker(source, queryParams); await updateMemoryUsage(repository, ranked.map((memory) => memory.id)); - cache.set(cacheKey, ranked); + cacheProvider.set(cacheKey, ranked); telemetry('memory:retrieved', { agent_id: queryParams.agent_id, diff --git a/test/memory-index.test.mjs b/test/memory-index.test.mjs index 21b5ff4..1e9709f 100644 --- a/test/memory-index.test.mjs +++ b/test/memory-index.test.mjs @@ -1,80 +1,146 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import crypto from 'node:crypto'; -import { triggerPruneDryRun } from '../src/services/memory/index.js'; +import { getRelevantMemories } from '../src/services/memory/index.js'; -function createMemory(overrides = {}) { +function createMockMemory(id) { return { - id: overrides.id ?? crypto.randomUUID(), - agent_id: overrides.agent_id ?? 'agent-1', - type: overrides.type ?? 'event', - pinned: overrides.pinned ?? false, - last_seen: overrides.last_seen ?? Date.now(), - merged_into: overrides.merged_into ?? null, - // Add other fields to match schema if necessary, though selectPrunableMemories mainly checks last_seen and pinned + id, + content: `content for ${id}`, + agent_id: 'agent-1', + created_at: Date.now(), + last_seen: Date.now(), }; } -test('triggerPruneDryRun uses repository listPruneCandidates if available', async () => { - const candidates = [createMemory({ id: 'c1' }), createMemory({ id: 'c2' })]; +test('getRelevantMemories uses cached results when available', async (t) => { + const cachedMemory = createMockMemory('cached-1'); + const cacheGetMock = t.mock.fn((key) => { + if (key.includes('test-query')) return [cachedMemory]; + return null; + }); + const cacheSetMock = t.mock.fn(); + + const mockCache = { + get: cacheGetMock, + set: cacheSetMock, + }; + + const repository = { + listMemories: t.mock.fn(async () => []), + updateMemoryUsage: async () => {}, + }; + + const result = await getRelevantMemories({ + agent_id: 'agent-1', + query: 'test-query', + cache: mockCache, + repository, + }); + + assert.equal(result.length, 1); + assert.equal(result[0].id, 'cached-1'); + assert.equal(cacheGetMock.mock.callCount(), 1); + // Should NOT call listMemories on cache hit + assert.equal(repository.listMemories.mock.callCount(), 0); + // Should NOT call cache.set on cache hit + assert.equal(cacheSetMock.mock.callCount(), 0); +}); + +test('getRelevantMemories calls ranker and updates cache on miss', async (t) => { + const memories = [createMockMemory('m1'), createMockMemory('m2')]; + const repository = { + listMemories: t.mock.fn(async () => memories), + updateMemoryUsage: t.mock.fn(async () => {}), + }; + + // Mock ranker to reverse the list + const ranker = t.mock.fn(async (source) => [...source].reverse()); + + const cacheSetMock = t.mock.fn(); + const mockCache = { + get: () => null, + set: cacheSetMock, + }; + + const result = await getRelevantMemories({ + agent_id: 'agent-1', + query: 'test-query', + repository, + ranker, + cache: mockCache, + }); + + assert.equal(result.length, 2); + assert.equal(result[0].id, 'm2'); + assert.equal(result[1].id, 'm1'); + + // Verify ranker called with correct source + assert.equal(ranker.mock.callCount(), 1); + const [sourceArg] = ranker.mock.calls[0].arguments; + assert.deepEqual(sourceArg, memories); + + // Verify cache updated + assert.equal(cacheSetMock.mock.callCount(), 1); + const [keyArg, valueArg] = cacheSetMock.mock.calls[0].arguments; + assert.ok(typeof keyArg === 'string'); + assert.deepEqual(valueArg, result); // Should cache the ranked result + + // Verify memory usage updated + assert.equal(repository.updateMemoryUsage.mock.callCount(), 2); +}); + +test('getRelevantMemories passes correct parameters to ranker', async (t) => { const repository = { - listPruneCandidates: async () => candidates, listMemories: async () => [], + updateMemoryUsage: async () => {}, + }; + const ranker = t.mock.fn(async () => []); + const mockCache = { get: () => null, set: () => {} }; + + const params = { + agent_id: 'agent-1', + query: 'my query', + tags: ['a', 'b'], + k: 5, + repository, + ranker, + cache: mockCache, }; - const result = await triggerPruneDryRun({ repository }); + await getRelevantMemories(params); - assert.equal(result.dryRun, true); - assert.equal(result.candidateCount, 2); - assert.deepEqual(result.candidates.map(c => c.id), ['c1', 'c2']); + assert.equal(ranker.mock.callCount(), 1); + const [_, queryParams] = ranker.mock.calls[0].arguments; + + assert.equal(queryParams.agent_id, 'agent-1'); + assert.equal(queryParams.query, 'my query'); + assert.deepEqual(queryParams.tags, ['a', 'b']); + assert.equal(queryParams.k, 5); + + // Ensure injected deps are NOT in queryParams + assert.equal(queryParams.repository, undefined); + assert.equal(queryParams.ranker, undefined); + assert.equal(queryParams.cache, undefined); }); -test('triggerPruneDryRun falls back to in-memory filtering if listPruneCandidates returns empty', async () => { - const now = Date.now(); - const retentionMs = 1000; - const oldMemory = createMemory({ id: 'old', last_seen: now - 2000 }); - const newMemory = createMemory({ id: 'new', last_seen: now - 500 }); - +test('getRelevantMemories triggers updateMemoryUsage on repository', async (t) => { + const m1 = createMockMemory('m1'); const repository = { - listPruneCandidates: async () => [], - listMemories: async () => [oldMemory, newMemory], + listMemories: async () => [m1], + updateMemoryUsage: t.mock.fn(async () => {}), }; + const ranker = async (source) => source; // identity ranker + const mockCache = { get: () => null, set: () => {} }; - const result = await triggerPruneDryRun({ repository, retentionMs, now }); + await getRelevantMemories({ + agent_id: 'agent-1', + repository, + ranker, + cache: mockCache, + }); - assert.equal(result.candidateCount, 1); - assert.equal(result.candidates[0].id, 'old'); -}); - -test('triggerPruneDryRun handles empty candidates correctly', async () => { - const repository = { - listPruneCandidates: async () => [], - listMemories: async () => [], - }; - - const result = await triggerPruneDryRun({ repository }); - - assert.equal(result.candidateCount, 0); - assert.deepEqual(result.candidates, []); -}); - -test('triggerPruneDryRun output structure validation', async () => { - const memory = createMemory({ id: 'test-mem', agent_id: 'agent-x', type: 'thought', pinned: false, last_seen: 100, merged_into: 'parent-mem' }); - const repository = { - listPruneCandidates: async () => [memory], - }; - - const result = await triggerPruneDryRun({ repository }); - - assert.equal(result.dryRun, true); - assert.ok(result.retentionMs > 0); - assert.equal(result.candidateCount, 1); - - const candidate = result.candidates[0]; - assert.equal(candidate.id, 'test-mem'); - assert.equal(candidate.agent_id, 'agent-x'); - assert.equal(candidate.type, 'thought'); - assert.equal(candidate.pinned, false); - assert.equal(candidate.last_seen, 100); - assert.equal(candidate.merged_into, 'parent-mem'); + // updateMemoryUsage helper calls repository.updateMemoryUsage for each memory + assert.equal(repository.updateMemoryUsage.mock.callCount(), 1); + const [idArg] = repository.updateMemoryUsage.mock.calls[0].arguments; + assert.equal(idArg, 'm1'); });