@decocms/mesh-sdk 1.2.1 → 1.2.3

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,368 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
5
+ import {
6
+ createBridgeTransportPair,
7
+ BridgeClientTransport,
8
+ BridgeServerTransport,
9
+ } from "./bridge-transport";
10
+
11
+ /** Poll until `condition` returns true, checking every 5ms up to `timeoutMs`. */
12
+ async function waitFor(
13
+ condition: () => boolean,
14
+ timeoutMs = 200,
15
+ ): Promise<void> {
16
+ const start = Date.now();
17
+ while (!condition()) {
18
+ if (Date.now() - start > timeoutMs) return;
19
+ await new Promise((resolve) => setTimeout(resolve, 5));
20
+ }
21
+ }
22
+
23
+ describe("BridgeTransport", () => {
24
+ describe("createBridgeTransportPair", () => {
25
+ it("should create a pair of transports", () => {
26
+ const { client, server, channel } = createBridgeTransportPair();
27
+
28
+ expect(client).toBeInstanceOf(BridgeClientTransport);
29
+ expect(server).toBeInstanceOf(BridgeServerTransport);
30
+ expect(channel).toBeDefined();
31
+ });
32
+
33
+ it("should create transports with microtask scheduling", () => {
34
+ const { client, server } = createBridgeTransportPair();
35
+ expect(client).toBeDefined();
36
+ expect(server).toBeDefined();
37
+ });
38
+ });
39
+
40
+ describe("message delivery", () => {
41
+ it("should deliver messages in order client->server", async () => {
42
+ const { client, server } = createBridgeTransportPair();
43
+
44
+ const receivedMessages: JSONRPCMessage[] = [];
45
+
46
+ await server.start();
47
+ server.onmessage = (message) => {
48
+ receivedMessages.push(message);
49
+ };
50
+
51
+ await client.start();
52
+
53
+ const msg1: JSONRPCMessage = {
54
+ jsonrpc: "2.0",
55
+ id: 1,
56
+ method: "test",
57
+ params: { foo: "bar" },
58
+ };
59
+ const msg2: JSONRPCMessage = {
60
+ jsonrpc: "2.0",
61
+ id: 2,
62
+ method: "test2",
63
+ params: { baz: "qux" },
64
+ };
65
+
66
+ await client.send(msg1);
67
+ await client.send(msg2);
68
+
69
+ // Wait for microtask to process (or sync mode processes immediately)
70
+ await new Promise((resolve) => queueMicrotask(resolve));
71
+
72
+ expect(receivedMessages).toHaveLength(2);
73
+ expect(receivedMessages[0]).toEqual(msg1);
74
+ expect(receivedMessages[1]).toEqual(msg2);
75
+ });
76
+
77
+ it("should deliver messages in order server->client", async () => {
78
+ const { client, server } = createBridgeTransportPair();
79
+
80
+ const receivedMessages: JSONRPCMessage[] = [];
81
+
82
+ await client.start();
83
+ client.onmessage = (message) => {
84
+ receivedMessages.push(message);
85
+ };
86
+
87
+ await server.start();
88
+
89
+ const msg1: JSONRPCMessage = {
90
+ jsonrpc: "2.0",
91
+ id: 1,
92
+ method: "test",
93
+ params: { foo: "bar" },
94
+ };
95
+ const msg2: JSONRPCMessage = {
96
+ jsonrpc: "2.0",
97
+ id: 2,
98
+ method: "test2",
99
+ params: { baz: "qux" },
100
+ };
101
+
102
+ await server.send(msg1);
103
+ await server.send(msg2);
104
+
105
+ // Wait for microtask to process (or sync mode processes immediately)
106
+ await new Promise((resolve) => queueMicrotask(resolve));
107
+
108
+ expect(receivedMessages).toHaveLength(2);
109
+ expect(receivedMessages[0]).toEqual(msg1);
110
+ expect(receivedMessages[1]).toEqual(msg2);
111
+ });
112
+
113
+ it("should batch multiple messages in a single microtask", async () => {
114
+ const { client, server } = createBridgeTransportPair();
115
+
116
+ const receivedMessages: JSONRPCMessage[] = [];
117
+ let flushCount = 0;
118
+
119
+ await server.start();
120
+ server.onmessage = (message) => {
121
+ receivedMessages.push(message);
122
+ flushCount++;
123
+ };
124
+
125
+ await client.start();
126
+
127
+ // Send multiple messages synchronously
128
+ await client.send({ jsonrpc: "2.0", id: 1, method: "test1" });
129
+ await client.send({ jsonrpc: "2.0", id: 2, method: "test2" });
130
+ await client.send({ jsonrpc: "2.0", id: 3, method: "test3" });
131
+
132
+ // Wait for microtask to process
133
+ await new Promise((resolve) => setTimeout(resolve, 10));
134
+
135
+ expect(receivedMessages).toHaveLength(3);
136
+ // All messages should be delivered in a single flush
137
+ expect(flushCount).toBeGreaterThanOrEqual(1);
138
+ });
139
+ });
140
+
141
+ describe("start()", () => {
142
+ it("should allow starting client transport", async () => {
143
+ const { client } = createBridgeTransportPair();
144
+ await expect(client.start()).resolves.toBeUndefined();
145
+ });
146
+
147
+ it("should allow starting server transport", async () => {
148
+ const { server } = createBridgeTransportPair();
149
+ await expect(server.start()).resolves.toBeUndefined();
150
+ });
151
+
152
+ it("should throw if started twice", async () => {
153
+ const { client } = createBridgeTransportPair();
154
+ await client.start();
155
+ await expect(client.start()).rejects.toThrow("already started");
156
+ });
157
+ });
158
+
159
+ describe("close()", () => {
160
+ it("should close client transport and notify server", async () => {
161
+ const { client, server } = createBridgeTransportPair();
162
+
163
+ await client.start();
164
+ await server.start();
165
+
166
+ let serverClosed = false;
167
+ server.onclose = () => {
168
+ serverClosed = true;
169
+ };
170
+
171
+ await client.close();
172
+
173
+ expect(serverClosed).toBe(true);
174
+ });
175
+
176
+ it("should close server transport and notify client", async () => {
177
+ const { client, server } = createBridgeTransportPair();
178
+
179
+ await client.start();
180
+ await server.start();
181
+
182
+ let clientClosed = false;
183
+ client.onclose = () => {
184
+ clientClosed = true;
185
+ };
186
+
187
+ await server.close();
188
+
189
+ expect(clientClosed).toBe(true);
190
+ });
191
+
192
+ it("should prevent sending messages after close", async () => {
193
+ const { client, server } = createBridgeTransportPair();
194
+
195
+ await client.start();
196
+ await server.start();
197
+
198
+ const receivedMessages: JSONRPCMessage[] = [];
199
+ server.onmessage = (msg) => receivedMessages.push(msg);
200
+
201
+ // Send first message and wait for delivery
202
+ await client.send({ jsonrpc: "2.0", id: 1, method: "test" });
203
+ await waitFor(() => receivedMessages.length >= 1);
204
+
205
+ expect(receivedMessages.length).toBeGreaterThanOrEqual(1);
206
+ const initialCount = receivedMessages.length;
207
+
208
+ // Close client
209
+ await client.close();
210
+
211
+ // Try to send after close (should be silent no-op)
212
+ await client.send({ jsonrpc: "2.0", id: 2, method: "test2" });
213
+
214
+ // Wait a bit to ensure no delivery
215
+ await new Promise((resolve) => setTimeout(resolve, 50));
216
+
217
+ // Should not receive new messages after close
218
+ expect(receivedMessages.length).toBe(initialCount);
219
+ });
220
+
221
+ it("should fire onclose exactly once", async () => {
222
+ const { client } = createBridgeTransportPair();
223
+
224
+ await client.start();
225
+
226
+ let closeCount = 0;
227
+ client.onclose = () => {
228
+ closeCount++;
229
+ };
230
+
231
+ await client.close();
232
+ await client.close(); // Try closing again
233
+
234
+ expect(closeCount).toBe(1);
235
+ });
236
+ });
237
+
238
+ describe("error handling", () => {
239
+ it("should catch and forward errors from onmessage handler", async () => {
240
+ const { client, server } = createBridgeTransportPair();
241
+
242
+ await server.start();
243
+ await client.start();
244
+
245
+ const errors: Error[] = [];
246
+ server.onerror = (error) => {
247
+ errors.push(error);
248
+ };
249
+
250
+ server.onmessage = () => {
251
+ throw new Error("Test error");
252
+ };
253
+
254
+ await client.send({ jsonrpc: "2.0", id: 1, method: "test" });
255
+
256
+ // Wait for microtask to process and error to be forwarded
257
+ await waitFor(() => errors.length >= 1);
258
+
259
+ expect(errors).toHaveLength(1);
260
+ expect(errors[0].message).toBe("Test error");
261
+ });
262
+
263
+ it("should continue processing messages after error", async () => {
264
+ const { client, server } = createBridgeTransportPair();
265
+
266
+ await server.start();
267
+ await client.start();
268
+
269
+ const receivedMessages: JSONRPCMessage[] = [];
270
+ const errors: Error[] = [];
271
+
272
+ let callCount = 0;
273
+ server.onmessage = (msg) => {
274
+ callCount++;
275
+ if (callCount === 1) {
276
+ throw new Error("First message error");
277
+ }
278
+ receivedMessages.push(msg);
279
+ };
280
+
281
+ server.onerror = (error) => {
282
+ errors.push(error);
283
+ };
284
+
285
+ await client.send({ jsonrpc: "2.0", id: 1, method: "test1" });
286
+ await client.send({ jsonrpc: "2.0", id: 2, method: "test2" });
287
+
288
+ // Wait for microtask to process
289
+ await new Promise((resolve) => setTimeout(resolve, 10));
290
+
291
+ expect(errors).toHaveLength(1);
292
+ expect(receivedMessages).toHaveLength(1);
293
+ expect(receivedMessages[0].id).toBe(2);
294
+ });
295
+ });
296
+
297
+ describe("integration with MCP SDK", () => {
298
+ it("should work with MCP Client and Server", async () => {
299
+ const { client: clientTransport, server: serverTransport } =
300
+ createBridgeTransportPair();
301
+
302
+ const client = new Client({ name: "test-client", version: "1.0.0" });
303
+ const server = new Server({ name: "test-server", version: "1.0.0" });
304
+
305
+ // Connect server first
306
+ await server.connect(serverTransport);
307
+
308
+ // Connect client
309
+ await client.connect(clientTransport);
310
+
311
+ // Verify connection is established
312
+ expect(clientTransport.started).toBe(true);
313
+ expect(serverTransport.started).toBe(true);
314
+
315
+ // Clean up
316
+ await client.close();
317
+ await server.close();
318
+ });
319
+
320
+ it("should handle initialize handshake", async () => {
321
+ const { client: clientTransport, server: serverTransport } =
322
+ createBridgeTransportPair();
323
+
324
+ const client = new Client({ name: "test-client", version: "1.0.0" });
325
+ const server = new Server({ name: "test-server", version: "1.0.0" });
326
+
327
+ await server.connect(serverTransport);
328
+ await client.connect(clientTransport);
329
+
330
+ // Wait for initialization
331
+ await new Promise((resolve) => setTimeout(resolve, 100));
332
+
333
+ // Verify connection is established (client should have received initialize response)
334
+ expect(clientTransport.started).toBe(true);
335
+ expect(serverTransport.started).toBe(true);
336
+
337
+ await client.close();
338
+ await server.close();
339
+ });
340
+ });
341
+
342
+ describe("send() before start()", () => {
343
+ it("should allow sending messages before start (messages may be queued)", async () => {
344
+ const { client, server } = createBridgeTransportPair();
345
+
346
+ // Send message before starting - should not throw
347
+ await expect(
348
+ client.send({ jsonrpc: "2.0", id: 1, method: "test1" }),
349
+ ).resolves.toBeUndefined();
350
+
351
+ // Set handler and start
352
+ const receivedMessages: JSONRPCMessage[] = [];
353
+ server.onmessage = (msg) => receivedMessages.push(msg);
354
+ await server.start();
355
+ await client.start();
356
+
357
+ // Send another message after start to verify normal operation
358
+ await client.send({ jsonrpc: "2.0", id: 2, method: "test2" });
359
+
360
+ // Wait for delivery
361
+ await new Promise((resolve) => setTimeout(resolve, 20));
362
+
363
+ // At least the message sent after start should be delivered
364
+ // (messages sent before start may or may not be delivered depending on timing)
365
+ expect(receivedMessages.length).toBeGreaterThanOrEqual(1);
366
+ });
367
+ });
368
+ });
@@ -0,0 +1,6 @@
1
+ export {
2
+ createBridgeTransportPair,
3
+ BridgeClientTransport,
4
+ BridgeServerTransport,
5
+ type BridgeTransportPair,
6
+ } from "@decocms/mcp-utils";
@@ -0,0 +1,26 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { StudioPackAgentId, isStudioPackAgent } from "./constants";
3
+
4
+ describe("StudioPackAgentId", () => {
5
+ test("generates the Store Manager id with the org suffix", () => {
6
+ expect(StudioPackAgentId.STORE_MANAGER("org_xyz")).toBe(
7
+ "studio-store-manager_org_xyz",
8
+ );
9
+ });
10
+ });
11
+
12
+ describe("isStudioPackAgent", () => {
13
+ test("recognises every studio-pack manager id, including the new store-manager", () => {
14
+ expect(isStudioPackAgent("studio-agent-manager_org_xyz")).toBe(true);
15
+ expect(isStudioPackAgent("studio-automation-manager_org_xyz")).toBe(true);
16
+ expect(isStudioPackAgent("studio-connection-manager_org_xyz")).toBe(true);
17
+ expect(isStudioPackAgent("studio-store-manager_org_xyz")).toBe(true);
18
+ });
19
+
20
+ test("rejects unrelated ids", () => {
21
+ expect(isStudioPackAgent(null)).toBe(false);
22
+ expect(isStudioPackAgent(undefined)).toBe(false);
23
+ expect(isStudioPackAgent("vir_abc")).toBe(false);
24
+ expect(isStudioPackAgent("decopilot_org_xyz")).toBe(false);
25
+ });
26
+ });