Files
evidence-dsl/tests/rigor/dsl-illegal-mutations.test.js
T

139 lines
5.5 KiB
JavaScript
Raw Normal View History

/**
* tests/rigor/dsl-illegal-mutations.test.js — js-rigor campaign that takes a
* valid Evidence DSL program and applies ONE subtle flaw to produce illegal
* DSL, asserting the compiler reliably REJECTS each mutation.
*
* Each mutation perturbs a single construct (swapped arg types, unknown fact,
* arity mismatch, reserved built-in type, duplicate evidence, unterminated
* block, malformed parameter list, type mismatch across params). A lowering or
* validation bug that silently accepted structurally-broken DSL would fail the
* invariant.
*
* Anti-vacuity: the `valid` mutation is the untouched DSL and MUST compile —
* proving the harness is not trivially rejecting everything.
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { rigor } from '@rigor/core';
import { Arbiter } from '@arbiter/core';
import { DSLCompiler } from '../../src/DSLCompiler.js';
const VALID_DSL = `
definition Employee { id: string }
definition Group { id: string }
definition Doc { id: string }
fact owns(user: Employee, doc: Doc)
fact member_of(user: Employee, group: Group)
fact can_access(group: Group, doc: Doc)
evidence can_read(user: Employee, doc: Doc) { owns(user, doc) }
evidence can_enter(user: Employee, doc: Doc) { member_of(user, *g) { can_access(g, doc) } }
`;
// Each mutation transforms the valid DSL into an illegal variant.
// `mustFail: false` marks the control mutation (untouched DSL — must compile).
const MUTATIONS = {
valid: {
desc: 'control (untouched DSL must compile)',
mustFail: false,
apply: () => VALID_DSL
},
swapped_arg_types: {
desc: 'swapped subject/object argument types',
mustFail: true,
apply: () => VALID_DSL.replace('fact owns(user: Employee, doc: Doc)', 'fact owns(doc: Doc, user: Employee)')
},
undefined_fact: {
desc: 'references an undeclared fact',
mustFail: true,
apply: () => VALID_DSL.replace('{ owns(user, doc) }', '{ ghost(user, doc) }')
},
arity_mismatch: {
desc: 'wrong argument arity on a binary fact',
mustFail: true,
apply: () => VALID_DSL.replace('{ owns(user, doc) }', '{ owns(user) }')
},
reserved_builtin_type: {
desc: 'redefines a reserved built-in type',
mustFail: true,
apply: () => VALID_DSL.replace('definition Employee { id: string }', 'definition User { id: string }')
},
duplicate_evidence: {
desc: 'duplicate evidence relation name',
mustFail: true,
apply: () => VALID_DSL + `\n evidence can_read(user: Employee, doc: Doc) { owns(user, doc) }`
},
unterminated_block: {
desc: 'missing closing brace',
mustFail: true,
apply: () => VALID_DSL.replace('{ owns(user, doc) }', '{ owns(user, doc)')
},
malformed_params: {
desc: 'malformed parameter list (missing comma)',
mustFail: true,
apply: () => VALID_DSL.replace('owns(user: Employee, doc: Doc)', 'owns(user: Employee doc: Doc)')
},
type_mismatch_arg: {
desc: 'passes an Employee where a Group is required',
mustFail: true,
apply: () => VALID_DSL.replace('{ can_access(g, doc) }', '{ can_access(user, doc) }')
},
wrong_evidence_arity: {
desc: 'evidence declared with mismatched parameter arity',
mustFail: true,
apply: () => VALID_DSL.replace('evidence can_read(user: Employee, doc: Doc) { owns(user, doc) }', 'evidence can_read(user: Employee) { owns(user, doc) }')
}
};
function checkMutation(mutationName) {
const mutation = MUTATIONS[mutationName];
if (!mutation) throw new Error(`unknown mutation name: ${JSON.stringify(mutationName)}`);
const dsl = mutation.apply();
const arbiter = new Arbiter();
const compiler = new DSLCompiler(arbiter);
const result = compiler.compile(dsl, `mut-${mutationName}`);
const success = result.success;
const errors = result.errors || [];
if (mutation.mustFail) {
if (success || errors.length === 0) {
throw new Error(`mutation '${mutationName}' was NOT rejected (${mutation.desc}). ` +
`success=${success}, errors=${JSON.stringify(errors)}`);
}
} else if (!success) {
throw new Error(`control mutation '${mutationName}' should compile but failed: ${JSON.stringify(errors)}`);
}
return { mutationName, ok: true };
}
describe('DSL illegal-mutation rejection (rigor)', () => {
it('every subtle one-flaw mutation is reliably rejected; the control compiles', async () => {
const report = await rigor.campaign(
[
rigor.fn('reject-mutation', checkMutation, rigor.args(
rigor.gen.oneOf(Object.keys(MUTATIONS))
))
],
rigor.crucible([
// `actual` is the fn's return; a contract violation (a must-fail
// mutation that compiled, a control that failed, or an unknown name)
// throws → actual undefined → this invariant fails.
rigor.invariant('rejection-contract', ({ actual }) =>
!!actual && actual.ok === true)
])
).run({ seed: 'dsl-illegal-mutations', effort: 400, artifacts: { dir: '', persist: 'never' } });
const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'rejection-contract');
assert.ok(inv, 'crucible invariant missing');
assert.equal(inv.passed, true, `rejection contract violated in ${inv.failureCount} cases`);
});
it('every mutation kind is exercised (no vacuous pass)', () => {
const seen = new Set();
for (const name of Object.keys(MUTATIONS)) {
// deterministic probe of each kind
seen.add(name);
checkMutation(name);
}
assert.equal(seen.size, Object.keys(MUTATIONS).length);
});
});