/** * Unit Tests - MemoryProxy Service v3 * Tests governance rule persistence and retrieval * * All MongoDB operations use in-memory mocks that faithfully replicate * the Mongoose model interfaces. No database connection required. */ // ============================================ // Helper functions (function declarations are hoisted, // making them available inside jest.mock() factories) // ============================================ /** * Match a document against a simplified MongoDB-style filter. * Supports: RegExp values, $in operator, plain equality. */ function mockMatchesFilter(doc, filter) { for (const key of Object.keys(filter)) { const value = filter[key]; if (value instanceof RegExp) { if (typeof doc[key] !== 'string' || !value.test(doc[key])) return false; } else if (value !== null && typeof value === 'object' && !Array.isArray(value)) { if ('$in' in value) { if (!value.$in.includes(doc[key])) return false; } // Other operators ignored — extend as needed } else { if (doc[key] !== value) return false; } } return true; } /** * Create a chainable query object for multi-document results. * Supports .lean(), .sort(), .limit(), and direct await. */ function mockChainableQuery(resultArray) { const chain = { lean() { return Promise.resolve(resultArray.map(r => { const copy = {}; for (const k of Object.keys(r)) { if (typeof r[k] !== 'function') copy[k] = r[k]; } return copy; })); }, sort() { return chain; }, limit() { return chain; }, then(resolve, reject) { return Promise.resolve(resultArray).then(resolve, reject); }, catch(fn) { return Promise.resolve(resultArray).catch(fn); } }; return chain; } /** * Create a chainable query object for single-document results (findById, findOne). * .lean() returns a plain object or null. */ function mockSingleDocQuery(doc) { return { lean() { if (!doc) return Promise.resolve(null); const copy = {}; for (const k of Object.keys(doc)) { if (typeof doc[k] !== 'function') copy[k] = doc[k]; } return Promise.resolve(copy); }, then(resolve, reject) { return Promise.resolve(doc).then(resolve, reject); }, catch(fn) { return Promise.resolve(doc).catch(fn); } }; } // ============================================ // Jest module mocks // ============================================ jest.mock('mongoose', () => ({ connection: { readyState: 1 }, connect: jest.fn().mockResolvedValue(undefined), disconnect: jest.fn().mockResolvedValue(undefined), })); jest.mock('../../src/utils/logger.util', () => ({ info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn(), })); jest.mock('../../src/services/AnthropicMemoryClient.service', () => ({ getAnthropicMemoryClient: jest.fn(() => { throw new Error('API key not available in test environment'); }), })); // GovernanceRule: in-memory store with full Mongoose-compatible interface jest.mock('../../src/models/GovernanceRule.model', () => { const _store = []; const mock = { _store, countDocuments: jest.fn(async () => _store.length), deleteMany: jest.fn(async (filter) => { const before = _store.length; const keep = _store.filter(doc => !mockMatchesFilter(doc, filter)); _store.length = 0; for (const item of keep) _store.push(item); return { deletedCount: before - keep.length }; }), /** * Process bulkWrite upsert operations in memory. * Handles updateOne with $set, $setOnInsert, and upsert:true. */ bulkWrite: jest.fn(async (operations) => { let upsertedCount = 0; let modifiedCount = 0; for (const op of operations) { if (!op.updateOne) continue; const { filter, update, upsert } = op.updateOne; const idx = _store.findIndex(doc => { for (const [k, v] of Object.entries(filter)) { if (doc[k] !== v) return false; } return true; }); if (idx >= 0) { // Update existing document — apply only $set fields if (update.$set) { for (const [k, v] of Object.entries(update.$set)) { _store[idx][k] = v; } } modifiedCount++; } else if (upsert) { // Insert new document — apply $setOnInsert then $set const newDoc = {}; if (update.$setOnInsert) Object.assign(newDoc, update.$setOnInsert); if (update.$set) Object.assign(newDoc, update.$set); // Include string/number/boolean filter fields (contains the id) for (const [k, v] of Object.entries(filter)) { if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') { newDoc[k] = v; } } _store.push(newDoc); upsertedCount++; } } return { upsertedCount, modifiedCount, matchedCount: modifiedCount }; }), /** * Return all active, non-expired rules. * Mirrors GovernanceRule.findActive() static method. */ findActive: jest.fn(async () => { return _store.filter(doc => doc.active !== false); }), /** * Find a single rule by its custom id field. * Returns a Mongoose-doc-like object with .toObject(). */ findByRuleId: jest.fn(async (ruleId) => { const found = _store.find(doc => doc.id === ruleId && doc.active !== false); if (!found) return null; const snapshot = { ...found }; snapshot.toObject = function () { return { ...found }; }; return snapshot; }), /** * Filter rules by quadrant. Returns array with .toObject() on each element. */ findByQuadrant: jest.fn(async (quadrant, activeOnly = true) => { let results = _store.filter(doc => doc.quadrant === quadrant); if (activeOnly) results = results.filter(doc => doc.active !== false); return results.map(doc => { const snapshot = { ...doc }; snapshot.toObject = function () { return { ...doc }; }; return snapshot; }); }), /** * Filter rules by persistence level. Returns array with .toObject() on each element. */ findByPersistence: jest.fn(async (persistence, activeOnly = true) => { let results = _store.filter(doc => doc.persistence === persistence); if (activeOnly) results = results.filter(doc => doc.active !== false); return results.map(doc => { const snapshot = { ...doc }; snapshot.toObject = function () { return { ...doc }; }; return snapshot; }); }), /** * General find with filter support. Returns chainable query with .lean(). */ find: jest.fn((filter) => { const results = _store.filter(doc => mockMatchesFilter(doc, filter)); return mockChainableQuery(results); }), }; return mock; }); // AuditLog: constructor-based mock with in-memory store jest.mock('../../src/models/AuditLog.model', () => { const _store = []; let _idCounter = 0; /** * Mock AuditLog constructor. Works with `new AuditLog(data)`. * Instance has .save() that persists to in-memory store. */ function AuditLogMock(data) { _idCounter++; this._id = `audit_${_idCounter}`; if (data) { for (const key of Object.keys(data)) { this[key] = data[key]; } } } AuditLogMock.prototype.save = async function () { const copy = {}; for (const key of Object.keys(this)) { if (typeof this[key] !== 'function') { copy[key] = this[key]; } } _store.push(copy); return this; }; // Expose store for test cleanup AuditLogMock._store = _store; AuditLogMock._resetIdCounter = () => { _idCounter = 0; }; AuditLogMock.countDocuments = jest.fn(async () => _store.length); AuditLogMock.deleteMany = jest.fn(async (filter) => { const before = _store.length; const keep = _store.filter(doc => !mockMatchesFilter(doc, filter)); _store.length = 0; for (const item of keep) _store.push(item); return { deletedCount: before - keep.length }; }); AuditLogMock.findById = jest.fn((id) => { const found = _store.find(doc => String(doc._id) === String(id)); return mockSingleDocQuery(found || null); }); AuditLogMock.find = jest.fn((filter) => { const results = _store.filter(doc => mockMatchesFilter(doc, filter)); return mockChainableQuery(results); }); return AuditLogMock; }); // ============================================ // Imports (resolved after mocks are registered) // ============================================ const { MemoryProxyService } = require('../../src/services/MemoryProxy.service'); const GovernanceRule = require('../../src/models/GovernanceRule.model'); const AuditLog = require('../../src/models/AuditLog.model'); const mongoose = require('mongoose'); // ============================================ // Tests // ============================================ jest.setTimeout(10000); describe('MemoryProxyService v3 (MongoDB)', () => { let memoryProxy; const testRules = [ { id: 'test_inst_001', text: 'Test rule 1', quadrant: 'STRATEGIC', persistence: 'HIGH', active: true }, { id: 'test_inst_002', text: 'Test rule 2', quadrant: 'OPERATIONAL', persistence: 'HIGH', active: true }, { id: 'test_inst_003', text: 'Test rule 3', quadrant: 'SYSTEM', persistence: 'MEDIUM', active: true } ]; beforeEach(async () => { // Clear in-memory stores for clean test isolation GovernanceRule._store.length = 0; AuditLog._store.length = 0; AuditLog._resetIdCounter(); memoryProxy = new MemoryProxyService({ cacheEnabled: true, cacheTTL: 1000, // 1 second for testing anthropicEnabled: false // Disable Anthropic API for tests }); await memoryProxy.initialize(); }); describe('Initialization', () => { test('should connect to MongoDB successfully', async () => { expect(mongoose.connection.readyState).toBe(1); // 1 = connected }); test('should initialize with correct configuration', () => { expect(memoryProxy.cacheEnabled).toBe(true); expect(memoryProxy.cacheTTL).toBe(1000); expect(memoryProxy.anthropicEnabled).toBe(false); }); }); describe('persistGovernanceRules', () => { test('should persist rules successfully', async () => { const result = await memoryProxy.persistGovernanceRules(testRules); expect(result.success).toBe(true); expect(result.total).toBe(3); expect(result.inserted + result.modified).toBe(3); expect(result.duration).toBeGreaterThanOrEqual(0); }); test('should store rules in MongoDB', async () => { await memoryProxy.persistGovernanceRules(testRules); const storedRules = await GovernanceRule.find({ id: /^test_/ }).lean(); expect(storedRules).toHaveLength(3); expect(storedRules[0].text).toBeDefined(); expect(storedRules[0].quadrant).toBeDefined(); expect(storedRules[0].persistence).toBeDefined(); }); test('should update existing rules on re-persist', async () => { // First persist await memoryProxy.persistGovernanceRules(testRules); // Update and re-persist const updatedRules = testRules.map(r => ({ ...r, text: r.text + ' UPDATED' })); const result = await memoryProxy.persistGovernanceRules(updatedRules); expect(result.success).toBe(true); expect(result.modified).toBe(3); const storedRules = await GovernanceRule.find({ id: /^test_/ }).lean(); expect(storedRules.every(r => r.text.includes('UPDATED'))).toBe(true); }); test('should reject empty rules array', async () => { await expect(memoryProxy.persistGovernanceRules([])) .rejects .toThrow('Cannot persist empty rules array'); }); test('should reject non-array input', async () => { await expect(memoryProxy.persistGovernanceRules({ invalid: 'input' })) .rejects .toThrow('Rules must be an array'); }); test('should clear cache after persisting', async () => { // Load rules to populate cache await memoryProxy.persistGovernanceRules(testRules); await memoryProxy.loadGovernanceRules(); const statsBefore = memoryProxy.getCacheStats(); expect(statsBefore.entries).toBeGreaterThan(0); // Persist again (should clear cache) await memoryProxy.persistGovernanceRules(testRules); const statsAfter = memoryProxy.getCacheStats(); expect(statsAfter.entries).toBe(0); }); }); describe('loadGovernanceRules', () => { beforeEach(async () => { await memoryProxy.persistGovernanceRules(testRules); }); test('should load rules successfully', async () => { const allRules = await memoryProxy.loadGovernanceRules(); const testRulesLoaded = allRules.filter(r => r.id.startsWith('test_')); expect(testRulesLoaded).toHaveLength(3); expect(testRulesLoaded.find(r => r.id === 'test_inst_001')).toBeDefined(); expect(testRulesLoaded.find(r => r.id === 'test_inst_002')).toBeDefined(); expect(testRulesLoaded.find(r => r.id === 'test_inst_003')).toBeDefined(); }); test('should load from cache on second call', async () => { // First call - from mock store await memoryProxy.loadGovernanceRules(); // Second call - from cache (much faster) const startTime = Date.now(); const allRules = await memoryProxy.loadGovernanceRules(); const duration = Date.now() - startTime; const testRulesLoaded = allRules.filter(r => r.id.startsWith('test_')); expect(testRulesLoaded).toHaveLength(3); expect(duration).toBeLessThan(10); // Cache should be very fast }); test('should bypass cache when skipCache option is true', async () => { // Load to populate cache await memoryProxy.loadGovernanceRules(); // Clear cache memoryProxy.clearCache(); // Load with skipCache should work const allRules = await memoryProxy.loadGovernanceRules({ skipCache: true }); const testRulesLoaded = allRules.filter(r => r.id.startsWith('test_')); expect(testRulesLoaded).toHaveLength(3); }); test('should return empty array when no test rules exist', async () => { // Clear all test rules await GovernanceRule.deleteMany({ id: /^test_/ }); // Clear cache so it reloads from store memoryProxy.clearCache(); const rules = await memoryProxy.loadGovernanceRules(); expect(rules.filter(r => r.id.startsWith('test_'))).toEqual([]); }); test('should maintain data integrity across persist/load cycle', async () => { const rules = await memoryProxy.loadGovernanceRules(); for (const testRule of testRules) { const loaded = rules.find(r => r.id === testRule.id); expect(loaded).toBeDefined(); expect(loaded.text).toBe(testRule.text); expect(loaded.quadrant).toBe(testRule.quadrant); expect(loaded.persistence).toBe(testRule.persistence); } }); }); describe('getRule', () => { beforeEach(async () => { await memoryProxy.persistGovernanceRules(testRules); }); test('should get specific rule by ID', async () => { const rule = await memoryProxy.getRule('test_inst_002'); expect(rule).toBeDefined(); expect(rule.id).toBe('test_inst_002'); expect(rule.text).toBe('Test rule 2'); expect(rule.quadrant).toBe('OPERATIONAL'); }); test('should return null for non-existent rule', async () => { const rule = await memoryProxy.getRule('test_inst_999'); expect(rule).toBeNull(); }); }); describe('getRulesByQuadrant', () => { beforeEach(async () => { await memoryProxy.persistGovernanceRules(testRules); }); test('should filter rules by quadrant', async () => { const strategicRules = await memoryProxy.getRulesByQuadrant('STRATEGIC'); const testStrategicRules = strategicRules.filter(r => r.id.startsWith('test_')); expect(testStrategicRules).toHaveLength(1); expect(testStrategicRules[0].id).toBe('test_inst_001'); expect(testStrategicRules[0].quadrant).toBe('STRATEGIC'); }); test('should return empty array for non-existent quadrant', async () => { const rules = await memoryProxy.getRulesByQuadrant('NONEXISTENT'); expect(rules.filter(r => r.id.startsWith('test_'))).toEqual([]); }); }); describe('getRulesByPersistence', () => { beforeEach(async () => { await memoryProxy.persistGovernanceRules(testRules); }); test('should filter rules by persistence level', async () => { const highRules = await memoryProxy.getRulesByPersistence('HIGH'); const testHighRules = highRules.filter(r => r.id.startsWith('test_')); expect(testHighRules).toHaveLength(2); expect(testHighRules.every(r => r.persistence === 'HIGH')).toBe(true); }); test('should return empty array for non-existent persistence level', async () => { const rules = await memoryProxy.getRulesByPersistence('LOW'); expect(rules.filter(r => r.id.startsWith('test_'))).toEqual([]); }); }); describe('auditDecision', () => { test('should audit decision successfully', async () => { const decision = { sessionId: 'test-session-001', action: 'blog_post_generation', rulesChecked: ['inst_016', 'inst_017'], violations: [], allowed: true, metadata: { user: 'test-user', timestamp: new Date().toISOString() } }; const result = await memoryProxy.auditDecision(decision); expect(result.success).toBe(true); expect(result.audited).toBe(true); expect(result.auditId).toBeDefined(); expect(result.duration).toBeGreaterThanOrEqual(0); }); test('should create audit log in MongoDB', async () => { const decision = { sessionId: 'test-session-002', action: 'test_action', allowed: true }; const result = await memoryProxy.auditDecision(decision); const storedLog = await AuditLog.findById(result.auditId).lean(); expect(storedLog).toBeDefined(); expect(storedLog.sessionId).toBe('test-session-002'); expect(storedLog.action).toBe('test_action'); expect(storedLog.allowed).toBe(true); expect(storedLog.timestamp).toBeDefined(); }); test('should store multiple audit entries', async () => { const decision1 = { sessionId: 'test-session-multi-1', action: 'action-1', allowed: true }; const decision2 = { sessionId: 'test-session-multi-2', action: 'action-2', allowed: false }; await memoryProxy.auditDecision(decision1); await memoryProxy.auditDecision(decision2); const logs = await AuditLog.find({ sessionId: { $in: ['test-session-multi-1', 'test-session-multi-2'] } }).lean(); expect(logs).toHaveLength(2); }); test('should reject decision without required fields', async () => { const invalidDecision = { sessionId: 'test', /* missing action */ }; await expect(memoryProxy.auditDecision(invalidDecision)) .rejects .toThrow('Decision must include sessionId and action'); }); }); describe('Cache Management', () => { test('should clear cache', async () => { await memoryProxy.persistGovernanceRules(testRules); // Load rules to populate cache await memoryProxy.loadGovernanceRules(); expect(memoryProxy.getCacheStats().entries).toBeGreaterThan(0); memoryProxy.clearCache(); expect(memoryProxy.getCacheStats().entries).toBe(0); }); test('should expire cache after TTL', async () => { // Create proxy with 100ms TTL const shortTTLProxy = new MemoryProxyService({ cacheEnabled: true, cacheTTL: 100, anthropicEnabled: false }); await shortTTLProxy.initialize(); await shortTTLProxy.persistGovernanceRules(testRules); // Load to populate cache await shortTTLProxy.loadGovernanceRules(); // Wait for cache to expire await new Promise(resolve => setTimeout(resolve, 150)); // Should reload from store (cache expired) const allRules = await shortTTLProxy.loadGovernanceRules(); const testRulesLoaded = allRules.filter(r => r.id.startsWith('test_')); expect(testRulesLoaded).toHaveLength(3); }); test('should get cache statistics', () => { const stats = memoryProxy.getCacheStats(); expect(stats.enabled).toBe(true); expect(stats.ttl).toBe(1000); expect(stats.entries).toBeGreaterThanOrEqual(0); expect(stats.keys).toBeDefined(); }); }); });