Files
core/tests/engine/partial-graph.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

215 lines
7.4 KiB
JavaScript

import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { Arbiter } from '../../src/core/Arbiter.js';
describe('Partial graph overlay', () => {
test('direct relation resolves from partial graph', () => {
const arbiter = new Arbiter();
arbiter.addNode('user:1', 'user');
arbiter.addNode('doc:1', 'doc');
arbiter.setRelationConfig('can_read', { type: 'direct' });
const missing = arbiter.check('user:1', 'can_read', 'doc:1');
assert.strictEqual(missing.possibility, 0);
const partialGraph = {
relations: [
{ src: 'user:1', relation: 'can_read', dst: 'doc:1', possibility: 1.0 }
]
};
const result = arbiter.check('user:1', 'can_read', 'doc:1', { partialGraph });
assert.strictEqual(result.possibility, 1.0);
});
test('persistent relation wins when partial conflicts on same triple', () => {
const arbiter = new Arbiter();
arbiter.addNode('user:1', 'user');
arbiter.addNode('doc:1', 'doc');
arbiter.setRelationConfig('can_read', { type: 'direct' });
arbiter.addRelation('user:1', 'can_read', 'doc:1', 0.2);
const baseline = arbiter.check('user:1', 'can_read', 'doc:1');
assert.strictEqual(baseline.possibility, 0.2);
const partialGraph = {
relations: [
{ src: 'user:1', relation: 'can_read', dst: 'doc:1', possibility: 0.9 }
]
};
const result = arbiter.check('user:1', 'can_read', 'doc:1', { partialGraph });
assert.strictEqual(result.possibility, 0.2);
});
test('mixed overlay precedence reports persistent source and audit conflict', () => {
const arbiter = new Arbiter();
arbiter.addNode('user:1', 'user');
arbiter.addNode('doc:1', 'doc');
arbiter.setRelationConfig('can_read', { type: 'direct' });
arbiter.addRelation('user:1', 'can_read', 'doc:1', 0.2);
const partialGraph = {
relations: [
{ src: 'user:1', relation: 'can_read', dst: 'doc:1', possibility: 0.9 }
]
};
const result = arbiter.explain('user:1', 'can_read', 'doc:1', { partialGraph });
assert.strictEqual(result.decision.possibility, 0.2);
assert.strictEqual(result.trace.path[0].source, 'persistent');
assert.strictEqual(result.audit.provenance.partial_fact_used, false);
assert.strictEqual(result.audit.provenance.provenance_conflicts.length, 1);
});
test('explain marks partial provenance', () => {
const arbiter = new Arbiter();
arbiter.addNode('user:1', 'user');
arbiter.addNode('doc:1', 'doc');
arbiter.setRelationConfig('can_read', { type: 'direct' });
const partialGraph = {
relations: [
{ src: 'user:1', relation: 'can_read', dst: 'doc:1', possibility: 1.0 }
]
};
const result = arbiter.explain('user:1', 'can_read', 'doc:1', { partialGraph });
assert.strictEqual(result.trace.path[0].source, 'partial');
});
test('partial nodes participate in chain rules', () => {
const arbiter = new Arbiter();
arbiter.addNode('user:1', 'user');
arbiter.addNode('account:1', 'account');
arbiter.setRelationConfig('device_link', { type: 'direct' });
arbiter.setRelationConfig('logged_in_as', { type: 'direct' });
arbiter.setRelationConfig('can_login', {
type: 'chain',
steps: [
{ relation: 'device_link', direction: 'out' },
{ relation: 'logged_in_as', direction: 'out' }
]
});
const partialGraph = {
nodes: [
{ key: 'device:abc', type: 'device' }
],
relations: [
{ src: 'user:1', relation: 'device_link', dst: 'device:abc', possibility: 1.0 },
{ src: 'device:abc', relation: 'logged_in_as', dst: 'account:1', possibility: 1.0 }
]
};
const result = arbiter.check('user:1', 'can_login', 'account:1', { partialGraph });
assert.ok(result.possibility > 0);
});
test('multi-hop explain includes partial path sources', () => {
const arbiter = new Arbiter();
arbiter.addNode('user:1', 'user');
arbiter.addNode('doc:1', 'doc');
arbiter.setRelationConfig('path', { type: 'multi_hop', relation: 'link', maxDepth: 3 });
const partialGraph = {
nodes: [
{ key: 'mid:1', type: 'group' }
],
relations: [
{ src: 'user:1', relation: 'link', dst: 'mid:1', possibility: 1.0 },
{ src: 'mid:1', relation: 'link', dst: 'doc:1', possibility: 1.0 }
]
};
const result = arbiter.explain('user:1', 'path', 'doc:1', { partialGraph });
const pathSteps = result.trace.rulePaths[0]?.pathSteps || [];
assert.ok(pathSteps.length > 0);
for (const step of pathSteps) {
assert.strictEqual(step.source, 'partial');
}
});
test('chain collected values include source', () => {
const arbiter = new Arbiter();
arbiter.addNode('user:1', 'user');
arbiter.addNode('account:1', 'account');
arbiter.setRelationConfig('device_link', { type: 'direct' });
arbiter.setRelationConfig('logged_in_as', { type: 'direct' });
arbiter.setRelationConfig('can_login', {
type: 'chain',
steps: [
{ relation: 'device_link', direction: 'out' },
{ relation: 'logged_in_as', direction: 'out' }
]
});
const partialGraph = {
nodes: [
{ key: 'device:abc', type: 'device' }
],
relations: [
{ src: 'user:1', relation: 'device_link', dst: 'device:abc', possibility: 1.0, value: 1 },
{ src: 'device:abc', relation: 'logged_in_as', dst: 'account:1', possibility: 1.0, value: 1 }
]
};
const result = arbiter.explain('user:1', 'can_login', 'account:1', { partialGraph });
const pathSteps = result.trace.rulePaths[0]?.pathSteps || [];
assert.ok(pathSteps.length > 0);
let sawPartial = false;
for (const step of pathSteps) {
assert.ok(step.source === 'partial' || step.source === 'persistent');
if (step.source === 'partial') sawPartial = true;
}
assert.ok(sawPartial);
const collectedValues = result.trace.values || [];
assert.ok(collectedValues.length > 0);
for (const cv of collectedValues) {
assert.strictEqual(cv.metadata.source, 'partial');
}
});
test('relational comparator uses partial value provenance', () => {
const arbiter = new Arbiter();
arbiter.addNode('user:1', 'user');
arbiter.addNode('account:1', 'account');
arbiter.setRelationConfig('device_risk', { type: 'direct' });
arbiter.setRelationConfig('risk_limit', { type: 'direct' });
arbiter.setRelationConfig('risk_ok', {
type: 'relational_comparator',
left: {
rule: { type: 'direct', relation: 'device_risk' },
extractValue: true,
valueRelation: 'device_risk',
evaluateFrom: 'auto'
},
right: {
rule: { type: 'direct', relation: 'risk_limit' },
extractValue: true,
valueRelation: 'risk_limit',
evaluateFrom: 'auto'
},
comparator: '<='
});
const partialGraph = {
relations: [
{ src: 'user:1', relation: 'device_risk', dst: 'account:1', possibility: 1.0, value: 0.2 },
{ src: 'user:1', relation: 'risk_limit', dst: 'account:1', possibility: 1.0, value: 0.8 }
]
};
const result = arbiter.explain('user:1', 'risk_ok', 'account:1', { partialGraph });
assert.ok(result.decision.possibility > 0);
const comparatorNode = result.trace.path.find(node => node.type === 'relational_comparator');
const details = comparatorNode?.details || {};
assert.strictEqual(details.leftSource, 'partial');
assert.strictEqual(details.rightSource, 'partial');
});
});