9111c4b20d
- Duration literals now accept s/m/h/d/w (was m/h/d/w): 'BEHAVES { ttl 30s }'
is 30s, and 'within 30s' temporal expressions parse.
- Definition fields are REQUIRED by default ('field: type'); 'field: type?'
marks a field optional. addNode enforces presence on insert, getSchema
exposes per-field requiredness, and updateNodeData still validates the
provided fields' types.
- 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 (bare-number shorthand is rejected); a non-value fact must not
carry a value; and every possibility must lie in [0, 1]. Violations throw a
clear provider-authoring error instead of silently injecting malformed edges.
Tests: DSLRuntimeTyping (duration units, required-field enforcement, schema
requiredness, value-type + shape + possibility validation).
97 lines
4.7 KiB
JavaScript
97 lines
4.7 KiB
JavaScript
/**
|
|
* 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/);
|
|
});
|
|
});
|