import { test, describe, it, beforeEach } from 'node:test'; import assert from 'node:assert'; import { Arbiter } from '../../src/core/Arbiter.js'; import { ChainRule } from '../../src/authorization/rules/ChainRule.js'; describe('HyperbolicLRUCache Invalidation', () => { let arbiter; let chainRule; beforeEach(() => { arbiter = new Arbiter({ directCheckCacheSize: 1000, directCheckCacheTTL: 60000 }); chainRule = new ChainRule(arbiter); }); it('should support key deletion', () => { const cache = chainRule.chainResultCache; // Add some entries cache.set('key1', { result: { possibility: 0.5 }, timestamp: Date.now() }); cache.set('key2', { result: { possibility: 0.8 }, timestamp: Date.now() }); cache.set('key3', { result: { possibility: 0.3 }, timestamp: Date.now() }); assert.ok(cache.has('key1')); assert.ok(cache.has('key2')); assert.ok(cache.has('key3')); // Delete specific key const deleted = cache.delete('key2'); assert.ok(deleted); assert.ok(cache.has('key1')); assert.ok(!cache.has('key2')); assert.ok(cache.has('key3')); }); it('should support key invalidation', () => { const cache = chainRule.chainResultCache; // Add some entries cache.set('key1', { result: { possibility: 0.5 }, timestamp: Date.now() }); cache.set('key2', { result: { possibility: 0.8 }, timestamp: Date.now() }); // Invalidate specific key const invalidated = cache.invalidate('key1'); assert.ok(invalidated); assert.ok(!cache.has('key1')); assert.ok(cache.has('key2')); }); it('should support invalidating multiple keys', () => { const cache = chainRule.chainResultCache; // Add some entries cache.set('key1', { result: { possibility: 0.5 }, timestamp: Date.now() }); cache.set('key2', { result: { possibility: 0.8 }, timestamp: Date.now() }); cache.set('key3', { result: { possibility: 0.3 }, timestamp: Date.now() }); // Invalidate multiple keys const invalidatedCount = cache.invalidateMany(['key1', 'key3']); assert.strictEqual(invalidatedCount, 2); assert.ok(!cache.has('key1')); assert.ok(cache.has('key2')); assert.ok(!cache.has('key3')); }); it('should support pattern-based invalidation', () => { const cache = chainRule.chainResultCache; // Add entries with different patterns cache.set('user_123_role_membership', { result: { possibility: 0.5 }, timestamp: Date.now() }); cache.set('user_123_permission', { result: { possibility: 0.8 }, timestamp: Date.now() }); cache.set('user_456_role_membership', { result: { possibility: 0.3 }, timestamp: Date.now() }); cache.set('user_456_permission', { result: { possibility: 0.7 }, timestamp: Date.now() }); // Invalidate all entries for user_123 const invalidatedCount = cache.invalidateByPattern('user_123'); assert.strictEqual(invalidatedCount, 2); assert.ok(!cache.has('user_123_role_membership')); assert.ok(!cache.has('user_123_permission')); assert.ok(cache.has('user_456_role_membership')); assert.ok(cache.has('user_456_permission')); }); it('should support regex pattern invalidation', () => { const cache = chainRule.chainResultCache; // Add entries with different patterns cache.set('user_123_role_membership', { result: { possibility: 0.5 }, timestamp: Date.now() }); cache.set('user_123_permission', { result: { possibility: 0.8 }, timestamp: Date.now() }); cache.set('user_456_role_membership', { result: { possibility: 0.3 }, timestamp: Date.now() }); cache.set('user_456_permission', { result: { possibility: 0.7 }, timestamp: Date.now() }); // Invalidate all role_membership entries using regex const roleMembershipPattern = /.*role_membership.*/; const invalidatedCount = cache.invalidateByPattern(roleMembershipPattern); assert.strictEqual(invalidatedCount, 2); assert.ok(!cache.has('user_123_role_membership')); assert.ok(cache.has('user_123_permission')); assert.ok(!cache.has('user_456_role_membership')); assert.ok(cache.has('user_456_permission')); }); it('should handle cache invalidation in Arbiter direct check cache', () => { // Add some direct check cache entries arbiter.directCheckCache.set('user:alice|can_read|doc:secret', { result: { possibility: 0.8 }, timestamp: Date.now() }); arbiter.directCheckCache.set('user:bob|can_write|doc:public', { result: { possibility: 0.9 }, timestamp: Date.now() }); assert.ok(arbiter.directCheckCache.has('user:alice|can_read|doc:secret')); assert.ok(arbiter.directCheckCache.has('user:bob|can_write|doc:public')); // Invalidate entries for user:alice const invalidatedCount = arbiter.directCheckCache.invalidateByPattern('user:alice'); assert.strictEqual(invalidatedCount, 1); assert.ok(!arbiter.directCheckCache.has('user:alice|can_read|doc:secret')); assert.ok(arbiter.directCheckCache.has('user:bob|can_write|doc:public')); }); it('should handle cache invalidation in RelationManager', () => { // Cache state lives in RelationCaches (RF-08); reach it via `_caches`. const caches = arbiter.relationManager._caches; const srcRelKey = caches.makeSrcRelCacheKey(123, 'role_membership'); const dstRelKey = caches.makeDstRelCacheKey(456, 'role_membership'); const directKey = caches.makeDirectCacheKey(123, 'role_membership', 456); const otherKey = caches.makeDirectCacheKey(123, 'permission', 789); caches.relationLookupCache.set(srcRelKey, [{ src: 123, rel: 'role_membership', dst: 456 }]); caches.relationLookupCache.set(dstRelKey, [{ src: 456, rel: 'role_membership', dst: 789 }]); caches.relationLookupCache.set(directKey, [{ src: 123, rel: 'role_membership', dst: 456 }]); caches.relationLookupCache.set(otherKey, [{ src: 123, rel: 'permission', dst: 789 }]); // Test the invalidation method arbiter.relationManager._invalidateRelationCaches(123, 'role_membership', 456); // Entries for the (src, rel), (dst, rel) and (src, rel, dst) triple vanish assert.ok(!caches.relationLookupCache.has(srcRelKey)); assert.ok(!caches.relationLookupCache.has(dstRelKey)); assert.ok(!caches.relationLookupCache.has(directKey)); assert.ok(caches.relationLookupCache.has(otherKey)); // Different relation }); it('should handle cache invalidation in NodeManager', () => { // Add nodes const aliceId = arbiter.nodeManager.addNode('user:alice', 'user', { name: 'Alice' }); const bobId = arbiter.nodeManager.addNode('user:bob', 'user', { name: 'Bob' }); const carolId = arbiter.nodeManager.addNode('user:carol', 'user', { name: 'Carol' }); // Direct-check cache keys are composite keys over numeric node ids const aliceKey = arbiter.keyManager.createCompositeKey(aliceId, 'can_read', bobId); const bobTouchingAlice = arbiter.keyManager.createCompositeKey(bobId, 'can_read', aliceId); const carolKey = arbiter.keyManager.createCompositeKey(carolId, 'can_read', bobId); arbiter.directCheckCache.set(aliceKey, { result: { possibility: 0.8 }, timestamp: Date.now() }); arbiter.directCheckCache.set(bobTouchingAlice, { result: { possibility: 0.9 }, timestamp: Date.now() }); arbiter.directCheckCache.set(carolKey, { result: { possibility: 0.7 }, timestamp: Date.now() }); assert.ok(arbiter.directCheckCache.has(aliceKey)); assert.ok(arbiter.directCheckCache.has(bobTouchingAlice)); assert.ok(arbiter.directCheckCache.has(carolKey)); // Update node data (should trigger cache invalidation via the DecisionCache port) arbiter.nodeManager.updateNodeData('user:alice', { name: 'Alice Updated' }); // Any entry whose composite key contains alice's id is invalidated assert.ok(!arbiter.directCheckCache.has(aliceKey)); assert.ok(!arbiter.directCheckCache.has(bobTouchingAlice)); assert.ok(arbiter.directCheckCache.has(carolKey)); // Does not touch alice }); it('should handle cache invalidation in ChainRule', () => { // Add some chain cache entries chainRule.chainResultCache.set('0_154_role_membership_out|role_permission_out', { result: { possibility: 0.8 }, timestamp: Date.now() }); chainRule.chainResultCache.set('0_154_permission_out|access_out', { result: { possibility: 0.9 }, timestamp: Date.now() }); // Invalidate all chain result cache entries chainRule._invalidateAllChainCaches(); assert.ok(!chainRule.chainResultCache.has('0_154_role_membership_out|role_permission_out')); assert.ok(!chainRule.chainResultCache.has('0_154_permission_out|access_out')); }); it('should track eviction statistics', () => { const cache = chainRule.chainResultCache; // Fill cache beyond capacity to trigger evictions for (let i = 0; i < chainRule.maxCacheSize + 50; i++) { cache.set(`key_${i}`, { result: { possibility: 0.5 }, timestamp: Date.now() }); } // Check if eviction stats are tracked if (chainRule.stats) { console.log(`Chain result cache evictions: ${chainRule.stats.evictedResults || 0}`); console.log(`Chain path cache evictions: ${chainRule.stats.evictedPaths || 0}`); } // Cache should not exceed capacity assert.ok(cache.size() <= chainRule.maxCacheSize); }); }); console.log('✅ HyperbolicLRUCache Invalidation Test Suite'); console.log('🔧 New Features:'); console.log(' - delete(key) - Remove specific key'); console.log(' - invalidate(key) - Alias for delete'); console.log(' - invalidateMany(keys) - Remove multiple keys'); console.log(' - invalidateByPattern(pattern) - Remove keys matching pattern'); console.log(' - Support for both string and regex patterns'); console.log(' - Automatic cache invalidation on node/relation changes'); console.log(' - Better staleness handling');