From 420df2df0ec2f9ab7ff9ea196aaff60473f1440c Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 14 Feb 2026 15:18:31 -0500 Subject: [PATCH] test(memory): add deterministic end-to-end memory test suite --- package.json | 2 +- tests/fixtures/memory-fixtures.js | 48 ++++++ tests/memory.test.js | 277 ++++++++++++++++++++++++++++++ 3 files changed, 326 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/memory-fixtures.js create mode 100644 tests/memory.test.js diff --git a/package.json b/package.json index 7176109..bb0642a 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "README.md" ], "scripts": { - "test": "node --test test/*.test.mjs", + "test": "node --test test/*.test.mjs tests/*.test.js", "lock:check:daily": "node bin/torch-lock.mjs check --cadence daily", "lock:check:weekly": "node bin/torch-lock.mjs check --cadence weekly", "lock:list": "node bin/torch-lock.mjs list", diff --git a/tests/fixtures/memory-fixtures.js b/tests/fixtures/memory-fixtures.js new file mode 100644 index 0000000..abb7d03 --- /dev/null +++ b/tests/fixtures/memory-fixtures.js @@ -0,0 +1,48 @@ +export const FIXED_NOW = 1_700_000_000_000; + +export const SYNTHETIC_EVENTS = [ + { + agent_id: 'agent-alpha', + content: 'Incident INC-101 resolved after restarting api service', + timestamp: FIXED_NOW - 5_000, + tags: ['incident', 'ops'], + metadata: { session_id: 'session-1' }, + }, + { + agent_id: 'agent-alpha', + content: 'Customer email alice@example.com confirmed recovery', + timestamp: FIXED_NOW - 3_000, + tags: ['incident', 'customer'], + metadata: { session_id: 'session-1' }, + }, + { + agent_id: 'agent-alpha', + content: 'Follow-up task created for postmortem', + timestamp: FIXED_NOW - 2_000, + tags: ['ops', 'task'], + metadata: { session_id: 'session-1' }, + }, +]; + +export const EXPECTED_SUMMARY = 'INC-101 resolved, customer contact [redacted:email], postmortem task queued'; + +export const EXPECTED_TAGS = ['incident', 'ops', 'customer', 'task']; + +export const BASE_MEMORY_RECORD = { + schema_version: 1, + id: 'memory-1', + agent_id: 'agent-alpha', + session_id: 'session-1', + type: 'event', + content: 'Incident INC-101 resolved after restarting api service', + summary: 'Incident resolved', + tags: ['incident', 'ops'], + importance: 0.7, + embedding_id: null, + created_at: FIXED_NOW - 5_000, + last_seen: FIXED_NOW - 2_000, + source: 'ingest', + ttl_days: 30, + merged_into: null, + pinned: false, +}; diff --git a/tests/memory.test.js b/tests/memory.test.js new file mode 100644 index 0000000..10488f4 --- /dev/null +++ b/tests/memory.test.js @@ -0,0 +1,277 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { normalizeMemoryItem, validateMemoryItem } from '../src/services/memory/schema.js'; +import { summarizeEvents } from '../src/services/memory/summarizer.js'; +import { ingestMemoryWindow } from '../src/services/memory/ingestor.js'; +import { filterAndRankMemories } from '../src/services/memory/retriever.js'; +import { createLifecyclePlan } from '../src/services/memory/pruner.js'; +import { startMemoryMaintenanceScheduler } from '../src/services/memory/scheduler.js'; +import { + BASE_MEMORY_RECORD, + EXPECTED_SUMMARY, + EXPECTED_TAGS, + FIXED_NOW, + SYNTHETIC_EVENTS, +} from './fixtures/memory-fixtures.js'; + +const wait = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms)); + +const withMockedNow = async (now, run) => { + const originalNow = Date.now; + Date.now = () => now; + try { + await run(); + } finally { + Date.now = originalNow; + } +}; + +test('schema validation covers required fields, coercion, and rejection paths', () => { + const normalized = normalizeMemoryItem({ + agent_id: 'agent-alpha', + content: 'synthetic content', + summary: 'synthetic summary', + tags: ['incident', 7, ''], + importance: 8, + ttl_days: 4.9, + pinned: 'true', + }); + + assert.equal(normalized.agent_id, 'agent-alpha'); + assert.deepEqual(normalized.tags, ['incident', '7']); + assert.equal(normalized.importance, 1); + assert.equal(normalized.ttl_days, 4); + assert.equal(normalized.pinned, true); + + const validation = validateMemoryItem({ + ...BASE_MEMORY_RECORD, + agent_id: '', + importance: 2, + ttl_days: -1, + }); + + assert.equal(validation.valid, false); + assert.ok(validation.errors.some((error) => error.field === 'agent_id')); + assert.ok(validation.errors.some((error) => error.field === 'importance')); + assert.ok(validation.errors.some((error) => error.field === 'ttl_days')); +}); + +test('summarizer parser accepts valid json and recovers after malformed json response', async () => { + let calls = 0; + const summary = await summarizeEvents(SYNTHETIC_EVENTS, { + generateSummary() { + calls += 1; + if (calls === 1) return '{"summary":"broken"'; + return JSON.stringify({ + summary: EXPECTED_SUMMARY, + importance: 0.78, + }); + }, + now: FIXED_NOW, + }); + + assert.equal(calls, 2); + assert.equal(summary.summary, EXPECTED_SUMMARY); + assert.equal(summary.importance, 0.78); +}); + +test('ingest integration persists memory records and links embeddings from event batches', async () => { + const inserted = []; + const linked = []; + + const repository = { + async insertMemory(memory) { + inserted.push(memory); + return memory; + }, + async linkEmbedding(memoryId, embedding) { + linked.push({ memoryId, embedding }); + }, + }; + + const records = await ingestMemoryWindow( + { + events: SYNTHETIC_EVENTS, + windowStart: FIXED_NOW - 10_000, + windowEnd: FIXED_NOW, + }, + { + repository, + now: FIXED_NOW, + maxChunkChars: 1_000, + embedText: () => [0.11, 0.89], + embedderAdapter: { + async embedText() { + return [0.11, 0.89]; + }, + async upsertVector() {}, + }, + generateSummary: async () => JSON.stringify({ + summary: EXPECTED_SUMMARY, + importance: 0.82, + }), + } + ); + + assert.equal(records.length, 1); + assert.equal(inserted.length, 1); + assert.equal(linked.length, 1); + assert.equal(inserted[0].summary, EXPECTED_SUMMARY); + assert.deepEqual(inserted[0].tags, EXPECTED_TAGS); + assert.equal(linked[0].memoryId, inserted[0].id); +}); + +test('retrieval ranking blends semantic, importance, and recency signals', async () => { + const records = [ + { + ...BASE_MEMORY_RECORD, + id: 'semantic-high', + importance: 0.2, + last_seen: FIXED_NOW - 200_000, + }, + { + ...BASE_MEMORY_RECORD, + id: 'importance-high', + summary: 'Minor semantic match', + importance: 0.95, + last_seen: FIXED_NOW - 90_000_000, + }, + { + ...BASE_MEMORY_RECORD, + id: 'recent-balanced', + summary: 'Recent task update', + importance: 0.7, + last_seen: FIXED_NOW - 10_000, + }, + ]; + + const ranked = await filterAndRankMemories(records, { + agent_id: 'agent-alpha', + query: 'incident resolved', + k: 3, + now: FIXED_NOW, + vectorAdapter: { + async embedText() { + return [1, 0]; + }, + async queryVector() { + return [ + { id: 'semantic-high', score: 0.99 }, + { id: 'recent-balanced', score: 0.52 }, + { id: 'importance-high', score: 0.15 }, + ]; + }, + }, + }); + + assert.deepEqual(ranked.map((item) => item.id), ['semantic-high', 'recent-balanced', 'importance-high']); +}); + +test('pruner policy protects pins, merges duplicates, and picks archive/delete outcomes', async () => { + const memories = [ + { + ...BASE_MEMORY_RECORD, + id: 'pinned-memory', + pinned: true, + importance: 0.01, + ttl_days: 1, + created_at: FIXED_NOW - (120 * 24 * 60 * 60 * 1000), + last_seen: FIXED_NOW - (120 * 24 * 60 * 60 * 1000), + }, + { + ...BASE_MEMORY_RECORD, + id: 'merge-a', + summary: 'service restart after outage', + importance: 0.2, + created_at: FIXED_NOW - 10_000, + last_seen: FIXED_NOW - 9_000, + }, + { + ...BASE_MEMORY_RECORD, + id: 'merge-b', + summary: 'outage triggered service restart', + importance: 0.2, + created_at: FIXED_NOW - 9_500, + last_seen: FIXED_NOW - 8_500, + tags: ['incident', 'ops'], + }, + { + ...BASE_MEMORY_RECORD, + id: 'delete-me', + importance: 0.01, + ttl_days: 1, + created_at: FIXED_NOW - (100 * 24 * 60 * 60 * 1000), + last_seen: FIXED_NOW - (100 * 24 * 60 * 60 * 1000), + pinned: false, + }, + ]; + + const plan = await createLifecyclePlan(memories, { + now: FIXED_NOW, + retentionMs: 30 * 24 * 60 * 60 * 1000, + similarityThreshold: 0.9, + duplicateWindowMs: 60_000, + getEmbedding(memory) { + if (memory.id === 'merge-a') return [1, 0]; + if (memory.id === 'merge-b') return [0.99, 0.01]; + return [0, 1]; + }, + generateSummary: async () => JSON.stringify({ + summary: 'Merged outage restart summary', + importance: 0.4, + }), + }); + + const actionById = new Map(plan.actions.map((action) => [action.id, action.action])); + assert.equal(actionById.get('pinned-memory'), 'keep'); + assert.equal(actionById.get('merge-a'), 'archive'); + assert.equal(actionById.get('merge-b'), 'keep'); + assert.equal(actionById.get('delete-me'), 'delete'); +}); + +test('scheduler smoke test handles mocked clock and lock contention', async () => { + const metrics = []; + + await withMockedNow(FIXED_NOW, async () => { + const scheduler = startMemoryMaintenanceScheduler({ + runImmediately: true, + maxRetries: 0, + random: () => 0, + emitMetric(name, payload) { + metrics.push({ name, payload }); + }, + lockProvider: { + async withLock(lockKey, task) { + if (lockKey.includes('consolidate-observations')) { + return { acquired: false, result: null }; + } + return { acquired: true, result: await task() }; + }, + }, + handlers: { + async ingestRecentRuntimeEvents() { + return { itemCount: 2 }; + }, + async consolidateObservations() { + return { itemCount: 0 }; + }, + async pruningCycle() { + return { itemCount: 1 }; + }, + async deepMergeArchivalMaintenance() { + return { itemCount: 1 }; + }, + }, + }); + + await wait(30); + scheduler.stop(); + }); + + const statuses = new Map(metrics.map((entry) => [entry.payload.job, entry.payload.status])); + assert.equal(statuses.get('ingestRecentRuntimeEvents'), 'success'); + assert.equal(statuses.get('consolidateObservations'), 'skipped_lock_unavailable'); + assert.equal(statuses.get('pruningCycle'), 'success'); + assert.equal(statuses.get('deepMergeArchivalMaintenance'), 'success'); +});