import { test, describe, it, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; import { Arbiter } from '../../src/index.js'; /** * Cache Invalidation Tests * * Tests that caches are properly invalidated when relations are modified, * ensuring that authorization results reflect the current state of the graph. */ describe('Cache Invalidation Tests', () => { let arbiter; beforeEach(() => { arbiter = new Arbiter({ embeddingDimensions: 256 }); // Set up basic relations arbiter.setRelationConfig('member_of', { type: 'direct' }); arbiter.setRelationConfig('owner', { type: 'direct' }); arbiter.setRelationConfig('can_read', { type: 'direct' }); // Create test entities arbiter.addNode('user:alice', 'user'); arbiter.addNode('user:bob', 'user'); arbiter.addNode('group:engineering', 'group'); arbiter.addNode('group:qa', 'group'); arbiter.addNode('document:spec', 'document'); arbiter.addNode('document:test-plan', 'document'); }); describe('Direct Relation Cache Invalidation', () => { it('invalidates cache when direct relations are added', () => { // Initial state: Alice has no access let result1 = arbiter.check('user:alice', 'can_read', 'document:spec'); assert.equal(result1.possibility, 0); // Add direct relation arbiter.addRelation('user:alice', 'can_read', 'document:spec', { possibility: 1.0 }); // Should now have access (cache should be invalidated) let result2 = arbiter.check('user:alice', 'can_read', 'document:spec'); assert.equal(result2.possibility, 1.0); // CI-001 fix: fast path → 'direct_match' assert.equal(result2.reason, 'direct_match'); }); it('invalidates cache when direct relations are removed', () => { // Set up initial relation arbiter.addRelation('user:alice', 'can_read', 'document:spec', { possibility: 1.0 }); // Verify access let result1 = arbiter.check('user:alice', 'can_read', 'document:spec'); assert.equal(result1.possibility, 1.0); // Remove relation arbiter.removeRelation('user:alice', 'can_read', 'document:spec'); // Should no longer have access (cache should be invalidated) let result2 = arbiter.check('user:alice', 'can_read', 'document:spec'); assert.equal(result2.possibility, 0); // CI-001 fix: fast path → 'no_relation' assert.equal(result2.reason, 'no_relation'); }); it('invalidates cache when relation possibility is updated', () => { // Set up initial relation with low possibility arbiter.addRelation('user:alice', 'can_read', 'document:spec', { possibility: 0.3 }); // Verify initial access let result1 = arbiter.check('user:alice', 'can_read', 'document:spec'); assert.equal(result1.possibility, 0.3); // Update relation with higher possibility arbiter.addRelation('user:alice', 'can_read', 'document:spec', { possibility: 0.8 }); // Should reflect new possibility (cache should be invalidated) let result2 = arbiter.check('user:alice', 'can_read', 'document:spec'); assert.equal(result2.possibility, 0.8); }); it('invalidates cache when relation values are updated', () => { // Set up relation with value arbiter.addRelation('user:alice', 'can_read', 'document:spec', { possibility: 1.0, value: 100 }); // Verify initial access with value let result1 = arbiter.check('user:alice', 'can_read', 'document:spec', { collectValues: true }); assert.equal(result1.possibility, 1.0); assert.ok(result1.collectedValues); assert.equal(result1.collectedValues[0].value, 100); // Update relation with new value arbiter.addRelation('user:alice', 'can_read', 'document:spec', { possibility: 1.0, value: 200 }); // Should reflect new value (cache should be invalidated) let result2 = arbiter.check('user:alice', 'can_read', 'document:spec', { collectValues: true }); assert.equal(result2.possibility, 1.0); assert.equal(result2.collectedValues[0].value, 200); }); }); describe('Tuple-to-Userset Cache Invalidation', () => { it('invalidates cache when intermediate relations change', () => { // Set up tuple-to-userset pattern arbiter.addRelation('user:alice', 'member_of', 'group:engineering'); arbiter.addRelation('document:spec', 'owner', 'group:engineering'); arbiter.setRelationConfig('group_access', { type: 'tuple_to_userset', tuplesetRelation: 'owner', computedRelation: 'member_of' }); // Verify initial access let result1 = arbiter.check('user:alice', 'group_access', 'document:spec'); assert.equal(result1.possibility, 1.0); // Remove intermediate relation arbiter.removeRelation('user:alice', 'member_of', 'group:engineering'); // Should no longer have access (cache should be invalidated) let result2 = arbiter.check('user:alice', 'group_access', 'document:spec'); assert.equal(result2.possibility, 0); }); it('invalidates cache when object relations change', () => { // Set up initial state arbiter.addRelation('user:alice', 'member_of', 'group:engineering'); arbiter.addRelation('document:spec', 'owner', 'group:engineering'); arbiter.setRelationConfig('group_access', { type: 'tuple_to_userset', tuplesetRelation: 'owner', computedRelation: 'member_of' }); // Verify initial access let result1 = arbiter.check('user:alice', 'group_access', 'document:spec'); assert.equal(result1.possibility, 1.0); // Change object ownership arbiter.removeRelation('document:spec', 'owner', 'group:engineering'); arbiter.addRelation('document:spec', 'owner', 'group:qa'); // Alice should no longer have access (cache should be invalidated) let result2 = arbiter.check('user:alice', 'group_access', 'document:spec'); assert.equal(result2.possibility, 0); }); }); describe('Chain Rule Cache Invalidation', () => { it('invalidates cache when chain relations change', () => { // Set up chain: user → group → document arbiter.addRelation('user:alice', 'member_of', 'group:engineering'); arbiter.addRelation('group:engineering', 'can_access', 'document:spec'); arbiter.setRelationConfig('chain_access', { type: 'chain', steps: [ { relation: 'member_of', direction: 'out' }, { relation: 'can_access', direction: 'out' } ] }); // Verify initial access let result1 = arbiter.check('user:alice', 'chain_access', 'document:spec'); assert.equal(result1.possibility, 1.0); // Break the chain arbiter.removeRelation('group:engineering', 'can_access', 'document:spec'); // Should no longer have access (cache should be invalidated) let result2 = arbiter.check('user:alice', 'chain_access', 'document:spec'); assert.equal(result2.possibility, 0); }); }); describe('Relational Comparator Cache Invalidation', () => { it('invalidates cache when compared values change', () => { // Add missing nodes arbiter.addNode('feature:premium', 'feature'); // Set up balance comparison arbiter.addRelation('user:alice', 'has_balance', 'user:alice', { value: 1000 }); arbiter.addRelation('feature:premium', 'has_price', 'feature:premium', { value: 800 }); arbiter.setRelationConfig('balance_check', { type: 'relational_comparator', left: { rule: { type: 'direct', relation: 'has_balance' }, extractValue: true }, right: { evaluateFrom: 'object', rule: { type: 'direct', relation: 'has_price' }, extractValue: true }, comparator: '>' }); // Verify initial comparison (1000 > 800 = true) let result1 = arbiter.check('user:alice', 'balance_check', 'feature:premium'); assert.equal(result1.possibility, 1.0); // Update balance to be lower arbiter.addRelation('user:alice', 'has_balance', 'user:alice', { value: 500 }); // Should reflect new comparison (500 > 800 = false, cache should be invalidated) let result2 = arbiter.check('user:alice', 'balance_check', 'feature:premium'); assert.equal(result2.possibility, 0); }); }); describe('Cache Performance Under Load', () => { it('maintains cache performance with frequent invalidations', () => { const startTime = Date.now(); // Perform many authorization checks with cache invalidations for (let i = 0; i < 100; i++) { // Add missing nodes arbiter.addNode(`document:test-${i}`, 'document'); // Add relation arbiter.addRelation('user:alice', 'can_read', `document:test-${i}`, { possibility: 1.0 }); // Check access const result = arbiter.check('user:alice', 'can_read', `document:test-${i}`); assert.equal(result.possibility, 1.0); // Remove relation (should invalidate cache) arbiter.removeRelation('user:alice', 'can_read', `document:test-${i}`); // Check access again (should be 0) const result2 = arbiter.check('user:alice', 'can_read', `document:test-${i}`); assert.equal(result2.possibility, 0); } const endTime = Date.now(); const duration = endTime - startTime; // Should complete within reasonable time (cache invalidation shouldn't be too slow) assert.ok(duration < 5000, `Cache invalidation took too long: ${duration}ms`); if (process.env.TEST_DEBUG === '1') console.log(`Cache invalidation performance: ${duration}ms for 100 operations`); }); }); describe('Cache Consistency', () => { it('ensures cache consistency across multiple authorization checks', () => { // Set up complex authorization scenario arbiter.addRelation('user:alice', 'member_of', 'group:engineering'); arbiter.addRelation('document:spec', 'owner', 'group:engineering'); arbiter.setRelationConfig('group_access', { type: 'tuple_to_userset', tuplesetRelation: 'owner', computedRelation: 'member_of' }); // Perform multiple checks const results = []; for (let i = 0; i < 10; i++) { results.push(arbiter.check('user:alice', 'group_access', 'document:spec')); } // All results should be identical (cache consistency) const firstResult = results[0]; for (let i = 1; i < results.length; i++) { assert.equal(results[i].possibility, firstResult.possibility); assert.equal(results[i].reason, firstResult.reason); } }); it('handles concurrent cache invalidations correctly', () => { // Set up initial state arbiter.addRelation('user:alice', 'can_read', 'document:spec', { possibility: 1.0 }); // Perform multiple operations that should invalidate cache const operations = [ () => arbiter.addRelation('user:alice', 'can_read', 'document:spec', { possibility: 0.5 }), () => arbiter.addRelation('user:alice', 'can_read', 'document:spec', { possibility: 0.8 }), () => arbiter.removeRelation('user:alice', 'can_read', 'document:spec'), () => arbiter.addRelation('user:alice', 'can_read', 'document:spec', { possibility: 1.0 }) ]; // Execute operations for (const operation of operations) { operation(); // Check that cache is properly invalidated const result = arbiter.check('user:alice', 'can_read', 'document:spec'); assert.ok(typeof result.possibility === 'number'); assert.ok(result.possibility >= 0 && result.possibility <= 1); } }); }); });