229 lines
7.5 KiB
TypeScript
Raw Permalink Normal View History

// packages/federation/tests/integration/inbound-routing.spec.ts
//
// FP-21-3: FederationPeer inbound → $inbox
//
// Predicate: FederationPeer converts inbound env.to.path (old per-op format
// like 'daemon.accepts.$introspect') into the actor's $inbox topic with op
// extracted into the MxEnvelope.
//
// 1. Inbound daemon.accepts.$introspect → actor's $inbox with op='$introspect'
// 2. Inbound daemon.accepts.scan → actor's $inbox with op='scan'
// 3. Reply reaches sender's ingress topic with matching correlationId
// 4. Two concurrent requests each get their own correct reply
import { describe, test, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import {
FederationPeer,
ExactInstanceResolver,
} from '@open-matrix/federation/federation_peer';
import { InMemoryFilterBroker } from '@open-matrix/federation/transport/inmem';
import {
getMxIngressTopic,
getInternalTopic,
createMxEnvV1,
serializeMxEnvV1,
} from '../../src';
function delay(ms: number): Promise<void> {
return new Promise(r => setTimeout(r, ms));
}
const REALM = 'test-realm';
const CLIENT_REALM = 'client-realm';
describe('FP-21-3: FederationPeer inbound → $inbox', () => {
let backbone: InMemoryFilterBroker;
let localBus: InMemoryFilterBroker;
let peer: FederationPeer;
beforeEach(async () => {
backbone = new InMemoryFilterBroker();
localBus = new InMemoryFilterBroker();
peer = new FederationPeer({
localRealm: REALM,
localBus,
backbone,
internalTimeoutMs: 2000,
});
await peer.start();
});
afterEach(() => {
peer.stop();
});
test('routes daemon.accepts.$introspect to $inbox with op=$introspect', async () => {
let received: Record<string, unknown> | null = null;
// Subscribe to the $inbox topic (what a real actor would listen on)
localBus.subscribe('daemon/$inbox', (_t, p) => {
received = JSON.parse(p);
});
// Send inbound command with old per-op path format
const cmd = createMxEnvV1({
msgType: 'Cmd',
toRealm: REALM,
toPath: 'daemon.accepts.$introspect',
fromRealm: CLIENT_REALM,
fromPath: '_client',
correlationId: 'test-corr-1',
replyToRealm: CLIENT_REALM,
replyToPath: '_replies.emits.1',
body: { depth: 1 },
});
backbone.publish(getMxIngressTopic(REALM, '00'), serializeMxEnvV1(cmd));
await delay(50);
assert.ok(received, 'Should receive on $inbox topic');
const r = received as Record<string, unknown>;
assert.equal(r.op, '$introspect', 'op should be $introspect');
assert.deepEqual(r.payload, { depth: 1 }, 'payload should match body');
assert.equal(r.correlationId, 'test-corr-1', 'correlationId should match');
assert.ok(r.replyTo, 'Should have replyTo');
});
test('routes daemon.accepts.scan to $inbox with op=scan', async () => {
let received: Record<string, unknown> | null = null;
localBus.subscribe('daemon/$inbox', (_t, p) => {
received = JSON.parse(p);
});
const cmd = createMxEnvV1({
msgType: 'Cmd',
toRealm: REALM,
toPath: 'daemon.accepts.scan',
fromRealm: CLIENT_REALM,
fromPath: '_client',
correlationId: 'test-corr-2',
replyToRealm: CLIENT_REALM,
replyToPath: '_replies.emits.2',
body: { path: '/home' },
});
backbone.publish(getMxIngressTopic(REALM, '00'), serializeMxEnvV1(cmd));
await delay(50);
assert.ok(received, 'Should receive on $inbox topic');
const r = received as Record<string, unknown>;
assert.equal(r.op, 'scan', 'op should be scan');
assert.deepEqual(r.payload, { path: '/home' }, 'payload should match body');
});
test('reply reaches sender with matching correlationId', async () => {
// Set up handler on $inbox that sends reply
localBus.subscribe('component/$inbox', (_t, p) => {
const req = JSON.parse(p);
localBus.publish(req.replyTo, JSON.stringify({
correlationId: req.correlationId,
ok: true,
result: { type: 'TestComponent', mount: 'component' },
}));
});
// Capture reply on backbone
let reply: Record<string, unknown> | null = null;
backbone.subscribe(getMxIngressTopic(CLIENT_REALM, '00'), (_t, p) => {
reply = JSON.parse(p);
});
const cmd = createMxEnvV1({
msgType: 'Cmd',
toRealm: REALM,
toPath: 'component.accepts.$introspect',
fromRealm: CLIENT_REALM,
fromPath: '_client',
correlationId: 'reply-test-corr',
replyToRealm: CLIENT_REALM,
replyToPath: '_replies.emits.reply-test',
body: { depth: 0 },
});
backbone.publish(getMxIngressTopic(REALM, '00'), serializeMxEnvV1(cmd));
await delay(100);
assert.ok(reply, 'Should receive reply on backbone');
const r = reply as Record<string, unknown>;
assert.equal(r.msgType, 'Result', 'reply should be Result type');
assert.equal(r.ok, true, 'reply should be ok');
assert.equal(r.correlationId, 'reply-test-corr', 'correlationId should match');
const body = r.body as { type: string; mount: string };
assert.equal(body.type, 'TestComponent', 'reply body should match');
assert.equal(body.mount, 'component', 'reply body should match');
});
test('two concurrent requests get their own correct replies', async () => {
// Handler on $inbox dispatches by op
localBus.subscribe('svc/$inbox', (_t, p) => {
const req = JSON.parse(p);
let result: unknown;
if (req.op === '$introspect') {
result = { type: 'ServiceActor', mount: 'svc' };
} else if (req.op === 'ping') {
result = { message: 'pong', echo: req.payload };
} else {
result = { error: 'unknown op' };
}
localBus.publish(req.replyTo, JSON.stringify({
correlationId: req.correlationId,
ok: true,
result,
}));
});
// Capture replies
const replies: Record<string, unknown>[] = [];
backbone.subscribe(getMxIngressTopic(CLIENT_REALM, '00'), (_t, p) => {
replies.push(JSON.parse(p));
});
// Send two concurrent requests with different ops
const cmd1 = createMxEnvV1({
msgType: 'Cmd',
toRealm: REALM,
toPath: 'svc.accepts.$introspect',
fromRealm: CLIENT_REALM,
fromPath: '_client',
correlationId: 'concurrent-1',
replyToRealm: CLIENT_REALM,
replyToPath: '_replies.emits.c1',
body: { depth: 1 },
});
const cmd2 = createMxEnvV1({
msgType: 'Cmd',
toRealm: REALM,
toPath: 'svc.accepts.ping',
fromRealm: CLIENT_REALM,
fromPath: '_client',
correlationId: 'concurrent-2',
replyToRealm: CLIENT_REALM,
replyToPath: '_replies.emits.c2',
body: { msg: 'hello' },
});
backbone.publish(getMxIngressTopic(REALM, '00'), serializeMxEnvV1(cmd1));
backbone.publish(getMxIngressTopic(REALM, '00'), serializeMxEnvV1(cmd2));
await delay(100);
assert.equal(replies.length, 2, 'Should receive 2 replies');
// Find replies by correlationId
const r1 = replies.find(r => r.correlationId === 'concurrent-1');
const r2 = replies.find(r => r.correlationId === 'concurrent-2');
assert.ok(r1, 'Should have reply for concurrent-1');
assert.ok(r2, 'Should have reply for concurrent-2');
const body1 = (r1 as Record<string, unknown>).body as { type: string };
assert.equal(body1.type, 'ServiceActor', 'introspect reply should have correct type');
const body2 = (r2 as Record<string, unknown>).body as { message: string };
assert.equal(body2.message, 'pong', 'ping reply should have pong');
});
});