Files
core/tests/engine/value-aggregation-properties.test.js
T
John Dvorak 717ae1031e 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.
2026-07-31 13:44:06 -07:00

166 lines
4.9 KiB
JavaScript

import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
import { Arbiter } from '../../src/core/Arbiter.js';
import { OWAFusion } from '../../src/utils/OWAFusion.js';
function createRng(seed) {
let state = seed >>> 0;
return () => {
state = (1664525 * state + 1013904223) >>> 0;
return state / 0x100000000;
};
}
function randInt(rng, max) {
return Math.floor(rng() * max);
}
function randFloat(rng, min = 0, max = 1) {
return min + (max - min) * rng();
}
function buildComparatorArbiter() {
const arbiter = new Arbiter();
arbiter.addNode('user:1', 'user');
arbiter.addNode('resource:1', 'resource');
arbiter.setRelationConfig('risk_score', { type: 'direct' });
arbiter.setRelationConfig('risk_bonus', { type: 'direct' });
arbiter.setRelationConfig('risk_noise', { type: 'direct' });
arbiter.setRelationConfig('risk_limit', { type: 'direct' });
arbiter.setRelationConfig('risk_ok_owa', {
type: 'relational_comparator',
comparator: '<=',
fallbackBehavior: 'deny',
left: {
rule: {
union: {
rules: [
{ type: 'direct', relation: 'risk_score' },
{ type: 'direct', relation: 'risk_bonus' },
{ type: 'direct', relation: 'risk_noise' }
],
aggregator: 'owa',
owaWeights: [0.5, 0.3, 0.2]
}
},
extractValue: true,
valueRelation: 'risk_score',
aggregator: 'owa',
owaWeights: [0.5, 0.3, 0.2]
},
right: {
rule: { type: 'direct', relation: 'risk_limit' },
extractValue: true,
valueRelation: 'risk_limit',
evaluateFrom: 'object'
}
});
arbiter.registerDependencyIndex(new Map([
['risk_score', {
all: new Set(['risk_ok_owa']),
byLevel: {
never: new Set(),
always: new Set(),
requires: new Set(),
when: new Set(),
unless: new Set(),
ordinary: new Set(['risk_ok_owa'])
}
}],
['risk_bonus', {
all: new Set(['risk_ok_owa']),
byLevel: {
never: new Set(),
always: new Set(),
requires: new Set(),
when: new Set(),
unless: new Set(),
ordinary: new Set(['risk_ok_owa'])
}
}],
['risk_noise', {
all: new Set(['risk_ok_owa']),
byLevel: {
never: new Set(),
always: new Set(),
requires: new Set(),
when: new Set(),
unless: new Set(),
ordinary: new Set(['risk_ok_owa'])
}
}],
['risk_limit', {
all: new Set(['risk_ok_owa']),
byLevel: {
never: new Set(),
always: new Set(),
requires: new Set(),
when: new Set(),
unless: new Set(),
ordinary: new Set(['risk_ok_owa'])
}
}]
]));
return arbiter;
}
describe('Comparator aggregation properties', () => {
test('OWA aggregated value drives comparator decision', () => {
const rng = createRng(24);
const arbiter = buildComparatorArbiter();
const metas = [{}, {}, {}];
for (let i = 0; i < 120; i++) {
const values = [
randFloat(rng, 0, 100),
randFloat(rng, 0, 100),
randFloat(rng, 0, 100)
];
const limit = randFloat(rng, 0, 100);
arbiter.removeRelation('user:1', 'risk_score', 'resource:1');
arbiter.removeRelation('user:1', 'risk_bonus', 'resource:1');
arbiter.removeRelation('user:1', 'risk_noise', 'resource:1');
arbiter.removeRelation('resource:1', 'risk_limit', 'resource:1');
arbiter.addRelation('user:1', 'risk_score', 'resource:1', 1.0, { value: values[0] });
arbiter.addRelation('user:1', 'risk_bonus', 'resource:1', 1.0, { value: values[1] });
arbiter.addRelation('user:1', 'risk_noise', 'resource:1', 1.0, { value: values[2] });
arbiter.addRelation('resource:1', 'risk_limit', 'resource:1', 1.0, { value: limit });
const fused = OWAFusion.fuseWithMeta(values, metas, [0.5, 0.3, 0.2], 'owa', true).value;
const expectedAllow = fused <= limit;
const result = arbiter.check('user:1', 'risk_ok_owa', 'resource:1', { fastPath: false });
const allow = result.possibility > 0;
assert.strictEqual(allow, expectedAllow, 'comparator respects aggregation');
}
});
test('increasing a component value does not decrease fused value', () => {
const rng = createRng(88);
const arbiter = buildComparatorArbiter();
const metas = [{}, {}, {}];
for (let i = 0; i < 120; i++) {
const values = [
randFloat(rng, 0, 100),
randFloat(rng, 0, 100),
randFloat(rng, 0, 100)
];
const index = randInt(rng, values.length);
const delta = randFloat(rng, 0, 20);
const bumped = values.slice();
bumped[index] += delta;
const base = OWAFusion.fuseWithMeta(values, metas, [0.5, 0.3, 0.2], 'owa', true).value;
const higher = OWAFusion.fuseWithMeta(bumped, metas, [0.5, 0.3, 0.2], 'owa', true).value;
assert.ok(higher >= base - 1e-6, 'fused value is monotonic');
}
});
});