Full SDK workspace: core, contracts, sdk, cli, browser-host, browser-kit, federation, omega-core, oracle, self-healing, strategies, tools, emacs. Clean extraction from the development monorepo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
229 lines
7.5 KiB
TypeScript
229 lines
7.5 KiB
TypeScript
// 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');
|
|
});
|
|
});
|