Files
core/tests/rules/value-manager-context.test.js
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

170 lines
6.9 KiB
JavaScript

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);
});
});