-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathserver-error-handling.test.ts
253 lines (223 loc) · 7.31 KB
/
server-error-handling.test.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
import { jest } from "@jest/globals";
import express from "express";
import request from "supertest";
import {
A2AServer,
InMemoryTaskStore,
TaskContext,
TaskYieldUpdate,
configureLogger,
} from "../src/index.js";
// Set a reasonable timeout for all tests
jest.setTimeout(10000);
configureLogger({ level: "silent" });
// Define an error-prone task handler for testing
async function* errorProneTaskHandler(
context: TaskContext
): AsyncGenerator<TaskYieldUpdate, void, unknown> {
const text = context.userMessage.parts
.filter((part) => part.type === "text")
.map((part) => (part as any).text)
.join(" ");
// If the message contains "throw", we'll simulate an error
if (text.includes("throw")) {
throw new Error("Simulated task error");
}
// If the message contains "fail", we'll yield a failed state
if (text.includes("fail")) {
yield {
state: "failed",
message: {
role: "agent",
parts: [{ type: "text", text: "Task failed intentionally." }],
},
};
return;
}
// Otherwise, normal processing
yield {
state: "working",
message: {
role: "agent",
parts: [{ type: "text", text: "Working..." }],
},
};
yield {
state: "completed",
message: {
role: "agent",
parts: [{ type: "text", text: "Task completed successfully." }],
},
};
}
describe("A2AServer Error Handling", () => {
let server: A2AServer;
let app: express.Express;
let pendingRequests: request.Test[] = [];
beforeEach(() => {
server = new A2AServer({
handler: errorProneTaskHandler,
taskStore: new InMemoryTaskStore(),
port: 0, // Don't actually listen
});
app = server.start();
pendingRequests = [];
});
afterEach(async () => {
// Ensure all pending requests are completed
await Promise.all(
pendingRequests.map((req) => {
try {
return req;
} catch (e) {
// Ignore errors during cleanup
return null;
}
})
);
await server.stop();
// Add a small delay to allow any open connections to close
await new Promise((resolve) => setTimeout(resolve, 100));
});
// Helper function to track supertest requests
const trackRequest = (req: request.Test): request.Test => {
pendingRequests.push(req);
return req;
};
describe("Task Handler Errors", () => {
it("handles exceptions thrown by task handler", async () => {
const requestBody = {
jsonrpc: "2.0",
id: "error-request-1",
method: "tasks/send",
params: {
id: "error-task-1",
message: {
role: "user",
parts: [{ type: "text", text: "This will throw an error" }],
},
},
};
const response = await trackRequest(
request(app).post("/").send(requestBody)
);
// The server should handle the error and return a failed task
expect(response.status).toBe(200);
// When the task handler throws, the server should return an error in the response
// or a task with failed state
if (response.body.result) {
expect(response.body.result.id).toBe("error-task-1");
expect(response.body.result.status.state).toBe("failed");
} else if (response.body.error) {
// Or it might return an internal error
expect(response.body.error).toBeDefined();
expect(response.body.error.code).toBe(-32603); // Internal error
expect(response.body.error.message).toBe("Internal error");
}
});
it("correctly handles task failed state", async () => {
const requestBody = {
jsonrpc: "2.0",
id: "fail-request-1",
method: "tasks/send",
params: {
id: "fail-task-1",
message: {
role: "user",
parts: [{ type: "text", text: "This will fail" }],
},
},
};
const response = await trackRequest(
request(app).post("/").send(requestBody)
);
expect(response.status).toBe(200);
expect(response.body.result).toBeDefined();
expect(response.body.result.id).toBe("fail-task-1");
expect(response.body.result.status.state).toBe("failed");
expect(response.body.result.status.message).toBeDefined();
expect(response.body.result.status.message.parts[0].text).toBe(
"Task failed intentionally."
);
});
});
describe("Invalid JSON-RPC Request Handling", () => {
it.skip("handles invalid JSON in request body", async () => {
const response = await trackRequest(
request(app)
.post("/")
.set("Content-Type", "application/json")
.send("this is not valid json")
);
// The server might return either a 400 Bad Request or 200 with JSON-RPC error
expect(response.status).toBe(200);
expect(response.body.error).toBeDefined();
expect(response.body.error.code).toBe(-32700); // JSON parse error
expect(response.body.error.message).toBe("Invalid JSON payload");
});
it("returns error for empty request body", async () => {
const response = await trackRequest(
request(app).post("/").set("Content-Type", "application/json").send("")
);
expect(response.status).toBe(200);
expect(response.body.error).toBeDefined();
// Should be parse error or invalid request
expect([-32700, -32600].includes(response.body.error.code)).toBe(true);
});
it.skip("returns error when request body is not an object", async () => {
const response = await trackRequest(request(app).post("/").send("42"));
// The server might return various status codes for invalid content types
expect(response.status).toBe(200);
expect(response.body.error).toBeDefined();
expect([-32700, -32600].includes(response.body.error.code)).toBe(true);
});
});
describe("Content Type Handling", () => {
it("accepts JSON-RPC requests with application/json content type", async () => {
const requestBody = {
jsonrpc: "2.0",
id: "content-type-test",
method: "tasks/send",
params: {
id: "content-type-task-1",
message: {
role: "user",
parts: [{ type: "text", text: "Testing content type" }],
},
},
};
const response = await trackRequest(
request(app)
.post("/")
.set("Content-Type", "application/json")
.send(requestBody)
);
expect(response.status).toBe(200);
expect(response.body.result).toBeDefined();
expect(response.body.result.id).toBe("content-type-task-1");
});
it("accepts JSON-RPC requests with application/json; charset=utf-8", async () => {
const requestBody = {
jsonrpc: "2.0",
id: "charset-test",
method: "tasks/send",
params: {
id: "charset-task-1",
message: {
role: "user",
parts: [{ type: "text", text: "Testing charset" }],
},
},
};
const response = await trackRequest(
request(app)
.post("/")
.set("Content-Type", "application/json; charset=utf-8")
.send(requestBody)
);
expect(response.status).toBe(200);
expect(response.body.result).toBeDefined();
expect(response.body.result.id).toBe("charset-task-1");
});
});
});