Files

216 lines
10 KiB
JavaScript
Raw Permalink Normal View History

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);
// Direct-check keys are rolling hashes; invalidation-by-node uses the
// per-node key index, so every injected key must be tracked.
arbiter.directCheckCache.set(aliceKey, { result: { possibility: 0.8 }, timestamp: Date.now() });
arbiter._trackDirectCheckKey('can_read', aliceId, bobId, aliceKey);
arbiter.directCheckCache.set(bobTouchingAlice, { result: { possibility: 0.9 }, timestamp: Date.now() });
arbiter._trackDirectCheckKey('can_read', bobId, aliceId, bobTouchingAlice);
arbiter.directCheckCache.set(carolKey, { result: { possibility: 0.7 }, timestamp: Date.now() });
arbiter._trackDirectCheckKey('can_read', carolId, bobId, carolKey);
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');