Files

134 lines
5.5 KiB
JavaScript
Raw Permalink Normal View History

import { test, describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import { Arbiter } from '../../../src/index.js';
import { ChainRule } from '../../../src/authorization/rules/ChainRule.js';
import { ValueContext } from '../../../src/authorization/ValueContext.js';
Arbiter.DEBUG = false;
describe('ChainRule', () => {
let arbiter;
let chainRule;
beforeEach(() => {
// Use actual Arbiter for realistic testing
arbiter = new Arbiter({ embeddingDimensions: 256 });
// Set up nodes
arbiter.addNode('user1', 'user');
arbiter.addNode('user2', 'user'); // Add user2 for the "no path" test
arbiter.addNode('acctA', 'account');
arbiter.addNode('acctB', 'account');
arbiter.addNode('usd', 'currency');
// Set up relations
arbiter.setRelationConfig('can_debit', { type: 'direct' });
arbiter.setRelationConfig('has_balance', { type: 'direct' });
// Add relations with specific possibility values
arbiter.addRelation('user1', 'can_debit', 'acctA', { possibility: 0.9 });
arbiter.addRelation('acctA', 'has_balance', 'usd', { possibility: 0.8, value: 100 });
chainRule = new ChainRule(arbiter);
});
it('returns correct possibility and collects point value for direct chain', () => {
const rule = {
type: 'chain',
steps: [
{ relation: 'can_debit', direction: 'out' },
{ relation: 'has_balance', direction: 'out' }
]
};
const valueContext = new ValueContext(arbiter);
const res = chainRule._evaluateRule('user1', 'user1', 'usd', 'usd', rule, {}, null, { valueContext, collectValues: true });
assert.strictEqual(res.possibility, 0.8); // min(0.9, 0.8)
assert.ok(Array.isArray(res.collectedValues));
assert.strictEqual(res.collectedValues.length, 1);
assert.strictEqual(res.collectedValues[0].value.min, 100);
assert.strictEqual(res.collectedValues[0].value.max, 100);
});
it('returns 0 possibility if no path exists', () => {
const rule = {
type: 'chain',
steps: [
{ relation: 'can_debit', direction: 'out' },
{ relation: 'has_balance', direction: 'out' }
]
};
// No relation for user2 - use numeric ID to avoid string key issues
const user2Id = arbiter.nodeIdByKey.get('user2');
const usdId = arbiter.nodeIdByKey.get('usd');
const res = chainRule._evaluateRule(user2Id, user2Id, usdId, usdId, rule, {}, null, {});
assert.strictEqual(res.possibility, 0);
assert.ok(Array.isArray(res.collectedValues));
assert.strictEqual(res.collectedValues.length, 0);
});
it('applies minPossibility threshold', () => {
const rule = {
type: 'chain',
steps: [
{ relation: 'can_debit', direction: 'out' },
{ relation: 'has_balance', direction: 'out' }
]
};
// Remove the original relation and add one with lower possibility
arbiter.removeRelation('acctA', 'has_balance', 'usd');
arbiter.addRelation('acctA', 'has_balance', 'usd', { possibility: 0.5, value: 100 });
const res = chainRule._evaluateRule('user1', 'user1', 'usd', 'usd', rule, {}, null, { minPossibility: 0.8, fastPath: true });
assert.strictEqual(res.possibility, 0);
assert.ok(Array.isArray(res.collectedValues));
assert.strictEqual(res.collectedValues.length, 0);
});
it('aggregates multiple values using interval fusion', () => {
const rule = {
type: 'chain',
steps: [
{ relation: 'can_debit', direction: 'out' },
{ relation: 'has_balance', direction: 'out' }
]
};
// Add second account for user1
arbiter.addRelation('user1', 'can_debit', 'acctB', { possibility: 0.8 });
arbiter.addRelation('acctB', 'has_balance', 'usd', { possibility: 0.7, value: 200 });
const valueContext = new ValueContext(arbiter);
const res = chainRule._evaluateRule('user1', 'user1', 'usd', 'usd', rule, {}, null, { valueContext, collectValues: true });
if (process.env.TEST_DEBUG === '1') console.log('Collected values:', res.collectedValues);
assert.strictEqual(res.possibility, 0.8); // max(min(0.9,0.8), min(0.8,0.7))
assert.ok(Array.isArray(res.collectedValues));
assert.strictEqual(res.collectedValues.length, 2);
const intervals = res.collectedValues.map((cv) => cv.value).sort((a, b) => a.min - b.min);
assert.deepStrictEqual(intervals[0], { min: 100, max: 100 });
assert.deepStrictEqual(intervals[1], { min: 200, max: 200 });
});
it('filters out values outside TTL', () => {
const rule = {
type: 'chain',
steps: [
{ relation: 'can_debit', direction: 'out' },
{ relation: 'has_balance', direction: 'out' }
]
};
// Remove the original relation and add one with old timestamp
arbiter.removeRelation('acctA', 'has_balance', 'usd');
arbiter.addRelation('acctA', 'has_balance', 'usd', {
possibility: 0.8,
value: 100,
changed_last_at: Date.now() - 2 * 24 * 60 * 60 * 1000
});
// Configure TTL for old values - values expire after 1 day
arbiter.valueManager.setTTL('has_balance', 24 * 60 * 60 * 1000); // 1 day TTL
// Test with old relation (2 days old) - should be filtered out by TTL expiration
const res = chainRule._evaluateRule('user1', 'user1', 'usd', 'usd', rule, {}, null, {});
assert.strictEqual(res.possibility, 0.8); // Authorization still works - path exists
assert.ok(Array.isArray(res.collectedValues));
// Old values should be filtered out due to TTL expiration
assert.strictEqual(res.collectedValues.length, 0);
});
});