221 lines
7.2 KiB
TypeScript
221 lines
7.2 KiB
TypeScript
|
|
import { test } from 'node:test';
|
||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import {
|
||
|
|
FederationPeer, TabInstanceResolver, ExactInstanceResolver,
|
||
|
|
} from '@open-matrix/federation/federation_peer';
|
||
|
|
import { InMemoryExactBroker, InMemoryFilterBroker } from '@open-matrix/federation/transport/inmem';
|
||
|
|
import { getInternalTopic } from '@open-matrix/federation/topics';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Unit tests for FederationPeer (formerly CrossRealmRouter).
|
||
|
|
*
|
||
|
|
* These tests verify:
|
||
|
|
* - Local routing via instanceResolver
|
||
|
|
* - Cross-realm detection (no backbone = error)
|
||
|
|
* - TabInstanceResolver topic rewriting
|
||
|
|
* - ExactInstanceResolver exact matching
|
||
|
|
* - invoke() request/reply semantics
|
||
|
|
*/
|
||
|
|
|
||
|
|
test('FederationPeer: routes local messages to local transport (TabInstanceResolver)', async () => {
|
||
|
|
const localBus = new InMemoryFilterBroker();
|
||
|
|
|
||
|
|
const router = new FederationPeer({
|
||
|
|
localRealm: 'flowpad.tab-123',
|
||
|
|
localBus,
|
||
|
|
instanceResolver: new TabInstanceResolver('flowpad.tab-123'),
|
||
|
|
});
|
||
|
|
|
||
|
|
// Track published messages
|
||
|
|
const published: Array<{ topic: string; payload: string }> = [];
|
||
|
|
localBus.subscribe('flowpad.tab-123.component.accepts.msg', (topic, payload) => {
|
||
|
|
published.push({ topic, payload });
|
||
|
|
});
|
||
|
|
|
||
|
|
// Route to local realm - TabInstanceResolver rewrites slash to dot format
|
||
|
|
await router.route('flowpad.tab-123/component.accepts.msg', { test: true });
|
||
|
|
|
||
|
|
assert.equal(published.length, 1);
|
||
|
|
assert.equal(published[0].topic, 'flowpad.tab-123.component.accepts.msg');
|
||
|
|
assert.deepEqual(JSON.parse(published[0].payload), { test: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
test('FederationPeer: base realm routes locally with TabInstanceResolver', async () => {
|
||
|
|
const localBus = new InMemoryFilterBroker();
|
||
|
|
|
||
|
|
const router = new FederationPeer({
|
||
|
|
localRealm: 'flowpad.tab-123',
|
||
|
|
localBus,
|
||
|
|
instanceResolver: new TabInstanceResolver('flowpad.tab-123'),
|
||
|
|
});
|
||
|
|
|
||
|
|
const published: Array<{ topic: string; payload: string }> = [];
|
||
|
|
localBus.subscribe('flowpad.tab-123.component.accepts.msg', (topic, payload) => {
|
||
|
|
published.push({ topic, payload });
|
||
|
|
});
|
||
|
|
|
||
|
|
// Route using base realm "flowpad" - should be rewritten to tab realm
|
||
|
|
await router.route('flowpad.component.accepts.msg', { test: true });
|
||
|
|
|
||
|
|
assert.equal(published.length, 1);
|
||
|
|
assert.ok(published[0].topic.includes('flowpad.tab-123'), 'Topic should include tab realm');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('FederationPeer: ExactInstanceResolver rejects different realms', async () => {
|
||
|
|
const localBus = new InMemoryExactBroker();
|
||
|
|
|
||
|
|
const router = new FederationPeer({
|
||
|
|
localRealm: 'flowpad.tab-123',
|
||
|
|
localBus,
|
||
|
|
// Default: ExactInstanceResolver
|
||
|
|
});
|
||
|
|
|
||
|
|
// flowpad.tab-456 is a DIFFERENT realm for ExactInstanceResolver
|
||
|
|
// Without backbone, cross-realm route just warns (no error)
|
||
|
|
const published: Array<{ topic: string; payload: string }> = [];
|
||
|
|
localBus.subscribe('flowpad.tab-456.component.accepts.msg', (topic, payload) => {
|
||
|
|
published.push({ topic, payload });
|
||
|
|
});
|
||
|
|
|
||
|
|
await router.route('flowpad.tab-456/component.accepts.msg', { test: true });
|
||
|
|
|
||
|
|
// Should NOT be delivered locally (different realm, no backbone)
|
||
|
|
assert.equal(published.length, 0, 'Different tab suffixes should not route locally');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('FederationPeer: cross-realm publishes to backbone ingress topic', async () => {
|
||
|
|
const localBus = new InMemoryExactBroker();
|
||
|
|
const backbone = new InMemoryFilterBroker();
|
||
|
|
|
||
|
|
const router = new FederationPeer({
|
||
|
|
localRealm: 'flowpad.tab-123',
|
||
|
|
localBus,
|
||
|
|
backbone,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Track messages on backbone
|
||
|
|
const backboneMessages: Array<{ topic: string; payload: string }> = [];
|
||
|
|
backbone.subscribe('daemon.$ingress.00', (topic, payload) => {
|
||
|
|
backboneMessages.push({ topic, payload });
|
||
|
|
});
|
||
|
|
|
||
|
|
// Route to foreign realm
|
||
|
|
await router.route('daemon/project.accepts.scan', { path: '/home' });
|
||
|
|
|
||
|
|
assert.equal(backboneMessages.length, 1);
|
||
|
|
// Should use ingress topic format: {realm}.$ingress.{shard}
|
||
|
|
assert.equal(backboneMessages[0].topic, 'daemon.$ingress.00');
|
||
|
|
|
||
|
|
// Payload should be wrapped with sourceRealm
|
||
|
|
const envelope = JSON.parse(backboneMessages[0].payload);
|
||
|
|
assert.equal(envelope.sourceRoot, 'flowpad.tab-123');
|
||
|
|
});
|
||
|
|
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
|
// invoke() method tests
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
test('FederationPeer.invoke: local invoke with request/reply semantics', async () => {
|
||
|
|
const localBus = new InMemoryFilterBroker();
|
||
|
|
|
||
|
|
const router = new FederationPeer({
|
||
|
|
localRealm: 'flowpad.tab-123',
|
||
|
|
localBus,
|
||
|
|
instanceResolver: new TabInstanceResolver('flowpad.tab-123'),
|
||
|
|
});
|
||
|
|
|
||
|
|
// Set up a responder on the local bus.
|
||
|
|
// With DNS hierarchical addressing, getInternalTopic returns semantic paths.
|
||
|
|
// FP-21-3: Cmd routes to $inbox with MxEnvelope format
|
||
|
|
const browserTopic = 'component/$inbox';
|
||
|
|
localBus.subscribe(browserTopic, (topic, payload) => {
|
||
|
|
const request = JSON.parse(payload);
|
||
|
|
const reply = {
|
||
|
|
ok: true,
|
||
|
|
result: { name: 'TestComponent', version: '1.0' },
|
||
|
|
};
|
||
|
|
localBus.publish(request.replyTo, JSON.stringify(reply));
|
||
|
|
});
|
||
|
|
|
||
|
|
// Invoke local method
|
||
|
|
const result = await router.invoke<{ name: string; version: string }>(
|
||
|
|
'flowpad.tab-123/component.accepts.getInfo',
|
||
|
|
{},
|
||
|
|
1000
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.equal(result.name, 'TestComponent');
|
||
|
|
assert.equal(result.version, '1.0');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('FederationPeer.invoke: local invoke timeout', async () => {
|
||
|
|
const localBus = new InMemoryExactBroker();
|
||
|
|
|
||
|
|
const router = new FederationPeer({
|
||
|
|
localRealm: 'flowpad.tab-123',
|
||
|
|
localBus,
|
||
|
|
});
|
||
|
|
|
||
|
|
// No responder - should timeout
|
||
|
|
await assert.rejects(
|
||
|
|
() => router.invoke(
|
||
|
|
'flowpad.tab-123/component.accepts.noResponse',
|
||
|
|
{},
|
||
|
|
100 // 100ms timeout
|
||
|
|
),
|
||
|
|
/Local RPC timeout/
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('FederationPeer.invoke: local invoke error handling', async () => {
|
||
|
|
const localBus = new InMemoryFilterBroker();
|
||
|
|
|
||
|
|
const router = new FederationPeer({
|
||
|
|
localRealm: 'flowpad.tab-123',
|
||
|
|
localBus,
|
||
|
|
instanceResolver: new TabInstanceResolver('flowpad.tab-123'),
|
||
|
|
});
|
||
|
|
|
||
|
|
// Set up a responder that returns an error
|
||
|
|
// FP-21-3: Cmd routes to $inbox
|
||
|
|
const browserTopic = 'component/$inbox';
|
||
|
|
localBus.subscribe(browserTopic, (topic, payload) => {
|
||
|
|
const request = JSON.parse(payload);
|
||
|
|
const reply = {
|
||
|
|
ok: false,
|
||
|
|
error: { message: 'Something went wrong' },
|
||
|
|
};
|
||
|
|
localBus.publish(request.replyTo, JSON.stringify(reply));
|
||
|
|
});
|
||
|
|
|
||
|
|
// Invoke should throw with error message
|
||
|
|
await assert.rejects(
|
||
|
|
() => router.invoke(
|
||
|
|
'flowpad.tab-123/component.accepts.failingMethod',
|
||
|
|
{},
|
||
|
|
1000
|
||
|
|
),
|
||
|
|
/Something went wrong/
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('FederationPeer.invoke: cross-realm requires backbone transport', async () => {
|
||
|
|
const localBus = new InMemoryExactBroker();
|
||
|
|
|
||
|
|
const router = new FederationPeer({
|
||
|
|
localRealm: 'flowpad.tab-123',
|
||
|
|
localBus,
|
||
|
|
// No backbone provided
|
||
|
|
});
|
||
|
|
|
||
|
|
// Cross-realm invoke should fail without backbone transport
|
||
|
|
await assert.rejects(
|
||
|
|
() => router.invoke(
|
||
|
|
'daemon/project.accepts.scan',
|
||
|
|
{},
|
||
|
|
1000
|
||
|
|
),
|
||
|
|
/No backbone transport/
|
||
|
|
);
|
||
|
|
});
|