717ae1031e
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.
269 lines
11 KiB
JavaScript
269 lines
11 KiB
JavaScript
import { test, describe, before, after } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { Arbiter } from '../../src/index.js';
|
|
import { BigGraphGenerator } from '../helpers/big-graph-generator.js';
|
|
|
|
describe.skip('Valid Path Chain Query Benchmark', () => {
|
|
let arbiter;
|
|
let generator;
|
|
let testArbiter;
|
|
let graphData;
|
|
let validPaths = [];
|
|
|
|
before(() => {
|
|
arbiter = new Arbiter({ fastConstructionMode: false });
|
|
generator = new BigGraphGenerator({ scale: 'large', seed: 12345 });
|
|
|
|
// Generate a larger, more realistic graph
|
|
graphData = generator.generateGraph('enterprise');
|
|
testArbiter = generator.loadIntoArbiter(graphData);
|
|
|
|
// Configure chain rules
|
|
testArbiter.setRelationConfig('can_read_via_role', {
|
|
type: 'chain',
|
|
steps: [
|
|
{ relation: 'member_of', direction: 'out' },
|
|
{ relation: 'can_read', direction: 'out' }
|
|
]
|
|
});
|
|
|
|
testArbiter.setRelationConfig('can_access_multi_hop', {
|
|
type: 'chain',
|
|
steps: [
|
|
{ relation: 'member_of', direction: 'out' },
|
|
{ relation: 'member_of', direction: 'out' },
|
|
{ relation: 'member_of', direction: 'out' },
|
|
{ relation: 'can_read', direction: 'out' }
|
|
]
|
|
});
|
|
|
|
// Pre-find valid paths that actually exist in the graph
|
|
if (process.env.TEST_DEBUG === '1') console.log('🔍 Finding valid chain paths...');
|
|
validPaths = findValidChainPaths(testArbiter, graphData);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Found ${validPaths.length} valid paths`);
|
|
});
|
|
|
|
after(() => {
|
|
arbiter = null;
|
|
generator = null;
|
|
testArbiter = null;
|
|
graphData = null;
|
|
validPaths = null;
|
|
});
|
|
|
|
/**
|
|
* Find valid chain paths that actually exist in the graph
|
|
*/
|
|
function findValidChainPaths(arbiter, graphData) {
|
|
const validPaths = [];
|
|
|
|
// Get all users and documents
|
|
const users = graphData.users.map(u => u.id);
|
|
const docs = graphData.documents.map(d => d.id);
|
|
|
|
// Sample a reasonable number of combinations to test
|
|
const maxTests = Math.min(1000, users.length * docs.length);
|
|
let tested = 0;
|
|
|
|
for (const user of users) {
|
|
if (tested >= maxTests) break;
|
|
|
|
for (const doc of docs) {
|
|
if (tested >= maxTests) break;
|
|
|
|
// Test if this path actually exists
|
|
try {
|
|
const result = arbiter.check(user, 'can_read_via_role', doc);
|
|
if (result.possibility > 0) {
|
|
validPaths.push({ user, doc, relation: 'can_read_via_role', result });
|
|
}
|
|
} catch (error) {
|
|
// Skip invalid paths
|
|
}
|
|
|
|
tested++;
|
|
}
|
|
}
|
|
|
|
return validPaths;
|
|
}
|
|
|
|
/**
|
|
* Measure QPS for a given operation with detailed analysis
|
|
*/
|
|
function measureQPSWithAnalysis(operation, duration = 2000, operationName = 'Operation') {
|
|
const startTime = Date.now();
|
|
const endTime = startTime + duration;
|
|
let operationCount = 0;
|
|
const latencies = [];
|
|
let successCount = 0;
|
|
let failureCount = 0;
|
|
|
|
// Clear caches before starting
|
|
if (testArbiter.relationManager && testArbiter.relationManager.chainRule) {
|
|
testArbiter.relationManager.chainRule.chainResultCache.clear();
|
|
testArbiter.relationManager.chainRule.chainPathCache.clear();
|
|
}
|
|
|
|
while (Date.now() < endTime) {
|
|
const opStart = process.hrtime.bigint();
|
|
|
|
try {
|
|
const result = operation();
|
|
operationCount++;
|
|
|
|
if (result && result.possibility > 0) {
|
|
successCount++;
|
|
} else {
|
|
failureCount++;
|
|
}
|
|
|
|
const opEnd = process.hrtime.bigint();
|
|
const latency = Number(opEnd - opStart) / 1000000; // Convert to milliseconds
|
|
latencies.push(latency);
|
|
} catch (error) {
|
|
operationCount++;
|
|
failureCount++;
|
|
const opEnd = process.hrtime.bigint();
|
|
const latency = Number(opEnd - opStart) / 1000000;
|
|
latencies.push(latency);
|
|
}
|
|
}
|
|
|
|
const actualDuration = Date.now() - startTime;
|
|
const qps = (operationCount / actualDuration) * 1000;
|
|
const avgLatency = latencies.reduce((a, b) => a + b, 0) / latencies.length;
|
|
|
|
// Sort once and reuse
|
|
const sortedLatencies = latencies.sort((a, b) => a - b);
|
|
const p95Latency = sortedLatencies[Math.floor(latencies.length * 0.95)];
|
|
const p99Latency = sortedLatencies[Math.floor(latencies.length * 0.99)];
|
|
|
|
return {
|
|
qps,
|
|
operationCount,
|
|
duration: actualDuration,
|
|
avgLatency,
|
|
p95Latency,
|
|
p99Latency,
|
|
maxLatency: sortedLatencies[sortedLatencies.length - 1],
|
|
minLatency: sortedLatencies[0],
|
|
successCount,
|
|
failureCount,
|
|
successRate: successCount / operationCount
|
|
};
|
|
}
|
|
|
|
test('Valid Path Chain Query - Cold Start', async () => {
|
|
if (process.env.TEST_DEBUG === '1') console.log('📊 Testing Valid Path Chain Query QPS (Cold Start)...');
|
|
|
|
if (validPaths.length === 0) {
|
|
if (process.env.TEST_DEBUG === '1') console.log(' ⚠️ No valid paths found - skipping test');
|
|
return;
|
|
}
|
|
|
|
let queryIndex = 0;
|
|
const result = measureQPSWithAnalysis(() => {
|
|
const path = validPaths[queryIndex % validPaths.length];
|
|
return testArbiter.check(path.user, path.relation, path.doc);
|
|
}, 3000, 'Valid Path Chain Query (Cold)');
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` QPS: ${result.qps.toFixed(2)}`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Operations: ${result.operationCount}`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Duration: ${result.duration}ms`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Avg Latency: ${result.avgLatency.toFixed(3)}ms`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` P95 Latency: ${result.p95Latency.toFixed(3)}ms`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` P99 Latency: ${result.p99Latency.toFixed(3)}ms`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Success Rate: ${(result.successRate * 100).toFixed(1)}%`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Success Count: ${result.successCount}`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Failure Count: ${result.failureCount}`);
|
|
|
|
// More realistic expectations for valid path queries
|
|
assert.ok(result.qps > 100, `Valid path chain query QPS ${result.qps.toFixed(0)} below 100 threshold`);
|
|
assert.ok(result.avgLatency < 50, `Valid path chain query latency ${result.avgLatency.toFixed(3)}ms too high`);
|
|
assert.ok(result.successRate > 0.8, `Success rate ${(result.successRate * 100).toFixed(1)}% too low for valid paths`);
|
|
});
|
|
|
|
test('Valid Path Multi-hop Chain Query - Cold Start', async () => {
|
|
if (process.env.TEST_DEBUG === '1') console.log('📊 Testing Valid Path Multi-hop Chain Query QPS (Cold Start)...');
|
|
|
|
// Find valid multi-hop paths
|
|
const multiHopPaths = [];
|
|
const users = graphData.users.map(u => u.id);
|
|
const docs = graphData.documents.map(d => d.id);
|
|
|
|
let tested = 0;
|
|
const maxTests = Math.min(500, users.length * docs.length);
|
|
|
|
for (const user of users) {
|
|
if (tested >= maxTests) break;
|
|
|
|
for (const doc of docs) {
|
|
if (tested >= maxTests) break;
|
|
|
|
try {
|
|
const result = testArbiter.check(user, 'can_access_multi_hop', doc);
|
|
if (result.possibility > 0) {
|
|
multiHopPaths.push({ user, doc, relation: 'can_access_multi_hop', result });
|
|
}
|
|
} catch (error) {
|
|
// Skip invalid paths
|
|
}
|
|
|
|
tested++;
|
|
}
|
|
}
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Found ${multiHopPaths.length} valid multi-hop paths`);
|
|
|
|
if (multiHopPaths.length === 0) {
|
|
if (process.env.TEST_DEBUG === '1') console.log(' ⚠️ No valid multi-hop paths found - skipping test');
|
|
return;
|
|
}
|
|
|
|
let queryIndex = 0;
|
|
const result = measureQPSWithAnalysis(() => {
|
|
const path = multiHopPaths[queryIndex % multiHopPaths.length];
|
|
return testArbiter.check(path.user, path.relation, path.doc);
|
|
}, 3000, 'Valid Path Multi-hop Chain Query (Cold)');
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` QPS: ${result.qps.toFixed(2)}`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Operations: ${result.operationCount}`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Duration: ${result.duration}ms`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Avg Latency: ${result.avgLatency.toFixed(3)}ms`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` P95 Latency: ${result.p95Latency.toFixed(3)}ms`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` P99 Latency: ${result.p99Latency.toFixed(3)}ms`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Success Rate: ${(result.successRate * 100).toFixed(1)}%`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Success Count: ${result.successCount}`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Failure Count: ${result.failureCount}`);
|
|
|
|
// Multi-hop should be slower than 2-hop
|
|
assert.ok(result.qps > 50, `Valid path multi-hop chain query QPS ${result.qps.toFixed(0)} below 50 threshold`);
|
|
assert.ok(result.avgLatency < 100, `Valid path multi-hop chain query latency ${result.avgLatency.toFixed(3)}ms too high`);
|
|
assert.ok(result.successRate > 0.7, `Success rate ${(result.successRate * 100).toFixed(1)}% too low for valid paths`);
|
|
});
|
|
|
|
test('Graph Statistics', async () => {
|
|
if (process.env.TEST_DEBUG === '1') console.log('📊 Graph Statistics:');
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Total Relations: ${graphData.relations.length}`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Total Users: ${graphData.users.length}`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Total Documents: ${graphData.documents.length}`);
|
|
|
|
const userCount = graphData.users.length;
|
|
const docCount = graphData.documents.length;
|
|
const groupCount = graphData.enterprises ? graphData.enterprises.length : 0;
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Users: ${userCount}`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Documents: ${docCount}`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Groups: ${groupCount}`);
|
|
|
|
const relationTypes = [...new Set(graphData.relations.map(r => r.relation))];
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Relation Types: ${relationTypes.length} (${relationTypes.join(', ')})`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Valid Chain Paths Found: ${validPaths.length}`);
|
|
|
|
// Verify we have a reasonable graph size
|
|
assert.ok(graphData.relations.length > 1000, `Graph too small: ${graphData.relations.length} relations`);
|
|
assert.ok(graphData.users.length > 50, `Graph too small: ${graphData.users.length} users`);
|
|
});
|
|
});
|