2026-08-03 08:48:39 -07:00
|
|
|
import { describe, test } from 'node:test';
|
|
|
|
|
import assert from 'node:assert/strict';
|
|
|
|
|
import { DSLCompiler } from '../src/DSLCompiler.js';
|
|
|
|
|
import { parse } from '../src/parser/GeneratedParser.js';
|
|
|
|
|
import { RuleGenerator } from '../src/generator/RuleGenerator.js';
|
|
|
|
|
|
|
|
|
|
function createMockArbiter() {
|
|
|
|
|
const relationConfigs = new Map();
|
|
|
|
|
return {
|
|
|
|
|
relationConfigs,
|
|
|
|
|
setRelationConfig(relation, config) {
|
|
|
|
|
relationConfigs.set(relation, config);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('DSL Compiler', () => {
|
|
|
|
|
const arbiter = createMockArbiter();
|
|
|
|
|
const compiler = new DSLCompiler(arbiter);
|
|
|
|
|
|
|
|
|
|
test('Basic parsing', () => {
|
|
|
|
|
const dsl = `
|
|
|
|
|
definition Employee {
|
|
|
|
|
role: string
|
|
|
|
|
isActive: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fact hasRole(user: Employee, role: string)
|
|
|
|
|
|
|
|
|
|
evidence canRead(user: Employee, doc: Account) {
|
|
|
|
|
hasRole(user, 'admin')
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const result = compiler.compile(dsl, 'test-basic');
|
|
|
|
|
assert.ok(result.success, 'Basic parsing should succeed');
|
|
|
|
|
assert.ok(result.program !== null, 'Program should be created');
|
|
|
|
|
assert.ok(result.generatedRules.size > 0, 'Rules should be generated');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('Basic compilation', () => {
|
|
|
|
|
const dsl = `
|
|
|
|
|
definition Employee {
|
|
|
|
|
role: string
|
|
|
|
|
isActive: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fact hasRole(user: Employee, role: string) CACHE eager
|
|
|
|
|
|
|
|
|
|
evidence canRead(user: Employee, doc: Account) {
|
|
|
|
|
hasRole(user, 'admin')
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const result = compiler.compile(dsl, 'test-compilation');
|
|
|
|
|
assert.ok(result.success, 'Basic compilation should succeed');
|
|
|
|
|
assert.ok(result.generatedRules.has('canRead'), 'canRead rule should be generated');
|
|
|
|
|
|
|
|
|
|
const canReadConfig = result.generatedRules.get('canRead');
|
|
|
|
|
assert.ok(canReadConfig.type === 'direct', 'canRead should be direct rule');
|
|
|
|
|
assert.ok(canReadConfig.relation === 'hasRole', 'canRead should use hasRole relation');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('Complex DSL compilation', () => {
|
|
|
|
|
const dsl = `
|
|
|
|
|
definition Employee {
|
|
|
|
|
role: string
|
|
|
|
|
isActive: boolean
|
|
|
|
|
clearance: string BEHAVES {
|
|
|
|
|
blurring adaptive confidence_95
|
|
|
|
|
} CACHE eager
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fact hasRole(user: Employee, role: string) CACHE eager
|
|
|
|
|
fact isMember(user: Employee, group: Device) transitive CACHE lazy
|
|
|
|
|
fact owns(user: Employee, doc: Account) CACHE eager
|
|
|
|
|
|
|
|
|
|
evidence canRead(user: Employee, doc: Account) {
|
|
|
|
|
owns(user, doc)
|
|
|
|
|
hasRole(user, 'admin')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
evidence canAccessCritical(user: Employee, resource: AuthSession) {
|
|
|
|
|
fusion min {
|
|
|
|
|
hasRole(user, 'admin'),
|
|
|
|
|
hasRole(user, 'superadmin')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fusion max {
|
|
|
|
|
hasRole(user, 'admin'),
|
|
|
|
|
hasRole(user, 'secret')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const result = compiler.compile(dsl, 'test-complex');
|
|
|
|
|
assert.ok(result.success, 'Complex DSL compilation should succeed');
|
|
|
|
|
assert.ok(result.generatedRules.has('canRead'), 'canRead rule should be generated');
|
|
|
|
|
assert.ok(result.generatedRules.has('canAccessCritical'), 'canAccessCritical rule should be generated');
|
|
|
|
|
|
|
|
|
|
const canReadConfig = result.generatedRules.get('canRead');
|
|
|
|
|
assert.ok(canReadConfig.type === 'logical', 'canRead should be logical rule');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('Error handling', () => {
|
|
|
|
|
const invalidDSL = `
|
|
|
|
|
definition Employee {
|
|
|
|
|
role: string
|
|
|
|
|
// Missing closing brace
|
|
|
|
|
|
|
|
|
|
fact hasRole(user: Employee, role: string)
|
|
|
|
|
// Missing semicolon
|
|
|
|
|
|
|
|
|
|
evidence canRead(user: Employee, doc: Account) {
|
|
|
|
|
// Invalid syntax
|
|
|
|
|
invalid syntax here
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const result = compiler.compile(invalidDSL, 'test-error');
|
|
|
|
|
assert.ok(!result.success, 'Invalid DSL should fail');
|
|
|
|
|
assert.ok(result.errors.length > 0, 'Should have error messages');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('Program management', () => {
|
|
|
|
|
const dsl1 = `
|
|
|
|
|
definition Employee {
|
|
|
|
|
role: string
|
|
|
|
|
isActive: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fact hasRole(user: Employee, role: string)
|
|
|
|
|
evidence canRead(user: Employee, doc: Account) {
|
|
|
|
|
hasRole(user, 'admin')
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const dsl2 = `
|
|
|
|
|
definition Employee {
|
|
|
|
|
role: string
|
|
|
|
|
isActive: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fact hasBalance(user: Employee, amount: number)
|
|
|
|
|
evidence canWithdraw(user: Employee, amount: number) {
|
|
|
|
|
hasBalance(user, amount)
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const result1 = compiler.compile(dsl1, 'test-auth');
|
|
|
|
|
assert.ok(result1.success, 'First program should compile');
|
|
|
|
|
|
|
|
|
|
const programs = { 'auth': dsl1, 'finance': dsl2 };
|
|
|
|
|
const result2 = compiler.compileMultiple(programs);
|
|
|
|
|
assert.ok(result2.success, 'Multiple programs should compile');
|
|
|
|
|
|
|
|
|
|
const authProgram = compiler.getCompiledProgram('test-auth');
|
|
|
|
|
assert.ok(authProgram !== null, 'Should retrieve compiled program');
|
|
|
|
|
|
|
|
|
|
const removed = compiler.removeCompiledProgram('test-auth');
|
|
|
|
|
assert.ok(removed, 'Should remove program');
|
|
|
|
|
|
|
|
|
|
compiler.clearCompiledPrograms();
|
|
|
|
|
const allPrograms = compiler.getAllCompiledPrograms();
|
|
|
|
|
assert.ok(allPrograms.size === 0, 'Should clear all programs');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('Validation', () => {
|
|
|
|
|
const validDSL = `
|
|
|
|
|
definition Employee {
|
|
|
|
|
role: string
|
|
|
|
|
isActive: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fact hasRole(user: Employee, role: string)
|
|
|
|
|
|
|
|
|
|
evidence canRead(user: Employee, doc: Account) {
|
|
|
|
|
hasRole(user, 'admin')
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const invalidDSL = `
|
|
|
|
|
definition Employee {
|
|
|
|
|
role: string
|
|
|
|
|
// Missing closing brace
|
|
|
|
|
|
|
|
|
|
fact hasRole(user: Employee, role: string)
|
|
|
|
|
// Missing semicolon
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const validResult = compiler.validate(validDSL);
|
|
|
|
|
assert.ok(validResult.success, 'Valid DSL should pass validation');
|
|
|
|
|
|
|
|
|
|
const invalidResult = compiler.validate(invalidDSL);
|
|
|
|
|
assert.ok(!invalidResult.success, 'Invalid DSL should fail validation');
|
|
|
|
|
assert.ok(invalidResult.errors.length > 0, 'Should have validation errors');
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-03 20:27:13 -07:00
|
|
|
test('Rejects duplicate fields within a definition', () => {
|
|
|
|
|
const dupFieldDSL = `
|
|
|
|
|
definition Employee { id: string id: string }
|
|
|
|
|
`;
|
|
|
|
|
const result = compiler.validate(dupFieldDSL);
|
|
|
|
|
assert.ok(!result.success, 'Duplicate field should fail validation');
|
|
|
|
|
assert.match(result.errors[0], /Duplicate field 'Employee.id'/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('Hints at the real constraint for common declaration mistakes', () => {
|
|
|
|
|
const withinOnFact = compiler.compile(`
|
|
|
|
|
definition Employee { id: string? }
|
|
|
|
|
fact owns(user: Employee, doc: Employee) within 1h
|
|
|
|
|
`, 'err-within');
|
|
|
|
|
assert.match(withinOnFact.errors[0], /within.*only valid on `source`/);
|
|
|
|
|
|
|
|
|
|
const doubleBehaves = compiler.compile(`
|
|
|
|
|
definition Employee { id: string? }
|
|
|
|
|
fact rel(user: Employee, doc: Employee) BEHAVES AS transitive BEHAVES { ttl 1h }
|
|
|
|
|
`, 'err-behaves');
|
|
|
|
|
assert.match(doubleBehaves.errors[0], /only one `BEHAVES` clause/);
|
|
|
|
|
|
|
|
|
|
const limitAfterBody = compiler.compile(`
|
|
|
|
|
definition Employee { id: string? }
|
|
|
|
|
fact owns(user: Employee, doc: Employee)
|
|
|
|
|
evidence can_read(user: Employee, doc: Employee) { owns(user, doc) } limit 5
|
|
|
|
|
`, 'err-limit');
|
|
|
|
|
assert.match(limitAfterBody.errors[0], /`limit` is only valid on pattern/);
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-03 08:48:39 -07:00
|
|
|
test('Rule generation', () => {
|
|
|
|
|
const dsl = `
|
|
|
|
|
definition Employee {
|
|
|
|
|
role: string
|
|
|
|
|
isActive: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fact hasRole(user: Employee, role: string) CACHE eager
|
|
|
|
|
fact isMember(user: Employee, group: Device) transitive CACHE lazy
|
|
|
|
|
|
|
|
|
|
evidence canRead(user: Employee, doc: Account) {
|
|
|
|
|
hasRole(user, 'admin')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
evidence canAccess(user: Employee, doc: Account) {
|
|
|
|
|
hasRole(user, 'reader')
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const result = compiler.compile(dsl, 'test-rules');
|
|
|
|
|
assert.ok(result.success, 'Rule generation should succeed');
|
|
|
|
|
|
|
|
|
|
const canReadConfig = result.generatedRules.get('canRead');
|
|
|
|
|
assert.ok(canReadConfig.type === 'direct', 'canRead should be direct rule');
|
|
|
|
|
|
|
|
|
|
const canAccessConfig = result.generatedRules.get('canAccess');
|
|
|
|
|
assert.ok(canAccessConfig.type === 'direct', 'canAccess should be direct rule');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('Multiple programs', () => {
|
|
|
|
|
const programs = {
|
|
|
|
|
'auth': `
|
|
|
|
|
definition Employee {
|
|
|
|
|
role: string
|
|
|
|
|
isActive: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fact hasRole(user: Employee, role: string)
|
|
|
|
|
evidence canRead(user: Employee, doc: Account) {
|
|
|
|
|
hasRole(user, 'admin')
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
'finance': `
|
|
|
|
|
definition Employee {
|
|
|
|
|
role: string
|
|
|
|
|
isActive: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fact hasBalance(user: Employee, amount: number)
|
|
|
|
|
evidence canWithdraw(user: Employee, amount: number) {
|
|
|
|
|
hasBalance(user, amount)
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
'invalid': `
|
|
|
|
|
// Invalid syntax
|
|
|
|
|
invalid syntax here
|
|
|
|
|
`
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const result = compiler.compileMultiple(programs);
|
|
|
|
|
assert.ok(!result.success, 'Should fail due to invalid program');
|
|
|
|
|
assert.ok(result.errors.length > 0, 'Should have errors');
|
|
|
|
|
assert.ok(result.results.auth.success, 'Auth program should succeed');
|
|
|
|
|
assert.ok(result.results.finance.success, 'Finance program should succeed');
|
|
|
|
|
assert.ok(!result.results.invalid.success, 'Invalid program should fail');
|
|
|
|
|
});
|
|
|
|
|
});
|