import assert from 'node:assert/strict'; import { describe, test } from 'node:test'; import { Arbiter } from '../../src/core/Arbiter.js'; describe('Rule result cache stats', () => { test('records cache hits on repeated aggregate checks', () => { const arbiter = new Arbiter({ enableRuleResultCache: true, ruleResultCacheTTL: 60000 }); arbiter.addNode('user:1', 'user'); arbiter.addNode('resource:1', 'resource'); arbiter.setRelationConfig('member', { type: 'direct' }); arbiter.setRelationConfig('allow', { union: { rules: [{ type: 'direct', relation: 'member' }], aggregator: 'max' } }); arbiter.registerDependencyIndex(new Map([['member', { all: new Set(['allow']), byLevel: { never: new Set(), always: new Set(), requires: new Set(), when: new Set(), unless: new Set(), ordinary: new Set(['allow']) } }]])); arbiter.addRelation('user:1', 'member', 'resource:1', 1.0); const userId = arbiter.resolveNodeId('user:1'); const objectId = arbiter.resolveNodeId('resource:1'); const config = arbiter.relationConfigs.get('allow'); arbiter.authChecker.ruleEvaluator.evaluateRule( userId, 'user:1', objectId, 'resource:1', config, new Set(), 'allow', { includeMeta: false, collectValues: false, cacheRuleResult: true } ); const baseKey = arbiter.keyManager.createCompositeKey(userId, 'allow', objectId); const cacheKey = `${baseKey}|logical`; assert.ok(arbiter.ruleResultCache.get(cacheKey), 'cache entry created'); arbiter.authChecker.ruleEvaluator.evaluateRule( userId, 'user:1', objectId, 'resource:1', config, new Set(), 'allow', { includeMeta: false, collectValues: false, cacheRuleResult: true } ); assert.ok(arbiter.ruleResultCacheStats.misses >= 1, 'cache miss recorded'); assert.ok(arbiter.ruleResultCacheStats.hits >= 1, 'cache hit recorded'); }); test('does not prepopulate cache on config set', () => { const arbiter = new Arbiter({ enableRuleResultCache: true, ruleResultCacheTTL: 60000 }); arbiter.addNode('user:1', 'user'); arbiter.addNode('resource:1', 'resource'); arbiter.setRelationConfig('risk_score', { type: 'direct' }); arbiter.setRelationConfig('risk_limit', { type: 'direct' }); arbiter.addRelation('user:1', 'risk_score', 'resource:1', 1.0, { value: 10 }); arbiter.addRelation('resource:1', 'risk_limit', 'resource:1', 1.0, { value: 20 }); arbiter.setRelationConfig('risk_ok_owa', { type: 'relational_comparator', comparator: '<=', left: { rule: { type: 'direct', relation: 'risk_score' }, extractValue: true, valueRelation: 'risk_score', aggregator: 'owa', owaWeights: [1] }, right: { rule: { type: 'direct', relation: 'risk_limit' }, extractValue: true, valueRelation: 'risk_limit', evaluateFrom: 'object' } }); const userId = arbiter.resolveNodeId('user:1'); const objectId = arbiter.resolveNodeId('resource:1'); const config = arbiter.relationConfigs.get('risk_ok_owa'); const cacheKey = arbiter.authChecker.ruleEvaluator._getRuleResultCacheKey( userId, 'risk_ok_owa', objectId, config ); assert.ok(!arbiter.ruleResultCache.get(cacheKey), 'cache not populated on config set'); arbiter.authChecker.ruleEvaluator.evaluateRule( userId, 'user:1', objectId, 'resource:1', config, new Set(), 'risk_ok_owa', { includeMeta: false, collectValues: true, cacheRuleResult: true } ); assert.ok(arbiter.ruleResultCache.get(cacheKey), 'cache populated after evaluation'); }); });