// 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 { 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 | 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; 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 | 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; 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 | 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; 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[] = []; 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).body as { type: string }; assert.equal(body1.type, 'ServiceActor', 'introspect reply should have correct type'); const body2 = (r2 as Record).body as { message: string }; assert.equal(body2.message, 'pong', 'ping reply should have pong'); }); });