Files
evidence-dsl/tests/DSLCompiler.test.js
John Dvorak 2a7f4c315b
CI / test (push) Successful in 20s
CI / publish (push) Has been skipped
feat: sources wired as recency-gated injectables; transitive closure, NOT scoping, recompile-scope uninstall; targeted parse errors
BEHAVES AS transitive now emits bounded multi_hop configs (direct checks and
evidence references), fixing a silent no-op. NOT builds keep _subjectAsObject
scoping so unary predicates negate the right node, and value-typed evidence
objects gate by exact edge value. Recompiling a scope uninstalls its stale
relation configs (compileMultiple coexistence preserved). Sources become
injectable relations honored by requiredFacts with a within-X recency gate.
Duplicate definition fields and three common declaration mistakes (within on a
fact, two BEHAVES clauses, limit on a non-pattern body) now produce targeted
errors. Provider edges referencing unknown nodes are warned and dropped.
2026-08-03 20:27:13 -07:00

296 lines
9.0 KiB
JavaScript

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