/** * tests/DSLRuntimeTyping.test.js — duration seconds, required fields, and * type validation of insertions / updates / provider retrievals. * * - Duration literals now accept s/m/h/d/w: `BEHAVES { ttl 30s }` is 30s. * - Definition fields are REQUIRED by default (`field: type`); `field: type?` * marks a field optional. addNode enforces presence on insert. * - Provider-returned edges are validated against the fact's declared typing: * a value-carrying fact must return { value, possibility } with a value of * the declared type, and possibilities must lie in [0, 1]. */ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { Arbiter } from '@arbiter/core'; import { DSLRuntime } from '../src/runtime/DSLRuntime.js'; describe('DSLRuntime typing', () => { it('accepts seconds/minutes/hours/days/weeks in duration literals', async () => { const dsl = ` definition Employee { id: string? } definition Doc { id: string? } fact *a(user: Employee, amount: number) BEHAVES { ttl 30s } fact *b(user: Employee, amount: number) BEHAVES { ttl 2m } fact *c(user: Employee, amount: number) BEHAVES { ttl 1h } fact *d(user: Employee, amount: number) BEHAVES { ttl 3d } fact *e(user: Employee, amount: number) BEHAVES { ttl 1w } `; const rt = new DSLRuntime(new Arbiter()).compile(dsl, 'rt-units'); assert.equal(rt.relations.get('a').ttlMs, 30_000); assert.equal(rt.relations.get('b').ttlMs, 120_000); assert.equal(rt.relations.get('c').ttlMs, 3_600_000); assert.equal(rt.relations.get('d').ttlMs, 259_200_000); assert.equal(rt.relations.get('e').ttlMs, 604_800_000); }); it('enforces required definition fields on node insert', () => { const rt = new DSLRuntime(new Arbiter()).compile(` definition Employee { id: string level: number active: boolean? } `, 'rt-req'); // id and level are required (no `?`); active is optional. assert.throws(() => rt.addNode('u:1', 'Employee', { level: 3 }), /missing required field 'Employee.id'/); assert.throws(() => rt.addNode('u:2', 'Employee', { id: 'u:2' }), /missing required field 'Employee.level'/); rt.addNode('u:3', 'Employee', { id: 'u:3', level: 5 }); // both required, no active -> ok rt.addNode('u:4', 'Employee', { id: 'u:4', level: 5, active: true }); }); it('exposes requiredness in the schema snapshot', () => { const rt = new DSLRuntime(new Arbiter()).compile(` definition Employee { id: string level: number? } `, 'rt-schema-req'); const employee = rt.getSchema().types.find(t => t.name === 'Employee'); assert.equal(employee.fields.find(f => f.name === 'id').required, true); assert.equal(employee.fields.find(f => f.name === 'level').required, false); }); it('validates a provider-returned value against the declared value type', async () => { const rt = new DSLRuntime(new Arbiter()).compile(` definition Employee { id: string? } definition Doc { id: string? } fact *balance(user: Employee, amount: number) evidence can_spend(user: Employee, doc: Doc) { balance(user, 1) } `, 'rt-valuetype'); rt.addNode('u:1', 'Employee', {}); rt.addNode('doc:9', 'Doc', {}); rt.registerFact('balance', async () => ({ possibility: 1.0, value: 'high' })); await assert.rejects(() => rt.check('u:1', 'can_spend', 'doc:9'), /must be number/); }); it('requires a value for a value-carrying fact (no bare-number shorthand)', async () => { const rt = new DSLRuntime(new Arbiter()).compile(` definition Employee { id: string? } definition Doc { id: string? } fact *balance(user: Employee, amount: number) evidence can_spend(user: Employee, doc: Doc) { balance(user, 1) } `, 'rt-valshape'); rt.addNode('u:1', 'Employee', {}); rt.addNode('doc:9', 'Doc', {}); rt.registerFact('balance', async () => 0.9); await assert.rejects(() => rt.check('u:1', 'can_spend', 'doc:9'), /value-carrying fact/); rt.registerFact('balance', async () => ({ possibility: 1.0 })); // missing value await assert.rejects(() => rt.check('u:1', 'can_spend', 'doc:9'), /must supply a 'value'/); }); it('rejects a provider-returned possibility outside [0, 1]', async () => { const rt = new DSLRuntime(new Arbiter()).compile(` definition Employee { id: string? } definition Doc { id: string? } fact *owns(user: Employee, doc: Doc) evidence can_read(user: Employee, doc: Doc) { owns(user, doc) } `, 'rt-poss'); rt.addNode('u:1', 'Employee', {}); rt.addNode('doc:9', 'Doc', {}); rt.registerFact('owns', async () => ({ possibility: 2.0 })); await assert.rejects(() => rt.check('u:1', 'can_read', 'doc:9'), /invalid possibility/); }); });