f9d4fbe2f0
The rigor suite had 98 campaigns drawing a random seed per process, making the suite nondeterministic on a deterministic engine (one real ~1/15 flake already caught in tuple-to-userset-rule.test.js). Every campaign now carries a fixed, per-test-unique seed: - 16 files touched, 100 run() calls, all 100 seeded (2 were already done) - seed names follow <file>-<purpose> kebab-case, unique within each file - no other content changed (effort, invariants, assertions untouched) Verification: full rigor suite green across 11 consecutive runs, full suite 852/790/0. Determinism is now structural, not incidental.
334 lines
12 KiB
JavaScript
334 lines
12 KiB
JavaScript
/**
|
|
* rigor/relational-comparator-router.test.js — js-rigor property tests for
|
|
* RelationalComparatorRouter._isQualitativeRule.
|
|
*
|
|
* The router dispatches to either the numeric or qualitative implementation
|
|
* based on the rule config. Properties:
|
|
* - rule.qualitative === true → qualitative (regardless of other fields)
|
|
* - any operand scaleName → qualitative
|
|
* - any operand decaySteps/baseBlurSteps (defined, non-null) → qualitative
|
|
* - rule.marginSteps (defined, non-null) → qualitative
|
|
* - otherwise → numeric
|
|
*
|
|
* getImplementationType is a pure pass-through to _isQualitativeRule.
|
|
*/
|
|
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { rigor } from '@rigor/core';
|
|
import { RelationalComparatorRouter } from '../../src/authorization/rules/RelationalComparatorRouter.js';
|
|
|
|
const SCALE_NAMES = ['five-point', 'ternary', 'ordinal-7'];
|
|
|
|
/**
|
|
* Build a stub arbiter that satisfies the rule's constructor.
|
|
* Neither _evaluateRule is called by these tests, but the constructor
|
|
* stores references that the rule may touch on dispatch.
|
|
*/
|
|
function makeStubArbiter() {
|
|
return {
|
|
nodeIdByKey: new Map(),
|
|
keyByNodeId: new Map(),
|
|
relations: [],
|
|
nodes: new Map()
|
|
};
|
|
}
|
|
|
|
function makeRouter() {
|
|
// _evaluateRule is what we test; pass a stub arbiter so the
|
|
// constructor doesn't crash. We never call _evaluateRule in these
|
|
// tests — we test _isQualitativeRule and getImplementationType
|
|
// directly.
|
|
return new RelationalComparatorRouter(makeStubArbiter(), {});
|
|
}
|
|
|
|
describe('RelationalComparatorRouter._isQualitativeRule (rigor)', () => {
|
|
it('rule.qualitative=true always wins', async () => {
|
|
async function check(rule) {
|
|
const router = makeRouter();
|
|
const result = router._isQualitativeRule(rule);
|
|
if (!result) {
|
|
throw new Error(
|
|
`expected qualitative=true to win, got numeric. rule=${JSON.stringify(rule)}`
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
const report = await rigor.campaign(
|
|
[
|
|
rigor.fn('check', check,
|
|
rigor.args(
|
|
rigor.gen.object({
|
|
qualitative: rigor.gen.constant(true),
|
|
// random other fields that should not matter
|
|
left: rigor.gen.option(rigor.gen.object({
|
|
scaleName: rigor.gen.option(rigor.gen.enum(SCALE_NAMES))
|
|
})),
|
|
right: rigor.gen.option(rigor.gen.object({
|
|
scaleName: rigor.gen.option(rigor.gen.enum(SCALE_NAMES))
|
|
})),
|
|
marginSteps: rigor.gen.option(rigor.gen.int(0, 5)),
|
|
fallbackBehavior: rigor.gen.option(rigor.gen.enum(['allow', 'deny']))
|
|
})
|
|
)
|
|
)
|
|
],
|
|
rigor.crucible([
|
|
rigor.invariant('qualitative-wins', ({ error, errorMessage }) => !error && !errorMessage)
|
|
])
|
|
).run({ seed: 'rc-router-qualitative-wins', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(report.toTAP());
|
|
const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'qualitative-wins');
|
|
assert.ok(inv);
|
|
assert.equal(inv.passed, true,
|
|
`qualitative=true did not win in ${inv.failureCount} cases`);
|
|
});
|
|
|
|
it('scaleName on either operand triggers qualitative', async () => {
|
|
async function check(operand, side) {
|
|
const router = makeRouter();
|
|
const rule = {
|
|
left: side === 'left' ? operand : {},
|
|
right: side === 'right' ? operand : {}
|
|
};
|
|
const result = router._isQualitativeRule(rule);
|
|
if (!result) {
|
|
throw new Error(
|
|
`expected scaleName on ${side} to trigger qualitative, got numeric. ` +
|
|
`operand=${JSON.stringify(operand)}`
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
const report = await rigor.campaign(
|
|
[
|
|
rigor.fn('check', check,
|
|
rigor.args(
|
|
rigor.gen.object({
|
|
scaleName: rigor.gen.enum(SCALE_NAMES)
|
|
}),
|
|
rigor.gen.enum(['left', 'right'])
|
|
)
|
|
)
|
|
],
|
|
rigor.crucible([
|
|
rigor.invariant('scaleName-triggers', ({ error, errorMessage }) => !error && !errorMessage)
|
|
])
|
|
).run({ seed: 'rc-router-scale-name', effort: 500 , artifacts: { dir: '', persist: 'never' }});
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(report.toTAP());
|
|
const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'scaleName-triggers');
|
|
assert.ok(inv);
|
|
assert.equal(inv.passed, true,
|
|
`scaleName did not trigger qualitative in ${inv.failureCount} cases`);
|
|
});
|
|
|
|
it('decaySteps/baseBlurSteps on either operand triggers qualitative', async () => {
|
|
async function check(operand, side) {
|
|
const router = makeRouter();
|
|
const rule = {
|
|
left: side === 'left' ? operand : {},
|
|
right: side === 'right' ? operand : {}
|
|
};
|
|
const result = router._isQualitativeRule(rule);
|
|
if (!result) {
|
|
throw new Error(
|
|
`expected ${JSON.stringify(Object.keys(operand))} on ${side} to trigger qualitative`
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
const report = await rigor.campaign(
|
|
[
|
|
rigor.fn('check', check,
|
|
rigor.args(
|
|
rigor.gen.oneOf([
|
|
rigor.gen.object({ decaySteps: rigor.gen.int(0, 10) }),
|
|
rigor.gen.object({ baseBlurSteps: rigor.gen.int(0, 10) })
|
|
]),
|
|
rigor.gen.enum(['left', 'right'])
|
|
)
|
|
)
|
|
],
|
|
rigor.crucible([
|
|
rigor.invariant('decay-blur-triggers', ({ error, errorMessage }) => !error && !errorMessage)
|
|
])
|
|
).run({ seed: 'rc-router-decay-blur', effort: 500 , artifacts: { dir: '', persist: 'never' }});
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(report.toTAP());
|
|
const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'decay-blur-triggers');
|
|
assert.ok(inv);
|
|
assert.equal(inv.passed, true,
|
|
`decaySteps/baseBlurSteps did not trigger qualitative in ${inv.failureCount} cases`);
|
|
});
|
|
|
|
it('marginSteps triggers qualitative (when defined and non-null)', async () => {
|
|
async function check(marginSteps) {
|
|
const router = makeRouter();
|
|
const rule = { marginSteps };
|
|
const result = router._isQualitativeRule(rule);
|
|
if (marginSteps === undefined || marginSteps === null) {
|
|
// marginSteps not present — should not trigger
|
|
if (result) throw new Error(`marginSteps=${marginSteps} should not trigger qualitative`);
|
|
} else {
|
|
if (!result) throw new Error(`marginSteps=${marginSteps} should trigger qualitative`);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
const report = await rigor.campaign(
|
|
[
|
|
rigor.fn('check', check,
|
|
rigor.args(
|
|
rigor.gen.option(rigor.gen.int(0, 10))
|
|
)
|
|
)
|
|
],
|
|
rigor.crucible([
|
|
rigor.invariant('marginSteps-correct', ({ error, errorMessage }) => !error && !errorMessage)
|
|
])
|
|
).run({ seed: 'rc-router-margin-steps', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(report.toTAP());
|
|
const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'marginSteps-correct');
|
|
assert.ok(inv);
|
|
assert.equal(inv.passed, true,
|
|
`marginSteps handling was wrong in ${inv.failureCount} cases`);
|
|
});
|
|
|
|
it('plain numeric rule (no qualitative flags) routes to numeric', async () => {
|
|
async function check(leftValue, rightValue, comparator) {
|
|
const router = makeRouter();
|
|
const rule = {
|
|
left: { rule: { type: 'direct' }, extractValue: true },
|
|
right: { rule: { type: 'direct' }, extractValue: true },
|
|
comparator,
|
|
leftValue, // injected for testing only — production ignores
|
|
rightValue
|
|
};
|
|
const result = router._isQualitativeRule(rule);
|
|
if (result) {
|
|
throw new Error(
|
|
`expected numeric, got qualitative. rule=${JSON.stringify(rule)}`
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
const report = await rigor.campaign(
|
|
[
|
|
rigor.fn('check', check,
|
|
rigor.args(
|
|
rigor.gen.int(-1000, 1000),
|
|
rigor.gen.int(-1000, 1000),
|
|
rigor.gen.enum(['>', '>=', '<', '<=', '==', '!='])
|
|
)
|
|
)
|
|
],
|
|
rigor.crucible([
|
|
rigor.invariant('plain-numeric', ({ error, errorMessage }) => !error && !errorMessage)
|
|
])
|
|
).run({ seed: 'rc-router-plain-numeric', effort: 800 , artifacts: { dir: '', persist: 'never' }});
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(report.toTAP());
|
|
const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'plain-numeric');
|
|
assert.ok(inv);
|
|
assert.equal(inv.passed, true,
|
|
`plain numeric rule routed to qualitative in ${inv.failureCount} cases`);
|
|
});
|
|
|
|
it('getImplementationType matches _isQualitativeRule', async () => {
|
|
async function check(rule) {
|
|
const router = makeRouter();
|
|
const fromPrivate = router._isQualitativeRule(rule);
|
|
const fromPublic = router.getImplementationType(rule);
|
|
const expected = fromPrivate ? 'qualitative' : 'numeric';
|
|
if (fromPublic !== expected) {
|
|
throw new Error(
|
|
`getImplementationType=${fromPublic} but _isQualitativeRule=${fromPrivate}`
|
|
);
|
|
}
|
|
return fromPublic;
|
|
}
|
|
|
|
const report = await rigor.campaign(
|
|
[
|
|
rigor.fn('check', check,
|
|
rigor.args(
|
|
rigor.gen.object({
|
|
qualitative: rigor.gen.option(rigor.gen.boolean()),
|
|
left: rigor.gen.option(rigor.gen.object({
|
|
scaleName: rigor.gen.option(rigor.gen.enum(SCALE_NAMES)),
|
|
decaySteps: rigor.gen.option(rigor.gen.int(0, 10))
|
|
})),
|
|
right: rigor.gen.option(rigor.gen.object({
|
|
scaleName: rigor.gen.option(rigor.gen.enum(SCALE_NAMES)),
|
|
decaySteps: rigor.gen.option(rigor.gen.int(0, 10))
|
|
})),
|
|
marginSteps: rigor.gen.option(rigor.gen.int(0, 10))
|
|
})
|
|
)
|
|
)
|
|
],
|
|
rigor.crucible([
|
|
rigor.invariant('getImplType-consistent', ({ error, errorMessage }) => !error && !errorMessage)
|
|
])
|
|
).run({ seed: 'rc-router-get-impl-type', effort: 1500 , artifacts: { dir: '', persist: 'never' }});
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(report.toTAP());
|
|
const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'getImplType-consistent');
|
|
assert.ok(inv);
|
|
assert.equal(inv.passed, true,
|
|
`getImplementationType disagreed with _isQualitativeRule in ${inv.failureCount} cases`);
|
|
});
|
|
|
|
it('_hasValidQualitativeProperty: undefined/null → false, anything else → true', async () => {
|
|
async function check(value) {
|
|
const router = makeRouter();
|
|
const result = router._hasValidQualitativeProperty(value);
|
|
if (value === undefined || value === null) {
|
|
if (result) throw new Error(`_hasValidQualitativeProperty(${value}) should be false`);
|
|
} else {
|
|
if (!result) throw new Error(`_hasValidQualitativeProperty(${value}) should be true`);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
const report = await rigor.campaign(
|
|
[
|
|
rigor.fn('check', check,
|
|
rigor.args(
|
|
// Exclude undefined/null from explicit inputs — the
|
|
// generator treats them as "absent". The campaign will
|
|
// also exercise the absent path via undefined when the
|
|
// record is shrunk to {}.
|
|
rigor.gen.option(
|
|
rigor.gen.oneOf([
|
|
rigor.gen.int(),
|
|
rigor.gen.float(),
|
|
rigor.gen.boolean(),
|
|
rigor.gen.string(),
|
|
rigor.gen.constant(0),
|
|
rigor.gen.constant(''),
|
|
rigor.gen.constant(false)
|
|
])
|
|
)
|
|
)
|
|
)
|
|
],
|
|
rigor.crucible([
|
|
rigor.invariant('hasValidProperty', ({ error, errorMessage }) => !error && !errorMessage)
|
|
])
|
|
).run({ seed: 'rc-router-has-valid-property', effort: 1000 , artifacts: { dir: '', persist: 'never' }});
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(report.toTAP());
|
|
const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'hasValidProperty');
|
|
assert.ok(inv);
|
|
assert.equal(inv.passed, true,
|
|
`_hasValidQualitativeProperty contract violated in ${inv.failureCount} cases`);
|
|
});
|
|
});
|