Merge pull request #250 from PR0M3TH3AN/test-improvement-get-relevant-memories-15158310533650216935

Test Improvement: Make getRelevantMemories testable and add unit tests
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-17 14:27:03 -05:00
commit c2ceb00174
2 files changed
+136 -65

No files matched your search

+9 -4
View File
@@ -198,10 +198,15 @@ export async function ingestEvents(events, options = {}) {
* @returns {Promise<import('./schema.js').MemoryRecord[]>} 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,
+127 -61
View File
@@ -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');
});