agent-inbox 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +44 -7
- package/README.md +67 -24
- package/dist/federation/connection-manager.d.ts +13 -2
- package/dist/federation/connection-manager.d.ts.map +1 -1
- package/dist/federation/connection-manager.js +109 -10
- package/dist/federation/connection-manager.js.map +1 -1
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -0
- package/dist/index.mjs.map +1 -0
- package/dist/ipc/ipc-server.d.ts +2 -0
- package/dist/ipc/ipc-server.d.ts.map +1 -1
- package/dist/ipc/ipc-server.js +48 -0
- package/dist/ipc/ipc-server.js.map +1 -1
- package/dist/map/map-client.d.ts +100 -0
- package/dist/map/map-client.d.ts.map +1 -1
- package/dist/map/map-client.js +61 -0
- package/dist/map/map-client.js.map +1 -1
- package/dist/mcp/mcp-proxy.d.ts +28 -0
- package/dist/mcp/mcp-proxy.d.ts.map +1 -0
- package/dist/mcp/mcp-proxy.js +280 -0
- package/dist/mcp/mcp-proxy.js.map +1 -0
- package/dist/mesh/delivery-bridge.d.ts +47 -0
- package/dist/mesh/delivery-bridge.d.ts.map +1 -0
- package/dist/mesh/delivery-bridge.js +73 -0
- package/dist/mesh/delivery-bridge.js.map +1 -0
- package/dist/mesh/mesh-connector.d.ts +29 -0
- package/dist/mesh/mesh-connector.d.ts.map +1 -0
- package/dist/mesh/mesh-connector.js +36 -0
- package/dist/mesh/mesh-connector.js.map +1 -0
- package/dist/mesh/mesh-transport.d.ts +70 -0
- package/dist/mesh/mesh-transport.d.ts.map +1 -0
- package/dist/mesh/mesh-transport.js +92 -0
- package/dist/mesh/mesh-transport.js.map +1 -0
- package/dist/mesh/type-mapper.d.ts +67 -0
- package/dist/mesh/type-mapper.d.ts.map +1 -0
- package/dist/mesh/type-mapper.js +165 -0
- package/dist/mesh/type-mapper.js.map +1 -0
- package/dist/types.d.ts +29 -2
- package/dist/types.d.ts.map +1 -1
- package/docs/CLAUDE-CODE-SWARM-PROPOSAL.md +137 -0
- package/package.json +7 -2
- package/src/federation/connection-manager.ts +125 -10
- package/src/index.ts +96 -5
- package/src/ipc/ipc-server.ts +58 -0
- package/src/map/map-client.ts +152 -0
- package/src/mcp/mcp-proxy.ts +326 -0
- package/src/mesh/delivery-bridge.ts +110 -0
- package/src/mesh/mesh-connector.ts +41 -0
- package/src/mesh/mesh-transport.ts +157 -0
- package/src/mesh/type-mapper.ts +239 -0
- package/src/types.ts +33 -1
- package/test/federation/integration.test.ts +37 -3
- package/test/federation/sdk-integration.test.ts +4 -8
- package/test/ipc-new-commands.test.ts +200 -0
- package/test/mcp-proxy.test.ts +191 -0
- package/test/mesh/delivery-bridge.test.ts +178 -0
- package/test/mesh/e2e-mesh.test.ts +527 -0
- package/test/mesh/e2e-real-meshpeer.test.ts +629 -0
- package/test/mesh/federation-mesh.test.ts +269 -0
- package/test/mesh/mesh-connector.test.ts +66 -0
- package/test/mesh/mesh-transport.test.ts +191 -0
- package/test/mesh/meshpeer-integration.test.ts +442 -0
- package/test/mesh/mock-mesh.ts +125 -0
- package/test/mesh/mock-meshpeer.ts +266 -0
- package/test/mesh/type-mapper.test.ts +226 -0
- package/docs/PLAN.md +0 -545
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 2 integration tests: agent-inbox + MeshPeer.
|
|
3
|
+
*
|
|
4
|
+
* Tests the full flow of agent-inbox using a mock MeshPeer:
|
|
5
|
+
* - connectViaMesh() registers agent-inbox as an agent
|
|
6
|
+
* - DeliveryHandler bridge routes MAP messages to inbox storage
|
|
7
|
+
* - ConnectionManager uses FederationGateway for cross-mesh routing
|
|
8
|
+
* - Type mapping preserves inbox fields through _meta roundtrip
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
12
|
+
import { EventEmitter } from "node:events";
|
|
13
|
+
import { InMemoryStorage } from "../../src/storage/memory.js";
|
|
14
|
+
import { MessageRouter } from "../../src/router/message-router.js";
|
|
15
|
+
import { MapClient } from "../../src/map/map-client.js";
|
|
16
|
+
import type {
|
|
17
|
+
MeshPeerLike,
|
|
18
|
+
MeshAgentConnection,
|
|
19
|
+
MeshMapServer,
|
|
20
|
+
MeshMapMessage,
|
|
21
|
+
MeshDeliveryHandler,
|
|
22
|
+
MeshFederationGateway,
|
|
23
|
+
} from "../../src/map/map-client.js";
|
|
24
|
+
import { ConnectionManager } from "../../src/federation/connection-manager.js";
|
|
25
|
+
import { DeliveryBridge } from "../../src/mesh/delivery-bridge.js";
|
|
26
|
+
import type { Message } from "../../src/types.js";
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Mock MeshPeer (simulates agentic-mesh v0.2.0)
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
class MockAgentConnection extends EventEmitter implements MeshAgentConnection {
|
|
33
|
+
readonly agentId: string;
|
|
34
|
+
readonly isRegistered = true;
|
|
35
|
+
private _registered = false;
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
agentId: string,
|
|
39
|
+
private server: MockMapServer
|
|
40
|
+
) {
|
|
41
|
+
super();
|
|
42
|
+
this.agentId = agentId;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async register() {
|
|
46
|
+
this._registered = true;
|
|
47
|
+
return {
|
|
48
|
+
id: this.agentId,
|
|
49
|
+
name: this.agentId,
|
|
50
|
+
state: "active" as const,
|
|
51
|
+
ownerId: null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async unregister() {
|
|
56
|
+
this._registered = false;
|
|
57
|
+
this.server.removeMessageHandler(this.agentId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async send(to: unknown, payload: unknown, meta?: Record<string, unknown>) {
|
|
61
|
+
return { messageId: `msg-${Date.now()}`, delivered: [] as string[] };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class MockMapServer extends EventEmitter implements MeshMapServer {
|
|
66
|
+
readonly systemId = "mock-mesh-system";
|
|
67
|
+
readonly systemName = "Mock Mesh";
|
|
68
|
+
|
|
69
|
+
private messageHandlers = new Map<string, (agentId: string, message: MeshMapMessage) => void>();
|
|
70
|
+
private deliveryHandler: MeshDeliveryHandler | null = null;
|
|
71
|
+
private agents = new Map<string, unknown>();
|
|
72
|
+
|
|
73
|
+
registerAgent(params: { agentId?: string; name?: string; role?: string }) {
|
|
74
|
+
const id = params.agentId ?? `agent-${Date.now()}`;
|
|
75
|
+
const agent = { id, name: params.name, role: params.role, state: "active", ownerId: null };
|
|
76
|
+
this.agents.set(id, agent);
|
|
77
|
+
return agent;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
unregisterAgent(agentId: string) {
|
|
81
|
+
this.agents.delete(agentId);
|
|
82
|
+
this.messageHandlers.delete(agentId);
|
|
83
|
+
return { id: agentId, state: "stopped", ownerId: null };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setMessageHandler(agentId: string, handler: (agentId: string, message: MeshMapMessage) => void) {
|
|
87
|
+
this.messageHandlers.set(agentId, handler);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
removeMessageHandler(agentId: string) {
|
|
91
|
+
this.messageHandlers.delete(agentId);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
setDeliveryHandler(handler: MeshDeliveryHandler): MeshDeliveryHandler {
|
|
95
|
+
const prev = this.deliveryHandler ?? {
|
|
96
|
+
async deliverToAgent() { return false; },
|
|
97
|
+
async forwardToPeer() { return false; },
|
|
98
|
+
};
|
|
99
|
+
this.deliveryHandler = handler;
|
|
100
|
+
return prev;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Simulate delivering a message to an agent via the message handler */
|
|
104
|
+
simulateIncomingMessage(agentId: string, message: MeshMapMessage) {
|
|
105
|
+
const handler = this.messageHandlers.get(agentId);
|
|
106
|
+
if (handler) {
|
|
107
|
+
handler(agentId, message);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Simulate delivering via the delivery handler (like MapServer's router would) */
|
|
112
|
+
async simulateDeliveryToAgent(agentId: string, message: MeshMapMessage): Promise<boolean> {
|
|
113
|
+
if (this.deliveryHandler) {
|
|
114
|
+
return this.deliveryHandler.deliverToAgent(agentId, message);
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
class MockFederationGateway extends EventEmitter implements MeshFederationGateway {
|
|
121
|
+
readonly localSystemId: string;
|
|
122
|
+
readonly remoteSystemId: string;
|
|
123
|
+
isConnected = true;
|
|
124
|
+
routedMessages: Array<{ message: unknown; targetAgentIds: string[] }> = [];
|
|
125
|
+
|
|
126
|
+
constructor(localSystemId: string, remoteSystemId: string) {
|
|
127
|
+
super();
|
|
128
|
+
this.localSystemId = localSystemId;
|
|
129
|
+
this.remoteSystemId = remoteSystemId;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async route(message: unknown, targetAgentIds: string[]): Promise<boolean> {
|
|
133
|
+
this.routedMessages.push({ message, targetAgentIds });
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
class MockMeshPeer extends EventEmitter implements MeshPeerLike {
|
|
139
|
+
readonly peerId: string;
|
|
140
|
+
readonly server: MockMapServer;
|
|
141
|
+
private agentConnections = new Map<string, MockAgentConnection>();
|
|
142
|
+
private gateways = new Map<string, MockFederationGateway>();
|
|
143
|
+
|
|
144
|
+
constructor(peerId: string) {
|
|
145
|
+
super();
|
|
146
|
+
this.peerId = peerId;
|
|
147
|
+
this.server = new MockMapServer();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async createAgent(config: {
|
|
151
|
+
agentId?: string;
|
|
152
|
+
name?: string;
|
|
153
|
+
role?: string;
|
|
154
|
+
scopes?: string[];
|
|
155
|
+
capabilities?: Record<string, unknown>;
|
|
156
|
+
metadata?: Record<string, unknown>;
|
|
157
|
+
}): Promise<MeshAgentConnection> {
|
|
158
|
+
const agentId = config.agentId ?? `agent-${Date.now()}`;
|
|
159
|
+
this.server.registerAgent({
|
|
160
|
+
agentId,
|
|
161
|
+
name: config.name,
|
|
162
|
+
role: config.role,
|
|
163
|
+
});
|
|
164
|
+
const conn = new MockAgentConnection(agentId, this.server);
|
|
165
|
+
await conn.register();
|
|
166
|
+
this.agentConnections.set(agentId, conn);
|
|
167
|
+
return conn;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async federateWith(remoteSystemId: string, _config?: unknown): Promise<MockFederationGateway> {
|
|
171
|
+
const gateway = new MockFederationGateway(this.peerId, remoteSystemId);
|
|
172
|
+
this.gateways.set(remoteSystemId, gateway);
|
|
173
|
+
return gateway;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
getFederationGateway(remoteSystemId: string): MockFederationGateway | undefined {
|
|
177
|
+
return this.gateways.get(remoteSystemId);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Tests
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
describe("Phase 2: MeshPeer integration", () => {
|
|
186
|
+
let storage: InMemoryStorage;
|
|
187
|
+
let events: EventEmitter;
|
|
188
|
+
let router: MessageRouter;
|
|
189
|
+
let mapClient: MapClient;
|
|
190
|
+
let meshPeer: MockMeshPeer;
|
|
191
|
+
|
|
192
|
+
beforeEach(() => {
|
|
193
|
+
storage = new InMemoryStorage();
|
|
194
|
+
events = new EventEmitter();
|
|
195
|
+
router = new MessageRouter(storage, events, "default");
|
|
196
|
+
mapClient = new MapClient(storage, router, events);
|
|
197
|
+
meshPeer = new MockMeshPeer("local-system");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
afterEach(async () => {
|
|
201
|
+
await mapClient.disconnect();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("connectViaMesh()", () => {
|
|
205
|
+
it("should register agent-inbox as an agent on the MeshPeer", async () => {
|
|
206
|
+
const systemId = await mapClient.connectViaMesh(meshPeer);
|
|
207
|
+
|
|
208
|
+
expect(systemId).toBe("mock-mesh-system");
|
|
209
|
+
expect(mapClient.getMeshPeer()).toBe(meshPeer);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("should set up message handler for incoming messages", async () => {
|
|
213
|
+
await mapClient.connectViaMesh(meshPeer);
|
|
214
|
+
|
|
215
|
+
const messageSpy = vi.fn();
|
|
216
|
+
events.on("message.created", messageSpy);
|
|
217
|
+
|
|
218
|
+
// Simulate an incoming MAP message to agent-inbox
|
|
219
|
+
meshPeer.server.simulateIncomingMessage("agent-inbox", {
|
|
220
|
+
id: "map-msg-1",
|
|
221
|
+
from: "remote-agent",
|
|
222
|
+
to: { agent: "agent-inbox" },
|
|
223
|
+
timestamp: Date.now(),
|
|
224
|
+
payload: { type: "text", text: "hello from mesh" },
|
|
225
|
+
meta: { priority: "high" },
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(messageSpy).toHaveBeenCalledTimes(1);
|
|
229
|
+
const storedMsg = messageSpy.mock.calls[0][0];
|
|
230
|
+
expect(storedMsg.sender_id).toBe("remote-agent");
|
|
231
|
+
expect(storedMsg.importance).toBe("high");
|
|
232
|
+
expect(storedMsg.content).toEqual({ type: "text", text: "hello from mesh" });
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("should disconnect cleanly", async () => {
|
|
236
|
+
await mapClient.connectViaMesh(meshPeer);
|
|
237
|
+
expect(mapClient.getMeshPeer()).toBe(meshPeer);
|
|
238
|
+
|
|
239
|
+
await mapClient.disconnect();
|
|
240
|
+
expect(mapClient.getMeshPeer()).toBeNull();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe("DeliveryBridge via setDeliveryHandler", () => {
|
|
245
|
+
it("should install delivery bridge on MapServer", async () => {
|
|
246
|
+
await mapClient.connectViaMesh(meshPeer);
|
|
247
|
+
|
|
248
|
+
// Install the delivery bridge
|
|
249
|
+
const bridge = new DeliveryBridge(storage, events, "default");
|
|
250
|
+
meshPeer.server.setDeliveryHandler(bridge);
|
|
251
|
+
|
|
252
|
+
// Simulate MapServer routing a message through the delivery handler
|
|
253
|
+
const delivered = await meshPeer.server.simulateDeliveryToAgent("agent-bob", {
|
|
254
|
+
id: "map-msg-2",
|
|
255
|
+
from: "agent-alice",
|
|
256
|
+
to: { agent: "agent-bob" },
|
|
257
|
+
timestamp: Date.now(),
|
|
258
|
+
payload: "hello from delivery bridge",
|
|
259
|
+
meta: { priority: "normal" },
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
expect(delivered).toBe(true);
|
|
263
|
+
|
|
264
|
+
// Verify message is in bob's inbox
|
|
265
|
+
const inbox = storage.getInbox("agent-bob");
|
|
266
|
+
expect(inbox).toHaveLength(1);
|
|
267
|
+
expect(inbox[0].sender_id).toBe("agent-alice");
|
|
268
|
+
expect(inbox[0].content).toEqual({ type: "text", text: "hello from delivery bridge" });
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("should preserve _meta fields through delivery", async () => {
|
|
272
|
+
await mapClient.connectViaMesh(meshPeer);
|
|
273
|
+
|
|
274
|
+
const bridge = new DeliveryBridge(storage, events, "default");
|
|
275
|
+
meshPeer.server.setDeliveryHandler(bridge);
|
|
276
|
+
|
|
277
|
+
await meshPeer.server.simulateDeliveryToAgent("agent-bob", {
|
|
278
|
+
id: "map-msg-3",
|
|
279
|
+
from: "agent-alice",
|
|
280
|
+
to: { agent: "agent-bob" },
|
|
281
|
+
timestamp: Date.now(),
|
|
282
|
+
payload: { type: "text", text: "review this" },
|
|
283
|
+
meta: { priority: "high" },
|
|
284
|
+
_meta: {
|
|
285
|
+
subject: "PR Review",
|
|
286
|
+
threadTag: "pr-42",
|
|
287
|
+
inReplyTo: "prev-msg-1",
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const inbox = storage.getInbox("agent-bob");
|
|
292
|
+
expect(inbox[0].subject).toBe("PR Review");
|
|
293
|
+
expect(inbox[0].thread_tag).toBe("pr-42");
|
|
294
|
+
expect(inbox[0].in_reply_to).toBe("prev-msg-1");
|
|
295
|
+
expect(inbox[0].importance).toBe("high");
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe("ConnectionManager with MeshPeer federation gateway", () => {
|
|
300
|
+
it("should use federateWith() when meshPeer is provided", async () => {
|
|
301
|
+
const cm = new ConnectionManager(
|
|
302
|
+
events,
|
|
303
|
+
{
|
|
304
|
+
systemId: "local-system",
|
|
305
|
+
trust: { allowedServers: [], scopePermissions: {}, requireAuth: false },
|
|
306
|
+
},
|
|
307
|
+
{ meshPeer: meshPeer as unknown as MeshPeerLike }
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const link = await cm.federate({
|
|
311
|
+
systemId: "remote-system",
|
|
312
|
+
meshPeerId: "remote-peer",
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
expect(link.transport).toBe("mesh");
|
|
316
|
+
expect(link.status).toBe("connected");
|
|
317
|
+
expect(cm.hasGateway("remote-system")).toBe(true);
|
|
318
|
+
|
|
319
|
+
await cm.destroy();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("should route messages through FederationGateway", async () => {
|
|
323
|
+
const cm = new ConnectionManager(
|
|
324
|
+
events,
|
|
325
|
+
{
|
|
326
|
+
systemId: "local-system",
|
|
327
|
+
trust: { allowedServers: [], scopePermissions: {}, requireAuth: false },
|
|
328
|
+
},
|
|
329
|
+
{ meshPeer: meshPeer as unknown as MeshPeerLike }
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
await cm.federate({
|
|
333
|
+
systemId: "remote-system",
|
|
334
|
+
meshPeerId: "remote-peer",
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const msg: Message = {
|
|
338
|
+
id: "msg-1",
|
|
339
|
+
scope: "default",
|
|
340
|
+
sender_id: "local-agent",
|
|
341
|
+
recipients: [{ agent_id: "bob@remote-system", kind: "to" }],
|
|
342
|
+
content: { type: "text", text: "hello via gateway" },
|
|
343
|
+
importance: "normal",
|
|
344
|
+
metadata: {},
|
|
345
|
+
created_at: new Date().toISOString(),
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const result = await cm.route(msg);
|
|
349
|
+
expect(result.delivered).toBe(true);
|
|
350
|
+
|
|
351
|
+
// Verify the gateway received the route call
|
|
352
|
+
const gateway = meshPeer.getFederationGateway("remote-system");
|
|
353
|
+
expect(gateway).toBeDefined();
|
|
354
|
+
expect(gateway!.routedMessages).toHaveLength(1);
|
|
355
|
+
expect(gateway!.routedMessages[0].targetAgentIds).toEqual(["bob"]);
|
|
356
|
+
|
|
357
|
+
await cm.destroy();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("should fall back to Phase 1 when meshPeer is not provided", async () => {
|
|
361
|
+
// Import mock mesh for Phase 1 fallback
|
|
362
|
+
const { createLinkedMeshPair } = await import("./mock-mesh.js");
|
|
363
|
+
const { MeshConnector } = await import("../../src/mesh/mesh-connector.js");
|
|
364
|
+
|
|
365
|
+
const [meshA, meshB] = createLinkedMeshPair("system-a", "system-b");
|
|
366
|
+
const meshConnector = new MeshConnector(meshA, "system-a");
|
|
367
|
+
|
|
368
|
+
const cm = new ConnectionManager(
|
|
369
|
+
events,
|
|
370
|
+
{
|
|
371
|
+
systemId: "system-a",
|
|
372
|
+
trust: { allowedServers: [], scopePermissions: {}, requireAuth: false },
|
|
373
|
+
},
|
|
374
|
+
{ meshConnector }
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
const link = await cm.federate({
|
|
378
|
+
systemId: "system-b",
|
|
379
|
+
meshPeerId: "system-b",
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
expect(link.transport).toBe("mesh");
|
|
383
|
+
expect(cm.hasTransport("system-b")).toBe(true);
|
|
384
|
+
// No gateway in Phase 1
|
|
385
|
+
expect(cm.hasGateway("system-b")).toBe(false);
|
|
386
|
+
|
|
387
|
+
await cm.destroy();
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe("_meta roundtrip", () => {
|
|
392
|
+
it("should preserve inbox fields through MAP message roundtrip", async () => {
|
|
393
|
+
const { mapMessageToInbox, inboxMessageToMap } = await import(
|
|
394
|
+
"../../src/mesh/type-mapper.js"
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
// Create an inbox message with all fields
|
|
398
|
+
const original: Message = {
|
|
399
|
+
id: "roundtrip-1",
|
|
400
|
+
scope: "default",
|
|
401
|
+
sender_id: "alice",
|
|
402
|
+
recipients: [{ agent_id: "bob", kind: "to" }],
|
|
403
|
+
content: { type: "text", text: "roundtrip test" },
|
|
404
|
+
subject: "Test Subject",
|
|
405
|
+
thread_tag: "thread-99",
|
|
406
|
+
in_reply_to: "prev-msg",
|
|
407
|
+
conversation_id: "conv-5",
|
|
408
|
+
importance: "high",
|
|
409
|
+
metadata: { customField: "keep" },
|
|
410
|
+
created_at: new Date().toISOString(),
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// Convert inbox → MAP
|
|
414
|
+
const mapData = inboxMessageToMap(original);
|
|
415
|
+
|
|
416
|
+
// Simulate sending through MAP (construct a full MapMessage)
|
|
417
|
+
const mapMsg = {
|
|
418
|
+
id: "map-roundtrip-1",
|
|
419
|
+
from: original.sender_id,
|
|
420
|
+
to: mapData.to,
|
|
421
|
+
timestamp: Date.now(),
|
|
422
|
+
payload: mapData.payload,
|
|
423
|
+
meta: mapData.meta,
|
|
424
|
+
_meta: mapData.meta._meta,
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// Convert MAP → inbox
|
|
428
|
+
const recovered = mapMessageToInbox(mapMsg as any, "default");
|
|
429
|
+
|
|
430
|
+
// Verify all inbox-specific fields survived the roundtrip
|
|
431
|
+
expect(recovered.sender_id).toBe(original.sender_id);
|
|
432
|
+
expect(recovered.content).toEqual(original.content);
|
|
433
|
+
expect(recovered.subject).toBe(original.subject);
|
|
434
|
+
expect(recovered.thread_tag).toBe(original.thread_tag);
|
|
435
|
+
expect(recovered.in_reply_to).toBe(original.in_reply_to);
|
|
436
|
+
expect(recovered.conversation_id).toBe(original.conversation_id);
|
|
437
|
+
expect(recovered.importance).toBe(original.importance);
|
|
438
|
+
expect(recovered.id).toBe(original.id); // inboxMessageId preserved
|
|
439
|
+
expect(recovered.metadata.customField).toBe("keep");
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock MeshContext and MessageChannel for testing MeshTransport
|
|
3
|
+
* without any real agentic-mesh networking.
|
|
4
|
+
*
|
|
5
|
+
* Two MockMeshContext instances can be "linked" so that messages
|
|
6
|
+
* sent from one arrive at the other — simulating a mesh connection.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { EventEmitter } from "node:events";
|
|
10
|
+
import type {
|
|
11
|
+
MeshContextLike,
|
|
12
|
+
MeshChannel,
|
|
13
|
+
MeshPeerInfo,
|
|
14
|
+
InboxWireMessage,
|
|
15
|
+
} from "../../src/mesh/mesh-transport.js";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// MockMessageChannel
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export class MockMessageChannel<T> extends EventEmitter implements MeshChannel<T> {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
private _open = false;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
name: string,
|
|
27
|
+
private mesh: MockMeshContext
|
|
28
|
+
) {
|
|
29
|
+
super();
|
|
30
|
+
this.name = name;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get isOpen(): boolean {
|
|
34
|
+
return this._open;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async open(): Promise<void> {
|
|
38
|
+
this._open = true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async close(): Promise<void> {
|
|
42
|
+
this._open = false;
|
|
43
|
+
this.removeAllListeners();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
send(peerId: string, message: T): boolean {
|
|
47
|
+
if (!this._open) return false;
|
|
48
|
+
|
|
49
|
+
// Deliver to linked mesh peers
|
|
50
|
+
const target = this.mesh._getLinkedPeer(peerId);
|
|
51
|
+
if (!target) return false;
|
|
52
|
+
|
|
53
|
+
const targetChannel = target._getChannel(this.name);
|
|
54
|
+
if (!targetChannel || !targetChannel.isOpen) return false;
|
|
55
|
+
|
|
56
|
+
// Simulate async delivery (next tick)
|
|
57
|
+
const from: MeshPeerInfo = {
|
|
58
|
+
id: this.mesh._peerId,
|
|
59
|
+
groups: [],
|
|
60
|
+
};
|
|
61
|
+
process.nextTick(() => {
|
|
62
|
+
targetChannel.emit("message", message, from);
|
|
63
|
+
});
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// MockMeshContext
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
export class MockMeshContext implements MeshContextLike {
|
|
73
|
+
readonly _peerId: string;
|
|
74
|
+
private channels = new Map<string, MockMessageChannel<unknown>>();
|
|
75
|
+
private linkedPeers = new Map<string, MockMeshContext>();
|
|
76
|
+
|
|
77
|
+
constructor(peerId: string) {
|
|
78
|
+
this._peerId = peerId;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
_getPeerId(): string {
|
|
82
|
+
return this._peerId;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
createChannel<T>(name: string): MeshChannel<T> {
|
|
86
|
+
let ch = this.channels.get(name);
|
|
87
|
+
if (!ch) {
|
|
88
|
+
ch = new MockMessageChannel<unknown>(name, this);
|
|
89
|
+
this.channels.set(name, ch);
|
|
90
|
+
}
|
|
91
|
+
return ch as unknown as MeshChannel<T>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** @internal Get a channel by name (for cross-mesh delivery). */
|
|
95
|
+
_getChannel(name: string): MockMessageChannel<unknown> | undefined {
|
|
96
|
+
return this.channels.get(name);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** @internal Get a linked peer by ID (for cross-mesh delivery). */
|
|
100
|
+
_getLinkedPeer(peerId: string): MockMeshContext | undefined {
|
|
101
|
+
return this.linkedPeers.get(peerId);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Link two MockMeshContext instances so they can exchange messages.
|
|
106
|
+
* Must be called on both sides (or use the static helper).
|
|
107
|
+
*/
|
|
108
|
+
linkPeer(other: MockMeshContext): void {
|
|
109
|
+
this.linkedPeers.set(other._peerId, other);
|
|
110
|
+
other.linkedPeers.set(this._peerId, this);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create a pair of linked mock mesh contexts.
|
|
116
|
+
*/
|
|
117
|
+
export function createLinkedMeshPair(
|
|
118
|
+
peerA: string,
|
|
119
|
+
peerB: string
|
|
120
|
+
): [MockMeshContext, MockMeshContext] {
|
|
121
|
+
const a = new MockMeshContext(peerA);
|
|
122
|
+
const b = new MockMeshContext(peerB);
|
|
123
|
+
a.linkPeer(b);
|
|
124
|
+
return [a, b];
|
|
125
|
+
}
|