import { describe, test } from 'node:test'; import assert from 'node:assert/strict'; import fc from 'fast-check'; import { Arbiter } from '../../src/core/Arbiter.js'; function hasPathWithin(adj, src, dst, maxDepth) { const visited = new Set([src]); let frontier = [src]; let depth = 0; while (frontier.length && depth < maxDepth) { const next = []; for (const node of frontier) { const neighbors = adj.get(node) || []; for (const n of neighbors) { if (n === dst) return true; if (!visited.has(n)) { visited.add(n); next.push(n); } } } frontier = next; depth++; } return false; } describe('Fast-check: multi-hop invariants', () => { test('zero-hop is denied unless explicitly enabled', () => { const arbiter = new Arbiter(); arbiter.addNode('node:0', 'node'); arbiter.setRelationConfig('link', { type: 'direct' }); arbiter.setRelationConfig('reachable', { type: 'multi_hop', relation: 'link', maxDepth: 2 }); const defaultResult = arbiter.check('node:0', 'reachable', 'node:0'); assert.strictEqual(defaultResult.possibility > 0, false); arbiter.setRelationConfig('reachable_allow', { type: 'multi_hop', relation: 'link', maxDepth: 2, allowZeroHop: true }); const allowedResult = arbiter.check('node:0', 'reachable_allow', 'node:0'); assert.strictEqual(allowedResult.possibility > 0, true); }); test('multi-hop reachability matches bounded BFS', () => { fc.assert( fc.property( fc.integer({ min: 2, max: 6 }), fc.integer({ min: 1, max: 4 }), fc.array( fc.record({ src: fc.integer({ min: 0, max: 5 }), dst: fc.integer({ min: 0, max: 5 }) }), { minLength: 1, maxLength: 20 } ), (nodeCount, maxDepth, edges) => { const arbiter = new Arbiter(); for (let i = 0; i < nodeCount; i++) arbiter.addNode(`node:${i}`, 'node'); arbiter.setRelationConfig('link', { type: 'direct' }); arbiter.setRelationConfig('reachable', { type: 'multi_hop', relation: 'link', maxDepth, allowZeroHop: false }); const adj = new Map(); for (const edge of edges) { const src = edge.src % nodeCount; const dst = edge.dst % nodeCount; arbiter.addRelation(`node:${src}`, 'link', `node:${dst}`, 1.0); const list = adj.get(src) || []; list.push(dst); adj.set(src, list); } for (let i = 0; i < nodeCount; i++) { for (let j = 0; j < nodeCount; j++) { const expected = hasPathWithin(adj, i, j, maxDepth); const result = arbiter.check(`node:${i}`, 'reachable', `node:${j}`); assert.strictEqual(result.possibility > 0, expected); } } } ), { numRuns: 30 } ); }); });