MemoryProxy.service.test.js was an integration test masquerading as a unit test — all 26 tests required a real MongoDB connection and failed with authentication timeouts in CI and local environments without credentials. Replaced with comprehensive in-memory mocks for GovernanceRule and AuditLog models that faithfully replicate the Mongoose interface: bulkWrite with upsert, findActive, findByRuleId, findByQuadrant, findByPersistence, deleteMany with regex/filter matching, chainable queries with .lean(), and constructor-based AuditLog with .save(). All 26 tests now pass in 0.37s (down from 260s of timeouts). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
658 lines
20 KiB
JavaScript
658 lines
20 KiB
JavaScript
/**
|
|
* 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();
|
|
});
|
|
});
|
|
});
|