import { describe, test } from 'node:test'; import assert from 'node:assert/strict'; import { DSLCompiler } from '../src/DSLCompiler.js'; function createMockArbiter() { const relationConfigs = new Map(); return { relationConfigs, setRelationConfig(relation, config) { relationConfigs.set(relation, config); } }; } // The compiler validates evidence bodies as generated rules, which only accept // predicate-call forms. Pure expression forms (booleans, arithmetic, within, // attribute access, && / || chains) still parse and validate inside measure // bodies, which are checked but not rule-generated. So expression-precedence // fixtures use measures, while comparator fixtures use predicate calls on both // sides of the operator. const SCORE_FACT = ` fact score(value: number) `; const EMPLOYEE_FIELDS = ` definition Employee { role: string isActive: boolean isSuspended: boolean isBlacklisted: boolean isTrusted: boolean lastActive: timestamp lastLogin: timestamp createdAt: timestamp lastActivity: timestamp hasEmergencyAccess: boolean balance: number score: number profile: Profile permissions: Permission[] } definition Profile { name: string permissions: Permission[] } definition Permission { name: string } definition Resource { name: string } definition Document { name: string } fact hasRole(user: any, role: string) fact isMember(user: any, group: any) fact hasPermission(user: any, resource: any, action: string) fact isActive(user: any) `; describe('Expression Parsing', () => { const arbiter = createMockArbiter(); const compiler = new DSLCompiler(arbiter); test('Arithmetic operator precedence', () => { const testCases = [ { input: 'score(1 + 2 * 3) > score(0)', expected: 'Should evaluate as 1 + (2 * 3) = 7', description: 'Multiplication before addition' }, { input: 'score(10 - 3 * 2) > score(0)', expected: 'Should evaluate as 10 - (3 * 2) = 4', description: 'Multiplication before subtraction' }, { input: 'score(8 / 2 * 4) > score(0)', expected: 'Should evaluate as (8 / 2) * 4 = 16', description: 'Left-associative division and multiplication' }, { input: 'score(2 + 3 * 4 - 5) > score(0)', expected: 'Should evaluate as 2 + (3 * 4) - 5 = 9', description: 'Mixed arithmetic with correct precedence' }, { input: 'score((1 + 2) * 3) > score(0)', expected: 'Should evaluate as (1 + 2) * 3 = 9', description: 'Parentheses override precedence' } ]; testCases.forEach(({ input, expected, description }) => { const dsl = SCORE_FACT + `evidence test() { ${input} }`; const result = compiler.compile(dsl, `test-arithmetic-${Date.now()}`); assert.ok(result.success, `${description} should parse successfully`); }); }); test('Logical operator precedence', () => { const testCases = [ { input: 'true && false || true', expected: 'Should evaluate as (true && false) || true = true', description: 'AND before OR' }, { input: 'false || true && false', expected: 'Should evaluate as false || (true && false) = false', description: 'AND before OR (alternative)' }, { input: 'NOT true && false', expected: 'Should evaluate as (NOT true) && false = false', description: 'NOT before AND' }, { input: 'true && NOT false', expected: 'Should evaluate as true && (NOT false) = true', description: 'NOT before AND (alternative)' }, { input: '(true || false) && true', expected: 'Should evaluate as (true || false) && true = true', description: 'Parentheses override logical precedence' } ]; testCases.forEach(({ input, expected, description }) => { const dsl = `measure test() { ${input} } PROVIDES boolean`; const result = compiler.compile(dsl, `test-logical-${Date.now()}`); assert.ok(result.success, `${description} should parse successfully`); }); }); test('Comparison operators', () => { const testCases = [ { input: 'score(1) == score(1)', description: 'Equality comparison', measure: false }, { input: 'score(1) != score(2)', description: 'Inequality comparison', measure: false }, { input: 'score(5) > score(3)', description: 'Greater than', measure: false }, { input: 'score(3) < score(5)', description: 'Less than', measure: false }, { input: 'score(4) >= score(4)', description: 'Greater than or equal', measure: false }, { input: 'score(4) <= score(4)', description: 'Less than or equal', measure: false }, { input: 'score(1) == score(1) && score(2) > score(1)', description: 'Comparison with logical operators', measure: true }, { input: 'score(1 + 2) == score(3)', description: 'Arithmetic in comparison', measure: false } ]; testCases.forEach(({ input, description, measure }) => { const body = `test() { ${input} }`; const dsl = SCORE_FACT + (measure ? `measure ${body} PROVIDES boolean` : `evidence ${body}`); const result = compiler.compile(dsl, `test-comparison-${Date.now()}`); assert.ok(result.success, `${description} should parse successfully`); }); }); test('Temporal expressions', () => { const testCases = [ { input: 'user.lastActive within 1h', description: 'Temporal within expression' }, { input: 'user.lastLogin within 24h', description: 'Temporal within with hours' }, { input: 'user.createdAt within 7d', description: 'Temporal within with days' }, { input: 'user.lastActivity within 1h && user.isActive', description: 'Temporal with logical operators' } ]; testCases.forEach(({ input, description }) => { const dsl = EMPLOYEE_FIELDS + `measure test(user: Employee) { ${input} } PROVIDES boolean`; const result = compiler.compile(dsl, `test-temporal-${Date.now()}`); assert.ok(result.success, `${description} should parse successfully`); }); }); test('Unary operators', () => { const testCases = [ { input: 'NOT true', description: 'NOT operator', params: '' }, { input: '! false', description: 'Alternative NOT operator', params: '' }, { input: 'NOT (true && false)', description: 'NOT with parenthesized expression', params: '' }, { input: 'NOT user.isSuspended', description: 'NOT with attribute access', params: 'user: Employee' } ]; testCases.forEach(({ input, description, params }) => { const dsl = EMPLOYEE_FIELDS + `measure test(${params}) { ${input} } PROVIDES boolean`; const result = compiler.compile(dsl, `test-unary-${Date.now()}`); assert.ok(result.success, `${description} should parse successfully`); }); }); test('Attribute access', () => { const testCases = [ { input: 'user.role', description: 'Simple attribute access' }, { input: 'user.profile.name', description: 'Nested attribute access' }, { input: 'user.permissions', description: 'Array access' }, { input: 'user.profile.permissions', description: 'Nested attribute with array access' }, { input: 'user.isActive && user.role == "admin"', description: 'Attribute access in logical expression' } ]; testCases.forEach(({ input, description }) => { const dsl = EMPLOYEE_FIELDS + `measure test(user: Employee) { ${input} } PROVIDES boolean`; const result = compiler.compile(dsl, `test-attribute-${Date.now()}`); assert.ok(result.success, `${description} should parse successfully`); }); }); test('Function calls', () => { const testCases = [ { input: 'hasRole(user, "admin")', description: 'Simple function call', measure: false }, { input: 'isMember(user, group)', description: 'Function call with variables', measure: false }, { input: 'hasPermission(user, resource, "read")', description: 'Function call with multiple arguments', measure: false }, { input: 'hasRole(user, "admin") && isActive(user)', description: 'Multiple function calls', measure: true }, { input: 'hasRole(user, user.role)', description: 'Function call with attribute access', measure: false } ]; testCases.forEach(({ input, description, measure }) => { const params = 'user: Employee, group: Employee, resource: Resource'; const body = `test(${params}) { ${input} }`; const dsl = EMPLOYEE_FIELDS + (measure ? `measure ${body} PROVIDES boolean` : `evidence ${body}`); const result = compiler.compile(dsl, `test-function-${Date.now()}`); assert.ok(result.success, `${description} should parse successfully`); }); }); test('Complex expressions', () => { const testCases = [ { input: 'user.isActive && (hasRole(user, "admin") || hasPermission(user, resource, "read"))', description: 'Complex logical expression with function calls' }, { input: 'user.balance > 100 && user.isActive && NOT user.isSuspended', description: 'Multiple conditions with NOT' }, { input: 'user.lastActive within 1h && (user.role == "admin" || user.hasEmergencyAccess)', description: 'Temporal with logical conditions' }, { input: 'hasRole(user, "admin") && user.isActive && NOT (user.isSuspended || user.isBlacklisted)', description: 'Complex negation with multiple conditions' }, { input: 'user.score > 0.8 && user.isTrusted && user.lastActivity within 24h', description: 'Multiple attribute conditions with temporal' } ]; testCases.forEach(({ input, description }) => { const params = 'user: Employee, resource: Resource, doc: Document'; const dsl = EMPLOYEE_FIELDS + `measure test(${params}) { ${input} } PROVIDES boolean`; const result = compiler.compile(dsl, `test-complex-${Date.now()}`); assert.ok(result.success, `${description} should parse successfully`); }); }); test('Expression error handling', () => { const testCases = [ { input: 'user.role ==', description: 'Incomplete comparison should fail' }, { input: 'user.role &&', description: 'Incomplete logical expression should fail' }, { input: 'hasRole(user,)', description: 'Function call with missing argument should fail' }, { input: 'user.role == "admin" &&', description: 'Incomplete logical expression should fail' } ]; testCases.forEach(({ input, description }) => { try { const dsl = `evidence test() { ${input} }`; const result = compiler.compile(dsl, `test-error-${Date.now()}`); assert.ok(!result.success, `${description} should fail to parse`); } catch { // Expected to fail } }); }); });