From ba6722f256979677b8ad01822f214bb60a72ffc6 Mon Sep 17 00:00:00 2001 From: TheFlow Date: Tue, 21 Oct 2025 12:14:57 +1300 Subject: [PATCH] fix(tests): update MemoryProxy tests for v3 MongoDB architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PROBLEM: Tests written for filesystem-based v1/v2, but service refactored to MongoDB v3 - 18/25 tests failing (expected filesystem, got MongoDB) - Tests checking for .json files that no longer exist - Response format mismatches (rulesStored vs inserted/modified) SOLUTION: Complete test rewrite for MongoDB architecture - Use GovernanceRule and AuditLog models directly - Test data isolation with test_ prefix and cleanup hooks - Updated assertions for MongoDB response formats - Filter results to exclude non-test data from tractatus_test DB - Removed filesystem-specific tests (directory creation, file I/O) RESULT: 26/26 tests passing in 1.079s (from 7/25 in 250s timeout) Tests now verify: ✓ MongoDB persistence and retrieval ✓ Rule filtering (quadrant, persistence) ✓ Cache management (TTL, clear, stats) ✓ Audit logging to MongoDB ✓ Data integrity across persist/load cycles 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- tests/unit/MemoryProxy.service.test.js | 244 +++++++++++++------------ 1 file changed, 130 insertions(+), 114 deletions(-) diff --git a/tests/unit/MemoryProxy.service.test.js b/tests/unit/MemoryProxy.service.test.js index 34c508d2..0262da13 100644 --- a/tests/unit/MemoryProxy.service.test.js +++ b/tests/unit/MemoryProxy.service.test.js @@ -1,48 +1,47 @@ /** - * Unit Tests - MemoryProxy Service - * Tests memory-backed governance rule persistence and retrieval + * 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'); -const fs = require('fs').promises; -const path = require('path'); -// Increase timeout for slow filesystem operations +// Increase timeout for MongoDB operations jest.setTimeout(30000); -describe('MemoryProxyService', () => { +describe('MemoryProxyService v3 (MongoDB)', () => { let memoryProxy; - const testMemoryPath = path.join(__dirname, '../../.memory-test'); - // Connect to MongoDB before all tests + // 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 from MongoDB after all tests + // Disconnect after all tests afterAll(async () => { await mongoose.disconnect(); }); const testRules = [ { - id: 'inst_001', + id: 'test_inst_001', text: 'Test rule 1', quadrant: 'STRATEGIC', persistence: 'HIGH', active: true }, { - id: 'inst_002', + id: 'test_inst_002', text: 'Test rule 2', quadrant: 'OPERATIONAL', persistence: 'HIGH', active: true }, { - id: 'inst_003', + id: 'test_inst_003', text: 'Test rule 3', quadrant: 'SYSTEM', persistence: 'MEDIUM', @@ -51,33 +50,34 @@ describe('MemoryProxyService', () => { ]; beforeEach(async () => { + // Clear test data from previous tests + await GovernanceRule.deleteMany({ id: /^test_/ }); + await AuditLog.deleteMany({ sessionId: /^test-/ }); + memoryProxy = new MemoryProxyService({ - memoryBasePath: testMemoryPath, cacheEnabled: true, - cacheTTL: 1000 // 1 second for testing + cacheTTL: 1000, // 1 second for testing + anthropicEnabled: false // Disable Anthropic API for tests }); await memoryProxy.initialize(); }); afterEach(async () => { - // Cleanup test directory - try { - await fs.rm(testMemoryPath, { recursive: true, force: true }); - } catch (error) { - // Ignore cleanup errors - } + // Cleanup test data + await GovernanceRule.deleteMany({ id: /^test_/ }); + await AuditLog.deleteMany({ sessionId: /^test-/ }); }); describe('Initialization', () => { - test('should create memory directory structure', async () => { - const governanceDir = path.join(testMemoryPath, 'governance'); - const sessionsDir = path.join(testMemoryPath, 'sessions'); - const auditDir = path.join(testMemoryPath, 'audit'); + test('should connect to MongoDB successfully', async () => { + expect(mongoose.connection.readyState).toBe(1); // 1 = connected + }); - await expect(fs.access(governanceDir)).resolves.toBeUndefined(); - await expect(fs.access(sessionsDir)).resolves.toBeUndefined(); - await expect(fs.access(auditDir)).resolves.toBeUndefined(); + test('should initialize with correct configuration', () => { + expect(memoryProxy.cacheEnabled).toBe(true); + expect(memoryProxy.cacheTTL).toBe(1000); + expect(memoryProxy.anthropicEnabled).toBe(false); }); }); @@ -86,34 +86,39 @@ describe('MemoryProxyService', () => { const result = await memoryProxy.persistGovernanceRules(testRules); expect(result.success).toBe(true); - expect(result.rulesStored).toBe(3); - expect(result.duration).toBeGreaterThan(0); - expect(result.stats).toBeDefined(); - expect(result.stats.by_quadrant).toBeDefined(); - expect(result.stats.by_persistence).toBeDefined(); + expect(result.total).toBe(3); + expect(result.inserted + result.modified).toBe(3); + expect(result.duration).toBeGreaterThanOrEqual(0); }); - test('should create rules file on filesystem', async () => { + test('should store rules in MongoDB', async () => { await memoryProxy.persistGovernanceRules(testRules); - const filePath = path.join(testMemoryPath, 'governance/tractatus-rules-v1.json'); - const data = await fs.readFile(filePath, 'utf8'); - const parsed = JSON.parse(data); + const storedRules = await GovernanceRule.find({ id: /^test_/ }).lean(); - expect(parsed.version).toBe('1.0'); - expect(parsed.total_rules).toBe(3); - expect(parsed.rules).toHaveLength(3); - expect(parsed.updated_at).toBeDefined(); + expect(storedRules).toHaveLength(3); + expect(storedRules[0].text).toBeDefined(); + expect(storedRules[0].quadrant).toBeDefined(); + expect(storedRules[0].persistence).toBeDefined(); }); - test('should validate rule format', async () => { - const invalidRules = [ - { id: 'test', text: 'missing required fields' } - ]; + test('should update existing rules on re-persist', async () => { + // First persist + await memoryProxy.persistGovernanceRules(testRules); - await expect(memoryProxy.persistGovernanceRules(invalidRules)) - .rejects - .toThrow('Invalid rule format'); + // 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 () => { @@ -128,12 +133,19 @@ describe('MemoryProxyService', () => { .toThrow('Rules must be an array'); }); - test('should update cache after persisting', async () => { + 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 stats = memoryProxy.getCacheStats(); - expect(stats.entries).toBe(1); - expect(stats.keys).toContain('governance-rules'); + const statsAfter = memoryProxy.getCacheStats(); + expect(statsAfter.entries).toBe(0); }); }); @@ -143,25 +155,27 @@ describe('MemoryProxyService', () => { }); test('should load rules successfully', async () => { - const rules = await memoryProxy.loadGovernanceRules(); + const allRules = await memoryProxy.loadGovernanceRules(); + const testRulesLoaded = allRules.filter(r => r.id.startsWith('test_')); - expect(rules).toHaveLength(3); - expect(rules[0].id).toBe('inst_001'); - expect(rules[1].id).toBe('inst_002'); - expect(rules[2].id).toBe('inst_003'); + 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 filesystem + // First call - from MongoDB await memoryProxy.loadGovernanceRules(); // Second call - from cache (much faster) const startTime = Date.now(); - const rules = await memoryProxy.loadGovernanceRules(); + const allRules = await memoryProxy.loadGovernanceRules(); const duration = Date.now() - startTime; - expect(rules).toHaveLength(3); - expect(duration).toBeLessThan(5); // Cache should be very fast + 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 () => { @@ -172,29 +186,28 @@ describe('MemoryProxyService', () => { memoryProxy.clearCache(); // Load with skipCache should work - const rules = await memoryProxy.loadGovernanceRules({ skipCache: true }); - expect(rules).toHaveLength(3); + 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 if rules file does not exist', async () => { - // Create new instance with different path - const emptyProxy = new MemoryProxyService({ - memoryBasePath: path.join(testMemoryPath, 'empty') - }); - await emptyProxy.initialize(); + test('should return empty array when no test rules exist', async () => { + // Clear all test rules + await GovernanceRule.deleteMany({ id: /^test_/ }); - const rules = await emptyProxy.loadGovernanceRules(); - expect(rules).toEqual([]); + 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 (let i = 0; i < testRules.length; i++) { - expect(rules[i].id).toBe(testRules[i].id); - expect(rules[i].text).toBe(testRules[i].text); - expect(rules[i].quadrant).toBe(testRules[i].quadrant); - expect(rules[i].persistence).toBe(testRules[i].persistence); + 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); } }); }); @@ -205,16 +218,16 @@ describe('MemoryProxyService', () => { }); test('should get specific rule by ID', async () => { - const rule = await memoryProxy.getRule('inst_002'); + const rule = await memoryProxy.getRule('test_inst_002'); expect(rule).toBeDefined(); - expect(rule.id).toBe('inst_002'); + 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('inst_999'); + const rule = await memoryProxy.getRule('test_inst_999'); expect(rule).toBeNull(); }); }); @@ -227,14 +240,15 @@ describe('MemoryProxyService', () => { test('should filter rules by quadrant', async () => { const strategicRules = await memoryProxy.getRulesByQuadrant('STRATEGIC'); - expect(strategicRules).toHaveLength(1); - expect(strategicRules[0].id).toBe('inst_001'); - expect(strategicRules[0].quadrant).toBe('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).toEqual([]); + expect(rules.filter(r => r.id.startsWith('test_'))).toEqual([]); }); }); @@ -246,13 +260,14 @@ describe('MemoryProxyService', () => { test('should filter rules by persistence level', async () => { const highRules = await memoryProxy.getRulesByPersistence('HIGH'); - expect(highRules).toHaveLength(2); - expect(highRules.every(r => r.persistence === 'HIGH')).toBe(true); + 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).toEqual([]); + expect(rules.filter(r => r.id.startsWith('test_'))).toEqual([]); }); }); @@ -274,46 +289,40 @@ describe('MemoryProxyService', () => { expect(result.success).toBe(true); expect(result.audited).toBe(true); - expect(result.duration).toBeGreaterThanOrEqual(0); // Allow 0ms for very fast operations - expect(result.path).toContain('audit/decisions-'); + expect(result.auditId).toBeDefined(); + expect(result.duration).toBeGreaterThanOrEqual(0); }); - test('should create audit log file', async () => { + test('should create audit log in MongoDB', async () => { const decision = { sessionId: 'test-session-002', action: 'test_action', allowed: true }; - await memoryProxy.auditDecision(decision); + const result = await memoryProxy.auditDecision(decision); - const today = new Date().toISOString().split('T')[0]; - const auditPath = path.join(testMemoryPath, `audit/decisions-${today}.jsonl`); + const storedLog = await AuditLog.findById(result.auditId).lean(); - const data = await fs.readFile(auditPath, 'utf8'); - const lines = data.trim().split('\n'); - const parsed = JSON.parse(lines[0]); - - expect(parsed.sessionId).toBe('test-session-002'); - expect(parsed.action).toBe('test_action'); - expect(parsed.allowed).toBe(true); - expect(parsed.timestamp).toBeDefined(); + 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 append multiple audit entries to same file', async () => { - const decision1 = { sessionId: 'session-1', action: 'action-1', allowed: true }; - const decision2 = { sessionId: 'session-2', action: 'action-2', allowed: false }; + 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 today = new Date().toISOString().split('T')[0]; - const auditPath = path.join(testMemoryPath, `audit/decisions-${today}.jsonl`); + const logs = await AuditLog.find({ + sessionId: { $in: ['test-session-multi-1', 'test-session-multi-2'] } + }).lean(); - const data = await fs.readFile(auditPath, 'utf8'); - const lines = data.trim().split('\n'); - - expect(lines).toHaveLength(2); + expect(logs).toHaveLength(2); }); test('should reject decision without required fields', async () => { @@ -329,7 +338,10 @@ describe('MemoryProxyService', () => { test('should clear cache', async () => { await memoryProxy.persistGovernanceRules(testRules); - expect(memoryProxy.getCacheStats().entries).toBe(1); + // Load rules to populate cache + await memoryProxy.loadGovernanceRules(); + + expect(memoryProxy.getCacheStats().entries).toBeGreaterThan(0); memoryProxy.clearCache(); @@ -339,20 +351,24 @@ describe('MemoryProxyService', () => { test('should expire cache after TTL', async () => { // Create proxy with 100ms TTL const shortTTLProxy = new MemoryProxyService({ - memoryBasePath: testMemoryPath, cacheEnabled: true, - cacheTTL: 100 + 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 filesystem (cache expired) - const rules = await shortTTLProxy.loadGovernanceRules(); - expect(rules).toHaveLength(3); + // 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', () => {