/** * Unit Tests - MemoryProxy Service v3 * Tests MongoDB-backed governance rule persistence and retrieval */ 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'); // Increase timeout for MongoDB operations jest.setTimeout(30000); describe('MemoryProxyService v3 (MongoDB)', () => { let memoryProxy; // Connect to test database before all tests beforeAll(async () => { const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_test'; await mongoose.connect(mongoUri); }); // Disconnect after all tests afterAll(async () => { await mongoose.disconnect(); }); 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 test data from previous tests await GovernanceRule.deleteMany({ id: /^test_/ }); await AuditLog.deleteMany({ sessionId: /^test-/ }); memoryProxy = new MemoryProxyService({ cacheEnabled: true, cacheTTL: 1000, // 1 second for testing anthropicEnabled: false // Disable Anthropic API for tests }); await memoryProxy.initialize(); }); afterEach(async () => { // Cleanup test data await GovernanceRule.deleteMany({ id: /^test_/ }); await AuditLog.deleteMany({ sessionId: /^test-/ }); }); 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 MongoDB 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_/ }); 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 MongoDB (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(); }); }); });