import { test, describe } from 'node:test'; import assert from 'node:assert/strict'; import { Arbiter } from '../../src/index.js'; describe('Arbiter Core Functionality', () => { test('adds nodes correctly', () => { const arbiter = new Arbiter(); const nodeId = arbiter.addNode('user1', 'user'); assert.strictEqual(nodeId, 0); assert.strictEqual(arbiter.nextNodeId, 1); assert.ok(arbiter.nodes.has(0)); assert.strictEqual(arbiter.nodeIdByKey.get('user1'), 0); assert.strictEqual(arbiter.keyByNodeId.get(0), 'user1'); const node = arbiter.nodes.get(0); assert.strictEqual(node.key, 'user1'); assert.strictEqual(node.type, 'user'); }); test('adds relations correctly', () => { const arbiter = new Arbiter(); arbiter.addNode('user1', 'user'); arbiter.addNode('project1', 'project'); const relationId = arbiter.addRelation('user1', 'can_read', 'project1', 0.8); assert.strictEqual(relationId, 0); assert.strictEqual(arbiter.relations.length, 1); const relation = arbiter.relations[0]; assert.strictEqual(relation.src, 0); // user1 nodeId assert.strictEqual(relation.dst, 1); // project1 nodeId assert.strictEqual(relation.rel, 'can_read'); assert.strictEqual(relation.possibility, 0.8); }); test('performs basic authorization checks', () => { const arbiter = new Arbiter(); // Setup arbiter.addNode('user1', 'user'); arbiter.addNode('project1', 'project'); arbiter.addRelation('user1', 'can_read', 'project1', 1.0); arbiter.setRelationConfig('can_read', { type: 'direct' }); const result = arbiter.check('user1', 'can_read', 'project1'); assert.strictEqual(result.possibility, 1); // CI-001 fix: with the fast path active, direct-type relations // produce reason='direct_match' (AuthorizationChecker.js:161) // instead of 'allow_rule_matched' (the slow-path reason). assert.strictEqual(result.reason, 'direct_match'); }); test('handles missing nodes gracefully', () => { const arbiter = new Arbiter(); arbiter.addNode('user1', 'user'); const result = arbiter.check('user1', 'can_read', 'nonexistent'); assert.strictEqual(result.possibility, 0); assert.strictEqual(result.reason, 'missing_node'); }); test('handles missing relations gracefully', () => { const arbiter = new Arbiter(); arbiter.addNode('user1', 'user'); arbiter.addNode('project1', 'project'); arbiter.setRelationConfig('can_read', { type: 'direct' }); const result = arbiter.check('user1', 'can_read', 'project1'); assert.strictEqual(result.possibility, 0); // CI-001 fix: fast-path's no-relation branch sets 'no_relation' // (AuthorizationChecker.js:174) assert.strictEqual(result.reason, 'no_relation'); }); test('supports binary mode checks', () => { const arbiter = new Arbiter(); arbiter.addNode('user1', 'user'); arbiter.addNode('project1', 'project'); arbiter.addRelation('user1', 'can_read', 'project1', 0.9); arbiter.setRelationConfig('can_read', { type: 'direct' }); const result = arbiter.check('user1', 'can_read', 'project1', { binary: true, minAllowPossibility: 0.8 }); assert.strictEqual(result.possibility, 0.9); assert.strictEqual(result.allow, true); assert.strictEqual(result.deny, false); }); test('supports fast path optimization', () => { const arbiter = new Arbiter(); arbiter.addNode('user1', 'user'); arbiter.addNode('project1', 'project'); arbiter.addRelation('user1', 'can_read', 'project1', 0.9); arbiter.setRelationConfig('can_read', { type: 'direct' }); const result = arbiter.check('user1', 'can_read', 'project1', { fastPath: true }); assert.strictEqual(result.possibility, 0.9); // CI-001 fix: fast path returns 'direct_match' assert.strictEqual(result.reason, 'direct_match'); }); test('handles early exit conditions', () => { const arbiter = new Arbiter(); arbiter.addNode('user1', 'user'); arbiter.addNode('project1', 'project'); arbiter.addRelation('user1', 'can_read', 'project1', 0.9); arbiter.setRelationConfig('can_read', { type: 'direct' }); const result = arbiter.check('user1', 'can_read', 'project1', { minAllowPossibility: 0.95 }); // CI-001 fix: threshold-not-met zero-out the possibility since // directRel.possibility (0.9) < effectiveThreshold (0.95). // AuthorizationChecker.js:140-144. assert.strictEqual(result.possibility, 0); assert.strictEqual(result.reason, 'threshold_not_met'); }); test('manages direct check cache', () => { const arbiter = new Arbiter({ directCheckCacheSize: 2 }); arbiter.addNode('user1', 'user'); arbiter.addNode('project1', 'project'); arbiter.addRelation('user1', 'can_read', 'project1', 1.0); arbiter.setRelationConfig('can_read', { type: 'direct' }); // First check - should cache. CI-001 fix: with the fast path // active, direct-type relations produce 'direct_match' reason. const result1 = arbiter.check('user1', 'can_read', 'project1'); assert.strictEqual(result1.possibility, 1); assert.strictEqual(result1.reason, 'direct_match'); // Second check - should hit the cache populated by the fast path. const result2 = arbiter.check('user1', 'can_read', 'project1'); assert.strictEqual(result2.possibility, 1); assert.strictEqual(result2.reason, 'direct_match'); assert.ok(arbiter.directCheckCache); assert.strictEqual(typeof arbiter.directCheckCache.has, 'function'); // Cache should now have the entry. assert.ok(arbiter.directCheckCache.has( arbiter.keyManager.createCompositeKey( arbiter.keyManager.getStringId('user1'), 'can_read', arbiter.keyManager.getStringId('project1') ) )); }); test('handles cycle detection', () => { const arbiter = new Arbiter(); arbiter.addNode('user1', 'user'); arbiter.addNode('user2', 'user'); arbiter.addRelation('user1', 'parent', 'user2', 1.0); arbiter.addRelation('user2', 'parent', 'user1', 1.0); // Creates cycle arbiter.setRelationConfig('parent', { type: 'parent' }); const result = arbiter.check('user1', 'parent', 'user2'); assert.strictEqual(result.possibility, 0); assert.strictEqual(result.reason, 'cycle'); }); test('supports inference mode', () => { const arbiter = new Arbiter(); arbiter.addNode('user1', 'user'); arbiter.addNode('project1', 'project'); arbiter.addRelation('user1', 'can_read', 'project1', 0.7); arbiter.setRelationConfig('can_read', { type: 'direct' }); const result = arbiter.check('user1', 'can_read', 'project1', { noInfer: false }); assert.strictEqual(result.possibility, 0.7); // CI-001 fix: direct-type relation, fast path → 'direct_match' assert.strictEqual(result.reason, 'direct_match'); }); test('handles multiple relation types', () => { const arbiter = new Arbiter(); arbiter.addNode('user1', 'user'); arbiter.addNode('group1', 'group'); arbiter.addNode('project1', 'project'); arbiter.addRelation('user1', 'member_of', 'group1', 1.0); arbiter.addRelation('group1', 'can_access', 'project1', 1.0); arbiter.setRelationConfig('member_of', { type: 'direct' }); arbiter.setRelationConfig('can_access', { type: 'direct' }); const result1 = arbiter.check('user1', 'member_of', 'group1'); const result2 = arbiter.check('group1', 'can_access', 'project1'); assert.strictEqual(result1.possibility, 1); assert.strictEqual(result2.possibility, 1); }); test('supports value collection', () => { const arbiter = new Arbiter(); arbiter.addNode('user1', 'user'); arbiter.addNode('account1', 'account'); arbiter.addRelation('user1', 'has_account', 'account1', 1.0, { value: 1000 }); arbiter.setRelationConfig('has_account', { type: 'direct' }); const result = arbiter.check('user1', 'has_account', 'account1', { collectValues: true }); assert.strictEqual(result.possibility, 1); assert.ok(result.collectedValues); assert.strictEqual(result.collectedValues.length, 1); assert.strictEqual(result.collectedValues[0].value, 1000); }); });