@mastra/server-adapters-test-suite 0.1.3-alpha.1 → 0.1.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.
- package/CHANGELOG.md +43 -0
- package/dist/index.js +176 -81
- package/dist/index.js.map +1 -1
- package/dist/route-adapter-test-suite.d.ts.map +1 -1
- package/dist/route-test-utils.d.ts.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error";
|
|
2
|
+
import { handleError } from "@mastra/server/handlers/error";
|
|
1
3
|
import { HTTPException, SERVER_ROUTES, WorkflowRegistry, createRoute } from "@mastra/server/server-adapter";
|
|
2
4
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
5
|
import * as zod from "zod";
|
|
@@ -57,6 +59,7 @@ function getRouteSpecificSchemaDefaults(route) {
|
|
|
57
59
|
predicateScope: "trace",
|
|
58
60
|
path: "entityName"
|
|
59
61
|
} };
|
|
62
|
+
if (route.path === "/agent-controller/:controllerId/sessions" && route.method === "POST") return { body: { resourceId: "test-resource" } };
|
|
60
63
|
if (route.path === "/datasets/:datasetId/compare") return { body: {
|
|
61
64
|
experimentIds: ["test-experiment-a", "test-experiment-b"],
|
|
62
65
|
baselineId: "test-experiment-a"
|
|
@@ -134,8 +137,10 @@ function generateValidDataFromSchema(schema, fieldName) {
|
|
|
134
137
|
def = getZodDef(schema);
|
|
135
138
|
}
|
|
136
139
|
if (typeName === "ZodOptional" || typeName === "ZodNullable") return generateValidDataFromSchema(def.innerType, fieldName);
|
|
137
|
-
if (typeName === "ZodDefault")
|
|
138
|
-
|
|
140
|
+
if (typeName === "ZodDefault") {
|
|
141
|
+
if ("_zod" in schema) return def.defaultValue;
|
|
142
|
+
else return def.defaultValue();
|
|
143
|
+
}
|
|
139
144
|
if (typeName === "ZodString") {
|
|
140
145
|
if ((def.checks ?? []).some((check) => {
|
|
141
146
|
const checkDef = check._zod?.def ?? check.def ?? check;
|
|
@@ -233,13 +238,19 @@ function generateValidDataFromSchema(schema, fieldName) {
|
|
|
233
238
|
if (typeName === "ZodUndefined") return void 0;
|
|
234
239
|
if (typeName === "ZodDate") return /* @__PURE__ */ new Date();
|
|
235
240
|
if (typeName === "ZodBigInt") return BigInt(0);
|
|
236
|
-
if (typeName === "ZodLiteral")
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
241
|
+
if (typeName === "ZodLiteral") {
|
|
242
|
+
if ("_zod" in schema) return def.values?.[0];
|
|
243
|
+
else return def.value;
|
|
244
|
+
}
|
|
245
|
+
if (typeName === "ZodEnum") {
|
|
246
|
+
if ("_zod" in schema) return Object.values(def.entries)[0];
|
|
247
|
+
else return def.values[0];
|
|
248
|
+
}
|
|
240
249
|
if (typeName === "ZodNativeEnum") return Object.values(def.values)[0];
|
|
241
|
-
if (typeName === "ZodArray")
|
|
242
|
-
|
|
250
|
+
if (typeName === "ZodArray") {
|
|
251
|
+
if ("_zod" in schema) return [generateValidDataFromSchema(def.element, fieldName)];
|
|
252
|
+
else return [generateValidDataFromSchema(def.type, fieldName)];
|
|
253
|
+
}
|
|
243
254
|
if (typeName === "ZodObject") {
|
|
244
255
|
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
245
256
|
const obj = {};
|
|
@@ -307,6 +318,7 @@ function getDefaultValidPathParams(route) {
|
|
|
307
318
|
if (route.path.includes(":controllerId")) params.controllerId = "test-controller";
|
|
308
319
|
if (route.path.includes(":responseId")) params.responseId = "test-response";
|
|
309
320
|
if (route.path.includes(":resourceId")) params.resourceId = "test-resource";
|
|
321
|
+
if (route.path.includes(":sessionId")) params.sessionId = "test-session";
|
|
310
322
|
if (route.path.includes(":modelConfigId")) params.modelConfigId = "id1";
|
|
311
323
|
if (route.path.includes(":scorerId") && route.path.includes("/stored/scorers/")) params.scorerId = "test-stored-scorer";
|
|
312
324
|
else if (route.path.includes(":scorerId")) params.scorerId = "test-scorer";
|
|
@@ -430,7 +442,8 @@ function createRouteTestSuite(config) {
|
|
|
430
442
|
});
|
|
431
443
|
});
|
|
432
444
|
routes.forEach((route) => {
|
|
433
|
-
|
|
445
|
+
const routeKey = `${route.method} ${route.path}`;
|
|
446
|
+
describe(routeKey, () => {
|
|
434
447
|
if (route.deprecated) {
|
|
435
448
|
it("should be marked as deprecated", () => {
|
|
436
449
|
expect(route.deprecated).toBe(true);
|
|
@@ -782,9 +795,7 @@ async function createDefaultTestContext() {
|
|
|
782
795
|
case "multiply":
|
|
783
796
|
result = a * b;
|
|
784
797
|
break;
|
|
785
|
-
case "divide":
|
|
786
|
-
result = b !== 0 ? a / b : Infinity;
|
|
787
|
-
break;
|
|
798
|
+
case "divide": result = b !== 0 ? a / b : Infinity;
|
|
788
799
|
}
|
|
789
800
|
return { result };
|
|
790
801
|
}
|
|
@@ -1336,9 +1347,10 @@ Follow these instructions for the test skill.
|
|
|
1336
1347
|
installedAt: (/* @__PURE__ */ new Date("2024-01-01T00:00:00.000Z")).toISOString()
|
|
1337
1348
|
};
|
|
1338
1349
|
fs.writeFileSync(path.join(agentSkillsDir, ".meta.json"), JSON.stringify(metaJson, null, 2));
|
|
1350
|
+
const filesystem = new LocalFilesystem({ basePath: tempDir });
|
|
1339
1351
|
const workspace = new Workspace({
|
|
1340
1352
|
id: "test-workspace",
|
|
1341
|
-
filesystem
|
|
1353
|
+
filesystem,
|
|
1342
1354
|
skills: ["skills"],
|
|
1343
1355
|
bm25: true
|
|
1344
1356
|
});
|
|
@@ -1362,7 +1374,8 @@ function createTestTool(overrides = {}) {
|
|
|
1362
1374
|
* Following the pattern from handler tests - uses actual MockMemory implementation
|
|
1363
1375
|
*/
|
|
1364
1376
|
function createMockMemory() {
|
|
1365
|
-
const
|
|
1377
|
+
const storage = new InMemoryStore();
|
|
1378
|
+
const mockMemory = new MockMemory({ storage });
|
|
1366
1379
|
mockMemory.__registerMastra = vi.fn();
|
|
1367
1380
|
return mockMemory;
|
|
1368
1381
|
}
|
|
@@ -1831,7 +1844,8 @@ function createRouteAdapterTestSuite(config) {
|
|
|
1831
1844
|
expect(response.status).toBe(200);
|
|
1832
1845
|
const data = response.data;
|
|
1833
1846
|
expect(data.filter).toEqual({ status: "active" });
|
|
1834
|
-
|
|
1847
|
+
const tags = Array.isArray(data.tag) ? data.tag : [data.tag];
|
|
1848
|
+
expect(tags).toContain("b");
|
|
1835
1849
|
expect(data.keys).not.toContain("__proto__");
|
|
1836
1850
|
expect(data.keys).not.toContain("constructor");
|
|
1837
1851
|
expect(data.keys).not.toContain("prototype");
|
|
@@ -2102,7 +2116,8 @@ function createRouteAdapterTestSuite(config) {
|
|
|
2102
2116
|
Object.entries(deprecatedByCategory).forEach(([category, routes]) => {
|
|
2103
2117
|
describe(category, () => {
|
|
2104
2118
|
routes.forEach((route) => {
|
|
2105
|
-
|
|
2119
|
+
const testName = `${route.method} ${route.path}`;
|
|
2120
|
+
describe(testName, () => {
|
|
2106
2121
|
it("should be marked as deprecated", () => {
|
|
2107
2122
|
expect(route.deprecated).toBe(true);
|
|
2108
2123
|
expect(route.openapi?.deprecated).toBe(true);
|
|
@@ -2162,7 +2177,8 @@ function createRouteAdapterTestSuite(config) {
|
|
|
2162
2177
|
Object.entries(routesByCategory).forEach(([category, routes]) => {
|
|
2163
2178
|
describe(category, () => {
|
|
2164
2179
|
routes.forEach((route) => {
|
|
2165
|
-
|
|
2180
|
+
const testName = `${route.method} ${route.path}`;
|
|
2181
|
+
describe(testName, () => {
|
|
2166
2182
|
it("should execute with valid request", async () => {
|
|
2167
2183
|
const request = buildRouteRequest(route);
|
|
2168
2184
|
const httpRequest = {
|
|
@@ -2197,7 +2213,8 @@ function createRouteAdapterTestSuite(config) {
|
|
|
2197
2213
|
query: request.query,
|
|
2198
2214
|
body: request.body
|
|
2199
2215
|
};
|
|
2200
|
-
|
|
2216
|
+
const response = await executeHttpRequest(app, httpRequest);
|
|
2217
|
+
expect(response.status).toBe(404);
|
|
2201
2218
|
});
|
|
2202
2219
|
if (route.path.includes(":workflowId")) it("should return 404 when workflow not found", async () => {
|
|
2203
2220
|
const request = buildRouteRequest(route, { pathParams: { workflowId: "non-existent-workflow" } });
|
|
@@ -2207,7 +2224,8 @@ function createRouteAdapterTestSuite(config) {
|
|
|
2207
2224
|
query: request.query,
|
|
2208
2225
|
body: request.body
|
|
2209
2226
|
};
|
|
2210
|
-
|
|
2227
|
+
const response = await executeHttpRequest(app, httpRequest);
|
|
2228
|
+
expect(response.status).toBe(404);
|
|
2211
2229
|
});
|
|
2212
2230
|
if (route.path.includes(":backgroundTaskId")) it("should return 404 when background task not found", async () => {
|
|
2213
2231
|
const request = buildRouteRequest(route, { pathParams: { backgroundTaskId: "non-existent-background-task" } });
|
|
@@ -2217,7 +2235,8 @@ function createRouteAdapterTestSuite(config) {
|
|
|
2217
2235
|
query: request.query,
|
|
2218
2236
|
body: request.body
|
|
2219
2237
|
};
|
|
2220
|
-
|
|
2238
|
+
const response = await executeHttpRequest(app, httpRequest);
|
|
2239
|
+
expect(response.status).toBe(404);
|
|
2221
2240
|
});
|
|
2222
2241
|
if (route.path.includes(":serverId")) it("should return 404 when MCP server not found (via :serverId)", async () => {
|
|
2223
2242
|
const request = buildRouteRequest(route, { pathParams: { serverId: "non-existent-server" } });
|
|
@@ -2227,7 +2246,8 @@ function createRouteAdapterTestSuite(config) {
|
|
|
2227
2246
|
query: request.query,
|
|
2228
2247
|
body: request.body
|
|
2229
2248
|
};
|
|
2230
|
-
|
|
2249
|
+
const response = await executeHttpRequest(app, httpRequest);
|
|
2250
|
+
expect(response.status).toBe(404);
|
|
2231
2251
|
});
|
|
2232
2252
|
if (route.path.includes("/mcp/v0/servers/:id")) it("should return 404 when MCP server not found (via :id)", async () => {
|
|
2233
2253
|
const request = buildRouteRequest(route, { pathParams: { id: "non-existent-server" } });
|
|
@@ -2237,7 +2257,8 @@ function createRouteAdapterTestSuite(config) {
|
|
|
2237
2257
|
query: request.query,
|
|
2238
2258
|
body: request.body
|
|
2239
2259
|
};
|
|
2240
|
-
|
|
2260
|
+
const response = await executeHttpRequest(app, httpRequest);
|
|
2261
|
+
expect(response.status).toBe(404);
|
|
2241
2262
|
});
|
|
2242
2263
|
if (route.path.includes(":processorId")) it("should return 404 when processor not found", async () => {
|
|
2243
2264
|
const request = buildRouteRequest(route, { pathParams: { processorId: "non-existent-processor" } });
|
|
@@ -2247,7 +2268,8 @@ function createRouteAdapterTestSuite(config) {
|
|
|
2247
2268
|
query: request.query,
|
|
2248
2269
|
body: request.body
|
|
2249
2270
|
};
|
|
2250
|
-
|
|
2271
|
+
const response = await executeHttpRequest(app, httpRequest);
|
|
2272
|
+
expect(response.status).toBe(404);
|
|
2251
2273
|
});
|
|
2252
2274
|
if (route.responseType === "stream") it("should be consumable via stream reader", async () => {
|
|
2253
2275
|
const request = buildRouteRequest(route);
|
|
@@ -2262,9 +2284,13 @@ function createRouteAdapterTestSuite(config) {
|
|
|
2262
2284
|
expect(response.stream).toBeDefined();
|
|
2263
2285
|
if (typeof response.stream.getReader === "function") {
|
|
2264
2286
|
const reader = response.stream.getReader();
|
|
2265
|
-
|
|
2287
|
+
const firstChunk = await reader.read();
|
|
2288
|
+
expect(firstChunk).toBeDefined();
|
|
2266
2289
|
reader.releaseLock();
|
|
2267
|
-
} else if (typeof response.stream[Symbol.asyncIterator] === "function")
|
|
2290
|
+
} else if (typeof response.stream[Symbol.asyncIterator] === "function") {
|
|
2291
|
+
const firstChunk = await response.stream[Symbol.asyncIterator]().next();
|
|
2292
|
+
expect(firstChunk).toBeDefined();
|
|
2293
|
+
}
|
|
2268
2294
|
});
|
|
2269
2295
|
if (route.queryParamSchema || route.bodySchema) it("should return 400 when schema validation fails", async () => {
|
|
2270
2296
|
const request = buildRouteRequest(route);
|
|
@@ -2321,7 +2347,8 @@ function createRouteAdapterTestSuite(config) {
|
|
|
2321
2347
|
}
|
|
2322
2348
|
}
|
|
2323
2349
|
};
|
|
2324
|
-
|
|
2350
|
+
const response = await executeHttpRequest(app, httpRequest);
|
|
2351
|
+
expect(response.status).toBeLessThan(500);
|
|
2325
2352
|
});
|
|
2326
2353
|
const bodySchema = route.bodySchema;
|
|
2327
2354
|
if (["POST", "PUT"].includes(route.method) && bodySchema) it("should spread body fields to handler params", async () => {
|
|
@@ -2382,7 +2409,8 @@ function createRouteAdapterTestSuite(config) {
|
|
|
2382
2409
|
expect(response.type).toBe("json");
|
|
2383
2410
|
const errorData = response.data;
|
|
2384
2411
|
expect(errorData).toBeDefined();
|
|
2385
|
-
|
|
2412
|
+
const hasErrorField = errorData.error !== void 0 || errorData.message !== void 0 || errorData.details !== void 0 || errorData.statusCode !== void 0;
|
|
2413
|
+
expect(hasErrorField).toBe(true);
|
|
2386
2414
|
});
|
|
2387
2415
|
});
|
|
2388
2416
|
SERVER_ROUTES.filter((r) => r.method === "POST" && r.bodySchema && !isExcluded(r)).forEach((route) => {
|
|
@@ -2498,41 +2526,88 @@ function createRouteAdapterTestSuite(config) {
|
|
|
2498
2526
|
expect(custom.getHandlerCalls()).toBe(1);
|
|
2499
2527
|
});
|
|
2500
2528
|
});
|
|
2529
|
+
describe("Run claim conflict responses (#27638)", () => {
|
|
2530
|
+
it.each([
|
|
2531
|
+
"AGENT_RESUME_ALREADY_CLAIMED",
|
|
2532
|
+
"WORKFLOW_RESUME_ALREADY_CLAIMED",
|
|
2533
|
+
"AGENT_RESUME_RUN_NOT_SUSPENDED",
|
|
2534
|
+
"WORKFLOW_RUN_NOT_SUSPENDED",
|
|
2535
|
+
"WORKFLOW_TIME_TRAVEL_ALREADY_CLAIMED",
|
|
2536
|
+
"WORKFLOW_TIME_TRAVEL_RUN_RUNNING"
|
|
2537
|
+
])("lets clients identify %s without matching message text", async (code) => {
|
|
2538
|
+
const error = new MastraError({
|
|
2539
|
+
id: code,
|
|
2540
|
+
domain: ErrorDomain.MASTRA_WORKFLOW,
|
|
2541
|
+
category: ErrorCategory.USER
|
|
2542
|
+
}, "Run conflict");
|
|
2543
|
+
const route = {
|
|
2544
|
+
method: "POST",
|
|
2545
|
+
path: "/test/run-claim-conflict",
|
|
2546
|
+
responseType: "json",
|
|
2547
|
+
handler: async () => handleError(error, "Unable to resume")
|
|
2548
|
+
};
|
|
2549
|
+
const mutableServerRoutes = SERVER_ROUTES;
|
|
2550
|
+
const originalLength = mutableServerRoutes.length;
|
|
2551
|
+
mutableServerRoutes.push(route);
|
|
2552
|
+
let conflictApp;
|
|
2553
|
+
try {
|
|
2554
|
+
({app: conflictApp} = await setupAdapter(context));
|
|
2555
|
+
} finally {
|
|
2556
|
+
mutableServerRoutes.splice(originalLength);
|
|
2557
|
+
}
|
|
2558
|
+
const response = await executeHttpRequest(conflictApp, {
|
|
2559
|
+
method: "POST",
|
|
2560
|
+
path: "/api/test/run-claim-conflict",
|
|
2561
|
+
body: {}
|
|
2562
|
+
});
|
|
2563
|
+
expect(response.status).toBe(409);
|
|
2564
|
+
expect(response.headers["content-type"]).toContain("application/json");
|
|
2565
|
+
expect(response.data).toEqual({
|
|
2566
|
+
error: "Run conflict",
|
|
2567
|
+
code
|
|
2568
|
+
});
|
|
2569
|
+
});
|
|
2570
|
+
});
|
|
2501
2571
|
describe("Route Prefix", () => {
|
|
2502
2572
|
it("should register routes at prefixed paths without double /api", async () => {
|
|
2503
2573
|
const prefixedApp = (await setupAdapter(context, { prefix: "/v2" })).app;
|
|
2504
|
-
|
|
2574
|
+
const response = await executeHttpRequest(prefixedApp, {
|
|
2505
2575
|
method: "GET",
|
|
2506
2576
|
path: "/v2/agents"
|
|
2507
|
-
})
|
|
2577
|
+
});
|
|
2578
|
+
expect(response.status).toBeLessThan(400);
|
|
2508
2579
|
});
|
|
2509
2580
|
it("should not have routes at double /api path when prefix is set", async () => {
|
|
2510
2581
|
const prefixedApp = (await setupAdapter(context, { prefix: "/v2" })).app;
|
|
2511
|
-
|
|
2582
|
+
const response = await executeHttpRequest(prefixedApp, {
|
|
2512
2583
|
method: "GET",
|
|
2513
2584
|
path: "/v2/api/agents"
|
|
2514
|
-
})
|
|
2585
|
+
});
|
|
2586
|
+
expect(response.status).toBe(404);
|
|
2515
2587
|
});
|
|
2516
2588
|
it("should normalize prefix with trailing slash", async () => {
|
|
2517
2589
|
const prefixedApp = (await setupAdapter(context, { prefix: "/mastra/" })).app;
|
|
2518
|
-
|
|
2590
|
+
const response = await executeHttpRequest(prefixedApp, {
|
|
2519
2591
|
method: "GET",
|
|
2520
2592
|
path: "/mastra/agents"
|
|
2521
|
-
})
|
|
2593
|
+
});
|
|
2594
|
+
expect(response.status).toBeLessThan(400);
|
|
2522
2595
|
});
|
|
2523
2596
|
it("should normalize prefix without leading slash", async () => {
|
|
2524
2597
|
const prefixedApp = (await setupAdapter(context, { prefix: "mastra" })).app;
|
|
2525
|
-
|
|
2598
|
+
const response = await executeHttpRequest(prefixedApp, {
|
|
2526
2599
|
method: "GET",
|
|
2527
2600
|
path: "/mastra/agents"
|
|
2528
|
-
})
|
|
2601
|
+
});
|
|
2602
|
+
expect(response.status).toBeLessThan(400);
|
|
2529
2603
|
});
|
|
2530
2604
|
it("should not have routes at double-slash path when prefix has trailing slash", async () => {
|
|
2531
2605
|
const prefixedApp = (await setupAdapter(context, { prefix: "/mastra/" })).app;
|
|
2532
|
-
|
|
2606
|
+
const response = await executeHttpRequest(prefixedApp, {
|
|
2533
2607
|
method: "GET",
|
|
2534
2608
|
path: "/mastra//agents"
|
|
2535
|
-
})
|
|
2609
|
+
});
|
|
2610
|
+
expect(response.status).toBe(404);
|
|
2536
2611
|
});
|
|
2537
2612
|
});
|
|
2538
2613
|
});
|
|
@@ -2696,10 +2771,11 @@ function createMCPRouteTestSuite(config) {
|
|
|
2696
2771
|
expect(res.data.tools).toEqual(expect.arrayContaining([expect.objectContaining({ name: "getWeather" }), expect.objectContaining({ name: "calculate" })]));
|
|
2697
2772
|
});
|
|
2698
2773
|
it("should return 404 for non-existent server", async () => {
|
|
2699
|
-
|
|
2774
|
+
const res = await executeHttpRequest(app, {
|
|
2700
2775
|
method: "GET",
|
|
2701
2776
|
path: "/api/mcp/non-existent/tools"
|
|
2702
|
-
})
|
|
2777
|
+
});
|
|
2778
|
+
expect(res.status).toBe(404);
|
|
2703
2779
|
});
|
|
2704
2780
|
});
|
|
2705
2781
|
describe("GET /api/mcp/:serverId/tools/:toolId", () => {
|
|
@@ -2716,10 +2792,11 @@ function createMCPRouteTestSuite(config) {
|
|
|
2716
2792
|
});
|
|
2717
2793
|
});
|
|
2718
2794
|
it("should return 404 for non-existent tool", async () => {
|
|
2719
|
-
|
|
2795
|
+
const res = await executeHttpRequest(app, {
|
|
2720
2796
|
method: "GET",
|
|
2721
2797
|
path: `/api/mcp/${mcpServer1.id}/tools/non-existent`
|
|
2722
|
-
})
|
|
2798
|
+
});
|
|
2799
|
+
expect(res.status).toBe(404);
|
|
2723
2800
|
});
|
|
2724
2801
|
});
|
|
2725
2802
|
describe("POST /api/mcp/:serverId/tools/:toolId/execute", () => {
|
|
@@ -2749,7 +2826,7 @@ function createMCPRouteTestSuite(config) {
|
|
|
2749
2826
|
});
|
|
2750
2827
|
});
|
|
2751
2828
|
it("should return 404 for non-existent server", async () => {
|
|
2752
|
-
|
|
2829
|
+
const res = await executeHttpRequest(app, {
|
|
2753
2830
|
method: "POST",
|
|
2754
2831
|
path: "/api/mcp/non-existent/tools/calculate/execute",
|
|
2755
2832
|
body: { data: {
|
|
@@ -2757,7 +2834,8 @@ function createMCPRouteTestSuite(config) {
|
|
|
2757
2834
|
a: 1,
|
|
2758
2835
|
b: 2
|
|
2759
2836
|
} }
|
|
2760
|
-
})
|
|
2837
|
+
});
|
|
2838
|
+
expect(res.status).toBe(404);
|
|
2761
2839
|
});
|
|
2762
2840
|
it("should return error for non-existent tool", async () => {
|
|
2763
2841
|
const res = await executeHttpRequest(app, {
|
|
@@ -3028,9 +3106,7 @@ function createMCPTransportTestSuite(config) {
|
|
|
3028
3106
|
case "multiply":
|
|
3029
3107
|
result = a * b;
|
|
3030
3108
|
break;
|
|
3031
|
-
case "divide":
|
|
3032
|
-
result = a / b;
|
|
3033
|
-
break;
|
|
3109
|
+
case "divide": result = a / b;
|
|
3034
3110
|
}
|
|
3035
3111
|
return { result };
|
|
3036
3112
|
}
|
|
@@ -3092,7 +3168,8 @@ function createMCPTransportTestSuite(config) {
|
|
|
3092
3168
|
path: "messages",
|
|
3093
3169
|
method: "POST"
|
|
3094
3170
|
}])("rejects the legacy $path route for a native server", async ({ path, method }) => {
|
|
3095
|
-
|
|
3171
|
+
const response = await fetch(`http://localhost:${port}/api/mcp/native-fixture/${path}`, { method });
|
|
3172
|
+
expect(response.status).toBe(404);
|
|
3096
3173
|
});
|
|
3097
3174
|
it("executes ordinary tools with the v2 request context and reports a suspended tool truthfully", async () => {
|
|
3098
3175
|
const base = `http://localhost:${port}/api/mcp/native-fixture/tools`;
|
|
@@ -3144,7 +3221,7 @@ function createMCPTransportTestSuite(config) {
|
|
|
3144
3221
|
describe("HTTP Transport (/api/mcp/:serverId/mcp)", () => {
|
|
3145
3222
|
describe("Error handling (raw HTTP)", () => {
|
|
3146
3223
|
it("should return 404 for non-existent server", async () => {
|
|
3147
|
-
|
|
3224
|
+
const res = await fetch(`http://localhost:${port}/api/mcp/non-existent/mcp`, {
|
|
3148
3225
|
method: "POST",
|
|
3149
3226
|
headers: {
|
|
3150
3227
|
"Content-Type": "application/json",
|
|
@@ -3163,7 +3240,8 @@ function createMCPTransportTestSuite(config) {
|
|
|
3163
3240
|
},
|
|
3164
3241
|
id: 1
|
|
3165
3242
|
})
|
|
3166
|
-
})
|
|
3243
|
+
});
|
|
3244
|
+
expect(res.status).toBe(404);
|
|
3167
3245
|
});
|
|
3168
3246
|
});
|
|
3169
3247
|
describe("Protocol operations (MCPClient)", () => {
|
|
@@ -3199,7 +3277,8 @@ function createMCPTransportTestSuite(config) {
|
|
|
3199
3277
|
const tools = await mcpClient.listTools();
|
|
3200
3278
|
expect(tools["server1_getWeather"]).toBeDefined();
|
|
3201
3279
|
expect(tools["server1_calculate"]).toBeDefined();
|
|
3202
|
-
|
|
3280
|
+
const server2Tools = Object.keys(tools).filter((k) => k.startsWith("server2_"));
|
|
3281
|
+
expect(server2Tools).toHaveLength(0);
|
|
3203
3282
|
});
|
|
3204
3283
|
});
|
|
3205
3284
|
describe("Tool execution errors (MCPClient)", () => {
|
|
@@ -3208,17 +3287,18 @@ function createMCPTransportTestSuite(config) {
|
|
|
3208
3287
|
let failingMastra;
|
|
3209
3288
|
let failingHttpServer;
|
|
3210
3289
|
beforeAll(async () => {
|
|
3290
|
+
const failingTool = createTool({
|
|
3291
|
+
id: "failingTool",
|
|
3292
|
+
description: "A tool that always throws an error",
|
|
3293
|
+
inputSchema: z.object({}),
|
|
3294
|
+
execute: async () => {
|
|
3295
|
+
throw new Error("Tool execution failed intentionally");
|
|
3296
|
+
}
|
|
3297
|
+
});
|
|
3211
3298
|
failingServer = new MCPServer({
|
|
3212
3299
|
name: "failingServer",
|
|
3213
3300
|
version: "1.0.0",
|
|
3214
|
-
tools: { failingTool
|
|
3215
|
-
id: "failingTool",
|
|
3216
|
-
description: "A tool that always throws an error",
|
|
3217
|
-
inputSchema: z.object({}),
|
|
3218
|
-
execute: async () => {
|
|
3219
|
-
throw new Error("Tool execution failed intentionally");
|
|
3220
|
-
}
|
|
3221
|
-
}) }
|
|
3301
|
+
tools: { failingTool }
|
|
3222
3302
|
});
|
|
3223
3303
|
failingMastra = new Mastra$1({ mcpServers: { "failing-server": failingServer } });
|
|
3224
3304
|
const serverSetup = await createServer(failingMastra);
|
|
@@ -3253,10 +3333,11 @@ function createMCPTransportTestSuite(config) {
|
|
|
3253
3333
|
describe("SSE Transport (/api/mcp/:serverId/sse)", () => {
|
|
3254
3334
|
describe("Error handling (raw HTTP)", () => {
|
|
3255
3335
|
it("should return 404 for non-existent server on GET /sse", async () => {
|
|
3256
|
-
|
|
3336
|
+
const res = await fetch(`http://localhost:${port}/api/mcp/non-existent/sse`);
|
|
3337
|
+
expect(res.status).toBe(404);
|
|
3257
3338
|
});
|
|
3258
3339
|
it("should return 404 for non-existent server on POST /messages", async () => {
|
|
3259
|
-
|
|
3340
|
+
const res = await fetch(`http://localhost:${port}/api/mcp/non-existent/messages`, {
|
|
3260
3341
|
method: "POST",
|
|
3261
3342
|
headers: { "Content-Type": "application/json" },
|
|
3262
3343
|
body: JSON.stringify({
|
|
@@ -3264,14 +3345,16 @@ function createMCPTransportTestSuite(config) {
|
|
|
3264
3345
|
method: "test",
|
|
3265
3346
|
id: 1
|
|
3266
3347
|
})
|
|
3267
|
-
})
|
|
3348
|
+
});
|
|
3349
|
+
expect(res.status).toBe(404);
|
|
3268
3350
|
});
|
|
3269
3351
|
});
|
|
3270
3352
|
describe("MCP 2.x servers (MCPClient)", () => {
|
|
3271
3353
|
it("does not serve the legacy SSE transport for a 2.x MCPServer", async (ctx) => {
|
|
3272
3354
|
if (mcpServer1.mcpVersion !== 2) ctx.skip();
|
|
3273
|
-
|
|
3274
|
-
expect((
|
|
3355
|
+
const res = await fetch(`http://localhost:${port}/api/mcp/${mcpServer1.id}/sse`);
|
|
3356
|
+
expect(res.status).toBe(404);
|
|
3357
|
+
const messages = await fetch(`http://localhost:${port}/api/mcp/${mcpServer1.id}/messages`, {
|
|
3275
3358
|
method: "POST",
|
|
3276
3359
|
headers: { "Content-Type": "application/json" },
|
|
3277
3360
|
body: JSON.stringify({
|
|
@@ -3279,7 +3362,8 @@ function createMCPTransportTestSuite(config) {
|
|
|
3279
3362
|
method: "tools/list",
|
|
3280
3363
|
id: 1
|
|
3281
3364
|
})
|
|
3282
|
-
})
|
|
3365
|
+
});
|
|
3366
|
+
expect(messages.status).toBe(404);
|
|
3283
3367
|
});
|
|
3284
3368
|
it("fails to connect through the SSE URL without downgrading", async (ctx) => {
|
|
3285
3369
|
if (mcpServer1.mcpVersion !== 2) ctx.skip();
|
|
@@ -3356,7 +3440,8 @@ function createMultipartTestSuite(config) {
|
|
|
3356
3440
|
body: formData
|
|
3357
3441
|
});
|
|
3358
3442
|
expect(response.status).toBe(200);
|
|
3359
|
-
|
|
3443
|
+
const result = await response.json();
|
|
3444
|
+
expect(result).toMatchObject({
|
|
3360
3445
|
success: true,
|
|
3361
3446
|
hasAudio: true
|
|
3362
3447
|
});
|
|
@@ -3391,10 +3476,11 @@ function createMultipartTestSuite(config) {
|
|
|
3391
3476
|
language: "en",
|
|
3392
3477
|
format: "mp3"
|
|
3393
3478
|
}));
|
|
3394
|
-
|
|
3479
|
+
const response = await fetch(`${serverInfo.baseUrl}/test/upload-with-options`, {
|
|
3395
3480
|
method: "POST",
|
|
3396
3481
|
body: formData
|
|
3397
|
-
})
|
|
3482
|
+
});
|
|
3483
|
+
expect(response.status).toBe(200);
|
|
3398
3484
|
expect(receivedBody.options).toBeDefined();
|
|
3399
3485
|
expect(typeof receivedBody.options).toBe("object");
|
|
3400
3486
|
expect(receivedBody.options.language).toBe("en");
|
|
@@ -3424,10 +3510,11 @@ function createMultipartTestSuite(config) {
|
|
|
3424
3510
|
])], { type: "audio/wav" });
|
|
3425
3511
|
formData.append("audio", audioBlob, "test.wav");
|
|
3426
3512
|
formData.append("name", "test-recording");
|
|
3427
|
-
|
|
3513
|
+
const response = await fetch(`${serverInfo.baseUrl}/test/upload-with-string`, {
|
|
3428
3514
|
method: "POST",
|
|
3429
3515
|
body: formData
|
|
3430
|
-
})
|
|
3516
|
+
});
|
|
3517
|
+
expect(response.status).toBe(200);
|
|
3431
3518
|
expect(receivedBody.name).toBe("test-recording");
|
|
3432
3519
|
});
|
|
3433
3520
|
it("should reject files exceeding size limit", async () => {
|
|
@@ -3449,10 +3536,11 @@ function createMultipartTestSuite(config) {
|
|
|
3449
3536
|
const formData = new FormData();
|
|
3450
3537
|
const largeBlob = new Blob([/* @__PURE__ */ new Uint8Array(200)], { type: "audio/wav" });
|
|
3451
3538
|
formData.append("audio", largeBlob, "large.wav");
|
|
3452
|
-
|
|
3539
|
+
const response = await fetch(`${serverInfo.baseUrl}/test/upload-limited`, {
|
|
3453
3540
|
method: "POST",
|
|
3454
3541
|
body: formData
|
|
3455
|
-
})
|
|
3542
|
+
});
|
|
3543
|
+
expect(response.status).toBeGreaterThanOrEqual(400);
|
|
3456
3544
|
});
|
|
3457
3545
|
it("should handle empty FormData gracefully", async () => {
|
|
3458
3546
|
const { adapter, app } = await setupAdapter(context);
|
|
@@ -3485,7 +3573,8 @@ function createMultipartTestSuite(config) {
|
|
|
3485
3573
|
body: formData
|
|
3486
3574
|
});
|
|
3487
3575
|
expect(response.status).toBe(200);
|
|
3488
|
-
|
|
3576
|
+
const result = await response.json();
|
|
3577
|
+
expect(result).toMatchObject({
|
|
3489
3578
|
success: true,
|
|
3490
3579
|
bodyKeys: []
|
|
3491
3580
|
});
|
|
@@ -3527,7 +3616,8 @@ function createMultipartTestSuite(config) {
|
|
|
3527
3616
|
body: formData
|
|
3528
3617
|
});
|
|
3529
3618
|
expect(response.status).toBe(200);
|
|
3530
|
-
|
|
3619
|
+
const result = await response.json();
|
|
3620
|
+
expect(result).toMatchObject({
|
|
3531
3621
|
hasFile1: true,
|
|
3532
3622
|
hasFile2: true
|
|
3533
3623
|
});
|
|
@@ -3556,7 +3646,8 @@ function createMultipartTestSuite(config) {
|
|
|
3556
3646
|
body: JSON.stringify({ message: "Hello, World!" })
|
|
3557
3647
|
});
|
|
3558
3648
|
expect(response.status).toBe(200);
|
|
3559
|
-
|
|
3649
|
+
const result = await response.json();
|
|
3650
|
+
expect(result).toMatchObject({
|
|
3560
3651
|
success: true,
|
|
3561
3652
|
message: "Hello, World!"
|
|
3562
3653
|
});
|
|
@@ -3656,7 +3747,8 @@ function createHttpLoggingTestSuite(config) {
|
|
|
3656
3747
|
logSpy = vi.spyOn(adapter.logger, "info");
|
|
3657
3748
|
await adapter.init();
|
|
3658
3749
|
await addRoute(app, "GET", "/test", () => ({ message: "success" }));
|
|
3659
|
-
|
|
3750
|
+
const response = await executeRequest(app, "GET", "http://localhost/test");
|
|
3751
|
+
expect(response.status).toBe(200);
|
|
3660
3752
|
expect(logSpy).toHaveBeenCalledWith(expect.stringMatching(/GET \/test 200 \d+ms/), expect.objectContaining({
|
|
3661
3753
|
method: "GET",
|
|
3662
3754
|
path: "/test",
|
|
@@ -3834,10 +3926,11 @@ function createBodyLimitTestSuite(config) {
|
|
|
3834
3926
|
return { receivedBody: body };
|
|
3835
3927
|
}
|
|
3836
3928
|
});
|
|
3837
|
-
|
|
3929
|
+
const response = await executeRequest(wiredApp, "POST", "http://localhost/test/route-body-limit", {
|
|
3838
3930
|
headers: { "Content-Type": "application/json" },
|
|
3839
3931
|
body: oversizedPayload
|
|
3840
|
-
})
|
|
3932
|
+
});
|
|
3933
|
+
expect(response.status).toBe(413);
|
|
3841
3934
|
expect(handlerCalled).toBe(false);
|
|
3842
3935
|
await cleanupApp?.(wiredApp);
|
|
3843
3936
|
});
|
|
@@ -3856,10 +3949,11 @@ function createBodyLimitTestSuite(config) {
|
|
|
3856
3949
|
return { receivedBody: body };
|
|
3857
3950
|
}
|
|
3858
3951
|
});
|
|
3859
|
-
|
|
3952
|
+
const response = await executeRequestWithoutContentLength(wiredApp, "POST", "http://localhost/test/route-body-limit-no-content-length", {
|
|
3860
3953
|
headers: { "Content-Type": "application/json" },
|
|
3861
3954
|
body: oversizedPayload
|
|
3862
|
-
})
|
|
3955
|
+
});
|
|
3956
|
+
expect(response.status).toBe(413);
|
|
3863
3957
|
expect(handlerCalled).toBe(false);
|
|
3864
3958
|
await cleanupApp?.(wiredApp);
|
|
3865
3959
|
});
|
|
@@ -3876,10 +3970,11 @@ function createBodyLimitTestSuite(config) {
|
|
|
3876
3970
|
responseType: "json",
|
|
3877
3971
|
handler: async ({ body }) => ({ receivedBody: body })
|
|
3878
3972
|
});
|
|
3879
|
-
|
|
3973
|
+
const response = await executeRequest(wiredApp, method, "http://localhost/test/body-limit", {
|
|
3880
3974
|
headers: { "Content-Type": "application/json" },
|
|
3881
3975
|
body: oversizedPayload
|
|
3882
|
-
})
|
|
3976
|
+
});
|
|
3977
|
+
expect(response.status).toBe(413);
|
|
3883
3978
|
await cleanupApp?.(wiredApp);
|
|
3884
3979
|
});
|
|
3885
3980
|
});
|