evidence-dsl: extract Evidence DSL v2 compiler from @arbiter/core
The Evidence DSL (ADR-000) is a thin declarative layer that compiles to engine rule types. It has zero runtime coupling to the core engine (DSLCompiler takes an arbiter as a duck-typed argument; the only shared code was the ip-utils helpers, now local). Extracting it into its own package keeps the core artifact free of the DSL surface. - @arbiter/evidence-dsl depends on @arbiter/core (config formats are the compilation target) - deep-path exports for the compiler, parser, generator, validation, and built-in functions (the surface the core's DSL tests consume) - tests moved alongside; generate-parser script + peggy devDep local - CI: test on push, publish on v* tags
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
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('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');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,283 @@
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('Type Definitions', () => {
|
||||
const arbiter = createMockArbiter();
|
||||
const compiler = new DSLCompiler(arbiter);
|
||||
|
||||
test('Basic definitions', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `definition User { role: string }`,
|
||||
description: 'Simple definition with one field'
|
||||
},
|
||||
{
|
||||
input: `definition User {
|
||||
role: string
|
||||
isActive: boolean
|
||||
}`,
|
||||
description: 'Definition with multiple fields'
|
||||
},
|
||||
{
|
||||
input: `definition Group {
|
||||
name: string
|
||||
description: string
|
||||
created: timestamp
|
||||
}`,
|
||||
description: 'Definition with different field types'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-basic-def-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
assert.ok(result.program.definitions.length > 0, 'Should have definitions');
|
||||
});
|
||||
});
|
||||
|
||||
test('Field types', () => {
|
||||
const testCases = [
|
||||
{ type: 'string', description: 'String field type' },
|
||||
{ type: 'number', description: 'Number field type' },
|
||||
{ type: 'boolean', description: 'Boolean field type' },
|
||||
{ type: 'timestamp', description: 'Timestamp field type' },
|
||||
{ type: 'User', description: 'Custom type field' },
|
||||
{ type: 'Permission', description: 'Another custom type field' }
|
||||
];
|
||||
|
||||
testCases.forEach(({ type, description }) => {
|
||||
const dsl = `definition Test { field: ${type} }`;
|
||||
const result = compiler.compile(dsl, `test-field-type-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Array types', () => {
|
||||
const testCases = [
|
||||
{ type: 'string[]', description: 'String array' },
|
||||
{ type: 'number[]', description: 'Number array' },
|
||||
{ type: 'boolean[]', description: 'Boolean array' },
|
||||
{ type: 'Permission[]', description: 'Custom type array' },
|
||||
{ type: 'User[]', description: 'User array' }
|
||||
];
|
||||
|
||||
testCases.forEach(({ type, description }) => {
|
||||
const dsl = `definition Test { items: ${type} }`;
|
||||
const result = compiler.compile(dsl, `test-array-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Behaviors', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `definition User {
|
||||
balance: number BEHAVES { decaying down hourly }
|
||||
}`,
|
||||
description: 'Decay behavior - down hourly'
|
||||
},
|
||||
{
|
||||
input: `definition User {
|
||||
reputation: number BEHAVES { decaying up daily }
|
||||
}`,
|
||||
description: 'Decay behavior - up daily'
|
||||
},
|
||||
{
|
||||
input: `definition User {
|
||||
score: number BEHAVES { decaying neutral weekly }
|
||||
}`,
|
||||
description: 'Decay behavior - neutral weekly'
|
||||
},
|
||||
{
|
||||
input: `definition User {
|
||||
stability: number BEHAVES { decaying stable monthly }
|
||||
}`,
|
||||
description: 'Decay behavior - stable monthly'
|
||||
},
|
||||
{
|
||||
input: `definition User {
|
||||
confidence: number BEHAVES { blurring fixed }
|
||||
}`,
|
||||
description: 'Blur behavior - fixed'
|
||||
},
|
||||
{
|
||||
input: `definition User {
|
||||
accuracy: number BEHAVES { blurring adaptive }
|
||||
}`,
|
||||
description: 'Blur behavior - adaptive'
|
||||
},
|
||||
{
|
||||
input: `definition User {
|
||||
precision: number BEHAVES { blurring confidence confidence_90 }
|
||||
}`,
|
||||
description: 'Blur behavior - confidence with level'
|
||||
},
|
||||
{
|
||||
input: `definition User {
|
||||
session: string BEHAVES { ttl 1h }
|
||||
}`,
|
||||
description: 'TTL behavior - hours'
|
||||
},
|
||||
{
|
||||
input: `definition User {
|
||||
token: string BEHAVES { ttl 24h }
|
||||
}`,
|
||||
description: 'TTL behavior - 24 hours'
|
||||
},
|
||||
{
|
||||
input: `definition User {
|
||||
cache: string BEHAVES { ttl 7d }
|
||||
}`,
|
||||
description: 'TTL behavior - days'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-behavior-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Caching', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `definition User {
|
||||
role: string CACHE eager
|
||||
}`,
|
||||
description: 'Eager caching'
|
||||
},
|
||||
{
|
||||
input: `definition User {
|
||||
score: number CACHE lazy
|
||||
}`,
|
||||
description: 'Lazy caching'
|
||||
},
|
||||
{
|
||||
input: `definition User {
|
||||
balance: number BEHAVES { decaying down hourly } CACHE eager
|
||||
}`,
|
||||
description: 'Behavior with eager caching'
|
||||
},
|
||||
{
|
||||
input: `definition User {
|
||||
reputation: number BEHAVES { blurring adaptive } CACHE lazy
|
||||
}`,
|
||||
description: 'Behavior with lazy caching'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-cache-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Complex definitions', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `definition User {
|
||||
role: string
|
||||
isActive: boolean
|
||||
lastActive: timestamp BEHAVES {
|
||||
decaying down hourly
|
||||
} CACHE lazy
|
||||
isSuspended: boolean
|
||||
balance: number BEHAVES {
|
||||
decaying down hourly
|
||||
} CACHE eager
|
||||
score: number BEHAVES {
|
||||
blurring adaptive confidence_95
|
||||
} CACHE lazy
|
||||
session: string BEHAVES {
|
||||
ttl 24h
|
||||
} CACHE eager
|
||||
}`,
|
||||
description: 'Complex definition with multiple behaviors and caching'
|
||||
},
|
||||
{
|
||||
input: `definition Group {
|
||||
name: string
|
||||
permissions: Permission[]
|
||||
members: User[]
|
||||
created: timestamp BEHAVES {
|
||||
decaying stable monthly
|
||||
} CACHE lazy
|
||||
isPublic: boolean CACHE eager
|
||||
}`,
|
||||
description: 'Definition with arrays and mixed behaviors'
|
||||
},
|
||||
{
|
||||
input: `definition Document {
|
||||
level: string
|
||||
owner: User
|
||||
tags: string[]
|
||||
content: string BEHAVES {
|
||||
blurring fixed
|
||||
} CACHE lazy
|
||||
accessCount: number BEHAVES {
|
||||
decaying up daily
|
||||
} CACHE eager
|
||||
expiresAt: timestamp BEHAVES {
|
||||
ttl 30d
|
||||
} CACHE eager
|
||||
}`,
|
||||
description: 'Definition with all behavior types'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-complex-def-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
assert.ok(result.program.definitions.length > 0, 'Should have definitions');
|
||||
});
|
||||
});
|
||||
|
||||
test('Definition error handling', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `definition User { role: string`,
|
||||
description: 'Missing closing brace should fail'
|
||||
},
|
||||
{
|
||||
input: `definition User { role: }`,
|
||||
description: 'Missing field type should fail'
|
||||
},
|
||||
{
|
||||
input: `definition User { : string }`,
|
||||
description: 'Missing field name should fail'
|
||||
},
|
||||
{
|
||||
input: `definition User { role: string BEHAVES { }`,
|
||||
description: 'Incomplete behavior should fail'
|
||||
},
|
||||
{
|
||||
input: `definition User { role: string CACHE }`,
|
||||
description: 'Incomplete cache directive should fail'
|
||||
},
|
||||
{
|
||||
input: `definition User { role: string BEHAVES { invalid } }`,
|
||||
description: 'Invalid behavior should fail'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
try {
|
||||
const result = compiler.compile(input, `test-def-error-${Date.now()}`);
|
||||
assert.ok(!result.success, `${description} should fail to parse`);
|
||||
} catch {
|
||||
// Expected to fail
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,374 @@
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('Evidence Rules', () => {
|
||||
const arbiter = createMockArbiter();
|
||||
const compiler = new DSLCompiler(arbiter);
|
||||
|
||||
test('Basic evidence', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
hasRole(user, 'admin')
|
||||
}`,
|
||||
description: 'Simple evidence with function call'
|
||||
},
|
||||
{
|
||||
input: `evidence canAccess(user: User, resource: Resource) {
|
||||
user.isActive
|
||||
}`,
|
||||
description: 'Evidence with attribute access'
|
||||
},
|
||||
{
|
||||
input: `evidence canModify(user: User, doc: Document) {
|
||||
user.isActive
|
||||
hasRole(user, 'admin')
|
||||
}`,
|
||||
description: 'Evidence with multiple conditions'
|
||||
},
|
||||
{
|
||||
input: `evidence canDelete(user: User, doc: Document) {
|
||||
owns(user, doc)
|
||||
user.isActive
|
||||
}`,
|
||||
description: 'Evidence with ownership and status'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-basic-evidence-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
assert.ok(result.program.evidence.length > 0, 'Should have evidence');
|
||||
});
|
||||
});
|
||||
|
||||
test('Defeasible logic', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `evidence canAccess(user: User, resource: Resource) {
|
||||
ALWAYS user.isActive
|
||||
}`,
|
||||
description: 'ALWAYS rule - strict requirement'
|
||||
},
|
||||
{
|
||||
input: `evidence canAccess(user: User, resource: Resource) {
|
||||
WHEN hasRole(user, 'admin')
|
||||
}`,
|
||||
description: 'WHEN rule - defeasible condition'
|
||||
},
|
||||
{
|
||||
input: `evidence canAccess(user: User, resource: Resource) {
|
||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
||||
}`,
|
||||
description: 'WHEN/UNLESS rule - defeasible with defeater'
|
||||
},
|
||||
{
|
||||
input: `evidence canAccess(user: User, resource: Resource) {
|
||||
REQUIRES hasClearance(user, resource.level)
|
||||
}`,
|
||||
description: 'REQUIRES rule - inverse defeater'
|
||||
},
|
||||
{
|
||||
input: `evidence canAccessCritical(user: User, resource: Resource) {
|
||||
ALWAYS user.isActive
|
||||
|
||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
||||
|
||||
REQUIRES hasClearance(user, resource.level)
|
||||
}`,
|
||||
description: 'Complex defeasible logic with all rule types'
|
||||
},
|
||||
{
|
||||
input: `evidence canAccessSensitive(user: User, doc: Document) {
|
||||
ALWAYS user.isActive
|
||||
|
||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
||||
|
||||
REQUIRES hasClearance(user, doc.level)
|
||||
|
||||
fusion majority {
|
||||
user.isTrusted
|
||||
user.hasRecentActivity
|
||||
}
|
||||
}`,
|
||||
description: 'Defeasible logic with fusion'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-defeasible-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Pattern matching', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
isMember(user, *group) {
|
||||
canRead(group, doc)
|
||||
}
|
||||
}`,
|
||||
description: 'Basic pattern matching with wildcard'
|
||||
},
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
isMember(user, *group) {
|
||||
canRead(group, doc)
|
||||
} limit 5
|
||||
}`,
|
||||
description: 'Pattern matching with limit'
|
||||
},
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
similar(doc, *similar) |similarity| {
|
||||
canRead(user, similar)
|
||||
} with similarity > 0.7
|
||||
}`,
|
||||
description: 'Pattern matching with binding and condition'
|
||||
},
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
similar(doc, *similar) |similarity| {
|
||||
canRead(user, similar)
|
||||
} with similarity > 0.7 limit 5
|
||||
}`,
|
||||
description: 'Pattern matching with binding, condition, and limit'
|
||||
},
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
isMember(user, *group) {
|
||||
isMember(group, *parentGroup) {
|
||||
canRead(parentGroup, doc)
|
||||
} limit 2
|
||||
} limit 3
|
||||
}`,
|
||||
description: 'Nested pattern matching'
|
||||
},
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
isFriend(user, *friend) {
|
||||
isMember(friend, *group) {
|
||||
canRead(group, doc)
|
||||
} limit 1
|
||||
} limit 5
|
||||
}`,
|
||||
description: 'Multi-hop pattern matching'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-pattern-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Fusion', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `evidence canAccess(user: User, resource: Resource) {
|
||||
fusion min {
|
||||
hasClearance(user, resource.level)
|
||||
user.isActive
|
||||
}
|
||||
}`,
|
||||
description: 'Min fusion - all conditions must be true'
|
||||
},
|
||||
{
|
||||
input: `evidence canAccess(user: User, resource: Resource) {
|
||||
fusion max {
|
||||
hasRole(user, 'admin')
|
||||
hasRole(user, 'superuser')
|
||||
}
|
||||
}`,
|
||||
description: 'Max fusion - any condition can be true'
|
||||
},
|
||||
{
|
||||
input: `evidence canAccess(user: User, resource: Resource) {
|
||||
fusion majority {
|
||||
hasClearance(user, 'secret')
|
||||
user.isTrusted
|
||||
user.hasRecentActivity
|
||||
}
|
||||
}`,
|
||||
description: 'Majority fusion - most conditions must be true'
|
||||
},
|
||||
{
|
||||
input: `evidence canAccessCritical(user: User, resource: Resource) {
|
||||
fusion min {
|
||||
hasClearance(user, resource.level)
|
||||
user.isActive
|
||||
NOT user.isBlacklisted
|
||||
}
|
||||
|
||||
fusion max {
|
||||
hasRole(user, 'admin')
|
||||
fusion majority {
|
||||
hasClearance(user, 'secret')
|
||||
user.isTrusted
|
||||
user.lastActive within 1hr
|
||||
}
|
||||
}
|
||||
}`,
|
||||
description: 'Nested fusion with different strategies'
|
||||
},
|
||||
{
|
||||
input: `evidence canAccess(user: User, resource: Resource) {
|
||||
fusion average {
|
||||
user.reputation
|
||||
user.activityScore
|
||||
user.verificationLevel
|
||||
}
|
||||
}`,
|
||||
description: 'Average fusion for numeric values'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-fusion-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Complex evidence', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
owns(user, doc)
|
||||
|
||||
isMember(user, *group) {
|
||||
canRead(group, doc)
|
||||
} limit 5
|
||||
|
||||
parentOf(user, *parent) {
|
||||
canRead(parent, doc)
|
||||
} limit 3
|
||||
|
||||
similar(doc, *similar) |similarity| {
|
||||
canRead(user, similar)
|
||||
} with similarity > 0.7 limit 5
|
||||
|
||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
||||
}`,
|
||||
description: 'Complex evidence with all features'
|
||||
},
|
||||
{
|
||||
input: `evidence canAccessCritical(user: User, resource: Resource) {
|
||||
ALWAYS user.isActive
|
||||
|
||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
||||
|
||||
REQUIRES hasClearance(user, resource.level)
|
||||
|
||||
fusion min {
|
||||
hasClearance(user, resource.level)
|
||||
user.isActive
|
||||
NOT user.isBlacklisted
|
||||
}
|
||||
|
||||
fusion max {
|
||||
hasRole(user, 'admin')
|
||||
fusion majority {
|
||||
hasClearance(user, 'secret')
|
||||
user.isTrusted
|
||||
user.lastActive within 1hr
|
||||
}
|
||||
}
|
||||
}`,
|
||||
description: 'Critical access with all rule types and fusion'
|
||||
},
|
||||
{
|
||||
input: `evidence canModify(user: User, doc: Document) {
|
||||
owns(user, doc)
|
||||
|
||||
isMember(user, *group) {
|
||||
canModify(group, doc)
|
||||
} limit 3
|
||||
|
||||
similar(doc, *similar) |similarity| {
|
||||
canModify(user, similar)
|
||||
similar.isEditable
|
||||
} with similarity > 0.8 limit 2
|
||||
|
||||
fusion majority {
|
||||
user.isTrusted
|
||||
user.hasRecentActivity
|
||||
doc.isPublic
|
||||
}
|
||||
}`,
|
||||
description: 'Modification access with similarity and fusion'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-complex-evidence-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Evidence error handling', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
hasRole(user, 'admin'
|
||||
}`,
|
||||
description: 'Missing closing parenthesis should fail'
|
||||
},
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
WHEN hasRole(user, 'admin') UNLESS
|
||||
}`,
|
||||
description: 'Incomplete UNLESS condition should fail'
|
||||
},
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
fusion min {
|
||||
hasRole(user, 'admin')
|
||||
}`,
|
||||
description: 'Incomplete fusion should fail'
|
||||
},
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
isMember(user, *group) {
|
||||
canRead(group, doc)
|
||||
} with
|
||||
}`,
|
||||
description: 'Incomplete with clause should fail'
|
||||
},
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
isMember(user, *group) {
|
||||
canRead(group, doc)
|
||||
} limit
|
||||
}`,
|
||||
description: 'Incomplete limit should fail'
|
||||
},
|
||||
{
|
||||
input: `evidence canRead(user: User, doc: Document) {
|
||||
invalid syntax here
|
||||
}`,
|
||||
description: 'Invalid syntax should fail'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
try {
|
||||
const result = compiler.compile(input, `test-evidence-error-${Date.now()}`);
|
||||
assert.ok(!result.success, `${description} should fail to parse`);
|
||||
} catch {
|
||||
// Expected to fail
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,233 @@
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('Expression Parsing', () => {
|
||||
const arbiter = createMockArbiter();
|
||||
const compiler = new DSLCompiler(arbiter);
|
||||
|
||||
test('Arithmetic operator precedence', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: '1 + 2 * 3',
|
||||
expected: 'Should evaluate as 1 + (2 * 3) = 7',
|
||||
description: 'Multiplication before addition'
|
||||
},
|
||||
{
|
||||
input: '10 - 3 * 2',
|
||||
expected: 'Should evaluate as 10 - (3 * 2) = 4',
|
||||
description: 'Multiplication before subtraction'
|
||||
},
|
||||
{
|
||||
input: '8 / 2 * 4',
|
||||
expected: 'Should evaluate as (8 / 2) * 4 = 16',
|
||||
description: 'Left-associative division and multiplication'
|
||||
},
|
||||
{
|
||||
input: '2 + 3 * 4 - 5',
|
||||
expected: 'Should evaluate as 2 + (3 * 4) - 5 = 9',
|
||||
description: 'Mixed arithmetic with correct precedence'
|
||||
},
|
||||
{
|
||||
input: '(1 + 2) * 3',
|
||||
expected: 'Should evaluate as (1 + 2) * 3 = 9',
|
||||
description: 'Parentheses override precedence'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, expected, description }) => {
|
||||
const dsl = `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 = `evidence test() { ${input} }`;
|
||||
const result = compiler.compile(dsl, `test-logical-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Comparison operators', () => {
|
||||
const testCases = [
|
||||
{ input: '1 == 1', description: 'Equality comparison' },
|
||||
{ input: '1 != 2', description: 'Inequality comparison' },
|
||||
{ input: '5 > 3', description: 'Greater than' },
|
||||
{ input: '3 < 5', description: 'Less than' },
|
||||
{ input: '4 >= 4', description: 'Greater than or equal' },
|
||||
{ input: '4 <= 4', description: 'Less than or equal' },
|
||||
{ input: '1 == 1 && 2 > 1', description: 'Comparison with logical operators' },
|
||||
{ input: '1 + 2 == 3', description: 'Arithmetic in comparison' }
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const dsl = `evidence test() { ${input} }`;
|
||||
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 = `evidence test() { ${input} }`;
|
||||
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' },
|
||||
{ input: '!false', description: 'Alternative NOT operator' },
|
||||
{ input: 'NOT (true && false)', description: 'NOT with parenthesized expression' },
|
||||
{ input: 'NOT user.isSuspended', description: 'NOT with attribute access' }
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const dsl = `evidence test() { ${input} }`;
|
||||
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[0]', description: 'Array access' },
|
||||
{ input: 'user.role.permissions[0]', description: 'Nested attribute with array access' },
|
||||
{ input: 'user.isActive && user.role == "admin"', description: 'Attribute access in logical expression' }
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const dsl = `evidence test() { ${input} }`;
|
||||
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' },
|
||||
{ input: 'isMember(user, group)', description: 'Function call with variables' },
|
||||
{ input: 'hasPermission(user, resource, "read")', description: 'Function call with multiple arguments' },
|
||||
{ input: 'hasRole(user, "admin") && isActive(user)', description: 'Multiple function calls' },
|
||||
{ input: 'hasRole(user, user.role)', description: 'Function call with attribute access' }
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const dsl = `evidence test() { ${input} }`;
|
||||
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 dsl = `evidence test() { ${input} }`;
|
||||
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
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,232 @@
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('Fact Declarations', () => {
|
||||
const arbiter = createMockArbiter();
|
||||
const compiler = new DSLCompiler(arbiter);
|
||||
|
||||
test('Basic facts', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `fact hasRole(user: User, role: string)`,
|
||||
description: 'Simple fact with two parameters'
|
||||
},
|
||||
{
|
||||
input: `fact isMember(user: User, group: Group)`,
|
||||
description: 'Fact with custom types'
|
||||
},
|
||||
{
|
||||
input: `fact owns(user: User, doc: Document)`,
|
||||
description: 'Fact with multiple custom types'
|
||||
},
|
||||
{
|
||||
input: `fact isActive(user: User)`,
|
||||
description: 'Fact with single parameter'
|
||||
},
|
||||
{
|
||||
input: `fact hasPermission(user: User, resource: Resource, action: string)`,
|
||||
description: 'Fact with three parameters'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-basic-fact-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
assert.ok(result.program.facts.length > 0, 'Should have facts');
|
||||
});
|
||||
});
|
||||
|
||||
test('Fact properties', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `fact isMember(user: User, group: Group) transitive`,
|
||||
description: 'Transitive fact'
|
||||
},
|
||||
{
|
||||
input: `fact isFriend(user: User, friend: User) symmetrical`,
|
||||
description: 'Symmetrical fact'
|
||||
},
|
||||
{
|
||||
input: `fact isMember(user: User, group: Group) transitive symmetrical`,
|
||||
description: 'Fact with multiple properties'
|
||||
},
|
||||
{
|
||||
input: `fact isColleague(user: User, colleague: User) symmetrical`,
|
||||
description: 'Symmetrical relationship fact'
|
||||
},
|
||||
{
|
||||
input: `fact isParentOf(parent: User, child: User) transitive`,
|
||||
description: 'Transitive hierarchical fact'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-fact-property-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Fact caching', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `fact hasRole(user: User, role: string) CACHE eager`,
|
||||
description: 'Eager cached fact'
|
||||
},
|
||||
{
|
||||
input: `fact isMember(user: User, group: Group) CACHE lazy`,
|
||||
description: 'Lazy cached fact'
|
||||
},
|
||||
{
|
||||
input: `fact isMember(user: User, group: Group) transitive CACHE eager`,
|
||||
description: 'Transitive fact with eager caching'
|
||||
},
|
||||
{
|
||||
input: `fact isFriend(user: User, friend: User) symmetrical CACHE lazy`,
|
||||
description: 'Symmetrical fact with lazy caching'
|
||||
},
|
||||
{
|
||||
input: `fact hasPermission(user: User, resource: Resource, action: string) CACHE eager`,
|
||||
description: 'Multi-parameter fact with eager caching'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-fact-cache-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Fact limits', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `fact isMember(user: User, group: Group) limit 10`,
|
||||
description: 'Fact with simple limit'
|
||||
},
|
||||
{
|
||||
input: `fact isFriend(user: User, friend: User) limit 100`,
|
||||
description: 'Fact with higher limit'
|
||||
},
|
||||
{
|
||||
input: `fact isMember(user: User, group: Group) transitive limit 5`,
|
||||
description: 'Transitive fact with limit'
|
||||
},
|
||||
{
|
||||
input: `fact isFriend(user: User, friend: User) symmetrical limit 50`,
|
||||
description: 'Symmetrical fact with limit'
|
||||
},
|
||||
{
|
||||
input: `fact isMember(user: User, group: Group) transitive CACHE lazy limit 3`,
|
||||
description: 'Fact with properties, caching, and limit'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-fact-limit-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Parameter types', () => {
|
||||
const testCases = [
|
||||
{ type: 'string', description: 'String parameter' },
|
||||
{ type: 'number', description: 'Number parameter' },
|
||||
{ type: 'boolean', description: 'Boolean parameter' },
|
||||
{ type: 'timestamp', description: 'Timestamp parameter' },
|
||||
{ type: 'User', description: 'Custom type parameter' },
|
||||
{ type: 'Group', description: 'Another custom type parameter' },
|
||||
{ type: 'Permission[]', description: 'Array type parameter' }
|
||||
];
|
||||
|
||||
testCases.forEach(({ type, description }) => {
|
||||
const dsl = `fact test(param: ${type})`;
|
||||
const result = compiler.compile(dsl, `test-param-type-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Complex facts', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `fact hasRole(user: User, role: string) CACHE eager
|
||||
fact isMember(user: User, group: Group) transitive CACHE lazy limit 10
|
||||
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 100
|
||||
fact owns(user: User, doc: Document) CACHE eager
|
||||
fact isSuspended(user: User) CACHE lazy`,
|
||||
description: 'Multiple facts with different configurations'
|
||||
},
|
||||
{
|
||||
input: `fact hasPermission(user: User, resource: Resource, action: string) CACHE eager
|
||||
fact isAdmin(user: User) CACHE eager
|
||||
fact isOwner(user: User, resource: Resource) CACHE eager
|
||||
fact hasAccess(user: User, resource: Resource, level: string) CACHE lazy`,
|
||||
description: 'Permission-related facts'
|
||||
},
|
||||
{
|
||||
input: `fact isMember(user: User, group: Group) transitive CACHE lazy limit 5
|
||||
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 50
|
||||
fact isColleague(user: User, colleague: User) symmetrical CACHE lazy limit 20
|
||||
fact isParentOf(parent: User, child: User) transitive CACHE eager limit 3`,
|
||||
description: 'Relationship facts with various properties'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-complex-facts-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
assert.ok(result.program.facts.length > 0, 'Should have facts');
|
||||
});
|
||||
});
|
||||
|
||||
test('Fact error handling', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `fact hasRole(user: User, role: string`,
|
||||
description: 'Missing closing parenthesis should fail'
|
||||
},
|
||||
{
|
||||
input: `fact hasRole(user: User, )`,
|
||||
description: 'Missing parameter name should fail'
|
||||
},
|
||||
{
|
||||
input: `fact hasRole(user: User, role: )`,
|
||||
description: 'Missing parameter type should fail'
|
||||
},
|
||||
{
|
||||
input: `fact hasRole(, role: string)`,
|
||||
description: 'Missing parameter name should fail'
|
||||
},
|
||||
{
|
||||
input: `fact hasRole(user: User, role: string) CACHE`,
|
||||
description: 'Incomplete cache directive should fail'
|
||||
},
|
||||
{
|
||||
input: `fact hasRole(user: User, role: string) limit`,
|
||||
description: 'Incomplete limit should fail'
|
||||
},
|
||||
{
|
||||
input: `fact hasRole(user: User, role: string) invalid`,
|
||||
description: 'Invalid property should fail'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
try {
|
||||
const result = compiler.compile(input, `test-fact-error-${Date.now()}`);
|
||||
assert.ok(!result.success, `${description} should fail to parse`);
|
||||
} catch {
|
||||
// Expected to fail
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,534 @@
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('Integration Tests', () => {
|
||||
const arbiter = createMockArbiter();
|
||||
const compiler = new DSLCompiler(arbiter);
|
||||
|
||||
test('Complete authorization system', () => {
|
||||
const completeSystem = `
|
||||
// Type definitions with complex behaviors
|
||||
definition User {
|
||||
role: string
|
||||
isActive: boolean
|
||||
lastActive: timestamp BEHAVES {
|
||||
decaying down hourly
|
||||
} CACHE lazy
|
||||
isSuspended: boolean
|
||||
balance: number BEHAVES {
|
||||
decaying down hourly
|
||||
} CACHE eager
|
||||
score: number BEHAVES {
|
||||
blurring adaptive confidence_95
|
||||
} CACHE lazy
|
||||
session: string BEHAVES {
|
||||
ttl 24h
|
||||
} CACHE eager
|
||||
clearance: string BEHAVES {
|
||||
blurring fixed
|
||||
} CACHE eager
|
||||
reputation: number BEHAVES {
|
||||
decaying up daily
|
||||
} CACHE lazy
|
||||
}
|
||||
|
||||
definition Group {
|
||||
name: string
|
||||
permissions: Permission[]
|
||||
level: string
|
||||
isPublic: boolean CACHE eager
|
||||
created: timestamp BEHAVES {
|
||||
decaying stable monthly
|
||||
} CACHE lazy
|
||||
}
|
||||
|
||||
definition Document {
|
||||
level: string
|
||||
owner: User
|
||||
tags: string[]
|
||||
content: string BEHAVES {
|
||||
blurring fixed
|
||||
} CACHE lazy
|
||||
accessCount: number BEHAVES {
|
||||
decaying up daily
|
||||
} CACHE eager
|
||||
expiresAt: timestamp BEHAVES {
|
||||
ttl 30d
|
||||
} CACHE eager
|
||||
isPublic: boolean CACHE eager
|
||||
}
|
||||
|
||||
definition Resource {
|
||||
level: string
|
||||
owner: User
|
||||
permissions: Permission[]
|
||||
isPublic: boolean CACHE eager
|
||||
accessCount: number BEHAVES {
|
||||
decaying up daily
|
||||
} CACHE eager
|
||||
}
|
||||
|
||||
// Facts with various properties and caching
|
||||
fact hasRole(user: User, role: string) CACHE eager
|
||||
fact isMember(user: User, group: Group) transitive CACHE lazy limit 10
|
||||
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 100
|
||||
fact owns(user: User, doc: Document) CACHE eager
|
||||
fact isSuspended(user: User) CACHE lazy
|
||||
fact hasPermission(user: User, resource: Resource, action: string) CACHE eager
|
||||
fact isAdmin(user: User) CACHE eager
|
||||
fact isOwner(user: User, resource: Resource) CACHE eager
|
||||
fact hasAccess(user: User, resource: Resource, level: string) CACHE lazy
|
||||
fact isColleague(user: User, colleague: User) symmetrical CACHE lazy limit 50
|
||||
fact isParentOf(parent: User, child: User) transitive CACHE eager limit 3
|
||||
|
||||
// Evidence rules with complex logic
|
||||
evidence canRead(user: User, doc: Document) {
|
||||
owns(user, doc)
|
||||
|
||||
isMember(user, *group) {
|
||||
canRead(group, doc)
|
||||
} limit 5
|
||||
|
||||
parentOf(user, *parent) {
|
||||
canRead(parent, doc)
|
||||
} limit 3
|
||||
|
||||
similar(doc, *similar) |similarity| {
|
||||
canRead(user, similar)
|
||||
} with similarity > 0.7 limit 5
|
||||
|
||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
||||
}
|
||||
|
||||
evidence canWrite(user: User, doc: Document) {
|
||||
owns(user, doc)
|
||||
|
||||
isMember(user, *group) {
|
||||
canWrite(group, doc)
|
||||
} limit 3
|
||||
|
||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
||||
|
||||
REQUIRES user.isActive
|
||||
}
|
||||
|
||||
evidence canDelete(user: User, doc: Document) {
|
||||
owns(user, doc)
|
||||
|
||||
ALWAYS user.isActive
|
||||
|
||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
||||
|
||||
REQUIRES user.isActive
|
||||
}
|
||||
|
||||
evidence canAccessCritical(user: User, resource: Resource) {
|
||||
fusion min {
|
||||
hasClearance(user, resource.level)
|
||||
user.isActive
|
||||
NOT user.isBlacklisted
|
||||
}
|
||||
|
||||
fusion max {
|
||||
hasRole(user, 'admin')
|
||||
fusion majority {
|
||||
hasClearance(user, 'secret')
|
||||
user.isTrusted
|
||||
user.lastActive within 1hr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
evidence canAccessSensitive(user: User, doc: Document) {
|
||||
ALWAYS user.isActive
|
||||
|
||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
||||
|
||||
REQUIRES hasClearance(user, doc.level)
|
||||
|
||||
fusion majority {
|
||||
user.isTrusted
|
||||
user.hasRecentActivity
|
||||
}
|
||||
}
|
||||
|
||||
// Measures for computed values
|
||||
measure userRole(user: User) {
|
||||
user.role
|
||||
} PROVIDES string
|
||||
|
||||
measure userPermissions(user: User) {
|
||||
fusion max {
|
||||
user.role.permissions
|
||||
user.group.permissions
|
||||
}
|
||||
} PROVIDES Permission[]
|
||||
|
||||
measure effectiveClearance(user: User) {
|
||||
fusion majority {
|
||||
user.clearance
|
||||
user.role.clearance
|
||||
user.group.clearance
|
||||
}
|
||||
} PROVIDES string
|
||||
|
||||
measure userTrustScore(user: User) {
|
||||
fusion average {
|
||||
user.reputation
|
||||
user.activityScore
|
||||
user.verificationLevel
|
||||
}
|
||||
} PROVIDES number
|
||||
|
||||
measure userBalance(user: User) {
|
||||
user.balance
|
||||
} PROVIDES number
|
||||
|
||||
measure userScore(user: User) {
|
||||
user.score
|
||||
} PROVIDES number
|
||||
`;
|
||||
|
||||
const result = compiler.compile(completeSystem, 'test-complete-system');
|
||||
assert.ok(result.success, 'Complete authorization system should compile successfully');
|
||||
assert.ok(result.program.definitions.length >= 4, 'Should have multiple definitions');
|
||||
assert.ok(result.program.facts.length >= 10, 'Should have multiple facts');
|
||||
assert.ok(result.program.evidence.length >= 5, 'Should have multiple evidence rules');
|
||||
assert.ok(result.program.measures.length >= 6, 'Should have multiple measures');
|
||||
});
|
||||
|
||||
test('Multi-domain system', () => {
|
||||
const multiDomain = `
|
||||
// Authentication domain
|
||||
definition User {
|
||||
role: string
|
||||
isActive: boolean
|
||||
lastActive: timestamp BEHAVES { decaying down hourly } CACHE lazy
|
||||
session: string BEHAVES { ttl 24h } CACHE eager
|
||||
}
|
||||
|
||||
fact hasRole(user: User, role: string) CACHE eager
|
||||
fact isActive(user: User) CACHE eager
|
||||
|
||||
evidence canAuthenticate(user: User) {
|
||||
user.isActive
|
||||
user.session within 24h
|
||||
}
|
||||
|
||||
// Authorization domain
|
||||
definition Resource {
|
||||
level: string
|
||||
owner: User
|
||||
permissions: Permission[]
|
||||
}
|
||||
|
||||
fact owns(user: User, resource: Resource) CACHE eager
|
||||
fact hasPermission(user: User, resource: Resource, action: string) CACHE eager
|
||||
|
||||
evidence canAccess(user: User, resource: Resource) {
|
||||
owns(user, resource)
|
||||
hasPermission(user, resource, 'read')
|
||||
}
|
||||
|
||||
// Finance domain
|
||||
definition Account {
|
||||
balance: number BEHAVES { decaying down hourly } CACHE eager
|
||||
owner: User
|
||||
isActive: boolean CACHE eager
|
||||
}
|
||||
|
||||
fact hasAccount(user: User, account: Account) CACHE eager
|
||||
fact hasBalance(user: User, amount: number) CACHE eager
|
||||
|
||||
evidence canWithdraw(user: User, amount: number) {
|
||||
hasBalance(user, amount)
|
||||
user.isActive
|
||||
}
|
||||
|
||||
// Social domain
|
||||
definition Group {
|
||||
name: string
|
||||
members: User[]
|
||||
isPublic: boolean CACHE eager
|
||||
}
|
||||
|
||||
fact isMember(user: User, group: Group) transitive CACHE lazy limit 10
|
||||
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 100
|
||||
|
||||
evidence canAccessGroup(user: User, group: Group) {
|
||||
isMember(user, group)
|
||||
group.isPublic
|
||||
}
|
||||
`;
|
||||
|
||||
const result = compiler.compile(multiDomain, 'test-multi-domain');
|
||||
assert.ok(result.success, 'Multi-domain system should compile successfully');
|
||||
assert.ok(result.program.definitions.length >= 4, 'Should have multiple domain definitions');
|
||||
assert.ok(result.program.facts.length >= 8, 'Should have multiple domain facts');
|
||||
assert.ok(result.program.evidence.length >= 4, 'Should have multiple domain evidence rules');
|
||||
});
|
||||
|
||||
test('Hierarchical access', () => {
|
||||
const hierarchicalSystem = `
|
||||
definition User {
|
||||
role: string
|
||||
level: string
|
||||
isActive: boolean
|
||||
clearance: string
|
||||
}
|
||||
|
||||
definition Organization {
|
||||
name: string
|
||||
level: string
|
||||
parent: Organization
|
||||
}
|
||||
|
||||
fact isMember(user: User, org: Organization) transitive CACHE lazy limit 5
|
||||
fact isParentOf(parent: Organization, child: Organization) transitive CACHE eager limit 3
|
||||
fact hasRole(user: User, role: string) CACHE eager
|
||||
fact hasClearance(user: User, level: string) CACHE eager
|
||||
|
||||
evidence canAccessOrg(user: User, org: Organization) {
|
||||
isMember(user, org)
|
||||
|
||||
isParentOf(org, *parentOrg) {
|
||||
canAccessOrg(user, parentOrg)
|
||||
} limit 3
|
||||
|
||||
WHEN hasRole(user, 'admin') UNLESS user.isSuspended
|
||||
}
|
||||
|
||||
evidence canAccessResource(user: User, resource: Resource) {
|
||||
isMember(user, *org) {
|
||||
canAccessResource(org, resource)
|
||||
} limit 5
|
||||
|
||||
parentOf(user, *parent) {
|
||||
canAccessResource(parent, resource)
|
||||
} limit 2
|
||||
}
|
||||
`;
|
||||
|
||||
const result = compiler.compile(hierarchicalSystem, 'test-hierarchical');
|
||||
assert.ok(result.success, 'Hierarchical access system should compile successfully');
|
||||
});
|
||||
|
||||
test('Similarity-based access', () => {
|
||||
const similaritySystem = `
|
||||
definition User {
|
||||
profile: string
|
||||
interests: string[]
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
definition Document {
|
||||
content: string
|
||||
tags: string[]
|
||||
isPublic: boolean
|
||||
owner: User
|
||||
}
|
||||
|
||||
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 100
|
||||
fact hasInterest(user: User, interest: string) CACHE lazy
|
||||
fact hasTag(doc: Document, tag: string) CACHE lazy
|
||||
|
||||
evidence canRead(user: User, doc: Document) {
|
||||
owns(user, doc)
|
||||
|
||||
similar(doc, *similar) |similarity| {
|
||||
canRead(user, similar)
|
||||
similar.isPublic
|
||||
} with similarity > 0.7 limit 10
|
||||
|
||||
isFriend(user, *friend) {
|
||||
canRead(friend, doc)
|
||||
} limit 5
|
||||
|
||||
fusion majority {
|
||||
user.interests
|
||||
doc.tags
|
||||
}
|
||||
}
|
||||
|
||||
evidence canRecommend(user: User, doc: Document) {
|
||||
similar(user, *similarUser) |similarity| {
|
||||
canRead(similarUser, doc)
|
||||
} with similarity > 0.8 limit 20
|
||||
|
||||
fusion average {
|
||||
user.profile
|
||||
doc.content
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const result = compiler.compile(similaritySystem, 'test-similarity');
|
||||
assert.ok(result.success, 'Similarity-based access system should compile successfully');
|
||||
});
|
||||
|
||||
test('Temporal access', () => {
|
||||
const temporalSystem = `
|
||||
definition User {
|
||||
lastActive: timestamp BEHAVES { decaying down hourly } CACHE lazy
|
||||
session: string BEHAVES { ttl 24h } CACHE eager
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
definition Event {
|
||||
startTime: timestamp
|
||||
endTime: timestamp
|
||||
isPublic: boolean
|
||||
}
|
||||
|
||||
fact hasAccess(user: User, event: Event) CACHE lazy
|
||||
fact isParticipant(user: User, event: Event) CACHE eager
|
||||
|
||||
evidence canAccessEvent(user: User, event: Event) {
|
||||
user.lastActive within 1h
|
||||
|
||||
isParticipant(user, event)
|
||||
|
||||
WHEN event.isPublic UNLESS user.isSuspended
|
||||
|
||||
fusion min {
|
||||
user.session within 24h
|
||||
user.isActive
|
||||
}
|
||||
}
|
||||
|
||||
evidence canAccessHistorical(user: User, event: Event) {
|
||||
user.lastActive within 24h
|
||||
|
||||
fusion majority {
|
||||
user.isActive
|
||||
user.session within 24h
|
||||
event.isPublic
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const result = compiler.compile(temporalSystem, 'test-temporal');
|
||||
assert.ok(result.success, 'Temporal access system should compile successfully');
|
||||
});
|
||||
|
||||
test('Complex behaviors', () => {
|
||||
const behaviorSystem = `
|
||||
definition User {
|
||||
balance: number BEHAVES { decaying down hourly } CACHE eager
|
||||
score: number BEHAVES { blurring adaptive confidence_95 } CACHE lazy
|
||||
session: string BEHAVES { ttl 24h } CACHE eager
|
||||
reputation: number BEHAVES { decaying up daily } CACHE lazy
|
||||
clearance: string BEHAVES { blurring fixed } CACHE eager
|
||||
lastActive: timestamp BEHAVES { decaying down hourly } CACHE lazy
|
||||
}
|
||||
|
||||
definition Document {
|
||||
content: string BEHAVES { blurring fixed } CACHE lazy
|
||||
accessCount: number BEHAVES { decaying up daily } CACHE eager
|
||||
expiresAt: timestamp BEHAVES { ttl 30d } CACHE eager
|
||||
isPublic: boolean CACHE eager
|
||||
}
|
||||
|
||||
fact hasBalance(user: User, amount: number) CACHE eager
|
||||
fact hasScore(user: User, score: number) CACHE lazy
|
||||
fact hasReputation(user: User, reputation: number) CACHE lazy
|
||||
|
||||
evidence canAccessDocument(user: User, doc: Document) {
|
||||
user.balance > 0
|
||||
|
||||
user.score > 0.5
|
||||
|
||||
user.reputation > 0.3
|
||||
|
||||
doc.accessCount < 1000
|
||||
|
||||
fusion majority {
|
||||
user.isActive
|
||||
user.lastActive within 1h
|
||||
doc.isPublic
|
||||
}
|
||||
}
|
||||
|
||||
measure userEffectiveScore(user: User) {
|
||||
fusion average {
|
||||
user.score
|
||||
user.reputation
|
||||
user.balance
|
||||
}
|
||||
} PROVIDES number
|
||||
|
||||
measure documentPopularity(doc: Document) {
|
||||
doc.accessCount
|
||||
} PROVIDES number
|
||||
`;
|
||||
|
||||
const result = compiler.compile(behaviorSystem, 'test-behaviors');
|
||||
assert.ok(result.success, 'Complex behaviors system should compile successfully');
|
||||
});
|
||||
|
||||
test('Performance scenarios', () => {
|
||||
const performanceSystem = `
|
||||
definition User {
|
||||
role: string
|
||||
isActive: boolean
|
||||
permissions: Permission[] CACHE eager
|
||||
}
|
||||
|
||||
definition Resource {
|
||||
level: string
|
||||
owner: User
|
||||
permissions: Permission[] CACHE eager
|
||||
}
|
||||
|
||||
// High-frequency facts with limits
|
||||
fact isMember(user: User, group: Group) transitive CACHE lazy limit 5
|
||||
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 50
|
||||
fact hasPermission(user: User, resource: Resource, action: string) CACHE eager
|
||||
fact owns(user: User, resource: Resource) CACHE eager
|
||||
|
||||
// Optimized evidence rules
|
||||
evidence canAccess(user: User, resource: Resource) {
|
||||
owns(user, resource)
|
||||
|
||||
isMember(user, *group) {
|
||||
canAccess(group, resource)
|
||||
} limit 3
|
||||
|
||||
WHEN hasPermission(user, resource, 'read')
|
||||
}
|
||||
|
||||
evidence canModify(user: User, resource: Resource) {
|
||||
owns(user, resource)
|
||||
|
||||
isMember(user, *group) {
|
||||
canModify(group, resource)
|
||||
} limit 2
|
||||
|
||||
WHEN hasPermission(user, resource, 'write')
|
||||
}
|
||||
|
||||
// Efficient measures
|
||||
measure userEffectivePermissions(user: User) {
|
||||
user.permissions
|
||||
} PROVIDES Permission[]
|
||||
|
||||
measure resourceAccessLevel(resource: Resource) {
|
||||
resource.level
|
||||
} PROVIDES string
|
||||
`;
|
||||
|
||||
const result = compiler.compile(performanceSystem, 'test-performance');
|
||||
assert.ok(result.success, 'Performance scenarios should compile successfully');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,306 @@
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('Measure Definitions', () => {
|
||||
const arbiter = createMockArbiter();
|
||||
const compiler = new DSLCompiler(arbiter);
|
||||
|
||||
test('Basic measures', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `measure userRole(user: User) {
|
||||
user.role
|
||||
} PROVIDES string`,
|
||||
description: 'Simple measure with attribute access'
|
||||
},
|
||||
{
|
||||
input: `measure userBalance(user: User) {
|
||||
user.balance
|
||||
} PROVIDES number`,
|
||||
description: 'Measure accessing numeric attribute'
|
||||
},
|
||||
{
|
||||
input: `measure isUserActive(user: User) {
|
||||
user.isActive
|
||||
} PROVIDES boolean`,
|
||||
description: 'Measure accessing boolean attribute'
|
||||
},
|
||||
{
|
||||
input: `measure userPermissions(user: User) {
|
||||
user.permissions
|
||||
} PROVIDES Permission[]`,
|
||||
description: 'Measure accessing array attribute'
|
||||
},
|
||||
{
|
||||
input: `measure userScore(user: User) {
|
||||
user.score
|
||||
} PROVIDES number`,
|
||||
description: 'Measure with behavior-inherited attribute'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-basic-measure-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
assert.ok(result.program.measures.length > 0, 'Should have measures');
|
||||
});
|
||||
});
|
||||
|
||||
test('Measure return types', () => {
|
||||
const testCases = [
|
||||
{ type: 'string', description: 'String return type' },
|
||||
{ type: 'number', description: 'Number return type' },
|
||||
{ type: 'boolean', description: 'Boolean return type' },
|
||||
{ type: 'timestamp', description: 'Timestamp return type' },
|
||||
{ type: 'Permission[]', description: 'Array return type' },
|
||||
{ type: 'User', description: 'Custom type return' },
|
||||
{ type: 'Group[]', description: 'Custom array return type' }
|
||||
];
|
||||
|
||||
testCases.forEach(({ type, description }) => {
|
||||
const dsl = `measure test() { true } PROVIDES ${type}`;
|
||||
const result = compiler.compile(dsl, `test-measure-return-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Measure aggregation', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `measure userPermissions(user: User) {
|
||||
aggregate {
|
||||
user.role.permissions
|
||||
user.group.permissions
|
||||
} USING majority
|
||||
} PROVIDES Permission[]`,
|
||||
description: 'Aggregation with majority strategy'
|
||||
},
|
||||
{
|
||||
input: `measure userClearance(user: User) {
|
||||
aggregate {
|
||||
user.clearance
|
||||
user.role.clearance
|
||||
user.group.clearance
|
||||
} USING max
|
||||
} PROVIDES string`,
|
||||
description: 'Aggregation with max strategy'
|
||||
},
|
||||
{
|
||||
input: `measure userScore(user: User) {
|
||||
aggregate {
|
||||
user.reputation
|
||||
user.activityScore
|
||||
user.verificationLevel
|
||||
} USING average
|
||||
} PROVIDES number`,
|
||||
description: 'Aggregation with average strategy'
|
||||
},
|
||||
{
|
||||
input: `measure userTrust(user: User) {
|
||||
aggregate {
|
||||
user.reputation
|
||||
user.activityScore
|
||||
user.verificationLevel
|
||||
user.socialProof
|
||||
} USING min
|
||||
} PROVIDES number`,
|
||||
description: 'Aggregation with min strategy'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-measure-aggregation-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Measure fusion', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `measure effectiveClearance(user: User) {
|
||||
fusion max {
|
||||
user.clearance
|
||||
user.role.clearance
|
||||
user.group.clearance
|
||||
}
|
||||
} PROVIDES string`,
|
||||
description: 'Fusion with max strategy'
|
||||
},
|
||||
{
|
||||
input: `measure userPermissions(user: User) {
|
||||
fusion min {
|
||||
user.role.permissions
|
||||
user.group.permissions
|
||||
}
|
||||
} PROVIDES Permission[]`,
|
||||
description: 'Fusion with min strategy'
|
||||
},
|
||||
{
|
||||
input: `measure userScore(user: User) {
|
||||
fusion majority {
|
||||
user.reputation
|
||||
user.activityScore
|
||||
user.verificationLevel
|
||||
}
|
||||
} PROVIDES number`,
|
||||
description: 'Fusion with majority strategy'
|
||||
},
|
||||
{
|
||||
input: `measure userTrust(user: User) {
|
||||
fusion average {
|
||||
user.reputation
|
||||
user.activityScore
|
||||
user.verificationLevel
|
||||
user.socialProof
|
||||
}
|
||||
} PROVIDES number`,
|
||||
description: 'Fusion with average strategy'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-measure-fusion-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Complex measures', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `measure userEffectivePermissions(user: User) {
|
||||
aggregate {
|
||||
user.role.permissions
|
||||
user.group.permissions
|
||||
user.directPermissions
|
||||
} USING majority
|
||||
} PROVIDES Permission[]`,
|
||||
description: 'Complex aggregation with multiple sources'
|
||||
},
|
||||
{
|
||||
input: `measure userTrustScore(user: User) {
|
||||
fusion average {
|
||||
user.reputation
|
||||
user.activityScore
|
||||
user.verificationLevel
|
||||
user.socialProof
|
||||
user.peerRatings
|
||||
}
|
||||
} PROVIDES number`,
|
||||
description: 'Complex fusion with multiple metrics'
|
||||
},
|
||||
{
|
||||
input: `measure userAccessLevel(user: User) {
|
||||
fusion max {
|
||||
user.clearance
|
||||
user.role.clearance
|
||||
user.group.clearance
|
||||
user.temporaryClearance
|
||||
}
|
||||
} PROVIDES string`,
|
||||
description: 'Complex clearance calculation'
|
||||
},
|
||||
{
|
||||
input: `measure userSimilarity(user1: User, user2: User) {
|
||||
similar(user1, user2) |similarity| {
|
||||
similarity
|
||||
} with similarity > 0.5
|
||||
} PROVIDES number`,
|
||||
description: 'Similarity measure with pattern matching'
|
||||
},
|
||||
{
|
||||
input: `measure userEffectiveRole(user: User) {
|
||||
fusion majority {
|
||||
user.role
|
||||
user.temporaryRole
|
||||
user.actingRole
|
||||
}
|
||||
} PROVIDES string`,
|
||||
description: 'Role determination with multiple sources'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description }) => {
|
||||
const result = compiler.compile(input, `test-complex-measure-${Date.now()}`);
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Measure error handling', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: `measure userRole(user: User) {
|
||||
user.role
|
||||
}`,
|
||||
description: 'Missing PROVIDES clause should fail',
|
||||
expectSuccess: false
|
||||
},
|
||||
{
|
||||
input: `measure userRole(user: User) {
|
||||
user.role
|
||||
} PROVIDES`,
|
||||
description: 'Incomplete PROVIDES clause should fail',
|
||||
expectSuccess: false
|
||||
},
|
||||
{
|
||||
input: `measure userRole(user: User) {
|
||||
user.role
|
||||
} PROVIDES string`,
|
||||
description: 'Valid measure should succeed',
|
||||
expectSuccess: true
|
||||
},
|
||||
{
|
||||
input: `measure userPermissions(user: User) {
|
||||
aggregate {
|
||||
user.role.permissions
|
||||
user.group.permissions
|
||||
} USING
|
||||
} PROVIDES Permission[]`,
|
||||
description: 'Incomplete USING clause should fail',
|
||||
expectSuccess: false
|
||||
},
|
||||
{
|
||||
input: `measure userScore(user: User) {
|
||||
fusion {
|
||||
user.reputation
|
||||
user.activityScore
|
||||
}
|
||||
} PROVIDES number`,
|
||||
description: 'Missing fusion strategy should fail',
|
||||
expectSuccess: false
|
||||
},
|
||||
{
|
||||
input: `measure userRole(user: User) {
|
||||
invalid syntax here
|
||||
} PROVIDES string`,
|
||||
description: 'Invalid syntax should fail',
|
||||
expectSuccess: false
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, description, expectSuccess }) => {
|
||||
try {
|
||||
const result = compiler.compile(input, `test-measure-error-${Date.now()}`);
|
||||
if (expectSuccess) {
|
||||
assert.ok(result.success, `${description} should parse successfully`);
|
||||
} else {
|
||||
assert.ok(!result.success, `${description} should fail to parse`);
|
||||
}
|
||||
} catch {
|
||||
if (!expectSuccess) {
|
||||
// Expected to fail
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import { describe, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { PeggyDSLParser } from '../src/parser/PeggyDSLParser.js';
|
||||
|
||||
describe('Peggy DSL Parser', () => {
|
||||
const parser = new PeggyDSLParser();
|
||||
|
||||
test('Basic parsing', () => {
|
||||
const dsl = `
|
||||
definition User {
|
||||
role: string
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
fact hasRole(user: User, role: string)
|
||||
|
||||
evidence canRead(user: User, doc: Document) {
|
||||
hasRole(user, 'admin')
|
||||
}
|
||||
`;
|
||||
|
||||
const program = parser.parse(dsl);
|
||||
assert.ok(program !== null, 'Program should be created');
|
||||
assert.ok(program.definitions.length === 1, 'Should have 1 definition');
|
||||
assert.ok(program.facts.length === 1, 'Should have 1 fact');
|
||||
assert.ok(program.evidence.length === 1, 'Should have 1 evidence');
|
||||
});
|
||||
|
||||
test('Complex DSL parsing', () => {
|
||||
const dsl = `
|
||||
definition User {
|
||||
role: string
|
||||
isActive: boolean
|
||||
clearance: string BEHAVES {
|
||||
blurring adaptive confidence_95
|
||||
} CACHE eager
|
||||
}
|
||||
|
||||
fact hasRole(user: User, role: string) CACHE eager
|
||||
fact isMember(user: User, group: Group) transitive CACHE lazy
|
||||
|
||||
evidence canRead(user: User, doc: Document) {
|
||||
hasRole(user, 'admin')
|
||||
|
||||
isMember(user, *group) {
|
||||
canRead(group, doc)
|
||||
} limit 5
|
||||
|
||||
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
|
||||
}
|
||||
`;
|
||||
|
||||
const program = parser.parse(dsl);
|
||||
assert.ok(program !== null, 'Program should be created');
|
||||
assert.ok(program.definitions.length === 1, 'Should have 1 definition');
|
||||
assert.ok(program.facts.length === 2, 'Should have 2 facts');
|
||||
assert.ok(program.evidence.length === 1, 'Should have 1 evidence');
|
||||
});
|
||||
|
||||
test('Error handling', () => {
|
||||
const invalidDSL = `
|
||||
definition User {
|
||||
role: string
|
||||
// Missing closing brace
|
||||
|
||||
fact hasRole(user: User, role: string)
|
||||
// Missing semicolon
|
||||
`;
|
||||
|
||||
assert.throws(
|
||||
() => parser.parse(invalidDSL),
|
||||
/Parsing failed/,
|
||||
'Should have parsing error message'
|
||||
);
|
||||
});
|
||||
|
||||
test('Validation', () => {
|
||||
const validDSL = `
|
||||
definition User {
|
||||
role: string
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
fact hasRole(user: User, role: string)
|
||||
`;
|
||||
|
||||
const invalidDSL = `
|
||||
definition User {
|
||||
role: string
|
||||
// Missing closing brace
|
||||
`;
|
||||
|
||||
const validResult = parser.validate(validDSL);
|
||||
assert.ok(validResult.success, 'Valid DSL should pass validation');
|
||||
assert.ok(validResult.program !== null, 'Valid DSL should return program');
|
||||
|
||||
const invalidResult = parser.validate(invalidDSL);
|
||||
assert.ok(!invalidResult.success, 'Invalid DSL should fail validation');
|
||||
assert.ok(invalidResult.errors.length > 0, 'Should have validation errors');
|
||||
});
|
||||
});
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
# Evidence DSL Test Suite
|
||||
|
||||
## Overview
|
||||
|
||||
This comprehensive test suite follows a **structural linguistic approach** to validate the Evidence DSL (Domain Specific Language) for authorization policies. The tests are organized incrementally from basic language primitives to complex integration scenarios.
|
||||
|
||||
## Test Structure
|
||||
|
||||
### 1. Structural Linguistic Tests (`StructuralLinguisticTests.js`)
|
||||
**Level: Comprehensive**
|
||||
- **Lexical Primitives**: Identifiers, literals, keywords, whitespace
|
||||
- **Basic Expressions**: Arithmetic, logical, comparison, temporal
|
||||
- **Type System**: Definitions, fields, behaviors, caching
|
||||
- **Fact System**: Declarations, properties, caching, limits
|
||||
- **Evidence System**: Rules, defeasible logic, pattern matching
|
||||
- **Measure System**: Aggregation, fusion, return types
|
||||
- **Complex Integration**: Multi-feature combinations
|
||||
|
||||
### 2. Expression Tests (`ExpressionTests.js`)
|
||||
**Level: Focused**
|
||||
- Arithmetic operator precedence
|
||||
- Logical operator precedence
|
||||
- Comparison operators
|
||||
- Temporal expressions
|
||||
- Unary operators
|
||||
- Attribute access
|
||||
- Function calls
|
||||
- Complex expressions
|
||||
- Error handling
|
||||
|
||||
### 3. Definition Tests (`DefinitionTests.js`)
|
||||
**Level: Focused**
|
||||
- Basic type definitions
|
||||
- Field types (string, number, boolean, timestamp, custom)
|
||||
- Array types
|
||||
- Behaviors (decay, blur, TTL)
|
||||
- Caching (eager, lazy)
|
||||
- Complex definitions
|
||||
- Error handling
|
||||
|
||||
### 4. Fact Tests (`FactTests.js`)
|
||||
**Level: Focused**
|
||||
- Basic fact declarations
|
||||
- Fact properties (transitive, symmetrical)
|
||||
- Fact caching
|
||||
- Fact limits
|
||||
- Parameter types
|
||||
- Complex facts
|
||||
- Error handling
|
||||
|
||||
### 5. Evidence Tests (`EvidenceTests.js`)
|
||||
**Level: Focused**
|
||||
- Basic evidence rules
|
||||
- Defeasible logic (ALWAYS, WHEN/UNLESS, REQUIRES)
|
||||
- Pattern matching with wildcards
|
||||
- Fusion strategies (min, max, majority, average)
|
||||
- Complex evidence composition
|
||||
- Error handling
|
||||
|
||||
### 6. Measure Tests (`MeasureTests.js`)
|
||||
**Level: Focused**
|
||||
- Basic measure definitions
|
||||
- Return types
|
||||
- Aggregation with different strategies
|
||||
- Fusion with different strategies
|
||||
- Complex measures
|
||||
- Error handling
|
||||
|
||||
### 7. Integration Tests (`IntegrationTests.js`)
|
||||
**Level: Integration**
|
||||
- Complete authorization systems
|
||||
- Multi-domain systems
|
||||
- Hierarchical access patterns
|
||||
- Similarity-based access
|
||||
- Temporal access patterns
|
||||
- Complex behaviors
|
||||
- Performance scenarios
|
||||
|
||||
## Test Runner (`TestRunner.js`)
|
||||
|
||||
The test runner orchestrates all test suites and provides:
|
||||
- **Comprehensive Testing**: Run all test suites
|
||||
- **Selective Testing**: Run specific test suites
|
||||
- **Level-based Testing**: Run tests by complexity level
|
||||
- **Detailed Reporting**: Summary and detailed results
|
||||
- **Coverage Analysis**: Language feature coverage
|
||||
|
||||
## Usage
|
||||
|
||||
### Run All Tests
|
||||
```javascript
|
||||
import { runAllTests } from '../../../../../lib/src/ast/tests/tests/TestRunner.js';
|
||||
|
||||
const results = runAllTests(arbiter);
|
||||
console.log(`Tests: ${results.passed}/${results.total} passed`);
|
||||
```
|
||||
|
||||
### Run Specific Test Suites
|
||||
```javascript
|
||||
import { runSpecificTests } from '../../../../../lib/src/ast/tests/tests/TestRunner.js';
|
||||
|
||||
const results = runSpecificTests(arbiter, [
|
||||
'Expression Tests',
|
||||
'Definition Tests'
|
||||
]);
|
||||
```
|
||||
|
||||
### Run Tests by Level
|
||||
```javascript
|
||||
import { runTestsByLevel } from '../../../../../lib/src/ast/tests/tests/TestRunner.js';
|
||||
|
||||
// Run only focused tests
|
||||
const results = runTestsByLevel(arbiter, 'focused');
|
||||
|
||||
// Run only integration tests
|
||||
const results = runTestsByLevel(arbiter, 'integration');
|
||||
```
|
||||
|
||||
## Language Feature Coverage
|
||||
|
||||
### ✅ Lexical Primitives
|
||||
- Identifiers (simple, with underscores, with numbers)
|
||||
- Literals (string, number, boolean, duration)
|
||||
- Keywords (reserved words)
|
||||
- Whitespace and comments
|
||||
|
||||
### ✅ Expression System
|
||||
- Arithmetic operators (+, -, *, /) with precedence
|
||||
- Logical operators (&&, ||, NOT) with precedence
|
||||
- Comparison operators (==, !=, >, <, >=, <=)
|
||||
- Temporal expressions (within)
|
||||
- Unary operators (NOT, !)
|
||||
- Attribute access (object.attribute)
|
||||
- Function calls (predicate(args))
|
||||
|
||||
### ✅ Type System
|
||||
- Type definitions with fields
|
||||
- Field types (string, number, boolean, timestamp, custom)
|
||||
- Array types (Type[])
|
||||
- Behaviors (decay, blur, TTL)
|
||||
- Caching directives (eager, lazy)
|
||||
|
||||
### ✅ Fact System
|
||||
- Fact declarations with parameters
|
||||
- Fact properties (transitive, symmetrical)
|
||||
- Caching directives
|
||||
- Limits for performance
|
||||
- Parameter types
|
||||
|
||||
### ✅ Evidence System
|
||||
- Basic evidence rules
|
||||
- Defeasible logic (ALWAYS, WHEN/UNLESS, REQUIRES)
|
||||
- Pattern matching with wildcards (*)
|
||||
- Binding clauses (|variable|)
|
||||
- With clauses (with condition)
|
||||
- Limits for pattern matching
|
||||
- Fusion strategies (min, max, majority, average)
|
||||
|
||||
### ✅ Measure System
|
||||
- Measure definitions
|
||||
- Return type specifications (PROVIDES)
|
||||
- Aggregation with strategies (USING)
|
||||
- Fusion with strategies
|
||||
- Complex value computation
|
||||
|
||||
### ✅ Integration Features
|
||||
- Multi-domain systems
|
||||
- Hierarchical access patterns
|
||||
- Similarity-based access
|
||||
- Temporal access patterns
|
||||
- Complex behavior combinations
|
||||
- Performance optimization scenarios
|
||||
|
||||
## Test Philosophy
|
||||
|
||||
### Structural Linguistic Approach
|
||||
The tests follow a structural linguistic methodology:
|
||||
|
||||
1. **Phonological Level**: Basic lexical elements (identifiers, literals)
|
||||
2. **Morphological Level**: Word formation (operators, keywords)
|
||||
3. **Syntactic Level**: Grammar rules (expressions, statements)
|
||||
4. **Semantic Level**: Meaning (types, behaviors, logic)
|
||||
5. **Pragmatic Level**: Usage (integration, real-world scenarios)
|
||||
|
||||
### Incremental Complexity
|
||||
Tests progress from simple to complex:
|
||||
- **Level 1**: Lexical primitives
|
||||
- **Level 2**: Basic expressions
|
||||
- **Level 3**: Type system
|
||||
- **Level 4**: Fact system
|
||||
- **Level 5**: Evidence system
|
||||
- **Level 6**: Measure system
|
||||
- **Level 7**: Complex integration
|
||||
|
||||
### Comprehensive Coverage
|
||||
Each language feature is tested for:
|
||||
- **Valid cases**: Correct syntax and semantics
|
||||
- **Invalid cases**: Error handling and recovery
|
||||
- **Edge cases**: Boundary conditions
|
||||
- **Integration**: Multi-feature combinations
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Prerequisites
|
||||
- Node.js environment
|
||||
- Arbiter instance for testing
|
||||
- All dependencies installed
|
||||
|
||||
### Basic Usage
|
||||
```bash
|
||||
# Run all tests
|
||||
npm test
|
||||
|
||||
# Run specific test file
|
||||
node src/ast/tests/StructuralLinguisticTests.js
|
||||
|
||||
# Run with specific arbiter
|
||||
node -e "
|
||||
import { runAllTests } from '../../../../../lib/src/ast/tests/src/ast/tests/TestRunner.js';
|
||||
const results = runAllTests(arbiter);
|
||||
console.log(results);
|
||||
"
|
||||
```
|
||||
|
||||
### Test Output
|
||||
The test runner provides:
|
||||
- **Progress indicators**: Real-time test execution
|
||||
- **Detailed results**: Pass/fail status for each test
|
||||
- **Error reporting**: Specific error messages for failures
|
||||
- **Performance metrics**: Execution time for each suite
|
||||
- **Coverage analysis**: Language feature coverage
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new tests:
|
||||
1. Follow the structural linguistic approach
|
||||
2. Test both valid and invalid cases
|
||||
3. Include error handling tests
|
||||
4. Document test purpose and expected behavior
|
||||
5. Maintain incremental complexity
|
||||
6. Update coverage documentation
|
||||
|
||||
## Test Maintenance
|
||||
|
||||
- **Regular Updates**: Keep tests current with language changes
|
||||
- **Performance Monitoring**: Track test execution time
|
||||
- **Coverage Analysis**: Ensure comprehensive feature coverage
|
||||
- **Error Handling**: Validate error messages and recovery
|
||||
- **Integration Testing**: Test real-world scenarios
|
||||
Reference in New Issue
Block a user