Files
core/tests/core/explain-audit-provenance.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

273 lines
9.1 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { Arbiter } from '../../src/core/Arbiter.js';
function setupDirectArbiter() {
const arbiter = new Arbiter();
arbiter.addNode('user:1', 'user');
arbiter.addNode('doc:1', 'doc');
arbiter.setRelationConfig('can_read', { type: 'direct' });
return arbiter;
}
test('ADR-035/042 explainability: audit captures provenance conflict and trust precedence', () => {
const arbiter = setupDirectArbiter();
arbiter.addRelation('user:1', 'can_read', 'doc:1', 0.25);
const partialGraph = {
relations: [
{ src: 'user:1', relation: 'can_read', dst: 'doc:1', possibility: 0.9 }
]
};
const explain = arbiter.explain('user:1', 'can_read', 'doc:1', { partialGraph });
assert.equal(explain.decision.possibility, 0.25);
assert.equal(explain.trace.path[0].source, 'persistent');
const provenance = explain.audit?.provenance;
assert.ok(provenance);
assert.equal(provenance.has_partial_inputs, true);
assert.equal(provenance.partial_fact_used, false);
assert.equal(provenance.effective_source, 'persistent');
assert.equal(provenance.provenance_conflicts.length, 1);
const conflict = provenance.provenance_conflicts[0];
assert.equal(conflict.reason_code, 'higher_trust_source_preferred');
assert.equal(conflict.winner_source, 'persistent');
assert.equal(conflict.loser_source, 'partial');
assert.equal(conflict.relation, 'can_read');
});
test('ADR-035/042 explainability: audit marks partial facts used with no conflict', () => {
const arbiter = setupDirectArbiter();
const partialGraph = {
relations: [
{ src: 'user:1', relation: 'can_read', dst: 'doc:1', possibility: 1.0 }
]
};
const explain = arbiter.explain('user:1', 'can_read', 'doc:1', { partialGraph });
assert.equal(explain.decision.possibility, 1.0);
assert.equal(explain.trace.path[0].source, 'partial');
const provenance = explain.audit?.provenance;
assert.ok(provenance);
assert.equal(provenance.has_partial_inputs, true);
assert.equal(provenance.partial_fact_used, true);
assert.equal(provenance.effective_source, 'partial');
assert.deepEqual(provenance.provenance_conflicts, []);
});
test('ADR-035/042 explainability: chain path conflicts are auditable per edge', () => {
const arbiter = new Arbiter();
arbiter.addNode('user:1', 'user');
arbiter.addNode('device:1', 'device');
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' }
]
});
arbiter.addRelation('user:1', 'device_link', 'device:1', 0.2);
arbiter.addRelation('device:1', 'logged_in_as', 'account:1', 0.2);
const partialGraph = {
relations: [
{ src: 'user:1', relation: 'device_link', dst: 'device:1', possibility: 0.95 },
{ src: 'device:1', relation: 'logged_in_as', dst: 'account:1', possibility: 0.95 }
]
};
const explain = arbiter.explain('user:1', 'can_login', 'account:1', { partialGraph });
assert.equal(explain.decision.possibility, 0.2);
const conflicts = explain.audit?.provenance?.provenance_conflicts || [];
assert.ok(conflicts.length >= 2);
const conflictRels = new Set(conflicts.map((c) => c.relation));
assert.ok(conflictRels.has('device_link'));
assert.ok(conflictRels.has('logged_in_as'));
for (const c of conflicts) {
assert.equal(c.reason_code, 'higher_trust_source_preferred');
assert.equal(c.winner_source, 'persistent');
assert.equal(c.loser_source, 'partial');
}
});
test('ADR-035/042 explainability: multi-hop path conflicts are auditable per edge', () => {
const arbiter = new Arbiter();
arbiter.addNode('user:1', 'user');
arbiter.addNode('group:1', 'group');
arbiter.addNode('doc:1', 'doc');
arbiter.setRelationConfig('can_reach', { type: 'multi_hop', relation: 'link', maxDepth: 3 });
arbiter.addRelation('user:1', 'link', 'group:1', 0.3);
arbiter.addRelation('group:1', 'link', 'doc:1', 0.3);
const partialGraph = {
relations: [
{ src: 'user:1', relation: 'link', dst: 'group:1', possibility: 0.9 },
{ src: 'group:1', relation: 'link', dst: 'doc:1', possibility: 0.9 }
]
};
const explain = arbiter.explain('user:1', 'can_reach', 'doc:1', { partialGraph });
assert.equal(explain.decision.possibility, 0.3);
const conflicts = explain.audit?.provenance?.provenance_conflicts || [];
assert.ok(conflicts.length >= 1);
for (const c of conflicts) {
assert.equal(c.relation, 'link');
assert.equal(c.reason_code, 'higher_trust_source_preferred');
assert.equal(c.winner_source, 'persistent');
}
});
test('ADR-035/042 explainability: conflict identities respect key redaction mode', () => {
const arbiter = setupDirectArbiter();
arbiter.addRelation('user:1', 'can_read', 'doc:1', 0.4);
const partialGraph = {
relations: [
{ src: 'user:1', relation: 'can_read', dst: 'doc:1', possibility: 0.95 }
]
};
const explain = arbiter.explain('user:1', 'can_read', 'doc:1', {
partialGraph,
redaction: 'keys'
});
const conflict = explain.audit?.provenance?.provenance_conflicts?.[0];
assert.ok(conflict);
assert.notEqual(conflict.src, 'user:1');
assert.notEqual(conflict.object, 'doc:1');
assert.equal(conflict.src.length, 64);
assert.equal(conflict.object.length, 64);
assert.equal(explain.request.userKey, conflict.src);
assert.equal(explain.request.objectKey, conflict.object);
});
test('ADR-035/042 explainability: hash redaction keeps keys and still includes hashes', () => {
const arbiter = setupDirectArbiter();
arbiter.addRelation('user:1', 'can_read', 'doc:1', 0.4);
const partialGraph = {
relations: [
{ src: 'user:1', relation: 'can_read', dst: 'doc:1', possibility: 0.95 }
]
};
const explain = arbiter.explain('user:1', 'can_read', 'doc:1', {
partialGraph,
redaction: 'hash'
});
const conflict = explain.audit?.provenance?.provenance_conflicts?.[0];
assert.ok(conflict);
assert.equal(conflict.src, 'user:1');
assert.equal(conflict.object, 'doc:1');
assert.equal(typeof explain.request.userKeyHash, 'string');
assert.equal(typeof explain.request.objectKeyHash, 'string');
assert.equal(explain.request.userKeyHash.length, 64);
assert.equal(explain.request.objectKeyHash.length, 64);
});
test('ADR-035/042 explainability: audit provenance contract has stable shape', () => {
const arbiter = setupDirectArbiter();
arbiter.addRelation('user:1', 'can_read', 'doc:1', 0.4);
const partialGraph = {
relations: [
{ src: 'user:1', relation: 'can_read', dst: 'doc:1', possibility: 0.95 }
]
};
const explain = arbiter.explain('user:1', 'can_read', 'doc:1', { partialGraph });
const provenance = explain.audit?.provenance;
assert.ok(provenance);
assert.deepEqual(Object.keys(provenance).sort(), [
'effective_source',
'has_partial_inputs',
'partial_fact_used',
'policy_conflicts',
'provenance_conflicts'
,
'used_facts'
]);
assert.equal(typeof provenance.has_partial_inputs, 'boolean');
assert.equal(typeof provenance.partial_fact_used, 'boolean');
assert.equal(typeof provenance.effective_source, 'string');
assert.ok(Array.isArray(provenance.provenance_conflicts));
assert.ok(Array.isArray(provenance.used_facts));
assert.ok(Array.isArray(provenance.policy_conflicts));
assert.ok(provenance.provenance_conflicts.length >= 1);
const conflict = provenance.provenance_conflicts[0];
assert.deepEqual(Object.keys(conflict).sort(), [
'decision_possibility',
'loser_source',
'object',
'partial_layer_name',
'partial_possibility',
'partial_reducer_applied',
'partial_source_class',
'persistent_possibility',
'reason_code',
'relation',
'resolution',
'rule_type',
'src',
'winner_source'
]);
});
test('ADR-042 explainability: used facts and policy conflicts are surfaced', () => {
const arbiter = new Arbiter();
arbiter.addNode('user:1', 'user');
arbiter.addNode('doc:1', 'doc');
// The direct rule checks 'gateway_context_ref', which IS allowlisted at
// the request_observed layer (can_read is not).
arbiter.setRelationConfig('can_read', { type: 'direct', relation: 'gateway_context_ref' });
const partialGraph = {
options: {
reducers: {
delegated_authority: 'strongest'
}
},
relations: [
{
src: 'user:1',
relation: 'gateway_context_ref',
dst: 'doc:1',
possibility: 1.0,
layer_name: 'request_observed',
source_class: 'gateway_observed'
}
]
};
const explain = arbiter.explain('user:1', 'can_read', 'doc:1', { partialGraph });
const provenance = explain.audit?.provenance;
assert.ok(provenance);
const used = provenance.used_facts[0];
assert.ok(used);
assert.equal(used.layer_name, 'request_observed');
assert.equal(used.source_class, 'gateway_observed');
assert.equal(used.source, 'partial');
assert.equal(used.used, true);
const policy = provenance.policy_conflicts;
assert.ok(policy.some((p) => p.kind === 'invalid_reducer_config'));
});