f6e3ae1922
Completes the rolling-hash rollout (previously only createChainKey hashed): - createCompositeKey, createSrcRelKey, createDstRelKey, and the valueRelationsBySrc/Dst keys now produce 53-bit rolling hashes instead of `src|rel|dst` string concatenation. Direction markers keep srcRel vs dstRel distinct; the composite/chain keys are exact integers usable as Map keys. - Direct-check cache invalidation is now key-TRACKED instead of pattern- matched: every direct-check result is registered under the checked relation, its base relations (reverse dependency index), and the subject/object node ids (Arbiter._trackDirectCheckKey). Relation-level invalidation deletes the tracked keys for each affected relation (covering config-override checks); node-level invalidation (node removal / updateNodeData) deletes by node id. This replaces the pipe-delimited-string regex matching that required the old key format. - DecisionCache.invalidateByNodeKey and invalidateAll route through the tracked indexes; tracking maps are cleared on full flush. Tests updated to the tracked contract (register injected keys via _trackDirectCheckKey); full suite green.
216 lines
10 KiB
JavaScript
216 lines
10 KiB
JavaScript
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');
|