@teneo-protocol/sdk 3.0.1 → 3.1.1

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.
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Unit tests for MessageRouter autosummon functionality
3
+ * Tests pre-flight autosummon, fallback autosummon, lifecycle events,
4
+ * cache hit/miss scenarios, and error handling with mocked dependencies.
5
+ */
6
+
7
+ import { describe, it, expect, vi } from "vitest";
8
+ import { EventEmitter } from "eventemitter3";
9
+ import { MessageRouter, MessageRouterConfig } from "../../../src/managers/message-router";
10
+ import { AgentRoomManager } from "../../../src/managers/agent-room-manager";
11
+ import { Logger } from "../../../src/types";
12
+
13
+ type EmittedEvent = { name: string; args: any[] };
14
+
15
+ /**
16
+ * Creates a mock wsClient backed by a real EventEmitter so that
17
+ * waitForEvent / .on() / .off() work correctly in MessageRouter.
18
+ */
19
+ function createMockWsClient() {
20
+ const ee = new EventEmitter();
21
+ const emittedEvents: EmittedEvent[] = [];
22
+ const originalEmit = ee.emit.bind(ee);
23
+
24
+ const mock = Object.assign(ee, {
25
+ isConnected: true,
26
+ sendMessage: vi.fn().mockResolvedValue(undefined),
27
+ _emittedEvents: emittedEvents
28
+ });
29
+
30
+ // Intercept emit to record events for assertions
31
+ mock.emit = ((...args: any[]) => {
32
+ const [name, ...rest] = args;
33
+ emittedEvents.push({ name, args: rest });
34
+ return originalEmit(name, ...rest);
35
+ }) as any;
36
+
37
+ return mock;
38
+ }
39
+
40
+ function createMockLogger(): Logger {
41
+ return { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
42
+ }
43
+
44
+ function createMockAgentRoomManager(opts: {
45
+ checkAgentInRoom?: boolean | undefined;
46
+ availableAgents?: Array<{ agent_id: string; agent_name: string }>;
47
+ addAgentThrows?: Error;
48
+ } = {}) {
49
+ return {
50
+ checkAgentInRoom: vi.fn().mockReturnValue(opts.checkAgentInRoom),
51
+ listAvailableAgents: vi.fn().mockResolvedValue(opts.availableAgents || []),
52
+ addAgentToRoom: opts.addAgentThrows
53
+ ? vi.fn().mockRejectedValue(opts.addAgentThrows)
54
+ : vi.fn().mockResolvedValue(undefined)
55
+ } as any as AgentRoomManager;
56
+ }
57
+
58
+ const QUOTE_DATA = {
59
+ data: {
60
+ task_id: "t1", agent_id: "news-agent", agent_name: "news-agent",
61
+ agent_wallet: "0x123", command: "latest", pricing: { price: 0 },
62
+ expires_at: new Date(Date.now() + 60000).toISOString(),
63
+ settlement_router: "0x", salt: "0x", facilitator_fee: "0", hook: "0x"
64
+ }
65
+ };
66
+
67
+ function createRouter(overrides: Partial<MessageRouterConfig> = {}) {
68
+ const wsClient = createMockWsClient();
69
+ const config: MessageRouterConfig = {
70
+ messageTimeout: 5000,
71
+ autoApproveQuotes: true,
72
+ quoteTimeout: 3000,
73
+ autoSummon: true,
74
+ paymentNetwork: "eip155:8453", // Avoids getDefaultNetwork() call
75
+ ...overrides
76
+ };
77
+
78
+ const webhookHandler = {
79
+ sendMessageWebhook: vi.fn().mockResolvedValue(undefined),
80
+ sendWebhook: vi.fn().mockResolvedValue(undefined)
81
+ } as any;
82
+
83
+ const responseFormatter = {
84
+ format: vi.fn((data: any) => ({ humanized: data.content || "", raw: data }))
85
+ } as any;
86
+
87
+ const router = new MessageRouter(
88
+ wsClient as any,
89
+ webhookHandler,
90
+ responseFormatter,
91
+ createMockLogger(),
92
+ config
93
+ );
94
+
95
+ return { router, wsClient };
96
+ }
97
+
98
+ /** Helper: fire quote:received to resolve a pending _requestQuoteInternal */
99
+ function resolveQuote(wsClient: ReturnType<typeof createMockWsClient>) {
100
+ wsClient.emit("quote:received", QUOTE_DATA);
101
+ }
102
+
103
+ describe("MessageRouter: Autosummon", () => {
104
+ describe("pre-flight autosummon (cache says agent NOT in room)", () => {
105
+ it("should add agent to room before sending command", async () => {
106
+ const { router, wsClient } = createRouter();
107
+ const arm = createMockAgentRoomManager({
108
+ checkAgentInRoom: false,
109
+ availableAgents: [{ agent_id: "news-agent", agent_name: "news-agent" }]
110
+ });
111
+ router.setAgentRoomManager(arm);
112
+
113
+ const promise = (router as any)._requestQuoteInternal(
114
+ "@news-agent latest", "room-1", undefined, false
115
+ );
116
+
117
+ await vi.waitFor(() => {
118
+ expect(arm.addAgentToRoom).toHaveBeenCalledWith("room-1", "news-agent");
119
+ });
120
+
121
+ resolveQuote(wsClient);
122
+ const result = await promise;
123
+ expect(result.agentId).toBe("news-agent");
124
+ });
125
+
126
+ it("should emit autosummon:start and autosummon:success events", async () => {
127
+ const { router, wsClient } = createRouter();
128
+ const arm = createMockAgentRoomManager({
129
+ checkAgentInRoom: false,
130
+ availableAgents: [{ agent_id: "news-agent", agent_name: "news-agent" }]
131
+ });
132
+ router.setAgentRoomManager(arm);
133
+
134
+ const promise = (router as any)._requestQuoteInternal(
135
+ "@news-agent latest", "room-1", undefined, false
136
+ );
137
+
138
+ await vi.waitFor(() => {
139
+ expect(arm.addAgentToRoom).toHaveBeenCalled();
140
+ });
141
+
142
+ const events = wsClient._emittedEvents;
143
+ const start = events.find((e) => e.name === "autosummon:start");
144
+ const success = events.find((e) => e.name === "autosummon:success");
145
+
146
+ expect(start).toBeDefined();
147
+ expect(start!.args).toEqual(["news-agent", "room-1"]);
148
+ expect(success).toBeDefined();
149
+ expect(success!.args).toEqual(["news-agent", "news-agent", "room-1"]);
150
+
151
+ resolveQuote(wsClient);
152
+ await promise;
153
+ });
154
+
155
+ it("should emit autosummon:failed when agent not found in available list", async () => {
156
+ const { router, wsClient } = createRouter();
157
+ const arm = createMockAgentRoomManager({
158
+ checkAgentInRoom: false,
159
+ availableAgents: [] // not found
160
+ });
161
+ router.setAgentRoomManager(arm);
162
+
163
+ const promise = (router as any)._requestQuoteInternal(
164
+ "@ghost-agent test", "room-1", undefined, false
165
+ );
166
+
167
+ await vi.waitFor(() => {
168
+ const failed = wsClient._emittedEvents.find((e) => e.name === "autosummon:failed");
169
+ expect(failed).toBeDefined();
170
+ });
171
+
172
+ const failed = wsClient._emittedEvents.find((e) => e.name === "autosummon:failed");
173
+ expect(failed!.args[0]).toBe("ghost-agent");
174
+ expect(failed!.args[1]).toBe("room-1");
175
+ expect(failed!.args[2]).toBe("Agent not found or offline");
176
+
177
+ // Command should still be sent (fallback continues)
178
+ expect(wsClient.sendMessage).toHaveBeenCalled();
179
+
180
+ resolveQuote(wsClient);
181
+ await promise.catch(() => {});
182
+ });
183
+ });
184
+
185
+ describe("skip pre-flight (agent already in room)", () => {
186
+ it("should not trigger autosummon when cache says agent IS in room", async () => {
187
+ const { router, wsClient } = createRouter();
188
+ const arm = createMockAgentRoomManager({ checkAgentInRoom: true });
189
+ router.setAgentRoomManager(arm);
190
+
191
+ const promise = (router as any)._requestQuoteInternal(
192
+ "@news-agent latest", "room-1", undefined, false
193
+ );
194
+
195
+ // Wait for sendMessage to be called (no pre-flight delay)
196
+ await vi.waitFor(() => {
197
+ expect(wsClient.sendMessage).toHaveBeenCalled();
198
+ });
199
+
200
+ expect(arm.addAgentToRoom).not.toHaveBeenCalled();
201
+ expect(wsClient._emittedEvents.find((e) => e.name === "autosummon:start")).toBeUndefined();
202
+
203
+ resolveQuote(wsClient);
204
+ await promise;
205
+ });
206
+ });
207
+
208
+ describe("skip pre-flight (cache empty — undefined)", () => {
209
+ it("should skip pre-flight and send directly when cache is empty", async () => {
210
+ const { router, wsClient } = createRouter();
211
+ const arm = createMockAgentRoomManager({ checkAgentInRoom: undefined });
212
+ router.setAgentRoomManager(arm);
213
+
214
+ const promise = (router as any)._requestQuoteInternal(
215
+ "@news-agent latest", "room-1", undefined, false
216
+ );
217
+
218
+ await vi.waitFor(() => {
219
+ expect(wsClient.sendMessage).toHaveBeenCalled();
220
+ });
221
+
222
+ expect(arm.addAgentToRoom).not.toHaveBeenCalled();
223
+ expect(arm.listAvailableAgents).not.toHaveBeenCalled();
224
+ expect(wsClient._emittedEvents.find((e) => e.name === "autosummon:start")).toBeUndefined();
225
+
226
+ resolveQuote(wsClient);
227
+ await promise;
228
+ });
229
+ });
230
+
231
+ describe("autoSummon disabled", () => {
232
+ it("should never trigger pre-flight when autoSummon is false", async () => {
233
+ const { router, wsClient } = createRouter({ autoSummon: false });
234
+ const arm = createMockAgentRoomManager({ checkAgentInRoom: false });
235
+ router.setAgentRoomManager(arm);
236
+
237
+ const promise = (router as any)._requestQuoteInternal(
238
+ "@news-agent latest", "room-1", undefined, false
239
+ );
240
+
241
+ await vi.waitFor(() => {
242
+ expect(wsClient.sendMessage).toHaveBeenCalled();
243
+ });
244
+
245
+ expect(arm.checkAgentInRoom).not.toHaveBeenCalled();
246
+ expect(arm.addAgentToRoom).not.toHaveBeenCalled();
247
+
248
+ resolveQuote(wsClient);
249
+ await promise;
250
+ });
251
+ });
252
+
253
+ describe("fallback autosummon (coordinator rejects)", () => {
254
+ it("should emit events when fallback path triggers on coordinator reject", async () => {
255
+ const { router, wsClient } = createRouter();
256
+ const arm = createMockAgentRoomManager({
257
+ checkAgentInRoom: undefined, // cache empty → pre-flight skipped
258
+ availableAgents: [{ agent_id: "news-agent", agent_name: "news-agent" }]
259
+ });
260
+ router.setAgentRoomManager(arm);
261
+
262
+ const promise = (router as any)._requestQuoteInternal(
263
+ "@news-agent latest", "room-1", undefined, false
264
+ );
265
+
266
+ // Wait for sendMessage, then fire coordinator reject
267
+ await vi.waitFor(() => {
268
+ expect(wsClient.sendMessage).toHaveBeenCalled();
269
+ });
270
+
271
+ wsClient.emit("agent:response", {
272
+ content: "agent news-agent does not have access to room room-1",
273
+ agentId: "coordinator"
274
+ });
275
+
276
+ // handleAutoSummon fires, adds agent, retries
277
+ await vi.waitFor(() => {
278
+ expect(arm.addAgentToRoom).toHaveBeenCalledWith("room-1", "news-agent");
279
+ });
280
+
281
+ const events = wsClient._emittedEvents;
282
+ expect(events.find((e) => e.name === "autosummon:start")).toBeDefined();
283
+ expect(events.find((e) => e.name === "autosummon:success")).toBeDefined();
284
+
285
+ // Retry sends another quote request — resolve it
286
+ resolveQuote(wsClient);
287
+ const result = await promise;
288
+ expect(result.agentId).toBe("news-agent");
289
+ });
290
+ });
291
+
292
+ describe("isRetry prevents double-summon", () => {
293
+ it("should skip pre-flight on retry even if cache says agent not in room", async () => {
294
+ const { router, wsClient } = createRouter();
295
+ const arm = createMockAgentRoomManager({ checkAgentInRoom: false });
296
+ router.setAgentRoomManager(arm);
297
+
298
+ const promise = (router as any)._requestQuoteInternal(
299
+ "@news-agent latest", "room-1", undefined, true // isRetry
300
+ );
301
+
302
+ await vi.waitFor(() => {
303
+ expect(wsClient.sendMessage).toHaveBeenCalled();
304
+ });
305
+
306
+ expect(arm.checkAgentInRoom).not.toHaveBeenCalled();
307
+ expect(arm.addAgentToRoom).not.toHaveBeenCalled();
308
+
309
+ resolveQuote(wsClient);
310
+ await promise;
311
+ });
312
+ });
313
+
314
+ describe("pre-flight failure falls through gracefully", () => {
315
+ it("should still send command if addAgentToRoom throws", async () => {
316
+ const { router, wsClient } = createRouter();
317
+ const arm = createMockAgentRoomManager({
318
+ checkAgentInRoom: false,
319
+ availableAgents: [{ agent_id: "news-agent", agent_name: "news-agent" }],
320
+ addAgentThrows: new Error("Network timeout")
321
+ });
322
+ router.setAgentRoomManager(arm);
323
+
324
+ const promise = (router as any)._requestQuoteInternal(
325
+ "@news-agent latest", "room-1", undefined, false
326
+ );
327
+
328
+ // Pre-flight tried, failed, but sendMessage should still be called
329
+ await vi.waitFor(() => {
330
+ expect(wsClient.sendMessage).toHaveBeenCalled();
331
+ });
332
+
333
+ resolveQuote(wsClient);
334
+ const result = await promise;
335
+ expect(result.agentId).toBe("news-agent");
336
+ });
337
+ });
338
+ });