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,169 @@
|
||||
import { test, describe, it, beforeEach, before } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { ValueManager } from '../../src/core/ValueManager.js';
|
||||
import { UnifiedKeyManager } from '../../src/core/UnifiedKeyManager.js';
|
||||
import { ValueContext } from '../../src/authorization/ValueContext.js';
|
||||
|
||||
// Minimal mock RelationManager
|
||||
class MockRelationManager {
|
||||
constructor(relations) {
|
||||
this._relations = relations;
|
||||
}
|
||||
getAllValueRelationsFromSrc(entityId, relationName) {
|
||||
return this._relations.filter(r => r.src === entityId && r.rel === relationName);
|
||||
}
|
||||
getRawValueRelationsByName(relationName) {
|
||||
return this._relations.filter(r => r.rel === relationName);
|
||||
}
|
||||
getRawValueRelationsForLocalContext() {
|
||||
return []; // Not used in basic tests
|
||||
}
|
||||
}
|
||||
|
||||
// Minimal mock Arbiter
|
||||
class MockArbiter {
|
||||
constructor(relations) {
|
||||
this.relationManager = new MockRelationManager(relations);
|
||||
this.nodeIdByKey = new Map();
|
||||
this.keyByNodeId = new Map();
|
||||
this.keyManager = new UnifiedKeyManager();
|
||||
relations.forEach(r => {
|
||||
this.nodeIdByKey.set(r.srcKey, r.src);
|
||||
this.nodeIdByKey.set(r.dstKey, r.dst);
|
||||
this.keyByNodeId.set(r.src, r.srcKey);
|
||||
this.keyByNodeId.set(r.dst, r.dstKey);
|
||||
});
|
||||
}
|
||||
|
||||
resolveKey(id) {
|
||||
return this.keyByNodeId.get(id);
|
||||
}
|
||||
|
||||
resolveNodeId(key) {
|
||||
return this.nodeIdByKey.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
describe('ValueManager & ValueContext', () => {
|
||||
let relations, arbiter, valueManager, valueContext;
|
||||
const now = Date.now();
|
||||
|
||||
beforeEach(() => {
|
||||
relations = [
|
||||
{ src: 1, dst: 2, rel: 'balance', value: 100, possibility: 1, reliability: 1, changed_last_at: now, srcKey: 'user:alice', dstKey: 'account:checking' },
|
||||
{ src: 1, dst: 3, rel: 'balance', value: 200, possibility: 0.8, reliability: 1, changed_last_at: now - 3600 * 1000, srcKey: 'user:alice', dstKey: 'account:savings' },
|
||||
{ src: 2, dst: 4, rel: 'price', value: 50, possibility: 1, reliability: 1, changed_last_at: now, srcKey: 'feature:basic', dstKey: 'price:basic' },
|
||||
];
|
||||
arbiter = new MockArbiter(relations);
|
||||
valueManager = new ValueManager(arbiter);
|
||||
valueContext = new ValueContext(arbiter);
|
||||
});
|
||||
|
||||
it('extracts and blurs values correctly', () => {
|
||||
const rel = relations[0];
|
||||
const blurred = valueManager.getBlurredValue(rel);
|
||||
assert.ok(blurred.interval);
|
||||
const EPS = 1e-6;
|
||||
const expectedMin = rel.value;
|
||||
const expectedMax = rel.value;
|
||||
assert.ok(Math.abs(blurred.interval.min - expectedMin) < EPS, `min: ${blurred.interval.min} vs ${expectedMin}`);
|
||||
assert.ok(Math.abs(blurred.interval.max - expectedMax) < EPS, `max: ${blurred.interval.max} vs ${expectedMax}`);
|
||||
assert.ok(Math.abs(blurred.possibility - 1) < EPS);
|
||||
});
|
||||
|
||||
it('applies decay and blur for old values', () => {
|
||||
const rel = relations[1];
|
||||
// Use default config: decay should apply
|
||||
const blurred = valueManager.getBlurredValue(rel);
|
||||
assert.ok(blurred.interval);
|
||||
assert.ok(blurred.possibility === rel.possibility);
|
||||
assert.ok(blurred.interval.min === rel.value);
|
||||
assert.ok(blurred.interval.max === rel.value);
|
||||
});
|
||||
|
||||
it('caches values in ValueContext', () => {
|
||||
const vals1 = valueContext.getValues(1, 'balance');
|
||||
const vals2 = valueContext.getValues(1, 'balance');
|
||||
assert.deepEqual(vals1, vals2);
|
||||
assert.ok(valueContext.cacheHits > 0);
|
||||
});
|
||||
|
||||
it('aggregates values (max, min, sum, average)', () => {
|
||||
const max = valueContext.getAggregatedValue(1, 'balance', 'max');
|
||||
assert.equal(max.value, 200);
|
||||
const min = valueContext.getAggregatedValue(1, 'balance', 'min');
|
||||
assert.equal(min.value, 100);
|
||||
const sum = valueContext.getAggregatedValue(1, 'balance', 'sum');
|
||||
assert.equal(sum.value, 300);
|
||||
const avg = valueContext.getAggregatedValue(1, 'balance', 'average');
|
||||
assert.ok(Math.abs(avg.value - 150) < 1e-6);
|
||||
});
|
||||
|
||||
it('returns empty for missing values', () => {
|
||||
const vals = valueContext.getValues(99, 'balance');
|
||||
assert.deepEqual(vals, []);
|
||||
const agg = valueContext.getAggregatedValue(99, 'balance', 'max');
|
||||
assert.equal(agg.hasValue, false);
|
||||
assert.equal(agg.value, null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('OWAFusion', () => {
|
||||
let OWAFusion;
|
||||
before(async () => {
|
||||
({ OWAFusion } = await import('../../src/utils/OWAFusion.js'));
|
||||
});
|
||||
it('aggregates values with max, min, average, sum, custom', () => {
|
||||
const values = [10, 20, 30];
|
||||
const metas = [{ label: 'a' }, { label: 'b' }, { label: 'c' }];
|
||||
// Max
|
||||
const max = OWAFusion.fuseWithMeta(values, metas, null, 'max');
|
||||
assert.ok(Math.abs(max.value - 30) < 1e-6);
|
||||
// Min
|
||||
const min = OWAFusion.fuseWithMeta(values, metas, null, 'min');
|
||||
assert.ok(Math.abs(min.value - 10) < 1e-6);
|
||||
// Average
|
||||
const avg = OWAFusion.fuseWithMeta(values, metas, null, 'average');
|
||||
assert.ok(Math.abs(avg.value - 20) < 1e-6);
|
||||
// Sum
|
||||
const sum = OWAFusion.fuseWithMeta(values, metas, null, 'sum', false);
|
||||
assert.ok(Math.abs(sum.value - 60) < 1e-6);
|
||||
// Custom weights (0.2, 0.3, 0.5) - OWA sorts values descending
|
||||
const custom = OWAFusion.fuseWithMeta(values, metas, [0.2, 0.3, 0.5], 'custom');
|
||||
const sortedValues = [...values].sort((a, b) => b - a); // [30, 20, 10]
|
||||
const customWeights = [0.2, 0.3, 0.5];
|
||||
const expected = sortedValues.reduce((sum, v, i) => sum + v * customWeights[i], 0);
|
||||
assert.ok(Math.abs(custom.value - expected) < 1e-6);
|
||||
});
|
||||
|
||||
it('aggregates intervals with max, min, sum, average', () => {
|
||||
const intervals = [
|
||||
{ min: 1, max: 2 },
|
||||
{ min: 3, max: 4 },
|
||||
{ min: 5, max: 6 }
|
||||
];
|
||||
const metas = [{ label: 'a' }, { label: 'b' }, { label: 'c' }];
|
||||
// Max
|
||||
const max = OWAFusion.fuseIntervalsWithMeta(intervals, metas, null, 'max');
|
||||
assert.ok(Math.abs(max.interval.max - 6) < 1e-6);
|
||||
// Min
|
||||
const min = OWAFusion.fuseIntervalsWithMeta(intervals, metas, null, 'min');
|
||||
assert.ok(Math.abs(min.interval.min - 1) < 1e-6);
|
||||
// Sum
|
||||
const sum = OWAFusion.fuseIntervalsWithMeta(intervals, metas, null, 'sum');
|
||||
assert.ok(Math.abs(sum.interval.min - 9) < 1e-6);
|
||||
assert.ok(Math.abs(sum.interval.max - 12) < 1e-6);
|
||||
// Average
|
||||
const avg = OWAFusion.fuseIntervalsWithMeta(intervals, metas, null, 'average');
|
||||
assert.ok(Math.abs(avg.interval.min - 3) < 1e-6);
|
||||
assert.ok(Math.abs(avg.interval.max - 4) < 1e-6);
|
||||
// Custom weights (0.5, 0.3, 0.2) - OWA sorts intervals by midpoint descending
|
||||
const custom = OWAFusion.fuseIntervalsWithMeta(intervals, metas, [0.5, 0.3, 0.2], 'custom');
|
||||
const sortedIntervals = [...intervals].sort((a, b) => ((b.min + b.max) / 2) - ((a.min + a.max) / 2));
|
||||
const customWeights = [0.5, 0.3, 0.2];
|
||||
const expectedMin = sortedIntervals.reduce((sum, iv, i) => sum + iv.min * customWeights[i], 0);
|
||||
const expectedMax = sortedIntervals.reduce((sum, iv, i) => sum + iv.max * customWeights[i], 0);
|
||||
assert.ok(Math.abs(custom.interval.min - expectedMin) < 1e-6);
|
||||
assert.ok(Math.abs(custom.interval.max - expectedMax) < 1e-6);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user