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:
@@ -0,0 +1,308 @@
|
||||
import { LogicalOperators } from '../../src/authorization/rules/LogicalOperators.js';
|
||||
import { describe, it, beforeEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
// Minimal mock ruleEvaluator for child rule evaluation
|
||||
function makeMockRuleEvaluator(resultsByRule) {
|
||||
return {
|
||||
evaluateRule: (userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) => {
|
||||
if (typeof rule === 'string') return resultsByRule[rule];
|
||||
if (rule && rule.mockKey) return resultsByRule[rule.mockKey];
|
||||
if (rule && rule.type && resultsByRule[rule.type]) return resultsByRule[rule.type];
|
||||
return resultsByRule.default || { possibility: 0, reliability: 1.0, meta: { reason: 'no_rule' }, collectedValues: [] };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('LogicalOperators', () => {
|
||||
let logicalOps;
|
||||
let arbiter;
|
||||
let ruleEvaluator;
|
||||
|
||||
beforeEach(() => {
|
||||
arbiter = {};
|
||||
});
|
||||
|
||||
it('evaluates union (max) correctly', () => {
|
||||
ruleEvaluator = makeMockRuleEvaluator({
|
||||
a: { possibility: 0.7, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
|
||||
b: { possibility: 0.4, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] },
|
||||
c: { possibility: 0.9, reliability: 0.7, meta: { ruleType: 'direct', rule: { mockKey: 'c' } }, collectedValues: [3] }
|
||||
});
|
||||
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
||||
const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }, { mockKey: 'c' }], aggregator: 'max' } };
|
||||
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
|
||||
assert.strictEqual(res.possibility, 0.9);
|
||||
assert.ok(res.meta.operation === 'union');
|
||||
assert.deepStrictEqual(res.collectedValues, [1, 2, 3]);
|
||||
});
|
||||
|
||||
it('evaluates intersection (min) correctly', () => {
|
||||
ruleEvaluator = makeMockRuleEvaluator({
|
||||
a: { possibility: 0.7, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
|
||||
b: { possibility: 0.4, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] },
|
||||
c: { possibility: 0.9, reliability: 0.7, meta: { ruleType: 'direct', rule: { mockKey: 'c' } }, collectedValues: [3] }
|
||||
});
|
||||
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
||||
const rule = { type: 'logical', intersection: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }, { mockKey: 'c' }], aggregator: 'min' } };
|
||||
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
|
||||
assert.strictEqual(res.possibility, 0.4);
|
||||
assert.ok(res.meta.operation === 'intersection');
|
||||
assert.deepStrictEqual(res.collectedValues, [1, 2, 3]);
|
||||
});
|
||||
|
||||
it('evaluates union (mean/average) correctly', () => {
|
||||
ruleEvaluator = makeMockRuleEvaluator({
|
||||
a: { possibility: 0.6, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
|
||||
b: { possibility: 0.8, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] }
|
||||
});
|
||||
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
||||
const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }], aggregator: 'mean' } };
|
||||
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
|
||||
assert.ok(Math.abs(res.possibility - 0.7) < 1e-8);
|
||||
assert.ok(res.meta.operation === 'union');
|
||||
});
|
||||
|
||||
it('evaluates intersection (pessimistic) correctly', () => {
|
||||
ruleEvaluator = makeMockRuleEvaluator({
|
||||
a: { possibility: 0.9, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
|
||||
b: { possibility: 0.6, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] }
|
||||
});
|
||||
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
||||
const rule = { type: 'logical', intersection: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }], aggregator: 'pessimistic' } };
|
||||
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
|
||||
assert.ok(res.possibility < 0.9 && res.possibility > 0.6);
|
||||
assert.ok(res.meta.operation === 'intersection');
|
||||
});
|
||||
|
||||
it('evaluates exclusion (A AND NOT B) correctly', () => {
|
||||
ruleEvaluator = makeMockRuleEvaluator({
|
||||
a: { possibility: 0.8, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
|
||||
b: { possibility: 0.5, reliability: 0.7, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] }
|
||||
});
|
||||
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
||||
const rule = { type: 'logical', exclusion: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }] } };
|
||||
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
|
||||
assert.ok(Math.abs(res.possibility - (0.8 * (1 - 0.5))) < 1e-8);
|
||||
assert.ok(res.meta.operation === 'exclusion');
|
||||
});
|
||||
|
||||
it('evaluates custom OWA weights for union', () => {
|
||||
ruleEvaluator = makeMockRuleEvaluator({
|
||||
a: { possibility: 0.2, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
|
||||
b: { possibility: 0.8, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] }
|
||||
});
|
||||
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
||||
const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }], owaWeights: [0.7, 0.3] } };
|
||||
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
|
||||
assert.ok(Math.abs(res.possibility - (0.8 * 0.7 + 0.2 * 0.3)) < 1e-8);
|
||||
assert.ok(res.meta.operation === 'union');
|
||||
});
|
||||
|
||||
it('evaluates binary defeasible logic (never overrides everything)', () => {
|
||||
// never wins over everything
|
||||
ruleEvaluator = makeMockRuleEvaluator({
|
||||
never: { possibility: 1, reliability: 0.95, meta: { ruleType: 'never' }, collectedValues: [1] },
|
||||
strict: { possibility: 1, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [2] },
|
||||
defeater: { possibility: 0.6, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [3] },
|
||||
defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [4] }
|
||||
});
|
||||
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
||||
const rule = {
|
||||
type: 'logical',
|
||||
never: { union: [{ mockKey: 'never' }] },
|
||||
always: { direct: { mockKey: 'strict' } },
|
||||
unless: { union: [{ mockKey: 'defeater' }] },
|
||||
when: { intersection: [{ mockKey: 'defeasible' }] },
|
||||
mode: 'binary'
|
||||
};
|
||||
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
|
||||
assert.strictEqual(res.possibility, 0);
|
||||
assert.ok(res.meta.never);
|
||||
assert.ok(res.meta.reason === 'never_rule_triggered');
|
||||
assert.ok(res.meta.mode === 'binary');
|
||||
});
|
||||
|
||||
it('evaluates binary defeasible logic (strict, defeater, defeasible)', () => {
|
||||
// strict wins
|
||||
ruleEvaluator = makeMockRuleEvaluator({
|
||||
strict: { possibility: 1, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [1] },
|
||||
defeater: { possibility: 0.6, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [2] },
|
||||
defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [3] }
|
||||
});
|
||||
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
||||
const rule = {
|
||||
type: 'logical',
|
||||
always: { direct: { mockKey: 'strict' } },
|
||||
unless: { union: [{ mockKey: 'defeater' }] },
|
||||
when: { intersection: [{ mockKey: 'defeasible' }] },
|
||||
mode: 'binary'
|
||||
};
|
||||
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
|
||||
assert.strictEqual(res.possibility, 1);
|
||||
assert.ok(res.meta.strict);
|
||||
assert.ok(res.meta.mode === 'binary');
|
||||
});
|
||||
|
||||
it('evaluates binary defeasible logic (requirements take precedence over defeaters)', () => {
|
||||
// requirements not met should win over defeaters
|
||||
ruleEvaluator = makeMockRuleEvaluator({
|
||||
strict: { possibility: 0, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [1] },
|
||||
requires: { possibility: 0.1, reliability: 0.9, meta: { ruleType: 'requires' }, collectedValues: [2] },
|
||||
defeater: { possibility: 0.8, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [3] },
|
||||
defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [4] }
|
||||
});
|
||||
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
||||
const rule = {
|
||||
type: 'logical',
|
||||
always: { direct: { mockKey: 'strict' } },
|
||||
requires: { union: [{ mockKey: 'requires' }] },
|
||||
unless: { union: [{ mockKey: 'defeater' }] },
|
||||
when: { intersection: [{ mockKey: 'defeasible' }] },
|
||||
mode: 'binary'
|
||||
};
|
||||
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
|
||||
assert.strictEqual(res.possibility, 0);
|
||||
assert.ok(res.meta.reason === 'requirements_not_met');
|
||||
assert.ok(res.meta.mode === 'binary');
|
||||
});
|
||||
|
||||
it('evaluates binary defeasible logic (defeater wins)', () => {
|
||||
// defeater wins when requirements are met
|
||||
ruleEvaluator = makeMockRuleEvaluator({
|
||||
strict: { possibility: 0, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [1] },
|
||||
requires: { possibility: 0.9, reliability: 0.9, meta: { ruleType: 'requires' }, collectedValues: [2] },
|
||||
defeater: { possibility: 0.8, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [3] },
|
||||
defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [4] }
|
||||
});
|
||||
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
||||
const rule = {
|
||||
type: 'logical',
|
||||
always: { direct: { mockKey: 'strict' } },
|
||||
requires: { union: [{ mockKey: 'requires' }] },
|
||||
unless: { union: [{ mockKey: 'defeater' }] },
|
||||
when: { intersection: [{ mockKey: 'defeasible' }] },
|
||||
mode: 'binary'
|
||||
};
|
||||
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
|
||||
assert.strictEqual(res.possibility, 0);
|
||||
assert.ok(res.meta.reason === 'defeated_by_unless');
|
||||
assert.ok(res.meta.mode === 'binary');
|
||||
});
|
||||
|
||||
it('evaluates threshold mode with never early exit', () => {
|
||||
ruleEvaluator = makeMockRuleEvaluator({
|
||||
never: { possibility: 0.9, reliability: 0.95, meta: { ruleType: 'never' }, collectedValues: [1] },
|
||||
strict: { possibility: 0.85, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [2] },
|
||||
defeater: { possibility: 0.2, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [3] },
|
||||
defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [4] }
|
||||
});
|
||||
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
||||
const rule = {
|
||||
type: 'logical',
|
||||
never: { union: [{ mockKey: 'never' }] },
|
||||
always: { direct: { mockKey: 'strict' } },
|
||||
unless: { union: [{ mockKey: 'defeater' }] },
|
||||
when: { intersection: [{ mockKey: 'defeasible' }] },
|
||||
mode: 'threshold'
|
||||
};
|
||||
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
|
||||
assert.strictEqual(res.possibility, 0);
|
||||
assert.ok(res.meta.earlyExit === 'never_rule');
|
||||
assert.ok(res.meta.never);
|
||||
});
|
||||
|
||||
it('evaluates threshold mode with early exit', () => {
|
||||
ruleEvaluator = makeMockRuleEvaluator({
|
||||
strict: { possibility: 0.85, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [1] },
|
||||
defeater: { possibility: 0.2, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [2] },
|
||||
defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [3] }
|
||||
});
|
||||
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
||||
const rule = {
|
||||
type: 'logical',
|
||||
always: { direct: { mockKey: 'strict' } },
|
||||
unless: { union: [{ mockKey: 'defeater' }] },
|
||||
when: { intersection: [{ mockKey: 'defeasible' }] },
|
||||
mode: 'threshold'
|
||||
};
|
||||
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
|
||||
assert.strictEqual(res.possibility, 0.85);
|
||||
assert.ok(res.meta.earlyExit === 'strict_rule');
|
||||
});
|
||||
|
||||
it('handles empty union/intersection/exclusion', () => {
|
||||
ruleEvaluator = makeMockRuleEvaluator({});
|
||||
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
||||
const unionRule = { type: 'logical', union: { rules: [] } };
|
||||
const intersectionRule = { type: 'logical', intersection: { rules: [] } };
|
||||
const exclusionRule = { type: 'logical', exclusion: { rules: [] } };
|
||||
assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', unionRule, {}, null, {}).possibility, 0);
|
||||
assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', intersectionRule, {}, null, {}).possibility, 0);
|
||||
assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', exclusionRule, {}, null, {}).possibility, 0);
|
||||
});
|
||||
|
||||
it('handles all-zero and all-one cases', () => {
|
||||
ruleEvaluator = makeMockRuleEvaluator({
|
||||
a: { possibility: 0, reliability: 1.0, meta: {}, collectedValues: [] },
|
||||
b: { possibility: 0, reliability: 1.0, meta: {}, collectedValues: [] }
|
||||
});
|
||||
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
||||
const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }] } };
|
||||
assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {}).possibility, 0);
|
||||
ruleEvaluator = makeMockRuleEvaluator({
|
||||
a: { possibility: 1, reliability: 1.0, meta: {}, collectedValues: [] },
|
||||
b: { possibility: 1, reliability: 1.0, meta: {}, collectedValues: [] }
|
||||
});
|
||||
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
||||
assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {}).possibility, 1);
|
||||
});
|
||||
|
||||
it('propagates meta and collectedValues', () => {
|
||||
ruleEvaluator = makeMockRuleEvaluator({
|
||||
a: { possibility: 0.5, reliability: 0.9, meta: { foo: 'bar' }, collectedValues: [1, 2] },
|
||||
b: { possibility: 0.8, reliability: 0.8, meta: { baz: 'qux' }, collectedValues: [3] }
|
||||
});
|
||||
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
||||
const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }] } };
|
||||
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
|
||||
assert.ok(res.meta);
|
||||
assert.deepStrictEqual(res.collectedValues, [1, 2, 3]);
|
||||
});
|
||||
|
||||
it('handles missing rule types gracefully', () => {
|
||||
ruleEvaluator = makeMockRuleEvaluator({});
|
||||
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
||||
const rule = { type: 'logical', union: { rules: [{ mockKey: 'missing' }] } };
|
||||
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
|
||||
assert.strictEqual(res.possibility, 0);
|
||||
assert.ok(res.meta && res.meta.reason === 'missing_rule');
|
||||
});
|
||||
|
||||
it('supports fastPath/early exit logic in union', () => {
|
||||
let callCount = 0;
|
||||
ruleEvaluator = {
|
||||
evaluateRule: (userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) => {
|
||||
callCount++;
|
||||
if (callCount === 1) return { possibility: 0.95, reliability: 0.9, meta: {}, collectedValues: [1] };
|
||||
return { possibility: 0.2, reliability: 0.8, meta: {}, collectedValues: [2] };
|
||||
}
|
||||
};
|
||||
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
||||
const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }] }, fastPath: true, minPossibility: 0.9 };
|
||||
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { fastPath: true, minPossibility: 0.9, collectValues: true });
|
||||
assert.strictEqual(res.possibility, 0.95);
|
||||
assert.deepStrictEqual(res.collectedValues, [1]);
|
||||
});
|
||||
|
||||
it('supports priority/aggregator effects', () => {
|
||||
ruleEvaluator = makeMockRuleEvaluator({
|
||||
a: { possibility: 0.7, reliability: 0.9, meta: { rule: { priority: 10 } }, collectedValues: [1] },
|
||||
b: { possibility: 0.6, reliability: 0.8, meta: { rule: { priority: 5 } }, collectedValues: [2] }
|
||||
});
|
||||
logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
|
||||
const rule = { type: 'logical', union: { rules: [{ mockKey: 'a', priority: 10 }, { mockKey: 'b', priority: 5 }], aggregator: 'priority' } };
|
||||
const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
|
||||
assert.ok(res.possibility > 0.6 && res.possibility < 0.8);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user