From a940172998d651b4c813657dcfd10fb6dfe8f521 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 18:55:55 +0000 Subject: [PATCH] feat(test): add tests for runPruneCycle - Added `test/memory-run-prune-cycle.test.mjs` to test `runPruneCycle` logic. - Covered active pruning, dry-run, off mode, telemetry/metrics, and scheduling. - Utilized environment isolation (temp dir) to prevent side effects on `.scheduler-memory/memory-store.json`. - Addressed testing gap for `runPruneCycle` in `src/services/memory/index.js`. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- test/memory-run-prune-cycle.test.mjs | 156 +++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 test/memory-run-prune-cycle.test.mjs diff --git a/test/memory-run-prune-cycle.test.mjs b/test/memory-run-prune-cycle.test.mjs new file mode 100644 index 0000000..a9092a3 --- /dev/null +++ b/test/memory-run-prune-cycle.test.mjs @@ -0,0 +1,156 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import os from 'node:os'; +import crypto from 'node:crypto'; + +const originalCwd = process.cwd(); +let tmpDir; + +function createMemory(overrides = {}) { + return { + schema_version: 1, + id: overrides.id ?? crypto.randomUUID(), + agent_id: overrides.agent_id ?? 'agent-1', + session_id: overrides.session_id ?? 's1', + type: overrides.type ?? 'event', + content: overrides.content ?? 'test content', + summary: overrides.summary ?? 'test summary', + tags: overrides.tags ?? [], + importance: overrides.importance ?? 0.5, + embedding_id: overrides.embedding_id ?? null, + created_at: overrides.created_at ?? Date.now(), + last_seen: overrides.last_seen ?? Date.now(), + source: overrides.source ?? 'ingest', + ttl_days: overrides.ttl_days ?? null, + merged_into: overrides.merged_into ?? null, + pinned: overrides.pinned ?? false, + }; +} + +// Global state variables for the module under test +let runPruneCycle; +let listMemories; + +// Setup temp environment and load module +test.before(async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'memory-prune-test-')); + + // Create .scheduler-memory directory + const memDir = path.join(tmpDir, '.scheduler-memory'); + await fs.mkdir(memDir, { recursive: true }); + + // Create initial state + const now = 200_000; + // retentionMs will be 100,000 + // cutoff = 100,000 + + const memories = [ + createMemory({ id: 'recent', last_seen: 150_000 }), // > 100,000 -> Keep + createMemory({ id: 'pinned', last_seen: 50_000, pinned: true }), // < 100,000 but pinned -> Keep + createMemory({ id: 'stale', last_seen: 50_000 }), // < 100,000 -> Delete + createMemory({ id: 'stale-dry', last_seen: 50_000 }), // < 100,000 -> Delete candidate + createMemory({ id: 'stale-off', last_seen: 50_000 }), // < 100,000 -> Delete candidate + ]; + + // memory-store.json expects an array of entries [[id, memory], ...] + const entries = memories.map(m => [m.id, m]); + await fs.writeFile(path.join(memDir, 'memory-store.json'), JSON.stringify(entries), 'utf8'); + + // Change CWD + process.chdir(tmpDir); + + // Import module (it will load the file we just wrote) + const mod = await import('../src/services/memory/index.js'); + runPruneCycle = mod.runPruneCycle; + listMemories = mod.listMemories; +}); + +test.after(async () => { + if (originalCwd) process.chdir(originalCwd); + if (tmpDir) await fs.rm(tmpDir, { recursive: true, force: true }); +}); + +test('runPruneCycle respects off mode', async () => { + const now = 200_000; + const retentionMs = 100_000; + + const result = await runPruneCycle({ + now, + retentionMs, + pruneMode: 'off', + }); + + assert.equal(result.mode, 'off'); + + // Verify nothing deleted + const memories = await listMemories(); + assert.equal(memories.length, 5); +}); + +test('runPruneCycle respects dry-run mode', async () => { + const now = 200_000; + const retentionMs = 100_000; + + const result = await runPruneCycle({ + now, + retentionMs, + pruneMode: 'dry-run', + }); + + assert.equal(result.mode, 'dry-run'); + // Expect 3 candidates: stale, stale-dry, stale-off + assert.equal(result.candidates.length, 3); + + // Verify nothing deleted + const memories = await listMemories(); + assert.equal(memories.length, 5); +}); + +test('runPruneCycle removes stale unpinned memories in active mode', async () => { + const now = 200_000; + const retentionMs = 100_000; + + const telemetry = []; + const metrics = []; + + const result = await runPruneCycle({ + now, + retentionMs, + pruneMode: 'active', + emitTelemetry: (event, payload) => telemetry.push({ event, payload }), + emitMetric: (metric, payload) => metrics.push({ metric, payload }), + }); + + assert.equal(result.pruned.length, 3); // stale, stale-dry, stale-off + + // Verify deletion + const memories = await listMemories(); + assert.equal(memories.length, 2); + const ids = memories.map(m => m.id); + assert.ok(ids.includes('recent')); + assert.ok(ids.includes('pinned')); + assert.ok(!ids.includes('stale')); + + // Verify telemetry + const prunedEvent = telemetry.find(t => t.event === 'memory:pruned'); + assert.ok(prunedEvent); + assert.equal(prunedEvent.payload.pruned_count, 3); + + // Verify metrics + const prunedMetric = metrics.find(m => m.metric === 'memory_pruned_total'); + assert.ok(prunedMetric); + assert.equal(prunedMetric.payload.count, 3); +}); + +test('runPruneCycle starts scheduler if requested', async () => { + const result = await runPruneCycle({ + scheduleEveryMs: 1000, + pruneMode: 'active', + }); + + assert.ok(result.scheduler); + assert.equal(typeof result.scheduler.stop, 'function'); + result.scheduler.stop(); +});