import { LogicalOperators } from '../../src/authorization/rules/LogicalOperators.js'; import { describe, it, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; // Minimal mock ruleEvaluator for child rule evaluation function makeMockRuleEvaluator(resultsByRule) { return { evaluateRule: (userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) => { if (typeof rule === 'string') return resultsByRule[rule]; if (rule && rule.mockKey) return resultsByRule[rule.mockKey]; if (rule && rule.type && resultsByRule[rule.type]) return resultsByRule[rule.type]; return resultsByRule.default || { possibility: 0, reliability: 1.0, meta: { reason: 'no_rule' }, collectedValues: [] }; } }; } describe('LogicalOperators', () => { let logicalOps; let arbiter; let ruleEvaluator; beforeEach(() => { arbiter = {}; }); it('evaluates union (max) correctly', () => { ruleEvaluator = makeMockRuleEvaluator({ a: { possibility: 0.7, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] }, b: { possibility: 0.4, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] }, c: { possibility: 0.9, reliability: 0.7, meta: { ruleType: 'direct', rule: { mockKey: 'c' } }, collectedValues: [3] } }); logicalOps = new LogicalOperators(arbiter, ruleEvaluator); const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }, { mockKey: 'c' }], aggregator: 'max' } }; const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true }); assert.strictEqual(res.possibility, 0.9); assert.ok(res.meta.operation === 'union'); assert.deepStrictEqual(res.collectedValues, [1, 2, 3]); }); it('evaluates intersection (min) correctly', () => { ruleEvaluator = makeMockRuleEvaluator({ a: { possibility: 0.7, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] }, b: { possibility: 0.4, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] }, c: { possibility: 0.9, reliability: 0.7, meta: { ruleType: 'direct', rule: { mockKey: 'c' } }, collectedValues: [3] } }); logicalOps = new LogicalOperators(arbiter, ruleEvaluator); const rule = { type: 'logical', intersection: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }, { mockKey: 'c' }], aggregator: 'min' } }; const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true }); assert.strictEqual(res.possibility, 0.4); assert.ok(res.meta.operation === 'intersection'); assert.deepStrictEqual(res.collectedValues, [1, 2, 3]); }); it('evaluates union (mean/average) correctly', () => { ruleEvaluator = makeMockRuleEvaluator({ a: { possibility: 0.6, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] }, b: { possibility: 0.8, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] } }); logicalOps = new LogicalOperators(arbiter, ruleEvaluator); const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }], aggregator: 'mean' } }; const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true }); assert.ok(Math.abs(res.possibility - 0.7) < 1e-8); assert.ok(res.meta.operation === 'union'); }); it('evaluates intersection (pessimistic) correctly', () => { ruleEvaluator = makeMockRuleEvaluator({ a: { possibility: 0.9, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] }, b: { possibility: 0.6, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] } }); logicalOps = new LogicalOperators(arbiter, ruleEvaluator); const rule = { type: 'logical', intersection: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }], aggregator: 'pessimistic' } }; const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true }); assert.ok(res.possibility < 0.9 && res.possibility > 0.6); assert.ok(res.meta.operation === 'intersection'); }); it('evaluates exclusion (A AND NOT B) correctly', () => { ruleEvaluator = makeMockRuleEvaluator({ a: { possibility: 0.8, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] }, b: { possibility: 0.5, reliability: 0.7, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] } }); logicalOps = new LogicalOperators(arbiter, ruleEvaluator); const rule = { type: 'logical', exclusion: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }] } }; const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true }); assert.ok(Math.abs(res.possibility - (0.8 * (1 - 0.5))) < 1e-8); assert.ok(res.meta.operation === 'exclusion'); }); it('evaluates custom OWA weights for union', () => { ruleEvaluator = makeMockRuleEvaluator({ a: { possibility: 0.2, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] }, b: { possibility: 0.8, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] } }); logicalOps = new LogicalOperators(arbiter, ruleEvaluator); const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }], owaWeights: [0.7, 0.3] } }; const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {}); assert.ok(Math.abs(res.possibility - (0.8 * 0.7 + 0.2 * 0.3)) < 1e-8); assert.ok(res.meta.operation === 'union'); }); it('evaluates binary defeasible logic (never overrides everything)', () => { // never wins over everything ruleEvaluator = makeMockRuleEvaluator({ never: { possibility: 1, reliability: 0.95, meta: { ruleType: 'never' }, collectedValues: [1] }, strict: { possibility: 1, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [2] }, defeater: { possibility: 0.6, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [3] }, defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [4] } }); logicalOps = new LogicalOperators(arbiter, ruleEvaluator); const rule = { type: 'logical', never: { union: [{ mockKey: 'never' }] }, always: { direct: { mockKey: 'strict' } }, unless: { union: [{ mockKey: 'defeater' }] }, when: { intersection: [{ mockKey: 'defeasible' }] }, mode: 'binary' }; const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {}); assert.strictEqual(res.possibility, 0); assert.ok(res.meta.never); assert.ok(res.meta.reason === 'never_rule_triggered'); assert.ok(res.meta.mode === 'binary'); }); it('evaluates binary defeasible logic (strict, defeater, defeasible)', () => { // strict wins ruleEvaluator = makeMockRuleEvaluator({ strict: { possibility: 1, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [1] }, defeater: { possibility: 0.6, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [2] }, defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [3] } }); logicalOps = new LogicalOperators(arbiter, ruleEvaluator); const rule = { type: 'logical', always: { direct: { mockKey: 'strict' } }, unless: { union: [{ mockKey: 'defeater' }] }, when: { intersection: [{ mockKey: 'defeasible' }] }, mode: 'binary' }; const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {}); assert.strictEqual(res.possibility, 1); assert.ok(res.meta.strict); assert.ok(res.meta.mode === 'binary'); }); it('evaluates binary defeasible logic (requirements take precedence over defeaters)', () => { // requirements not met should win over defeaters ruleEvaluator = makeMockRuleEvaluator({ strict: { possibility: 0, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [1] }, requires: { possibility: 0.1, reliability: 0.9, meta: { ruleType: 'requires' }, collectedValues: [2] }, defeater: { possibility: 0.8, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [3] }, defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [4] } }); logicalOps = new LogicalOperators(arbiter, ruleEvaluator); const rule = { type: 'logical', always: { direct: { mockKey: 'strict' } }, requires: { union: [{ mockKey: 'requires' }] }, unless: { union: [{ mockKey: 'defeater' }] }, when: { intersection: [{ mockKey: 'defeasible' }] }, mode: 'binary' }; const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {}); assert.strictEqual(res.possibility, 0); assert.ok(res.meta.reason === 'requirements_not_met'); assert.ok(res.meta.mode === 'binary'); }); it('evaluates binary defeasible logic (defeater wins)', () => { // defeater wins when requirements are met ruleEvaluator = makeMockRuleEvaluator({ strict: { possibility: 0, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [1] }, requires: { possibility: 0.9, reliability: 0.9, meta: { ruleType: 'requires' }, collectedValues: [2] }, defeater: { possibility: 0.8, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [3] }, defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [4] } }); logicalOps = new LogicalOperators(arbiter, ruleEvaluator); const rule = { type: 'logical', always: { direct: { mockKey: 'strict' } }, requires: { union: [{ mockKey: 'requires' }] }, unless: { union: [{ mockKey: 'defeater' }] }, when: { intersection: [{ mockKey: 'defeasible' }] }, mode: 'binary' }; const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {}); assert.strictEqual(res.possibility, 0); assert.ok(res.meta.reason === 'defeated_by_unless'); assert.ok(res.meta.mode === 'binary'); }); it('evaluates threshold mode with never early exit', () => { ruleEvaluator = makeMockRuleEvaluator({ never: { possibility: 0.9, reliability: 0.95, meta: { ruleType: 'never' }, collectedValues: [1] }, strict: { possibility: 0.85, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [2] }, defeater: { possibility: 0.2, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [3] }, defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [4] } }); logicalOps = new LogicalOperators(arbiter, ruleEvaluator); const rule = { type: 'logical', never: { union: [{ mockKey: 'never' }] }, always: { direct: { mockKey: 'strict' } }, unless: { union: [{ mockKey: 'defeater' }] }, when: { intersection: [{ mockKey: 'defeasible' }] }, mode: 'threshold' }; const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {}); assert.strictEqual(res.possibility, 0); assert.ok(res.meta.earlyExit === 'never_rule'); assert.ok(res.meta.never); }); it('evaluates threshold mode with early exit', () => { ruleEvaluator = makeMockRuleEvaluator({ strict: { possibility: 0.85, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [1] }, defeater: { possibility: 0.2, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [2] }, defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [3] } }); logicalOps = new LogicalOperators(arbiter, ruleEvaluator); const rule = { type: 'logical', always: { direct: { mockKey: 'strict' } }, unless: { union: [{ mockKey: 'defeater' }] }, when: { intersection: [{ mockKey: 'defeasible' }] }, mode: 'threshold' }; const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {}); assert.strictEqual(res.possibility, 0.85); assert.ok(res.meta.earlyExit === 'strict_rule'); }); it('handles empty union/intersection/exclusion', () => { ruleEvaluator = makeMockRuleEvaluator({}); logicalOps = new LogicalOperators(arbiter, ruleEvaluator); const unionRule = { type: 'logical', union: { rules: [] } }; const intersectionRule = { type: 'logical', intersection: { rules: [] } }; const exclusionRule = { type: 'logical', exclusion: { rules: [] } }; assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', unionRule, {}, null, {}).possibility, 0); assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', intersectionRule, {}, null, {}).possibility, 0); assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', exclusionRule, {}, null, {}).possibility, 0); }); it('handles all-zero and all-one cases', () => { ruleEvaluator = makeMockRuleEvaluator({ a: { possibility: 0, reliability: 1.0, meta: {}, collectedValues: [] }, b: { possibility: 0, reliability: 1.0, meta: {}, collectedValues: [] } }); logicalOps = new LogicalOperators(arbiter, ruleEvaluator); const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }] } }; assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {}).possibility, 0); ruleEvaluator = makeMockRuleEvaluator({ a: { possibility: 1, reliability: 1.0, meta: {}, collectedValues: [] }, b: { possibility: 1, reliability: 1.0, meta: {}, collectedValues: [] } }); logicalOps = new LogicalOperators(arbiter, ruleEvaluator); assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {}).possibility, 1); }); it('propagates meta and collectedValues', () => { ruleEvaluator = makeMockRuleEvaluator({ a: { possibility: 0.5, reliability: 0.9, meta: { foo: 'bar' }, collectedValues: [1, 2] }, b: { possibility: 0.8, reliability: 0.8, meta: { baz: 'qux' }, collectedValues: [3] } }); logicalOps = new LogicalOperators(arbiter, ruleEvaluator); const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }] } }; const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true }); assert.ok(res.meta); assert.deepStrictEqual(res.collectedValues, [1, 2, 3]); }); it('handles missing rule types gracefully', () => { ruleEvaluator = makeMockRuleEvaluator({}); logicalOps = new LogicalOperators(arbiter, ruleEvaluator); const rule = { type: 'logical', union: { rules: [{ mockKey: 'missing' }] } }; const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {}); assert.strictEqual(res.possibility, 0); assert.ok(res.meta && res.meta.reason === 'missing_rule'); }); it('supports fastPath/early exit logic in union', () => { let callCount = 0; ruleEvaluator = { evaluateRule: (userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) => { callCount++; if (callCount === 1) return { possibility: 0.95, reliability: 0.9, meta: {}, collectedValues: [1] }; return { possibility: 0.2, reliability: 0.8, meta: {}, collectedValues: [2] }; } }; logicalOps = new LogicalOperators(arbiter, ruleEvaluator); const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }] }, fastPath: true, minPossibility: 0.9 }; const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { fastPath: true, minPossibility: 0.9, collectValues: true }); assert.strictEqual(res.possibility, 0.95); assert.deepStrictEqual(res.collectedValues, [1]); }); it('supports priority/aggregator effects', () => { ruleEvaluator = makeMockRuleEvaluator({ a: { possibility: 0.7, reliability: 0.9, meta: { rule: { priority: 10 } }, collectedValues: [1] }, b: { possibility: 0.6, reliability: 0.8, meta: { rule: { priority: 5 } }, collectedValues: [2] } }); logicalOps = new LogicalOperators(arbiter, ruleEvaluator); const rule = { type: 'logical', union: { rules: [{ mockKey: 'a', priority: 10 }, { mockKey: 'b', priority: 5 }], aggregator: 'priority' } }; const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {}); assert.ok(res.possibility > 0.6 && res.possibility < 0.8); }); });