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,107 @@
|
||||
/**
|
||||
* SnapshotBinary round-trip tests.
|
||||
*
|
||||
* Verifies:
|
||||
* - serialize/deserialize preserves nodes and relations
|
||||
* - Deserialized snapshots are marked _snapshotReadOnly
|
||||
* - UTF-8 keys survive round-trip
|
||||
* - Relation configs survive round-trip
|
||||
*
|
||||
* Run: node --test --test-force-exit lib/tests/property-based/snapshot-binary.test.js
|
||||
*/
|
||||
import { Arbiter } from '../../src/core/Arbiter.js';
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
function createArbiterFactory() {
|
||||
return () => new Arbiter({ fastConstructionMode: true, enableInference: false });
|
||||
}
|
||||
|
||||
async function loadSnapshotModule() {
|
||||
return import('../../src/core/SnapshotBinary.js');
|
||||
}
|
||||
|
||||
describe('SnapshotBinary — Round-Trip', () => {
|
||||
it('serialize and deserialize preserves nodes and relations', async () => {
|
||||
const { serializeArbiterSnapshot, deserializeArbiterSnapshot } = await loadSnapshotModule();
|
||||
|
||||
const original = new Arbiter({ fastConstructionMode: true, enableInference: false });
|
||||
original.addNode('user:alice', 'user');
|
||||
original.addNode('project:secret', 'project');
|
||||
original.addRelation('user:alice', 'can_read', 'project:secret', { possibility: 0.9, value: 100 });
|
||||
original.setRelationConfig('can_read', { type: 'direct' });
|
||||
original.enableCondensedSnapshot();
|
||||
|
||||
const buffer = serializeArbiterSnapshot(original);
|
||||
assert.ok(buffer instanceof ArrayBuffer);
|
||||
assert.ok(buffer.byteLength > 0);
|
||||
|
||||
const restored = deserializeArbiterSnapshot(buffer, createArbiterFactory());
|
||||
assert.ok(restored.nodeIdByKey.has('user:alice'));
|
||||
assert.ok(restored.nodeIdByKey.has('project:secret'));
|
||||
|
||||
const nodes = [...restored.nodes.values()].filter(Boolean);
|
||||
assert.ok(nodes.length >= 2);
|
||||
});
|
||||
|
||||
it('deserialized snapshot is marked read-only', async () => {
|
||||
const { serializeArbiterSnapshot, deserializeArbiterSnapshot } = await loadSnapshotModule();
|
||||
|
||||
const original = new Arbiter({ fastConstructionMode: true, enableInference: false });
|
||||
original.addNode('user:alice', 'user');
|
||||
original.addNode('project:secret', 'project');
|
||||
original.addRelation('user:alice', 'can_read', 'project:secret', { possibility: 0.9 });
|
||||
original.enableCondensedSnapshot();
|
||||
|
||||
const buffer = serializeArbiterSnapshot(original);
|
||||
const restored = deserializeArbiterSnapshot(buffer, createArbiterFactory());
|
||||
|
||||
assert.strictEqual(restored._snapshotReadOnly, true);
|
||||
assert.strictEqual(restored.snapshotEnabled, true);
|
||||
assert.ok(restored.snapshotGraph);
|
||||
});
|
||||
|
||||
it('UTF-8 keys survive round-trip', async () => {
|
||||
const { serializeArbiterSnapshot, deserializeArbiterSnapshot } = await loadSnapshotModule();
|
||||
|
||||
const original = new Arbiter({ fastConstructionMode: true, enableInference: false });
|
||||
original.addNode('user:alice', 'user');
|
||||
original.addNode('project:résumé', 'project');
|
||||
original.addRelation('user:alice', 'can_read', 'project:résumé', { possibility: 0.9 });
|
||||
original.setRelationConfig('can_read', { type: 'direct' });
|
||||
original.enableCondensedSnapshot();
|
||||
|
||||
const buffer = serializeArbiterSnapshot(original);
|
||||
const restored = deserializeArbiterSnapshot(buffer, createArbiterFactory());
|
||||
|
||||
assert.ok(restored.nodeIdByKey.has('project:résumé'));
|
||||
assert.strictEqual(restored.relationConfigs.has('can_read'), true);
|
||||
});
|
||||
|
||||
it('relation configs survive round-trip', async () => {
|
||||
const { serializeArbiterSnapshot, deserializeArbiterSnapshot } = await loadSnapshotModule();
|
||||
|
||||
const original = new Arbiter({ fastConstructionMode: true, enableInference: false });
|
||||
original.addNode('user:alice', 'user');
|
||||
original.addNode('project:secret', 'project');
|
||||
original.addRelation('user:alice', 'can_access', 'project:secret', {
|
||||
possibility: 0.8,
|
||||
value: 42,
|
||||
reliability: 0.95
|
||||
});
|
||||
original.setRelationConfig('can_access', { type: 'chain', steps: [{ relation: 'parent', direction: 'out' }] });
|
||||
original.setRelationConfig('can_read', { type: 'direct', minPossibility: 0.5 });
|
||||
original.enableCondensedSnapshot();
|
||||
|
||||
const buffer = serializeArbiterSnapshot(original);
|
||||
const restored = deserializeArbiterSnapshot(buffer, createArbiterFactory());
|
||||
|
||||
const accessConfig = restored.relationConfigs.get('can_access');
|
||||
assert.strictEqual(accessConfig && accessConfig.type, 'chain');
|
||||
assert.deepStrictEqual(accessConfig && accessConfig.steps, [{ relation: 'parent', direction: 'out' }]);
|
||||
|
||||
const readConfig = restored.relationConfigs.get('can_read');
|
||||
assert.strictEqual(readConfig && readConfig.type, 'direct');
|
||||
assert.strictEqual(readConfig && readConfig.minPossibility, 0.5);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user