@mastra/server-adapters-test-suite 0.1.4-alpha.0 → 0.1.4-alpha.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @mastra/server-adapters-test-suite
2
2
 
3
+ ## 0.1.4-alpha.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Add a reusable real-HTTP disconnect suite covering durable and evented stream starts, resumes, tool approvals, observer replay, and explicit cancellation, with ordinary agents as the cancellation control. ([#28162](https://github.com/mastra-ai/mastra/pull/28162))
8
+
9
+ - Skipped the run-scoped abort route (`POST /agents/:agentId/runs/:runId/abort`) in the generic adapter route suite, since it returns 501 for the suite's non-durable agent by design. Adapter suites no longer fail on this route. ([#28301](https://github.com/mastra-ai/mastra/pull/28301))
10
+
11
+ - Updated dependencies [[`f396b43`](https://github.com/mastra-ai/mastra/commit/f396b435c72042fc6d9e82c361fa047675995812), [`f396b43`](https://github.com/mastra-ai/mastra/commit/f396b435c72042fc6d9e82c361fa047675995812), [`0d7d5cb`](https://github.com/mastra-ai/mastra/commit/0d7d5cbdccacc0b03697b3400579ea520e382061), [`0d7d5cb`](https://github.com/mastra-ai/mastra/commit/0d7d5cbdccacc0b03697b3400579ea520e382061), [`f5ebbf3`](https://github.com/mastra-ai/mastra/commit/f5ebbf3b2dc935b8fc068996b4db478815f05bd5), [`11c83d5`](https://github.com/mastra-ai/mastra/commit/11c83d53331588e06623d5d4a35baa606bbaa424), [`703b0a1`](https://github.com/mastra-ai/mastra/commit/703b0a1cb342d3a6fce72b5b07685fbb31695674), [`1283717`](https://github.com/mastra-ai/mastra/commit/1283717ecfc652da293aafe6ff582afa07ca84a5), [`3be96ae`](https://github.com/mastra-ai/mastra/commit/3be96ae2f65a2ff3b2189fb79fd03b7eedee3dbe)]:
12
+ - @mastra/core@1.80.0-alpha.1
13
+ - @mastra/server@1.80.0-alpha.1
14
+ - @mastra/mcp@2.6.0-alpha.0
15
+
3
16
  ## 0.1.4-alpha.0
4
17
 
5
18
  ### Patch Changes
@@ -0,0 +1,23 @@
1
+ import { Agent } from '@mastra/core/agent';
2
+ import { Mastra } from '@mastra/core/mastra';
3
+ export interface DurableDisconnectTestConfig {
4
+ setupModel: () => Promise<{
5
+ model: ConstructorParameters<typeof Agent>[0]['model'];
6
+ addResponse: (sequenceIndex: number, response: {
7
+ content?: string;
8
+ toolCalls?: {
9
+ id: string;
10
+ name: string;
11
+ arguments: Record<string, unknown>;
12
+ }[];
13
+ }) => void;
14
+ getRequestCount: () => number;
15
+ close: () => Promise<void>;
16
+ }>;
17
+ startServer: (mastra: Mastra, onDisconnect: (path: string) => void) => Promise<{
18
+ baseUrl: string;
19
+ close: () => Promise<void>;
20
+ }>;
21
+ }
22
+ export declare function createDurableDisconnectTestSuite(config: DurableDisconnectTestConfig): void;
23
+ //# sourceMappingURL=durable-disconnect-test-suite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"durable-disconnect-test-suite.d.ts","sourceRoot":"","sources":["../src/durable-disconnect-test-suite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAO7C,MAAM,WAAW,2BAA2B;IAC1C,UAAU,EAAE,MAAM,OAAO,CAAC;QACxB,KAAK,EAAE,qBAAqB,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QACvD,WAAW,EAAE,CACX,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE;YACR,OAAO,CAAC,EAAE,MAAM,CAAC;YACjB,SAAS,CAAC,EAAE;gBAAE,EAAE,EAAE,MAAM,CAAC;gBAAC,IAAI,EAAE,MAAM,CAAC;gBAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;aAAE,EAAE,CAAC;SAChF,KACE,IAAI,CAAC;QACV,eAAe,EAAE,MAAM,MAAM,CAAC;QAC9B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;KAC5B,CAAC,CAAC;IACH,WAAW,EAAE,CACX,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,KACjC,OAAO,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;KAC5B,CAAC,CAAC;CACJ;AAWD,wBAAgB,gCAAgC,CAAC,MAAM,EAAE,2BAA2B,QAmPnF"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { createRouteAdapterTestSuite } from './route-adapter-test-suite.js';
2
+ export { createDurableDisconnectTestSuite, type DurableDisconnectTestConfig } from './durable-disconnect-test-suite.js';
2
3
  export type { AdapterTestSuiteConfig, AdapterTestContext, AdapterSetupOptions, HttpRequest, HttpResponse, } from './test-helpers.js';
3
4
  export { createMCPRouteTestSuite } from './mcp-route-test-suite.js';
4
5
  export { createMCPTransportTestSuite, type MCPTransportTestConfig } from './mcp-transport-test-suite.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AACzE,YAAY,EACV,sBAAsB,EACtB,kBAAkB,EAClB,mBAAmB,EACnB,WAAW,EACX,YAAY,GACb,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,2BAA2B,EAAE,KAAK,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACtG,OAAO,EAAE,wBAAwB,EAAE,KAAK,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AACjG,OAAO,EAAE,0BAA0B,EAAE,KAAK,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AACxG,OAAO,EAAE,wBAAwB,EAAE,KAAK,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AAClG,OAAO,EAAE,6BAA6B,EAAE,KAAK,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AAElH,OAAO,EACL,wBAAwB,EACxB,6BAA6B,EAC7B,mCAAmC,EACnC,4BAA4B,EAC5B,gBAAgB,GACjB,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,EAAE,gCAAgC,EAAE,KAAK,2BAA2B,EAAE,MAAM,iCAAiC,CAAC;AACrH,YAAY,EACV,sBAAsB,EACtB,kBAAkB,EAClB,mBAAmB,EACnB,WAAW,EACX,YAAY,GACb,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,2BAA2B,EAAE,KAAK,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACtG,OAAO,EAAE,wBAAwB,EAAE,KAAK,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AACjG,OAAO,EAAE,0BAA0B,EAAE,KAAK,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AACxG,OAAO,EAAE,wBAAwB,EAAE,KAAK,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AAClG,OAAO,EAAE,6BAA6B,EAAE,KAAK,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AAElH,OAAO,EACL,wBAAwB,EACxB,6BAA6B,EAC7B,mCAAmC,EACnC,4BAA4B,EAC5B,gBAAgB,GACjB,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ import { CompositeVoice } from "@mastra/core/voice";
25
25
  import { createStep, createWorkflow } from "@mastra/core/workflows";
26
26
  import { LocalFilesystem, Workspace } from "@mastra/core/workspace";
27
27
  import { MCPClient, MCPServer, defineEvent } from "@mastra/mcp";
28
+ import { createDurableAgent, createEventedAgent } from "@mastra/core/agent/durable";
28
29
  import { Mastra as Mastra$1 } from "@mastra/core/mastra";
29
30
  import http from "http";
30
31
  import { MCPServerBase } from "@mastra/core/mcp";
@@ -2171,6 +2172,7 @@ function createRouteAdapterTestSuite(config) {
2171
2172
  "/agents/:agentId/observe",
2172
2173
  "/mcp/:serverId/events/:eventName/stream",
2173
2174
  "/agents/:agentId/recover",
2175
+ "/agents/:agentId/runs/:runId/abort",
2174
2176
  "/agents/:agentId/plans/file",
2175
2177
  "/tool-providers/:providerId/connections/:connectionId",
2176
2178
  "/tool-providers/:providerId/connections/:connectionId/usage",
@@ -3012,6 +3014,278 @@ function createRouteAdapterTestSuite(config) {
3012
3014
  });
3013
3015
  }
3014
3016
  //#endregion
3017
+ //#region src/durable-disconnect-test-suite.ts
3018
+ function deferred() {
3019
+ let resolve;
3020
+ return {
3021
+ promise: new Promise((r) => {
3022
+ resolve = r;
3023
+ }),
3024
+ resolve
3025
+ };
3026
+ }
3027
+ function createDurableDisconnectTestSuite(config) {
3028
+ describe.each([
3029
+ "plain",
3030
+ "durable",
3031
+ "evented"
3032
+ ])("HTTP observer disconnect: %s (COR-1489)", (kind) => {
3033
+ let server;
3034
+ let fixture;
3035
+ let mastra;
3036
+ afterEach(async () => {
3037
+ if (mastra) await mastra.shutdown();
3038
+ if (server) await server.close();
3039
+ if (fixture) await fixture.close();
3040
+ server = void 0;
3041
+ mastra = void 0;
3042
+ fixture = void 0;
3043
+ });
3044
+ for (const route of [
3045
+ "stream",
3046
+ "stream-alias",
3047
+ "stream-until-idle",
3048
+ "resume-stream",
3049
+ "resume-alias",
3050
+ "resume-stream-until-idle",
3051
+ "approve-tool-call",
3052
+ "decline-tool-call"
3053
+ ]) for (const explicitAbort of [
3054
+ false,
3055
+ true,
3056
+ "run",
3057
+ "threadless"
3058
+ ]) {
3059
+ if (typeof explicitAbort === "string" && (kind === "plain" || !["stream", "stream-alias"].includes(route))) continue;
3060
+ const threadless = explicitAbort === "threadless";
3061
+ it(`${route}: disconnect ${explicitAbort ? `then explicit ${explicitAbort === true ? "thread" : explicitAbort} abort stops execution` : "preserves durable execution and replay"}`, async () => {
3062
+ fixture = await config.setupModel();
3063
+ const entered = deferred();
3064
+ const gate = deferred();
3065
+ const disconnected = deferred();
3066
+ const calls = [];
3067
+ let toolSignal;
3068
+ const storage = new InMemoryStore();
3069
+ const memory = new MockMemory({ storage });
3070
+ const slow = createTool({
3071
+ id: "slow",
3072
+ description: "Execute a numbered step",
3073
+ inputSchema: z.object({ n: z.number() }),
3074
+ execute: async ({ n }, context) => {
3075
+ calls.push(n);
3076
+ if (n === 1) {
3077
+ toolSignal = context?.abortSignal;
3078
+ entered.resolve();
3079
+ await Promise.race([gate.promise, new Promise((resolve) => {
3080
+ if (toolSignal?.aborted) return resolve();
3081
+ toolSignal?.addEventListener("abort", () => resolve(), { once: true });
3082
+ })]);
3083
+ }
3084
+ return { n };
3085
+ }
3086
+ });
3087
+ const research = createTool({
3088
+ id: "research",
3089
+ description: "Background research",
3090
+ inputSchema: z.object({}),
3091
+ background: { enabled: true },
3092
+ execute: async () => ({ done: true })
3093
+ });
3094
+ let authorizations = 0;
3095
+ const authorize = createTool({
3096
+ id: "authorize",
3097
+ description: "Authorize the steps",
3098
+ inputSchema: z.object({}),
3099
+ requireApproval: true,
3100
+ execute: async () => {
3101
+ authorizations++;
3102
+ return { approved: true };
3103
+ }
3104
+ });
3105
+ const resumed = route.startsWith("resume") || route.endsWith("tool-call");
3106
+ const offset = resumed ? 1 : 0;
3107
+ const inner = new Agent({
3108
+ id: "disconnect-agent",
3109
+ name: "Disconnect agent",
3110
+ instructions: "Execute three steps.",
3111
+ model: fixture.model,
3112
+ ...threadless ? {} : { memory },
3113
+ tools: {
3114
+ slow,
3115
+ research,
3116
+ authorize
3117
+ },
3118
+ backgroundTasks: { tools: { research: true } }
3119
+ });
3120
+ const agent = kind === "durable" ? createDurableAgent({ agent: inner }) : kind === "evented" ? createEventedAgent({ agent: inner }) : inner;
3121
+ const otherAgent = createDurableAgent({ agent: new Agent({
3122
+ id: "other-agent",
3123
+ name: "Other agent",
3124
+ instructions: "Do not run.",
3125
+ model: fixture.model
3126
+ }) });
3127
+ mastra = new Mastra$1({
3128
+ agents: {
3129
+ agent,
3130
+ otherAgent
3131
+ },
3132
+ storage,
3133
+ logger: false,
3134
+ backgroundTasks: {
3135
+ enabled: true,
3136
+ globalConcurrency: 5,
3137
+ perAgentConcurrency: 3
3138
+ }
3139
+ });
3140
+ server = await config.startServer(mastra, (path) => {
3141
+ if (!resumed || !path.endsWith("/stream")) disconnected.resolve();
3142
+ });
3143
+ const base = `${server.baseUrl}/api/agents/agent`;
3144
+ if (resumed) fixture.addResponse(0, { toolCalls: [{
3145
+ id: "authorize-1",
3146
+ name: "authorize",
3147
+ arguments: {}
3148
+ }] });
3149
+ for (let n = 1; n <= 3; n++) fixture.addResponse(offset + n - 1, { toolCalls: [{
3150
+ id: `slow-${n}`,
3151
+ name: "slow",
3152
+ arguments: { n }
3153
+ }] });
3154
+ fixture.addResponse(offset + 3, { content: "done: 3 results" });
3155
+ const runId = `disconnect-${kind}-${route}-${explicitAbort}`;
3156
+ const thread = {
3157
+ thread: runId,
3158
+ resource: "disconnect-user"
3159
+ };
3160
+ if (resumed) {
3161
+ const parked = await fetch(`${base}/stream`, {
3162
+ method: "POST",
3163
+ headers: { "content-type": "application/json" },
3164
+ body: JSON.stringify({
3165
+ messages: "Start",
3166
+ runId,
3167
+ memory: thread,
3168
+ maxSteps: 6
3169
+ })
3170
+ });
3171
+ expect(parked.status).toBe(200);
3172
+ const reader = parked.body.getReader();
3173
+ let received = "";
3174
+ while (!received.includes("tool-call-approval")) {
3175
+ const { done, value } = await reader.read();
3176
+ expect(done).toBe(false);
3177
+ received += new TextDecoder().decode(value);
3178
+ }
3179
+ await reader.cancel();
3180
+ }
3181
+ const client = new AbortController();
3182
+ const request = fetch(`${base}/${route === "stream-alias" ? "stream" : route === "resume-alias" ? "resume-stream" : route}`, {
3183
+ method: "POST",
3184
+ headers: { "content-type": "application/json" },
3185
+ signal: client.signal,
3186
+ body: JSON.stringify({
3187
+ ...resumed ? {
3188
+ resumeData: { approved: true },
3189
+ toolCallId: "authorize-1"
3190
+ } : { messages: "Start" },
3191
+ runId,
3192
+ ...threadless ? {} : { memory: thread },
3193
+ maxSteps: 6,
3194
+ ...["stream-alias", "resume-alias"].includes(route) ? { untilIdle: true } : {}
3195
+ })
3196
+ }).then(async (response) => {
3197
+ expect(response.status).toBe(200);
3198
+ await response.text();
3199
+ }).catch((error) => {
3200
+ if (error.name !== "AbortError") throw error;
3201
+ });
3202
+ try {
3203
+ await entered.promise;
3204
+ client.abort();
3205
+ await disconnected.promise;
3206
+ await request;
3207
+ if (kind !== "plain" && explicitAbort) {
3208
+ const runScoped = typeof explicitAbort === "string";
3209
+ if (runScoped) {
3210
+ const denied = await fetch(`${server.baseUrl}/api/agents/otherAgent/runs/${runId}/abort`, { method: "POST" });
3211
+ expect(denied.status).toBe(403);
3212
+ expect(toolSignal?.aborted).toBe(false);
3213
+ }
3214
+ const response = await fetch(`${base}/${runScoped ? `runs/${runId}/abort` : "threads/abort"}`, {
3215
+ method: "POST",
3216
+ headers: { "content-type": "application/json" },
3217
+ ...runScoped ? {} : { body: JSON.stringify({
3218
+ threadId: runId,
3219
+ resourceId: thread.resource,
3220
+ expectedRunId: runId
3221
+ }) }
3222
+ });
3223
+ const result = await response.json();
3224
+ expect(response.status).toBe(200);
3225
+ expect(result).toEqual({ aborted: true });
3226
+ }
3227
+ const completes = kind !== "plain" && !explicitAbort;
3228
+ if (!completes) await vi.waitFor(() => expect(toolSignal?.aborted).toBe(true));
3229
+ else expect(toolSignal?.aborted).toBe(false);
3230
+ const observing = kind === "plain" ? void 0 : fetch(`${base}/observe`, {
3231
+ method: "POST",
3232
+ headers: { "content-type": "application/json" },
3233
+ body: JSON.stringify({ runId })
3234
+ });
3235
+ gate.resolve();
3236
+ if (observing) {
3237
+ const response = await observing;
3238
+ expect(response.status).toBe(200);
3239
+ const replay = await response.text();
3240
+ expect(replay).toContain("finish");
3241
+ if (completes) expect(replay).toContain("done: 3 results");
3242
+ else expect(replay).toContain("abort");
3243
+ }
3244
+ await vi.waitFor(() => expect(calls).toEqual(completes ? [
3245
+ 1,
3246
+ 2,
3247
+ 3
3248
+ ] : [1]));
3249
+ expect(fixture.getRequestCount()).toBe(offset + (completes ? 4 : 1));
3250
+ if (resumed) expect(authorizations).toBe(route === "decline-tool-call" ? 0 : 1);
3251
+ if (kind !== "plain") {
3252
+ const workflows = await storage.getStore("workflows");
3253
+ await vi.waitFor(async () => {
3254
+ for (const workflowName of ["durable-agentic-loop", "durable-agentic-execution"]) expect(await workflows.loadWorkflowSnapshot({
3255
+ workflowName,
3256
+ runId
3257
+ })).toBeFalsy();
3258
+ });
3259
+ }
3260
+ if (completes) {
3261
+ await vi.waitFor(async () => {
3262
+ const { messages } = await memory.recall({
3263
+ threadId: runId,
3264
+ resourceId: thread.resource
3265
+ });
3266
+ expect(messages.flatMap((m) => m.content.parts ?? []).some((p) => p.type === "text" && p.text === "done: 3 results")).toBe(true);
3267
+ });
3268
+ const replay = await fetch(`${base}/observe`, {
3269
+ method: "POST",
3270
+ headers: { "content-type": "application/json" },
3271
+ body: JSON.stringify({ runId })
3272
+ }).then((response) => response.text());
3273
+ expect(replay).toContain("done: 3 results");
3274
+ expect(replay).toContain("finish");
3275
+ const lateAbort = await fetch(`${base}/runs/${runId}/abort`, { method: "POST" });
3276
+ expect(lateAbort.status).toBe(200);
3277
+ expect(await lateAbort.json()).toEqual({ aborted: false });
3278
+ expect(fixture.getRequestCount()).toBe(offset + 4);
3279
+ }
3280
+ } finally {
3281
+ gate.resolve();
3282
+ client.abort();
3283
+ }
3284
+ }, 3e4);
3285
+ }
3286
+ });
3287
+ }
3288
+ //#endregion
3015
3289
  //#region src/mcp-route-test-suite.ts
3016
3290
  /**
3017
3291
  * Creates a standardized integration test suite for MCP registry routes
@@ -4674,6 +4948,6 @@ function createCustomRouteFGATestSuite(config) {
4674
4948
  });
4675
4949
  }
4676
4950
  //#endregion
4677
- export { consumeSSEStream, createBodyLimitTestSuite, createCustomRouteFGATestSuite, createDefaultTestContext, createHttpLoggingTestSuite, createMCPRouteTestSuite, createMCPTransportTestSuite, createMultipartTestSuite, createRouteAdapterTestSuite, createStreamWithSensitiveData, createStreamWithUnserializableChunk, expectSerializedStreamChunks };
4951
+ export { consumeSSEStream, createBodyLimitTestSuite, createCustomRouteFGATestSuite, createDefaultTestContext, createDurableDisconnectTestSuite, createHttpLoggingTestSuite, createMCPRouteTestSuite, createMCPTransportTestSuite, createMultipartTestSuite, createRouteAdapterTestSuite, createStreamWithSensitiveData, createStreamWithUnserializableChunk, expectSerializedStreamChunks };
4678
4952
 
4679
4953
  //# sourceMappingURL=index.js.map