initial commit: @arbiter/core authorization engine with js-rigor hardening

Zanzibar-style authorization graph engine (direct/chain/TTU/defeasible/
binary modes, condensed snapshots, value relations) with 39 rigor test
campaigns. Includes fixes for snapshot binary writer/reader format
mismatch (snapshot-of-snapshot corruption), possibility write-boundary
validation, empty-graph snapshot serialization, relation lookup cache
direction collision, config-redefinition cache invalidation, binary
threshold semantics, defeasible compiled routing, and comparator
reason whitelisting.
This commit is contained in:
John Dvorak
2026-07-31 13:44:06 -07:00
commit 717ae1031e
373 changed files with 654131 additions and 0 deletions
+303
View File
@@ -0,0 +1,303 @@
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);
}
});
});
});