agent-inbox 0.1.8 → 0.2.0

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.
Files changed (75) hide show
  1. package/CLAUDE.md +44 -7
  2. package/README.md +67 -24
  3. package/dist/cli.d.ts +20 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +89 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/federation/connection-manager.d.ts +13 -2
  8. package/dist/federation/connection-manager.d.ts.map +1 -1
  9. package/dist/federation/connection-manager.js +109 -10
  10. package/dist/federation/connection-manager.js.map +1 -1
  11. package/dist/index.d.mts +2 -0
  12. package/dist/index.d.ts +25 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +58 -5
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +1 -0
  17. package/dist/index.mjs.map +1 -0
  18. package/dist/ipc/ipc-server.d.ts +2 -0
  19. package/dist/ipc/ipc-server.d.ts.map +1 -1
  20. package/dist/ipc/ipc-server.js +48 -0
  21. package/dist/ipc/ipc-server.js.map +1 -1
  22. package/dist/map/map-client.d.ts +100 -0
  23. package/dist/map/map-client.d.ts.map +1 -1
  24. package/dist/map/map-client.js +61 -0
  25. package/dist/map/map-client.js.map +1 -1
  26. package/dist/mcp/mcp-proxy.d.ts +28 -0
  27. package/dist/mcp/mcp-proxy.d.ts.map +1 -0
  28. package/dist/mcp/mcp-proxy.js +280 -0
  29. package/dist/mcp/mcp-proxy.js.map +1 -0
  30. package/dist/mesh/delivery-bridge.d.ts +47 -0
  31. package/dist/mesh/delivery-bridge.d.ts.map +1 -0
  32. package/dist/mesh/delivery-bridge.js +73 -0
  33. package/dist/mesh/delivery-bridge.js.map +1 -0
  34. package/dist/mesh/mesh-connector.d.ts +29 -0
  35. package/dist/mesh/mesh-connector.d.ts.map +1 -0
  36. package/dist/mesh/mesh-connector.js +36 -0
  37. package/dist/mesh/mesh-connector.js.map +1 -0
  38. package/dist/mesh/mesh-transport.d.ts +70 -0
  39. package/dist/mesh/mesh-transport.d.ts.map +1 -0
  40. package/dist/mesh/mesh-transport.js +92 -0
  41. package/dist/mesh/mesh-transport.js.map +1 -0
  42. package/dist/mesh/type-mapper.d.ts +67 -0
  43. package/dist/mesh/type-mapper.d.ts.map +1 -0
  44. package/dist/mesh/type-mapper.js +165 -0
  45. package/dist/mesh/type-mapper.js.map +1 -0
  46. package/dist/types.d.ts +29 -2
  47. package/dist/types.d.ts.map +1 -1
  48. package/docs/CLAUDE-CODE-SWARM-PROPOSAL.md +137 -0
  49. package/package.json +10 -2
  50. package/src/cli.ts +94 -0
  51. package/src/federation/connection-manager.ts +125 -10
  52. package/src/index.ts +96 -5
  53. package/src/ipc/ipc-server.ts +58 -0
  54. package/src/map/map-client.ts +152 -0
  55. package/src/mcp/mcp-proxy.ts +326 -0
  56. package/src/mesh/delivery-bridge.ts +110 -0
  57. package/src/mesh/mesh-connector.ts +41 -0
  58. package/src/mesh/mesh-transport.ts +157 -0
  59. package/src/mesh/type-mapper.ts +239 -0
  60. package/src/types.ts +33 -1
  61. package/test/federation/integration.test.ts +37 -3
  62. package/test/federation/sdk-integration.test.ts +4 -8
  63. package/test/ipc-new-commands.test.ts +200 -0
  64. package/test/mcp-proxy.test.ts +191 -0
  65. package/test/mesh/delivery-bridge.test.ts +178 -0
  66. package/test/mesh/e2e-mesh.test.ts +527 -0
  67. package/test/mesh/e2e-real-meshpeer.test.ts +629 -0
  68. package/test/mesh/federation-mesh.test.ts +269 -0
  69. package/test/mesh/mesh-connector.test.ts +66 -0
  70. package/test/mesh/mesh-transport.test.ts +191 -0
  71. package/test/mesh/meshpeer-integration.test.ts +442 -0
  72. package/test/mesh/mock-mesh.ts +125 -0
  73. package/test/mesh/mock-meshpeer.ts +266 -0
  74. package/test/mesh/type-mapper.test.ts +226 -0
  75. package/docs/PLAN.md +0 -545
@@ -0,0 +1,200 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import * as net from "node:net";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { EventEmitter } from "node:events";
6
+ import { InMemoryStorage } from "../src/storage/memory.js";
7
+ import { MessageRouter } from "../src/router/message-router.js";
8
+ import { IpcServer } from "../src/ipc/ipc-server.js";
9
+
10
+ function tmpSocketPath(): string {
11
+ return path.join(os.tmpdir(), `inbox-test-${Date.now()}-${Math.random().toString(36).slice(2)}.sock`);
12
+ }
13
+
14
+ function sendCommand(socketPath: string, command: object): Promise<Record<string, unknown>> {
15
+ return new Promise((resolve, reject) => {
16
+ const client = net.createConnection(socketPath, () => {
17
+ client.write(JSON.stringify(command) + "\n");
18
+ });
19
+ let buffer = "";
20
+ client.on("data", (data) => {
21
+ buffer += data.toString();
22
+ const idx = buffer.indexOf("\n");
23
+ if (idx !== -1) {
24
+ const line = buffer.slice(0, idx);
25
+ client.end();
26
+ resolve(JSON.parse(line));
27
+ }
28
+ });
29
+ client.on("error", reject);
30
+ });
31
+ }
32
+
33
+ describe("IPC read_thread command", () => {
34
+ let storage: InMemoryStorage;
35
+ let events: EventEmitter;
36
+ let router: MessageRouter;
37
+ let server: IpcServer;
38
+ let socketPath: string;
39
+
40
+ beforeEach(async () => {
41
+ storage = new InMemoryStorage();
42
+ events = new EventEmitter();
43
+ router = new MessageRouter(storage, events, "default");
44
+ socketPath = tmpSocketPath();
45
+ server = new IpcServer(socketPath, router, storage);
46
+ await server.start();
47
+ });
48
+
49
+ afterEach(async () => {
50
+ await server.stop();
51
+ });
52
+
53
+ it("should return empty thread for unknown tag", async () => {
54
+ const resp = await sendCommand(socketPath, {
55
+ action: "read_thread",
56
+ threadTag: "nonexistent",
57
+ });
58
+ expect(resp.ok).toBe(true);
59
+ expect(resp.threadTag).toBe("nonexistent");
60
+ expect(resp.count).toBe(0);
61
+ expect(resp.messages).toEqual([]);
62
+ });
63
+
64
+ it("should return messages in a thread", async () => {
65
+ // Send messages with same threadTag
66
+ await sendCommand(socketPath, {
67
+ action: "send",
68
+ from: "alice",
69
+ to: "bob",
70
+ payload: "first message",
71
+ threadTag: "task-42",
72
+ });
73
+ await sendCommand(socketPath, {
74
+ action: "send",
75
+ from: "bob",
76
+ to: "alice",
77
+ payload: "reply to first",
78
+ threadTag: "task-42",
79
+ });
80
+ // Different thread
81
+ await sendCommand(socketPath, {
82
+ action: "send",
83
+ from: "alice",
84
+ to: "charlie",
85
+ payload: "unrelated",
86
+ threadTag: "task-99",
87
+ });
88
+
89
+ const resp = await sendCommand(socketPath, {
90
+ action: "read_thread",
91
+ threadTag: "task-42",
92
+ scope: "default",
93
+ });
94
+
95
+ expect(resp.ok).toBe(true);
96
+ expect(resp.count).toBe(2);
97
+ const messages = resp.messages as Array<{ sender_id: string }>;
98
+ expect(messages).toHaveLength(2);
99
+ const senders = messages.map((m) => m.sender_id);
100
+ expect(senders).toContain("alice");
101
+ expect(senders).toContain("bob");
102
+ });
103
+ });
104
+
105
+ describe("IPC list_agents command", () => {
106
+ let storage: InMemoryStorage;
107
+ let events: EventEmitter;
108
+ let router: MessageRouter;
109
+ let server: IpcServer;
110
+ let socketPath: string;
111
+
112
+ beforeEach(async () => {
113
+ storage = new InMemoryStorage();
114
+ events = new EventEmitter();
115
+ router = new MessageRouter(storage, events, "default");
116
+ socketPath = tmpSocketPath();
117
+ server = new IpcServer(socketPath, router, storage);
118
+ await server.start();
119
+ });
120
+
121
+ afterEach(async () => {
122
+ await server.stop();
123
+ });
124
+
125
+ it("should return empty list with no agents", async () => {
126
+ const resp = await sendCommand(socketPath, {
127
+ action: "list_agents",
128
+ });
129
+ expect(resp.ok).toBe(true);
130
+ expect(resp.count).toBe(0);
131
+ expect(resp.agents).toEqual([]);
132
+ });
133
+
134
+ it("should list registered agents", async () => {
135
+ // Register agents via notify
136
+ await sendCommand(socketPath, {
137
+ action: "notify",
138
+ event: {
139
+ type: "agent.spawn",
140
+ agent: {
141
+ agentId: "gsd-executor",
142
+ name: "executor",
143
+ scopes: ["swarm:gsd"],
144
+ metadata: { role: "executor" },
145
+ },
146
+ },
147
+ });
148
+ await sendCommand(socketPath, {
149
+ action: "notify",
150
+ event: {
151
+ type: "agent.spawn",
152
+ agent: {
153
+ agentId: "gsd-verifier",
154
+ name: "verifier",
155
+ scopes: ["swarm:gsd"],
156
+ },
157
+ },
158
+ });
159
+
160
+ const resp = await sendCommand(socketPath, {
161
+ action: "list_agents",
162
+ });
163
+
164
+ expect(resp.ok).toBe(true);
165
+ expect(resp.count).toBe(2);
166
+ const agents = resp.agents as Array<{ agentId: string; location: string }>;
167
+ expect(agents).toHaveLength(2);
168
+ const ids = agents.map((a) => a.agentId);
169
+ expect(ids).toContain("gsd-executor");
170
+ expect(ids).toContain("gsd-verifier");
171
+ expect(agents[0].location).toBe("local");
172
+ });
173
+
174
+ it("should filter agents by scope", async () => {
175
+ await sendCommand(socketPath, {
176
+ action: "notify",
177
+ event: {
178
+ type: "agent.spawn",
179
+ agent: { agentId: "team1-a", name: "a", scopes: ["team1"] },
180
+ },
181
+ });
182
+ await sendCommand(socketPath, {
183
+ action: "notify",
184
+ event: {
185
+ type: "agent.spawn",
186
+ agent: { agentId: "team2-b", name: "b", scopes: ["team2"] },
187
+ },
188
+ });
189
+
190
+ const resp = await sendCommand(socketPath, {
191
+ action: "list_agents",
192
+ scope: "team1",
193
+ });
194
+
195
+ expect(resp.ok).toBe(true);
196
+ expect(resp.count).toBe(1);
197
+ const agents = resp.agents as Array<{ agentId: string }>;
198
+ expect(agents[0].agentId).toBe("team1-a");
199
+ });
200
+ });
@@ -0,0 +1,191 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import * as net from "node:net";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { EventEmitter } from "node:events";
6
+ import { InMemoryStorage } from "../src/storage/memory.js";
7
+ import { MessageRouter } from "../src/router/message-router.js";
8
+ import { IpcServer } from "../src/ipc/ipc-server.js";
9
+ import { InboxMcpProxy } from "../src/mcp/mcp-proxy.js";
10
+
11
+ function tmpSocketPath(): string {
12
+ return path.join(os.tmpdir(), `inbox-proxy-test-${Date.now()}-${Math.random().toString(36).slice(2)}.sock`);
13
+ }
14
+
15
+ /**
16
+ * Helper: directly send an IPC command (bypass proxy, used for setup/verification).
17
+ */
18
+ function sendIpc(socketPath: string, command: object): Promise<Record<string, unknown>> {
19
+ return new Promise((resolve, reject) => {
20
+ const client = net.createConnection(socketPath, () => {
21
+ client.write(JSON.stringify(command) + "\n");
22
+ });
23
+ let buffer = "";
24
+ client.on("data", (data) => {
25
+ buffer += data.toString();
26
+ const idx = buffer.indexOf("\n");
27
+ if (idx !== -1) {
28
+ const line = buffer.slice(0, idx);
29
+ client.end();
30
+ resolve(JSON.parse(line));
31
+ }
32
+ });
33
+ client.on("error", reject);
34
+ });
35
+ }
36
+
37
+ describe("InboxMcpProxy", () => {
38
+ let storage: InMemoryStorage;
39
+ let events: EventEmitter;
40
+ let router: MessageRouter;
41
+ let ipcServer: IpcServer;
42
+ let socketPath: string;
43
+ let proxy: InboxMcpProxy;
44
+
45
+ beforeEach(async () => {
46
+ storage = new InMemoryStorage();
47
+ events = new EventEmitter();
48
+ router = new MessageRouter(storage, events, "test-scope");
49
+ socketPath = tmpSocketPath();
50
+ ipcServer = new IpcServer(socketPath, router, storage);
51
+ await ipcServer.start();
52
+ proxy = new InboxMcpProxy(socketPath, "test-agent", "test-scope");
53
+ });
54
+
55
+ afterEach(async () => {
56
+ await ipcServer.stop();
57
+ });
58
+
59
+ it("should be constructable with socket path, agent ID, and scope", () => {
60
+ expect(proxy).toBeDefined();
61
+ expect(proxy.server).toBeDefined();
62
+ });
63
+
64
+ it("should proxy send via IPC and message lands in storage", async () => {
65
+ // Use the proxy's internal sendIpc (test via IPC directly since MCP stdio is hard to test)
66
+ // Instead, verify the proxy's IPC client works by sending directly and checking storage
67
+ const resp = await sendIpc(socketPath, {
68
+ action: "send",
69
+ from: "test-agent",
70
+ to: "recipient",
71
+ payload: "hello from proxy",
72
+ });
73
+ expect(resp.ok).toBe(true);
74
+ expect(resp.messageId).toBeTruthy();
75
+
76
+ // Verify message is in shared storage
77
+ const msg = storage.getMessage(resp.messageId as string);
78
+ expect(msg).toBeDefined();
79
+ expect(msg!.sender_id).toBe("test-agent");
80
+ });
81
+
82
+ it("should see messages sent to the IPC server via check_inbox", async () => {
83
+ // Send a message via IPC
84
+ await sendIpc(socketPath, {
85
+ action: "send",
86
+ from: "external",
87
+ to: "my-agent",
88
+ payload: "you have a task",
89
+ });
90
+
91
+ // Check inbox via IPC (same path proxy would use)
92
+ const resp = await sendIpc(socketPath, {
93
+ action: "check_inbox",
94
+ agentId: "my-agent",
95
+ unreadOnly: true,
96
+ });
97
+
98
+ expect(resp.ok).toBe(true);
99
+ const messages = resp.messages as Array<{ sender_id: string }>;
100
+ expect(messages).toHaveLength(1);
101
+ expect(messages[0].sender_id).toBe("external");
102
+ });
103
+
104
+ it("should read threads via IPC", async () => {
105
+ await sendIpc(socketPath, {
106
+ action: "send",
107
+ from: "alice",
108
+ to: "bob",
109
+ payload: "msg 1",
110
+ threadTag: "thread-1",
111
+ });
112
+ await sendIpc(socketPath, {
113
+ action: "send",
114
+ from: "bob",
115
+ to: "alice",
116
+ payload: "msg 2",
117
+ threadTag: "thread-1",
118
+ });
119
+
120
+ const resp = await sendIpc(socketPath, {
121
+ action: "read_thread",
122
+ threadTag: "thread-1",
123
+ scope: "test-scope",
124
+ });
125
+
126
+ expect(resp.ok).toBe(true);
127
+ expect(resp.count).toBe(2);
128
+ });
129
+
130
+ it("should list agents via IPC", async () => {
131
+ await sendIpc(socketPath, {
132
+ action: "notify",
133
+ event: {
134
+ type: "agent.spawn",
135
+ agent: { agentId: "agent-a", name: "Agent A", scopes: ["test-scope"] },
136
+ },
137
+ });
138
+
139
+ const resp = await sendIpc(socketPath, {
140
+ action: "list_agents",
141
+ });
142
+
143
+ expect(resp.ok).toBe(true);
144
+ expect(resp.count).toBe(1);
145
+ const agents = resp.agents as Array<{ agentId: string }>;
146
+ expect(agents[0].agentId).toBe("agent-a");
147
+ });
148
+
149
+ it("should handle unavailable socket gracefully", async () => {
150
+ const badProxy = new InboxMcpProxy("/tmp/nonexistent-socket.sock", "agent", "default");
151
+ // Access internal sendIpc method indirectly — the proxy shouldn't crash
152
+ // We test this by verifying the class instantiates without error
153
+ expect(badProxy).toBeDefined();
154
+ });
155
+ });
156
+
157
+ describe("InboxMcpProxy default agent ID", () => {
158
+ let storage: InMemoryStorage;
159
+ let events: EventEmitter;
160
+ let router: MessageRouter;
161
+ let ipcServer: IpcServer;
162
+ let socketPath: string;
163
+
164
+ beforeEach(async () => {
165
+ storage = new InMemoryStorage();
166
+ events = new EventEmitter();
167
+ router = new MessageRouter(storage, events, "default");
168
+ socketPath = tmpSocketPath();
169
+ ipcServer = new IpcServer(socketPath, router, storage);
170
+ await ipcServer.start();
171
+ });
172
+
173
+ afterEach(async () => {
174
+ await ipcServer.stop();
175
+ });
176
+
177
+ it("should use default agent ID as sender when from is not specified", async () => {
178
+ // The proxy's defaultAgentId should be used when send_message doesn't specify from.
179
+ // We test this by sending via IPC with the expected default and checking storage.
180
+ const resp = await sendIpc(socketPath, {
181
+ action: "send",
182
+ from: "gsd-executor", // proxy would inject this as default
183
+ to: "observer",
184
+ payload: "status update",
185
+ });
186
+
187
+ expect(resp.ok).toBe(true);
188
+ const msg = storage.getMessage(resp.messageId as string);
189
+ expect(msg!.sender_id).toBe("gsd-executor");
190
+ });
191
+ });
@@ -0,0 +1,178 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import { EventEmitter } from "node:events";
3
+ import { InMemoryStorage } from "../../src/storage/memory.js";
4
+ import { DeliveryBridge } from "../../src/mesh/delivery-bridge.js";
5
+ import type { MeshDeliveryHandler } from "../../src/mesh/delivery-bridge.js";
6
+ import type { MapMessage } from "../../src/mesh/type-mapper.js";
7
+
8
+ function makeMapMessage(overrides: Partial<MapMessage> = {}): MapMessage {
9
+ return {
10
+ id: "map-msg-1",
11
+ from: "agent-alice",
12
+ to: { agent: "agent-bob" },
13
+ timestamp: Date.now(),
14
+ payload: { type: "text", text: "hello via bridge" },
15
+ meta: { priority: "normal" },
16
+ ...overrides,
17
+ };
18
+ }
19
+
20
+ describe("DeliveryBridge", () => {
21
+ let storage: InMemoryStorage;
22
+ let events: EventEmitter;
23
+ let bridge: DeliveryBridge;
24
+
25
+ beforeEach(() => {
26
+ storage = new InMemoryStorage();
27
+ events = new EventEmitter();
28
+ bridge = new DeliveryBridge(storage, events, "test-scope");
29
+ });
30
+
31
+ describe("deliverToAgent", () => {
32
+ it("should store incoming MAP message in agent-inbox storage", async () => {
33
+ const mapMsg = makeMapMessage();
34
+ const result = await bridge.deliverToAgent("agent-bob", mapMsg);
35
+
36
+ expect(result).toBe(true);
37
+
38
+ // Verify message is in storage
39
+ const inbox = storage.getInbox("agent-bob");
40
+ expect(inbox).toHaveLength(1);
41
+ expect(inbox[0].sender_id).toBe("agent-alice");
42
+ expect(inbox[0].scope).toBe("test-scope");
43
+ expect(inbox[0].content).toEqual({ type: "text", text: "hello via bridge" });
44
+ });
45
+
46
+ it("should emit message.created event", async () => {
47
+ const spy = vi.fn();
48
+ events.on("message.created", spy);
49
+
50
+ await bridge.deliverToAgent("agent-bob", makeMapMessage());
51
+
52
+ expect(spy).toHaveBeenCalledTimes(1);
53
+ expect(spy.mock.calls[0][0].sender_id).toBe("agent-alice");
54
+ });
55
+
56
+ it("should add target agent to recipients if not already present", async () => {
57
+ const mapMsg = makeMapMessage({
58
+ to: { agent: "agent-carol" }, // Different from target agentId
59
+ });
60
+
61
+ await bridge.deliverToAgent("agent-bob", mapMsg);
62
+
63
+ const inbox = storage.getInbox("agent-bob");
64
+ expect(inbox).toHaveLength(1);
65
+ const recipients = inbox[0].recipients;
66
+ const bobRecipient = recipients.find((r) => r.agent_id === "agent-bob");
67
+ expect(bobRecipient).toBeDefined();
68
+ expect(bobRecipient?.kind).toBe("to");
69
+ });
70
+
71
+ it("should not duplicate agent in recipients if already present", async () => {
72
+ const mapMsg = makeMapMessage({
73
+ to: { agent: "agent-bob" },
74
+ });
75
+
76
+ await bridge.deliverToAgent("agent-bob", mapMsg);
77
+
78
+ const inbox = storage.getInbox("agent-bob");
79
+ const bobRecipients = inbox[0].recipients.filter(
80
+ (r) => r.agent_id === "agent-bob"
81
+ );
82
+ expect(bobRecipients).toHaveLength(1);
83
+ });
84
+
85
+ it("should preserve _meta fields from MAP message", async () => {
86
+ const mapMsg = makeMapMessage({
87
+ _meta: {
88
+ subject: "Review request",
89
+ threadTag: "thread-42",
90
+ },
91
+ });
92
+
93
+ await bridge.deliverToAgent("agent-bob", mapMsg);
94
+
95
+ const inbox = storage.getInbox("agent-bob");
96
+ expect(inbox[0].subject).toBe("Review request");
97
+ expect(inbox[0].thread_tag).toBe("thread-42");
98
+ });
99
+
100
+ it("should handle string payload (normalizes to text content)", async () => {
101
+ const mapMsg = makeMapMessage({ payload: "plain text message" });
102
+
103
+ await bridge.deliverToAgent("agent-bob", mapMsg);
104
+
105
+ const inbox = storage.getInbox("agent-bob");
106
+ expect(inbox[0].content).toEqual({ type: "text", text: "plain text message" });
107
+ });
108
+
109
+ it("should map priority to importance", async () => {
110
+ const urgentMsg = makeMapMessage({ meta: { priority: "urgent" } });
111
+ await bridge.deliverToAgent("agent-bob", urgentMsg);
112
+
113
+ const inbox = storage.getInbox("agent-bob");
114
+ expect(inbox[0].importance).toBe("urgent");
115
+ });
116
+ });
117
+
118
+ describe("forwardToPeer", () => {
119
+ it("should delegate to previous handler if available", async () => {
120
+ const mockPrevHandler: MeshDeliveryHandler = {
121
+ deliverToAgent: vi.fn().mockResolvedValue(true),
122
+ forwardToPeer: vi.fn().mockResolvedValue(true),
123
+ };
124
+
125
+ const bridgeWithPrev = new DeliveryBridge(
126
+ storage, events, "test-scope", mockPrevHandler
127
+ );
128
+
129
+ const mapMsg = makeMapMessage();
130
+ const result = await bridgeWithPrev.forwardToPeer(
131
+ "peer-1", ["agent-bob"], mapMsg
132
+ );
133
+
134
+ expect(result).toBe(true);
135
+ expect(mockPrevHandler.forwardToPeer).toHaveBeenCalledWith(
136
+ "peer-1", ["agent-bob"], mapMsg
137
+ );
138
+ });
139
+
140
+ it("should return false when no previous handler", async () => {
141
+ const result = await bridge.forwardToPeer(
142
+ "peer-1", ["agent-bob"], makeMapMessage()
143
+ );
144
+ expect(result).toBe(false);
145
+ });
146
+ });
147
+
148
+ describe("routeToFederation", () => {
149
+ it("should delegate to previous handler if available", async () => {
150
+ const mockPrevHandler: MeshDeliveryHandler = {
151
+ deliverToAgent: vi.fn().mockResolvedValue(true),
152
+ forwardToPeer: vi.fn().mockResolvedValue(true),
153
+ routeToFederation: vi.fn().mockResolvedValue(true),
154
+ };
155
+
156
+ const bridgeWithPrev = new DeliveryBridge(
157
+ storage, events, "test-scope", mockPrevHandler
158
+ );
159
+
160
+ const mapMsg = makeMapMessage();
161
+ const result = await bridgeWithPrev.routeToFederation(
162
+ "remote-sys", ["agent-bob"], mapMsg
163
+ );
164
+
165
+ expect(result).toBe(true);
166
+ expect(mockPrevHandler.routeToFederation).toHaveBeenCalledWith(
167
+ "remote-sys", ["agent-bob"], mapMsg
168
+ );
169
+ });
170
+
171
+ it("should return false when no previous handler", async () => {
172
+ const result = await bridge.routeToFederation(
173
+ "remote-sys", ["agent-bob"], makeMapMessage()
174
+ );
175
+ expect(result).toBe(false);
176
+ });
177
+ });
178
+ });