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
+348
View File
@@ -0,0 +1,348 @@
import { BaseRule } from '../../src/authorization/rules/BaseRule.js';
import { Arbiter } from '../../src/core/Arbiter.js';
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
// Create a concrete implementation of BaseRule for testing
class TestRule extends BaseRule {
constructor(arbiter) {
super(arbiter);
}
canBatchProcess() {
return true;
}
_evaluateRule(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) {
// Simple test implementation
return {
possibility: 0.8,
reliability: 0.9,
collectedValues: [
{
value: 100,
possibility: 0.8,
path: [userKey, objectKey],
source: {
entityKey: userKey,
relation: 'test',
step: 1
},
metadata: {
timestamp: Date.now(),
reliability: 0.9,
decay: null
}
}
],
meta: {
ruleType: 'test',
userKey,
objectKey
},
reason: 'test_evaluation'
};
}
}
describe('BaseRule - Core Rule Infrastructure', () => {
let testRule;
let arbiter;
beforeEach(() => {
arbiter = new Arbiter({ embeddingDimensions: 256 });
testRule = new TestRule(arbiter);
// Set up test entities
arbiter.addNode('user:alice', 'user');
arbiter.addNode('project:secret', 'project');
});
describe('Constructor and Initialization', () => {
it('prevents direct instantiation of BaseRule', () => {
assert.throws(() => {
new BaseRule(arbiter);
}, Error, 'BaseRule is abstract and cannot be instantiated directly');
});
});
describe('Standard Evaluation Interface', () => {
it('evaluates rules with proper input validation', () => {
const userId = arbiter.nodeIdByKey.get('user:alice');
const objectId = arbiter.nodeIdByKey.get('project:secret');
const rule = { type: 'test' };
const visited = new Set();
const result = testRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation');
assert.ok(result, 'Should return result');
assert.strictEqual(result.possibility, 0.8, 'Should return correct possibility');
assert.strictEqual(result.reliability, 0.9, 'Should return correct reliability');
assert.ok(Array.isArray(result.collectedValues), 'Should return collected values');
assert.strictEqual(result.reason, 'test_evaluation', 'Should return correct reason');
});
it('tracks performance metrics', () => {
const userId = arbiter.nodeIdByKey.get('user:alice');
const objectId = arbiter.nodeIdByKey.get('project:secret');
const rule = { type: 'test' };
const visited = new Set();
testRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation');
assert.strictEqual(testRule.evaluationCount, 1, 'Should increment evaluation count');
assert.ok(testRule.totalEvaluationTime >= 0, 'Should track evaluation time');
});
it('handles invalid inputs gracefully', () => {
const result = testRule.evaluate(null, null, null, null, null, null, null);
assert.ok(result, 'Should return error result');
assert.strictEqual(result.possibility_allow, 0, 'Should return zero possibility for invalid input');
assert.ok(result.reason.includes('missing'), 'Should indicate error in reason');
});
});
describe('Input Validation', () => {
it('validates required parameters', () => {
const userId = arbiter.nodeIdByKey.get('user:alice');
const objectId = arbiter.nodeIdByKey.get('project:secret');
const rule = { type: 'test' };
const visited = new Set();
// Valid inputs should pass
const validation = testRule._validateInputs(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation', {});
assert.ok(validation.valid, 'Should validate valid inputs');
// Invalid inputs should fail
const invalidValidation = testRule._validateInputs(null, null, null, null, null, null, null, {});
assert.strictEqual(invalidValidation.valid, false, 'Should reject invalid inputs');
});
it('validates rule configuration', () => {
const userId = arbiter.nodeIdByKey.get('user:alice');
const objectId = arbiter.nodeIdByKey.get('project:secret');
const visited = new Set();
// Missing rule type - BaseRule doesn't validate rule type, just that it's an object
const noTypeValidation = testRule._validateInputs(userId, 'user:alice', objectId, 'project:secret', {}, visited, 'test_relation', {});
assert.ok(noTypeValidation.valid, 'Should accept rule without type');
// Valid rule
const validRule = { type: 'test' };
const validValidation = testRule._validateInputs(userId, 'user:alice', objectId, 'project:secret', validRule, visited, 'test_relation', {});
assert.ok(validValidation.valid, 'Should accept valid rule');
});
});
describe('Options Normalization', () => {
it('normalizes evaluation options', () => {
const options = {
fastPath: true,
minAllowPossibility: 0.8,
trackEvaluation: true
};
const normalized = testRule._normalizeOptions(options, { type: 'test' });
assert.strictEqual(normalized.fastPath, true, 'Should preserve fastPath');
assert.strictEqual(normalized.minAllowPossibility, 0.8, 'Should preserve minAllowPossibility');
assert.strictEqual(normalized.trackEvaluation, true, 'Should preserve trackEvaluation');
});
it('provides default values for missing options', () => {
const normalized = testRule._normalizeOptions({}, { type: 'test' });
assert.strictEqual(normalized.fastPath, false, 'Should default fastPath to false');
assert.strictEqual(normalized.minPossibility, null, 'Should default minPossibility to null');
assert.strictEqual(normalized.trackEvaluation, true, 'Should default trackEvaluation to true');
});
});
describe('Early Exit Optimization', () => {
it('supports early exit with minAllowPossibility', () => {
const options = {
minAllowPossibility: 0.9,
fastPath: true
};
const earlyExit = testRule._checkEarlyExit(options, { type: 'test' });
// BaseRule doesn't implement early exit by default
assert.strictEqual(earlyExit, null, 'Should return null for no early exit');
});
it('supports early exit with maxDenyPossibility', () => {
const options = {
maxDenyPossibility: 0.1,
fastPath: true
};
const earlyExit = testRule._checkEarlyExit(options, { type: 'test' });
// BaseRule doesn't implement early exit by default
assert.strictEqual(earlyExit, null, 'Should return null for no early exit');
});
});
describe('Result Post-Processing', () => {
it('post-processes rule evaluation results', () => {
const rawResult = {
possibility: 0.8,
reliability: 0.9,
collectedValues: [],
meta: { ruleType: 'test' },
reason: 'test'
};
const rule = { type: 'test' };
const options = { fastPath: false };
const processed = testRule._postProcessResult(rawResult, rule, options);
assert.strictEqual(processed.possibility, 0.8, 'Should preserve possibility');
assert.strictEqual(processed.reliability, 0.9, 'Should preserve reliability');
assert.ok(Array.isArray(processed.collectedValues), 'Should preserve collected values');
});
it('applies minimum rule possibility threshold', () => {
const rawResult = {
possibility: 0.3,
reliability: 0.9,
collectedValues: [],
meta: { ruleType: 'test' },
reason: 'test'
};
const rule = { type: 'test', minRulePossibility: 0.5 };
const options = { fastPath: false };
const processed = testRule._postProcessResult(rawResult, rule, options);
// BaseRule doesn't apply minimum threshold by default
assert.strictEqual(processed.possibility, 0.3, 'Should preserve original possibility');
});
});
describe('Error Handling', () => {
it('creates standardized error results', () => {
const errorResult = testRule._createErrorResult('test_error', { details: 'test details' });
assert.strictEqual(errorResult.possibility_allow, 0, 'Should return zero possibility for errors');
assert.strictEqual(errorResult.reliability, 1.0, 'Should return reliability for errors');
assert.strictEqual(errorResult.reason, 'test_error', 'Should include error reason');
assert.ok(errorResult.details, 'Should include error details');
});
it('handles evaluation exceptions gracefully', () => {
// Create a rule that throws an exception
class ErrorRule extends BaseRule {
constructor(arbiter) {
super(arbiter);
}
_evaluateRule() {
throw new Error('Test exception');
}
}
const errorRule = new ErrorRule(arbiter);
const userId = arbiter.nodeIdByKey.get('user:alice');
const objectId = arbiter.nodeIdByKey.get('project:secret');
const rule = { type: 'error' };
const visited = new Set();
const result = errorRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation');
assert.strictEqual(result.possibility_allow, 0, 'Should return zero possibility for exceptions');
assert.ok(result.reason.includes('error'), 'Should indicate error in reason');
});
});
describe.skip('Batch Processing Support', () => {
it('tracks batch evaluation performance', () => {
const queries = [{
userId: arbiter.nodeIdByKey.get('user:alice'),
userKey: 'user:alice',
objectId: arbiter.nodeIdByKey.get('project:secret'),
objectKey: 'project:secret',
rule: { type: 'test' },
visited: new Set(),
currentRelation: 'test_relation',
options: {}
}];
testRule.batchEvaluate(queries);
assert.strictEqual(testRule.batchEvaluationCount, 1, 'Should increment batch count');
assert.ok(testRule.totalBatchEvaluationTime >= 0, 'Should track batch time');
});
});
describe('Performance Metrics', () => {
it('tracks evaluation statistics', () => {
const userId = arbiter.nodeIdByKey.get('user:alice');
const objectId = arbiter.nodeIdByKey.get('project:secret');
const rule = { type: 'test' };
const visited = new Set();
// Run multiple evaluations
for (let i = 0; i < 5; i++) {
testRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation');
}
assert.strictEqual(testRule.evaluationCount, 5, 'Should track evaluation count');
assert.ok(testRule.totalEvaluationTime >= 0, 'Should track total time');
});
it('provides performance summary', () => {
const userId = arbiter.nodeIdByKey.get('user:alice');
const objectId = arbiter.nodeIdByKey.get('project:secret');
const rule = { type: 'test' };
const visited = new Set();
testRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation');
const stats = testRule.getPerformanceStats();
assert.strictEqual(stats.evaluationCount, 1, 'Should report evaluation count');
assert.ok(stats.averageEvaluationTime >= 0, 'Should report average time');
});
});
describe('Value Context Integration', () => {
it('passes value context to rule evaluation', () => {
const userId = arbiter.nodeIdByKey.get('user:alice');
const objectId = arbiter.nodeIdByKey.get('project:secret');
const rule = { type: 'test' };
const visited = new Set();
const valueContext = { testValue: 42 };
const result = testRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation', { valueContext });
assert.ok(result, 'Should handle value context');
assert.strictEqual(result.possibility, 0.8, 'Should return correct result');
});
});
describe('Abstract Method Requirements', () => {
it('requires concrete implementations to define _evaluateRule', () => {
class IncompleteRule extends BaseRule {
constructor(arbiter) {
super(arbiter);
}
// Missing _evaluateRule implementation
}
const incompleteRule = new IncompleteRule(arbiter);
const userId = arbiter.nodeIdByKey.get('user:alice');
const objectId = arbiter.nodeIdByKey.get('project:secret');
const rule = { type: 'incomplete' };
const visited = new Set();
// Should handle missing implementation gracefully
const result = incompleteRule.evaluate(userId, 'user:alice', objectId, 'project:secret', rule, visited, 'test_relation');
assert.ok(result, 'Should return result even with missing implementation');
});
});
});