mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-09-14 14:01:28 +08:00
3cec3bf94a
Phase 8 (BEYOND-SOTA MOAT):
domain/federation-protocol.ts — 4 wire message types over ADR-104:
pr_artifact_request — peer asks for a PR over (graphId, queryNode)
with lastKnownGraphHash + lastKnownResultHash
for stale-detection
pr_artifact_response — peer ships a Phase 7 signed envelope back
pr_artifact_delta — sparse delta over a known base resultHash
pr_artifact_stale — graph changed; consumer must recompute
All Zod-validated, discriminated-union routable.
application/federation-server.ts — receives requests, rate-limits per
peer (default 60/min), computes via Phase 2-6 adapters + runPageRank,
seals via Phase 7 sealArtifact. Returns stale rejects when graphHash
mismatches or graphId unknown.
application/federation-client.ts — sends requests via FederationTransport;
verifies inbound envelopes against trusted-public-keys list; falls back
to local compute (origin: 'peer' / 'stale-fallback' /
'untrusted-fallback' / 'local-fallback') with reason.
inProcessTransport() helper stitches a client to a server for in-process
testing without spinning up real ADR-104 wiring.
104/104 tests passing (+6 for Phase 8):
- phase8-federation.test.ts: 6 (verifiable peer artifact, untrusted-key
fallback, stale graphHash, unknown graphId, rate limit per peer,
unreachable-peer local fallback)
All 8 phases of ADR-123 are now feature-complete. Plugin builds clean,
ships at 0.1.0-alpha.1 with 104 passing tests across the eleven wedges
(W1-W11) + Wedge 12 streaming + the beyond-SOTA Phase 7+8 federation
artifact pipeline.
Next: publish ruflo-graph-intelligence to npm, open + merge PR, create
announcement gist with intro/features/usages (practical to exotic).
Co-Authored-By: RuFlo <ruv@ruv.net>
195 lines
6.5 KiB
TypeScript
195 lines
6.5 KiB
TypeScript
/**
|
|
* Phase 8 Tests — Federation Distribution of Signed PR Vectors (beyond-SOTA)
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { FederationServer } from '../src/application/federation-server.js';
|
|
import {
|
|
FederationClient,
|
|
inProcessTransport,
|
|
} from '../src/application/federation-client.js';
|
|
import { generateWitnessKey } from '../src/infrastructure/witness-signer.js';
|
|
import { resetRegistry, getRegistry, type SublinearAdapter } from '../src/domain/adapter.js';
|
|
import type { SparseMatrix } from '../src/domain/types.js';
|
|
import { createHash } from 'node:crypto';
|
|
|
|
function ddAdapter(graphId: string, n = 6): SublinearAdapter {
|
|
const entries = [];
|
|
const nodeIndex: Record<string, number> = {};
|
|
const indexNode: string[] = [];
|
|
for (let i = 0; i < n; i++) {
|
|
nodeIndex[`n${i}`] = i;
|
|
indexNode.push(`n${i}`);
|
|
entries.push({ row: i, col: i, value: 5 });
|
|
if (i > 0) entries.push({ row: i, col: i - 1, value: -1 });
|
|
if (i < n - 1) entries.push({ row: i, col: i + 1, value: -1 });
|
|
}
|
|
const contentHash = createHash('sha256')
|
|
.update(JSON.stringify({ graphId, n }))
|
|
.digest('hex');
|
|
const matrix: SparseMatrix = {
|
|
graphId,
|
|
size: n,
|
|
entries,
|
|
nodeIndex,
|
|
indexNode,
|
|
capturedAt: '2026-05-19T00:00:00Z',
|
|
contentHash,
|
|
};
|
|
return {
|
|
graphId,
|
|
ownerPlugin: 'test',
|
|
async exportAsSparseMatrix() {
|
|
return matrix;
|
|
},
|
|
};
|
|
}
|
|
|
|
describe('Federation round-trip', () => {
|
|
beforeEach(() => resetRegistry());
|
|
|
|
it('client receives a verifiable peer artifact', async () => {
|
|
const adapter = ddAdapter('fed:test', 6);
|
|
getRegistry().register(adapter);
|
|
const key = generateWitnessKey();
|
|
const server = new FederationServer({
|
|
installationId: 'inst-A',
|
|
witnessKey: key,
|
|
witnessKeyId: 'key-v1',
|
|
transport: { async send() { return null; }, onMessage() { return () => {}; } },
|
|
});
|
|
const client = new FederationClient({
|
|
installationId: 'inst-B',
|
|
transport: inProcessTransport(server),
|
|
trustedPublicKeys: [key.publicKeyHex],
|
|
});
|
|
const result = await client.fetchPageRank({
|
|
peer: 'inst-A',
|
|
graphId: 'fed:test',
|
|
nodeId: 'n2',
|
|
});
|
|
expect(result.origin).toBe('peer');
|
|
expect(result.verification?.valid).toBe(true);
|
|
expect(result.result.score).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
it('client falls back to local on untrusted signer', async () => {
|
|
const adapter = ddAdapter('fed:test', 6);
|
|
getRegistry().register(adapter);
|
|
const strangerKey = generateWitnessKey();
|
|
const server = new FederationServer({
|
|
installationId: 'inst-A',
|
|
witnessKey: strangerKey,
|
|
witnessKeyId: 'key-v1',
|
|
transport: { async send() { return null; }, onMessage() { return () => {}; } },
|
|
});
|
|
const client = new FederationClient({
|
|
installationId: 'inst-B',
|
|
transport: inProcessTransport(server),
|
|
trustedPublicKeys: ['0'.repeat(64)], // stranger NOT in trust list
|
|
});
|
|
const result = await client.fetchPageRank({
|
|
peer: 'inst-A',
|
|
graphId: 'fed:test',
|
|
nodeId: 'n2',
|
|
});
|
|
expect(result.origin).toBe('untrusted-fallback');
|
|
expect(result.fallbackReason).toMatch(/trusted list/i);
|
|
expect(result.result.score).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
it('server sends pr_artifact_stale when graphHash differs', async () => {
|
|
const adapter = ddAdapter('fed:test', 6);
|
|
getRegistry().register(adapter);
|
|
const key = generateWitnessKey();
|
|
const server = new FederationServer({
|
|
installationId: 'inst-A',
|
|
witnessKey: key,
|
|
witnessKeyId: 'key-v1',
|
|
transport: { async send() { return null; }, onMessage() { return () => {}; } },
|
|
});
|
|
const client = new FederationClient({
|
|
installationId: 'inst-B',
|
|
transport: inProcessTransport(server),
|
|
trustedPublicKeys: [key.publicKeyHex],
|
|
});
|
|
const result = await client.fetchPageRank({
|
|
peer: 'inst-A',
|
|
graphId: 'fed:test',
|
|
nodeId: 'n2',
|
|
lastKnownGraphHash: '0'.repeat(64), // doesn't match what server has
|
|
});
|
|
expect(result.origin).toBe('stale-fallback');
|
|
expect(result.fallbackReason).toMatch(/changed/);
|
|
});
|
|
|
|
it('server returns stale-reject for an unknown graphId', async () => {
|
|
const key = generateWitnessKey();
|
|
const server = new FederationServer({
|
|
installationId: 'inst-A',
|
|
witnessKey: key,
|
|
witnessKeyId: 'key-v1',
|
|
transport: { async send() { return null; }, onMessage() { return () => {}; } },
|
|
});
|
|
const client = new FederationClient({
|
|
installationId: 'inst-B',
|
|
transport: inProcessTransport(server),
|
|
});
|
|
// No adapter registered on client OR server side
|
|
try {
|
|
await client.fetchPageRank({
|
|
peer: 'inst-A',
|
|
graphId: 'fed:nonexistent',
|
|
nodeId: 'n2',
|
|
});
|
|
throw new Error('expected localCompute to throw');
|
|
} catch (err) {
|
|
expect((err as Error).message).toMatch(/no adapter/);
|
|
}
|
|
});
|
|
|
|
it('server rate-limits per peer', async () => {
|
|
const adapter = ddAdapter('fed:test', 6);
|
|
getRegistry().register(adapter);
|
|
const key = generateWitnessKey();
|
|
const server = new FederationServer({
|
|
installationId: 'inst-A',
|
|
witnessKey: key,
|
|
witnessKeyId: 'key-v1',
|
|
transport: { async send() { return null; }, onMessage() { return () => {}; } },
|
|
rateLimitPerPeerPerMinute: 2,
|
|
});
|
|
const client = new FederationClient({
|
|
installationId: 'inst-B',
|
|
transport: inProcessTransport(server),
|
|
trustedPublicKeys: [key.publicKeyHex],
|
|
});
|
|
const r1 = await client.fetchPageRank({ peer: 'inst-A', graphId: 'fed:test', nodeId: 'n2' });
|
|
const r2 = await client.fetchPageRank({ peer: 'inst-A', graphId: 'fed:test', nodeId: 'n3' });
|
|
const r3 = await client.fetchPageRank({ peer: 'inst-A', graphId: 'fed:test', nodeId: 'n4' });
|
|
expect(r1.origin).toBe('peer');
|
|
expect(r2.origin).toBe('peer');
|
|
expect(r3.origin).toBe('stale-fallback');
|
|
expect(r3.fallbackReason).toMatch(/rate limit/i);
|
|
});
|
|
|
|
it('client falls back to local compute when no usable response', async () => {
|
|
const adapter = ddAdapter('fed:test', 6);
|
|
getRegistry().register(adapter);
|
|
const client = new FederationClient({
|
|
installationId: 'inst-B',
|
|
transport: {
|
|
async send() { return null; }, // server unreachable
|
|
onMessage() { return () => {}; },
|
|
},
|
|
});
|
|
const result = await client.fetchPageRank({
|
|
peer: 'inst-A',
|
|
graphId: 'fed:test',
|
|
nodeId: 'n2',
|
|
});
|
|
expect(result.origin).toBe('local-fallback');
|
|
expect(result.result.score).toBeGreaterThanOrEqual(0);
|
|
});
|
|
});
|