Files
core/tests/rules/realistic-chain-performance.test.js
John Dvorak 717ae1031e initial commit: @arbiter/core authorization engine with js-rigor hardening
Zanzibar-style authorization graph engine (direct/chain/TTU/defeasible/
binary modes, condensed snapshots, value relations) with 39 rigor test
campaigns. Includes fixes for snapshot binary writer/reader format
mismatch (snapshot-of-snapshot corruption), possibility write-boundary
validation, empty-graph snapshot serialization, relation lookup cache
direction collision, config-redefinition cache invalidation, binary
threshold semantics, defeasible compiled routing, and comparator
reason whitelisting.
2026-07-31 13:44:06 -07:00

432 lines
15 KiB
JavaScript

/**
* Test realistic chain performance with positive queries
*/
import { test as _test } from 'node:test';
const test = process.env.RUN_PERF_TESTS === '1' ? _test : _test.skip;
import assert from 'node:assert/strict';
import { BigGraphGenerator } from '../helpers/big-graph-generator.js';
import { ChainRule } from '../../src/authorization/rules/ChainRule.js';
test('measures realistic chain QPS with positive queries', async () => {
if (process.env.TEST_DEBUG === '1') console.log('🎯 Testing realistic chain QPS with positive queries...');
const generator = new BigGraphGenerator();
const graphData = generator.generateGraph('enterprise');
const arbiter = generator.loadIntoArbiter(graphData);
const rule = new ChainRule(arbiter);
// Create a realistic chain rule that will have positive results
const chainRule = {
type: 'chain',
steps: [
{ relation: 'role_membership', direction: 'out' },
{ relation: 'role_permission', direction: 'out' }
],
collectValues: false,
valueAggregation: 'sum'
};
// Find user-object pairs that actually have chain paths
if (process.env.TEST_DEBUG === '1') console.log(' Finding realistic chain paths...');
// Get users with role memberships
const usersWithRoles = graphData.relations
.filter(r => r.relation === 'role_membership' && r.src.startsWith('user:'))
.map(r => r.src);
// Get objects with role permissions
const objectsWithPermissions = graphData.relations
.filter(r => r.relation === 'role_permission' && r.dst.startsWith('doc:'))
.map(r => r.dst);
if (process.env.TEST_DEBUG === '1') console.log(` Found ${usersWithRoles.length} users with roles`);
if (process.env.TEST_DEBUG === '1') console.log(` Found ${objectsWithPermissions.length} objects with permissions`);
// Create realistic test pairs that should have chain paths
const realisticPairs = [];
for (let i = 0; i < Math.min(10, usersWithRoles.length, objectsWithPermissions.length); i++) {
realisticPairs.push({
user: usersWithRoles[i],
object: objectsWithPermissions[i]
});
}
if (process.env.TEST_DEBUG === '1') console.log(` Testing with ${realisticPairs.length} realistic pairs...`);
// Test 1: Without caching (clear cache between queries)
if (process.env.TEST_DEBUG === '1') console.log(' Test 1: Without caching...');
const start1 = Date.now();
let queryCount1 = 0;
let positiveResults1 = 0;
const end1 = start1 + 3000; // 3 seconds
while (Date.now() < end1) {
for (const pair of realisticPairs) {
// Clear cache to simulate no caching
rule.chainResultCache.clear();
rule.chainPathCache.clear();
const result = rule.evaluate(
arbiter.nodeIdByKey.get(pair.user),
pair.user,
arbiter.nodeIdByKey.get(pair.object),
pair.object,
chainRule,
new Set(),
'can_read_via_role',
{}
);
if (result.possibility > 0) {
positiveResults1++;
}
queryCount1++;
}
}
const duration1 = Date.now() - start1;
const qps1 = (queryCount1 / duration1) * 1000;
const positiveRate1 = (positiveResults1 / queryCount1) * 100;
if (process.env.TEST_DEBUG === '1') console.log(` Without caching: ${qps1.toFixed(2)} QPS (${queryCount1} queries)`);
if (process.env.TEST_DEBUG === '1') console.log(` Positive results: ${positiveResults1} (${positiveRate1.toFixed(1)}%)`);
// Test 2: With caching (let cache accumulate)
if (process.env.TEST_DEBUG === '1') console.log(' Test 2: With caching...');
rule.chainResultCache.clear();
rule.chainPathCache.clear();
const start2 = Date.now();
let queryCount2 = 0;
let positiveResults2 = 0;
const end2 = start2 + 3000; // 3 seconds
while (Date.now() < end2) {
for (const pair of realisticPairs) {
const result = rule.evaluate(
arbiter.nodeIdByKey.get(pair.user),
pair.user,
arbiter.nodeIdByKey.get(pair.object),
pair.object,
chainRule,
new Set(),
'can_read_via_role',
{}
);
if (result.possibility > 0) {
positiveResults2++;
}
queryCount2++;
}
}
const duration2 = Date.now() - start2;
const qps2 = (queryCount2 / duration2) * 1000;
const positiveRate2 = (positiveResults2 / queryCount2) * 100;
if (process.env.TEST_DEBUG === '1') console.log(` With caching: ${qps2.toFixed(2)} QPS (${queryCount2} queries)`);
if (process.env.TEST_DEBUG === '1') console.log(` Positive results: ${positiveResults2} (${positiveRate2.toFixed(1)}%)`);
if (process.env.TEST_DEBUG === '1') console.log(` Cache size: ${rule.chainResultCache.size} results, ${rule.chainPathCache.size} paths`);
// Calculate improvement
const improvement = qps2 / qps1;
if (process.env.TEST_DEBUG === '1') console.log(` QPS improvement: ${improvement.toFixed(2)}x faster`);
// Verify improvement
assert.ok(improvement > 1, `Caching should improve QPS (${improvement.toFixed(2)}x)`);
assert.ok(positiveRate1 > 0, 'Should have some positive results');
assert.ok(positiveRate2 > 0, 'Should have some positive results');
if (process.env.TEST_DEBUG === '1') console.log(' ✅ Realistic chain caching improves QPS');
});
test('measures chain QPS with actual chain traversal', async () => {
if (process.env.TEST_DEBUG === '1') console.log('🔗 Testing chain QPS with actual chain traversal...');
const generator = new BigGraphGenerator();
const graphData = generator.generateGraph('enterprise');
const arbiter = generator.loadIntoArbiter(graphData);
const rule = new ChainRule(arbiter);
// Create a chain rule that requires actual traversal
const chainRule = {
type: 'chain',
steps: [
{ relation: 'role_membership', direction: 'out' },
{ relation: 'role_permission', direction: 'out' }
],
collectValues: false,
valueAggregation: 'sum'
};
// Find actual chain paths in the graph
if (process.env.TEST_DEBUG === '1') console.log(' Finding actual chain paths...');
const actualChains = [];
// Look for users with role memberships
const roleMemberships = graphData.relations.filter(r => r.relation === 'role_membership');
for (const membership of roleMemberships.slice(0, 5)) {
const userId = membership.src;
const roleId = membership.dst;
// Find permissions for this role
const rolePermissions = graphData.relations.filter(r =>
r.relation === 'role_permission' && r.src === roleId
);
for (const permission of rolePermissions.slice(0, 2)) {
actualChains.push({
user: userId,
object: permission.dst,
role: roleId
});
}
}
if (process.env.TEST_DEBUG === '1') console.log(` Found ${actualChains.length} actual chain paths`);
if (actualChains.length === 0) {
if (process.env.TEST_DEBUG === '1') console.log(' No actual chain paths found, skipping test');
return;
}
// Test with actual chain paths
if (process.env.TEST_DEBUG === '1') console.log(' Testing with actual chain paths...');
// Test 1: Without caching
if (process.env.TEST_DEBUG === '1') console.log(' Test 1: Without caching...');
const start1 = Date.now();
let queryCount1 = 0;
let successfulChains1 = 0;
const end1 = start1 + 2000; // 2 seconds
while (Date.now() < end1) {
for (const chain of actualChains) {
// Clear cache to simulate no caching
rule.chainResultCache.clear();
rule.chainPathCache.clear();
const result = rule.evaluate(
arbiter.nodeIdByKey.get(chain.user),
chain.user,
arbiter.nodeIdByKey.get(chain.object),
chain.object,
chainRule,
new Set(),
'can_read_via_role',
{}
);
if (result.possibility > 0) {
successfulChains1++;
}
queryCount1++;
}
}
const duration1 = Date.now() - start1;
const qps1 = (queryCount1 / duration1) * 1000;
const successRate1 = (successfulChains1 / queryCount1) * 100;
if (process.env.TEST_DEBUG === '1') console.log(` Without caching: ${qps1.toFixed(2)} QPS (${queryCount1} queries)`);
if (process.env.TEST_DEBUG === '1') console.log(` Successful chains: ${successfulChains1} (${successRate1.toFixed(1)}%)`);
// Test 2: With caching
if (process.env.TEST_DEBUG === '1') console.log(' Test 2: With caching...');
rule.chainResultCache.clear();
rule.chainPathCache.clear();
const start2 = Date.now();
let queryCount2 = 0;
let successfulChains2 = 0;
const end2 = start2 + 2000; // 2 seconds
while (Date.now() < end2) {
for (const chain of actualChains) {
const result = rule.evaluate(
arbiter.nodeIdByKey.get(chain.user),
chain.user,
arbiter.nodeIdByKey.get(chain.object),
chain.object,
chainRule,
new Set(),
'can_read_via_role',
{}
);
if (result.possibility > 0) {
successfulChains2++;
}
queryCount2++;
}
}
const duration2 = Date.now() - start2;
const qps2 = (queryCount2 / duration2) * 1000;
const successRate2 = (successfulChains2 / queryCount2) * 100;
if (process.env.TEST_DEBUG === '1') console.log(` With caching: ${qps2.toFixed(2)} QPS (${queryCount2} queries)`);
if (process.env.TEST_DEBUG === '1') console.log(` Successful chains: ${successfulChains2} (${successRate2.toFixed(1)}%)`);
if (process.env.TEST_DEBUG === '1') console.log(` Cache size: ${rule.chainResultCache.size} results, ${rule.chainPathCache.size} paths`);
// Calculate improvement
const improvement = qps2 / qps1;
if (process.env.TEST_DEBUG === '1') console.log(` QPS improvement: ${improvement.toFixed(2)}x faster`);
// Verify improvement
assert.ok(improvement > 1, `Caching should improve QPS (${improvement.toFixed(2)}x)`);
assert.ok(successRate1 > 0, 'Should have some successful chains');
assert.ok(successRate2 > 0, 'Should have some successful chains');
if (process.env.TEST_DEBUG === '1') console.log(' ✅ Actual chain traversal caching improves QPS');
});
test('measures chain QPS with realistic workload', async () => {
if (process.env.TEST_DEBUG === '1') console.log('📊 Testing chain QPS with realistic workload...');
const generator = new BigGraphGenerator();
const graphData = generator.generateGraph('enterprise');
const arbiter = generator.loadIntoArbiter(graphData);
const rule = new ChainRule(arbiter);
// Create a realistic chain rule
const chainRule = {
type: 'chain',
steps: [
{ relation: 'role_membership', direction: 'out' },
{ relation: 'role_permission', direction: 'out' }
],
collectValues: false,
valueAggregation: 'sum'
};
// Create a realistic workload: 70% positive, 30% negative
if (process.env.TEST_DEBUG === '1') console.log(' Creating realistic workload (70% positive, 30% negative)...');
const workload = [];
// 70% positive queries (should succeed)
const positiveQueries = [];
const roleMemberships = graphData.relations.filter(r => r.relation === 'role_membership');
const rolePermissions = graphData.relations.filter(r => r.relation === 'role_permission');
for (let i = 0; i < Math.min(7, roleMemberships.length, rolePermissions.length); i++) {
positiveQueries.push({
user: roleMemberships[i].src,
object: rolePermissions[i].dst,
expected: 'positive'
});
}
// 30% negative queries (should fail)
const negativeQueries = [];
const users = graphData.users.slice(0, 3);
const objects = graphData.documents.slice(0, 3);
for (let i = 0; i < 3; i++) {
negativeQueries.push({
user: users[i].key,
object: objects[i].key,
expected: 'negative'
});
}
workload.push(...positiveQueries, ...negativeQueries);
if (process.env.TEST_DEBUG === '1') console.log(` Workload: ${positiveQueries.length} positive, ${negativeQueries.length} negative queries`);
// Test 1: Without caching
if (process.env.TEST_DEBUG === '1') console.log(' Test 1: Without caching...');
const start1 = Date.now();
let queryCount1 = 0;
let positiveCount1 = 0;
const end1 = start1 + 3000; // 3 seconds
while (Date.now() < end1) {
for (const query of workload) {
// Clear cache to simulate no caching
rule.chainResultCache.clear();
rule.chainPathCache.clear();
const result = rule.evaluate(
arbiter.nodeIdByKey.get(query.user),
query.user,
arbiter.nodeIdByKey.get(query.object),
query.object,
chainRule,
new Set(),
'can_read_via_role',
{}
);
if (result.possibility > 0) {
positiveCount1++;
}
queryCount1++;
}
}
const duration1 = Date.now() - start1;
const qps1 = (queryCount1 / duration1) * 1000;
const positiveRate1 = (positiveCount1 / queryCount1) * 100;
if (process.env.TEST_DEBUG === '1') console.log(` Without caching: ${qps1.toFixed(2)} QPS (${queryCount1} queries)`);
if (process.env.TEST_DEBUG === '1') console.log(` Positive rate: ${positiveRate1.toFixed(1)}%`);
// Test 2: With caching
if (process.env.TEST_DEBUG === '1') console.log(' Test 2: With caching...');
rule.chainResultCache.clear();
rule.chainPathCache.clear();
const start2 = Date.now();
let queryCount2 = 0;
let positiveCount2 = 0;
const end2 = start2 + 3000; // 3 seconds
while (Date.now() < end2) {
for (const query of workload) {
const result = rule.evaluate(
arbiter.nodeIdByKey.get(query.user),
query.user,
arbiter.nodeIdByKey.get(query.object),
query.object,
chainRule,
new Set(),
'can_read_via_role',
{}
);
if (result.possibility > 0) {
positiveCount2++;
}
queryCount2++;
}
}
const duration2 = Date.now() - start2;
const qps2 = (queryCount2 / duration2) * 1000;
const positiveRate2 = (positiveCount2 / queryCount2) * 100;
if (process.env.TEST_DEBUG === '1') console.log(` With caching: ${qps2.toFixed(2)} QPS (${queryCount2} queries)`);
if (process.env.TEST_DEBUG === '1') console.log(` Positive rate: ${positiveRate2.toFixed(1)}%`);
if (process.env.TEST_DEBUG === '1') console.log(` Cache size: ${rule.chainResultCache.size} results, ${rule.chainPathCache.size} paths`);
// Calculate improvement
const improvement = qps2 / qps1;
if (process.env.TEST_DEBUG === '1') console.log(` QPS improvement: ${improvement.toFixed(2)}x faster`);
// Verify improvement
assert.ok(improvement > 1, `Caching should improve QPS (${improvement.toFixed(2)}x)`);
assert.ok(positiveRate1 > 0, 'Should have some positive results');
assert.ok(positiveRate2 > 0, 'Should have some positive results');
if (process.env.TEST_DEBUG === '1') console.log(' ✅ Realistic workload caching improves QPS');
});