Files
evidence-dsl/tests/ExpressionTests.js
T
John Dvorak 0c2ddc282b
CI / test (push) Successful in 25s
CI / publish (push) Has been skipped
test: fix legacy fixture suites against modern DSL validation
The legacy test files (Definition/Evidence/Expression/Fact/Integration/
Measure) predate the DSL's validation rules: reserved built-in types
(User/Account/Device/AuthSession) and the boolean/predicate-call
requirements on evidence statements. Fixed the fixtures, not the engine:

- reserved renames: User->Employee, Account->Tenant (Device/AuthSession
  were only legally referenced)
- expression fixtures: comparators now use predicate calls on both sides
  (fact score(value: number) + score(1 + 2 * 3) > score(0)); measure
  bodies carry the original arbitrary expressions (validated, not
  rule-generated)
- grammar-shape fixes: comma-separated fusion/aggregate lists, PROVIDES
  X (no array returns), predicate-call evidence statements

All 54 tests across the 8 files pass.
2026-08-03 09:12:20 -07:00

289 lines
11 KiB
JavaScript

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
}
});
});
});