@teneo-protocol/sdk 3.0.1 → 3.1.2
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/CHANGELOG.md +21 -0
- package/README.md +234 -4
- package/dist/managers/message-router.d.ts +35 -0
- package/dist/managers/message-router.d.ts.map +1 -1
- package/dist/managers/message-router.js +143 -2
- package/dist/managers/message-router.js.map +1 -1
- package/dist/payments/networks.js +1 -1
- package/dist/payments/networks.js.map +1 -1
- package/dist/payments/payment-client.d.ts.map +1 -1
- package/dist/payments/payment-client.js +5 -3
- package/dist/payments/payment-client.js.map +1 -1
- package/dist/teneo-sdk.d.ts +5 -4
- package/dist/teneo-sdk.d.ts.map +1 -1
- package/dist/teneo-sdk.js +26 -5
- package/dist/teneo-sdk.js.map +1 -1
- package/dist/types/config.d.ts +29 -3
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +21 -2
- package/dist/types/config.js.map +1 -1
- package/dist/types/error-codes.d.ts +3 -0
- package/dist/types/error-codes.d.ts.map +1 -1
- package/dist/types/error-codes.js +4 -0
- package/dist/types/error-codes.js.map +1 -1
- package/dist/types/events.d.ts +3 -0
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/events.js.map +1 -1
- package/package.json +1 -1
- package/src/managers/message-router.ts +183 -3
- package/src/payments/networks.ts +1 -1
- package/src/payments/payment-client.ts +6 -3
- package/src/teneo-sdk.ts +28 -5
- package/src/types/config.ts +23 -2
- package/src/types/error-codes.ts +5 -0
- package/src/types/events.ts +5 -0
- package/tests/unit/managers/message-router-autosummon.test.ts +338 -0
- package/tests/unit/sdk-new-methods.test.ts +26 -0
|
@@ -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
|
+
});
|
|
@@ -105,6 +105,7 @@ describe("TeneoSDK New Methods", () => {
|
|
|
105
105
|
|
|
106
106
|
expect(sendMessageSpy).toHaveBeenCalledWith({
|
|
107
107
|
type: "tx_result",
|
|
108
|
+
timestamp: expect.any(String),
|
|
108
109
|
data: {
|
|
109
110
|
task_id: "task-123",
|
|
110
111
|
status: "confirmed",
|
|
@@ -118,6 +119,7 @@ describe("TeneoSDK New Methods", () => {
|
|
|
118
119
|
|
|
119
120
|
expect(sendMessageSpy).toHaveBeenCalledWith({
|
|
120
121
|
type: "tx_result",
|
|
122
|
+
timestamp: expect.any(String),
|
|
121
123
|
data: {
|
|
122
124
|
task_id: "task-456",
|
|
123
125
|
status: "failed",
|
|
@@ -131,6 +133,7 @@ describe("TeneoSDK New Methods", () => {
|
|
|
131
133
|
|
|
132
134
|
expect(sendMessageSpy).toHaveBeenCalledWith({
|
|
133
135
|
type: "tx_result",
|
|
136
|
+
timestamp: expect.any(String),
|
|
134
137
|
data: {
|
|
135
138
|
task_id: "task-789",
|
|
136
139
|
status: "rejected"
|
|
@@ -138,6 +141,28 @@ describe("TeneoSDK New Methods", () => {
|
|
|
138
141
|
});
|
|
139
142
|
});
|
|
140
143
|
|
|
144
|
+
it("should include room when provided", async () => {
|
|
145
|
+
await sdk.sendTxResult("task-100", "confirmed", "0xhash", undefined, "room-abc");
|
|
146
|
+
|
|
147
|
+
expect(sendMessageSpy).toHaveBeenCalledWith({
|
|
148
|
+
type: "tx_result",
|
|
149
|
+
room: "room-abc",
|
|
150
|
+
timestamp: expect.any(String),
|
|
151
|
+
data: {
|
|
152
|
+
task_id: "task-100",
|
|
153
|
+
status: "confirmed",
|
|
154
|
+
tx_hash: "0xhash"
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should not include room when not provided", async () => {
|
|
160
|
+
await sdk.sendTxResult("task-100", "rejected");
|
|
161
|
+
|
|
162
|
+
const sentMessage = sendMessageSpy.mock.calls[0][0];
|
|
163
|
+
expect(sentMessage).not.toHaveProperty("room");
|
|
164
|
+
});
|
|
165
|
+
|
|
141
166
|
it("should not include tx_hash when not provided", async () => {
|
|
142
167
|
await sdk.sendTxResult("task-100", "rejected");
|
|
143
168
|
|
|
@@ -157,6 +182,7 @@ describe("TeneoSDK New Methods", () => {
|
|
|
157
182
|
|
|
158
183
|
expect(sendMessageSpy).toHaveBeenCalledWith({
|
|
159
184
|
type: "tx_result",
|
|
185
|
+
timestamp: expect.any(String),
|
|
160
186
|
data: {
|
|
161
187
|
task_id: "task-200",
|
|
162
188
|
status: "failed",
|