1.13.0: measure + evidence compose; value-graph now a registry dep
- DSLValueGraph integrates the evidence DSL with @arbiter/value-graph: measures become typed value-graph nodes; attach() wires runtime.measure through the graph. - DSLRuntime.check() now retrieves required MEASURE values and injects them as value-carrying self-edges, so an evidence comparator over a measure (e.g. budget_used(user) <= budget_limit(user)) evaluates — measure and evidence compose (provider-sourced AND value-graph-sourced), with 3 new composition tests. - BUILTIN_TYPES: bigint → buffer (the value-graph wire is JSON-free/bigint-free). - @arbiter/value-graph: file:../value-graph → ^0.1.0 (registry); CI auth adds @push-stream-std registry for the transitive dep. - rigor core ^3.1.2 / probe ^0.0.8.
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* tests/rigor/dsl-value-graph.test.js — js-rigor deep tests for the
|
||||
* DSLValueGraph integration (evidence DSL declares the value-graph's typing;
|
||||
* measures retrieve through the graph; attributes are stored directly).
|
||||
*
|
||||
* Covers, as property campaigns:
|
||||
* - setValue/getValue round-trips across every declared value type
|
||||
* - type enforcement (wrong-typed writes always reject)
|
||||
* - parameter-binding validation (wrong-typed params always reject)
|
||||
* - subject scoping (different subjects never share entries)
|
||||
* - attach() parity (runtime.measure === value-graph retrieval)
|
||||
* - external resolvers compute, and a stored value wins over the resolver
|
||||
* - invalidation clears a stored value (next retrieval → null)
|
||||
* - schema derivation from generated measure declarations
|
||||
* - multi-measure independence
|
||||
*/
|
||||
import { describe, it } from 'node:test';
|
||||
import { rigor, reducers } from '@rigor/core';
|
||||
import { Arbiter } from '@arbiter/core';
|
||||
import { DSLRuntime, DSLValueGraph } from '../../src/index.js';
|
||||
|
||||
const DSL = `
|
||||
definition Tenant { id: string }
|
||||
measure budget(tenant: Tenant, feature: string) { } PROVIDES number
|
||||
measure label(user: string) { } PROVIDES string
|
||||
measure active(user: string) { } PROVIDES boolean
|
||||
measure tags(user: string) { } PROVIDES array
|
||||
`;
|
||||
|
||||
function makeDvg() {
|
||||
const rt = new DSLRuntime(new Arbiter()).compile(DSL, 'vg-rigor');
|
||||
return new DSLValueGraph(rt);
|
||||
}
|
||||
|
||||
async function expectPass(name, actions, checks, config = {}) {
|
||||
const report = await rigor.campaign(actions, rigor.crucible(checks))
|
||||
.run({ seed: `dsl-vg-${name}`, effort: 120, artifacts: { dir: '', persist: 'never' }, ...config });
|
||||
if (report.status !== 'passed') {
|
||||
const detail = (report.failures || []).slice(0, 5)
|
||||
.map((f) => JSON.stringify({ action: f.actionName || f.action, inv: f.name, args: f.args, msg: f.message }));
|
||||
throw new Error(`campaign '${name}' failed.\n${detail.join('\n')}`);
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
describe('DSLValueGraph integration (rigor)', () => {
|
||||
it('setValue/getValue round-trips every declared value type', async () => {
|
||||
const actions = [
|
||||
rigor.fn('rt_number', (v) => {
|
||||
const dvg = makeDvg();
|
||||
dvg.setValue('budget', { __subject: 't', tenant: 't', feature: 'x' }, v);
|
||||
const e = dvg.getValue('budget', { __subject: 't', tenant: 't', feature: 'x' });
|
||||
return e ? e.value : 'MISSING';
|
||||
}, rigor.args(rigor.gen.int(-100000, 100000))),
|
||||
rigor.fn('rt_string', (v) => {
|
||||
const dvg = makeDvg();
|
||||
dvg.setValue('label', { __subject: 't', user: 'u' }, v);
|
||||
const e = dvg.getValue('label', { __subject: 't', user: 'u' });
|
||||
return e ? e.value : 'MISSING';
|
||||
}, rigor.args(rigor.gen.asciiString())),
|
||||
rigor.fn('rt_bool', (v) => {
|
||||
const dvg = makeDvg();
|
||||
dvg.setValue('active', { __subject: 't', user: 'u' }, v);
|
||||
const e = dvg.getValue('active', { __subject: 't', user: 'u' });
|
||||
return e ? e.value : 'MISSING';
|
||||
}, rigor.args(rigor.gen.boolean())),
|
||||
rigor.fn('rt_array', (v) => {
|
||||
const dvg = makeDvg();
|
||||
dvg.setValue('tags', { __subject: 't', user: 'u' }, v);
|
||||
const e = dvg.getValue('tags', { __subject: 't', user: 'u' });
|
||||
return e ? e.value : null;
|
||||
}, rigor.args(rigor.gen.array(rigor.gen.string(), 0, 5))),
|
||||
];
|
||||
const checks = [
|
||||
rigor.after('rt_number', (ctx) => ctx.actual === ctx.args[0]),
|
||||
rigor.after('rt_string', (ctx) => ctx.actual === ctx.args[0]),
|
||||
rigor.after('rt_bool', (ctx) => ctx.actual === ctx.args[0]),
|
||||
rigor.after('rt_array', (ctx) => JSON.stringify(ctx.actual) === JSON.stringify(ctx.args[0])),
|
||||
];
|
||||
await expectPass('roundtrip', actions, checks, { effort: 200 });
|
||||
});
|
||||
|
||||
it('type enforcement: wrong-typed writes always reject, right-typed always pass', async () => {
|
||||
const actions = [
|
||||
rigor.fn('type_ok', (v) => {
|
||||
const dvg = makeDvg();
|
||||
dvg.setValue('budget', { __subject: 't', tenant: 't', feature: 'x' }, v);
|
||||
return dvg.getValue('budget', { __subject: 't', tenant: 't', feature: 'x' }).value;
|
||||
}, rigor.args(rigor.gen.int(-100, 100))),
|
||||
rigor.fn('type_reject', (v) => {
|
||||
const dvg = makeDvg();
|
||||
try { dvg.setValue('label', { user: 'u' }, v); return { ok: true }; }
|
||||
catch (e) { return { ok: false, msg: e.message }; }
|
||||
}, rigor.args(rigor.gen.oneOf([rigor.gen.int(0, 100), rigor.gen.boolean(), rigor.gen.array(rigor.gen.int(0, 9), 1, 3)]))),
|
||||
rigor.fn('param_reject', (v) => {
|
||||
const dvg = makeDvg();
|
||||
try { dvg.setValue('budget', { tenant: 't', feature: v }, 1); return { ok: true }; }
|
||||
catch (e) { return { ok: false, msg: e.message }; }
|
||||
}, rigor.args(rigor.gen.oneOf([rigor.gen.constant(42), rigor.gen.boolean(), rigor.gen.array(rigor.gen.int(0, 9), 1, 2)]))),
|
||||
];
|
||||
const checks = [
|
||||
rigor.after('type_ok', (ctx) => ctx.actual === ctx.args[0]),
|
||||
rigor.after('type_reject', (ctx) => ctx.actual.ok === false && /must match declared type 'string'/.test(ctx.actual.msg)),
|
||||
rigor.after('param_reject', (ctx) => ctx.actual.ok === false && /parameter 'feature'/.test(ctx.actual.msg)),
|
||||
];
|
||||
await expectPass('typecheck', actions, checks, { effort: 200 });
|
||||
});
|
||||
|
||||
it('subjects never share entries', async () => {
|
||||
const actions = [
|
||||
rigor.fn('subject_isolation', (a, b) => {
|
||||
const dvg = makeDvg();
|
||||
dvg.setValue('budget', { __subject: 'A', tenant: 'A', feature: 'x' }, a);
|
||||
dvg.setValue('budget', { __subject: 'B', tenant: 'B', feature: 'x' }, b);
|
||||
const ea = dvg.getValue('budget', { __subject: 'A', tenant: 'A', feature: 'x' });
|
||||
const eb = dvg.getValue('budget', { __subject: 'B', tenant: 'B', feature: 'x' });
|
||||
return { a: ea ? ea.value : null, b: eb ? eb.value : null };
|
||||
}, rigor.args(rigor.gen.int(-50, 50), rigor.gen.int(-50, 50))),
|
||||
];
|
||||
const checks = [
|
||||
rigor.after('subject_isolation', (ctx) => ctx.actual.a === ctx.args[0] && ctx.actual.b === ctx.args[1]),
|
||||
];
|
||||
await expectPass('subjects', actions, checks, { effort: 120 });
|
||||
});
|
||||
|
||||
it('attach() makes runtime.measure retrieve through the value-graph', async () => {
|
||||
const actions = [
|
||||
rigor.fn('attach_parity', (value, cb) => {
|
||||
const rt = new DSLRuntime(new Arbiter()).compile(DSL, 'vg-rigor');
|
||||
const dvg = new DSLValueGraph(rt);
|
||||
dvg.attach();
|
||||
dvg.setValue('budget', { __subject: 't', tenant: 't', feature: 'x' }, value);
|
||||
rt.measure('budget', { __subject: 't', tenant: 't', feature: 'x' }).then(
|
||||
(r) => cb(null, r.value),
|
||||
(e) => cb(e)
|
||||
);
|
||||
}, rigor.args(rigor.gen.int(0, 100), rigor.handler(reducers.first()))),
|
||||
];
|
||||
const checks = [
|
||||
rigor.after('attach_parity', (ctx) => ctx.error == null && ctx.actual === ctx.args[0]),
|
||||
];
|
||||
await expectPass('attach', actions, checks, { effort: 120 });
|
||||
});
|
||||
|
||||
it('external resolvers compute; a stored value wins over the resolver', async () => {
|
||||
const actions = [
|
||||
rigor.fn('resolver_compute', (value, cb) => {
|
||||
const dvg = makeDvg();
|
||||
dvg.resolve('budget', (s, p, ctx, done) => done(null, { value, unit: 'tokens', source: 'resolver' }));
|
||||
dvg.measure('budget', { __subject: 't', tenant: 't', feature: 'x' }).then(
|
||||
(r) => cb(null, { value: r.value, source: r.source }),
|
||||
(e) => cb(e)
|
||||
);
|
||||
}, rigor.args(rigor.gen.int(0, 100), rigor.handler(reducers.first()))),
|
||||
rigor.fn('stored_wins', (value, cb) => {
|
||||
const dvg = makeDvg();
|
||||
dvg.resolve('budget', (s, p, ctx, done) => done(null, 999));
|
||||
dvg.setValue('budget', { __subject: 't', tenant: 't', feature: 'x' }, value);
|
||||
dvg.measure('budget', { __subject: 't', tenant: 't', feature: 'x' }).then(
|
||||
(r) => cb(null, r.value),
|
||||
(e) => cb(e)
|
||||
);
|
||||
}, rigor.args(rigor.gen.int(0, 100), rigor.handler(reducers.first()))),
|
||||
];
|
||||
const checks = [
|
||||
rigor.after('resolver_compute', (ctx) => ctx.error == null && ctx.actual.value === ctx.args[0] && ctx.actual.source === 'resolver'),
|
||||
rigor.after('stored_wins', (ctx) => ctx.error == null && ctx.actual === ctx.args[0]),
|
||||
];
|
||||
await expectPass('resolvers', actions, checks, { effort: 120 });
|
||||
});
|
||||
|
||||
it('invalidation clears a stored value; the next retrieval is null', async () => {
|
||||
const actions = [
|
||||
rigor.fn('invalidate_clears', (value, cb) => {
|
||||
const dvg = makeDvg();
|
||||
dvg.setValue('budget', { __subject: 't', tenant: 't', feature: 'x' }, value);
|
||||
dvg.vg.invalidate('t', 'budget', { tenant: 't', feature: 'x' });
|
||||
dvg.measure('budget', { __subject: 't', tenant: 't', feature: 'x' }).then(
|
||||
(r) => cb(null, r.value),
|
||||
(e) => cb(e)
|
||||
);
|
||||
}, rigor.args(rigor.gen.int(0, 100), rigor.handler(reducers.first()))),
|
||||
];
|
||||
const checks = [
|
||||
rigor.after('invalidate_clears', (ctx) => ctx.error == null && ctx.actual === null),
|
||||
];
|
||||
await expectPass('invalidate', actions, checks, { effort: 120 });
|
||||
});
|
||||
|
||||
it('schema derivation: every generated measure gets its declared returnType and params', async () => {
|
||||
const actions = [
|
||||
rigor.fn('schema_ok', (name, type) => {
|
||||
const dsl = `definition T { id: string } measure ${name}(p: T) { } PROVIDES ${type}`;
|
||||
const rt = new DSLRuntime(new Arbiter()).compile(dsl, 'gen');
|
||||
const dvg = new DSLValueGraph(rt);
|
||||
const spec = dvg.schema()[name];
|
||||
return { returnType: spec && spec.returnType, params: spec ? spec.params.map((x) => x.name) : null };
|
||||
}, rigor.args(
|
||||
rigor.gen.oneOf(['balance', 'rate', 'quota', 'volume', 'meter']),
|
||||
rigor.gen.oneOf(['number', 'string', 'boolean', 'array'])
|
||||
)),
|
||||
];
|
||||
const checks = [
|
||||
rigor.after('schema_ok', (ctx) => ctx.actual.returnType === ctx.args[1] && JSON.stringify(ctx.actual.params) === JSON.stringify(['p'])),
|
||||
];
|
||||
await expectPass('schema', actions, checks, { effort: 120 });
|
||||
});
|
||||
|
||||
it('measures are independent (no cross-measure cache sharing)', async () => {
|
||||
const actions = [
|
||||
rigor.fn('multi_measure', (a, b) => {
|
||||
const dvg = makeDvg();
|
||||
dvg.setValue('budget', { __subject: 't', tenant: 't', feature: 'x' }, a);
|
||||
dvg.setValue('label', { __subject: 't', user: 'u' }, String(b));
|
||||
return {
|
||||
budget: dvg.getValue('budget', { __subject: 't', tenant: 't', feature: 'x' }).value,
|
||||
label: dvg.getValue('label', { __subject: 't', user: 'u' }).value
|
||||
};
|
||||
}, rigor.args(rigor.gen.int(0, 100), rigor.gen.int(0, 100))),
|
||||
];
|
||||
const checks = [
|
||||
rigor.after('multi_measure', (ctx) => ctx.actual.budget === ctx.args[0] && ctx.actual.label === String(ctx.args[1])),
|
||||
];
|
||||
await expectPass('independence', actions, checks, { effort: 120 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user