@pagopa/dx-mcpserver 0.2.5 → 0.2.6
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/dist/config.js +1 -1
- package/package.json +8 -8
- package/dist/__tests__/http-endpoints.test.js +0 -240
- package/dist/__tests__/index.test.js +0 -169
- package/dist/__tests__/session.test.js +0 -66
- package/dist/config/__tests__/aws-config.test.js +0 -15
- package/dist/config/__tests__/constants.test.js +0 -31
- package/dist/services/__tests__/bedrock-retrieve-and-generate.test.js +0 -116
- package/dist/services/__tests__/bedrock.test.js +0 -205
- package/dist/services/__tests__/resolve-to-website-url.test.js +0 -67
- package/dist/tools/__tests__/QueryValidation.test.js +0 -79
- package/dist/tools/__tests__/query-pago-pa-dx-documentation.test.js +0 -47
- package/dist/tools/__tests__/registry.test.js +0 -64
- package/dist/utils/__tests__/error-handling.test.js +0 -167
package/dist/config.js
CHANGED
|
@@ -14,7 +14,7 @@ export const envSchema = z.object({
|
|
|
14
14
|
BEDROCK_MODEL_ARN: z.string().optional(),
|
|
15
15
|
GITHUB_SEARCH_ORG: z.string().optional(),
|
|
16
16
|
LOG_LEVEL: logLevelSchema,
|
|
17
|
-
PORT: z.coerce.number().int().
|
|
17
|
+
PORT: z.coerce.number().int().min(0).optional(),
|
|
18
18
|
REQUIRED_ORGANIZATIONS: z.string().optional(),
|
|
19
19
|
});
|
|
20
20
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pagopa/dx-mcpserver",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "An MCP server that supports developers using DX tools.",
|
|
6
6
|
"repository": {
|
|
@@ -19,25 +19,25 @@
|
|
|
19
19
|
"dx": "./dist/cli.js"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@aws-sdk/client-bedrock-agent-runtime": "^3.
|
|
23
|
-
"@aws-sdk/client-s3": "^3.
|
|
24
|
-
"@aws-sdk/s3-request-presigner": "^3.
|
|
22
|
+
"@aws-sdk/client-bedrock-agent-runtime": "^3.1057.0",
|
|
23
|
+
"@aws-sdk/client-s3": "^3.1057.0",
|
|
24
|
+
"@aws-sdk/s3-request-presigner": "^3.1057.0",
|
|
25
25
|
"@logtape/logtape": "^1.3.8",
|
|
26
26
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
27
27
|
"@octokit/rest": "^22.0.1",
|
|
28
28
|
"axios": "^1.16.0",
|
|
29
29
|
"zod": "^4.4.2",
|
|
30
|
-
"@pagopa/
|
|
31
|
-
"@pagopa/
|
|
30
|
+
"@pagopa/azure-tracing": "^0.5.3",
|
|
31
|
+
"@pagopa/dx-mcpprompts": "^0.2.9"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^22.19.17",
|
|
35
|
-
"@vitest/coverage-v8": "^
|
|
35
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
36
36
|
"eslint": "^10.3.0",
|
|
37
37
|
"prettier": "3.8.3",
|
|
38
38
|
"tsx": "^4.21.0",
|
|
39
39
|
"typescript": "~5.9.3",
|
|
40
|
-
"vitest": "^
|
|
40
|
+
"vitest": "^4.1.8",
|
|
41
41
|
"@pagopa/eslint-config": "^6.0.4"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { loadConfig } from "../config.js";
|
|
3
|
-
// Mock the prompts package to avoid Vite resolution issues in CI
|
|
4
|
-
vi.mock("@pagopa/dx-mcpprompts", () => ({
|
|
5
|
-
getEnabledPrompts: vi.fn().mockResolvedValue([]),
|
|
6
|
-
}));
|
|
7
|
-
// Helper to create mock Bedrock responses
|
|
8
|
-
function createMockBedrockResponse(command) {
|
|
9
|
-
if (command.constructor.name === "RetrieveAndGenerateCommand") {
|
|
10
|
-
return Promise.resolve({
|
|
11
|
-
citations: [
|
|
12
|
-
{
|
|
13
|
-
retrievedReferences: [
|
|
14
|
-
{
|
|
15
|
-
content: { text: "Reference content", type: "TEXT" },
|
|
16
|
-
location: {
|
|
17
|
-
s3Location: {
|
|
18
|
-
uri: "s3://bucket/docs/terraform/setup.md",
|
|
19
|
-
},
|
|
20
|
-
type: "S3",
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
],
|
|
24
|
-
},
|
|
25
|
-
],
|
|
26
|
-
output: { text: "This is how you setup Terraform." },
|
|
27
|
-
sessionId: "test-session",
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
else if (command.constructor.name === "RetrieveCommand") {
|
|
31
|
-
return Promise.resolve({
|
|
32
|
-
retrievalResults: [
|
|
33
|
-
{
|
|
34
|
-
content: { text: "Azure naming conventions guide", type: "TEXT" },
|
|
35
|
-
location: {
|
|
36
|
-
s3Location: { uri: "s3://bucket/docs/azure/naming.md" },
|
|
37
|
-
type: "S3",
|
|
38
|
-
},
|
|
39
|
-
score: 0.95,
|
|
40
|
-
},
|
|
41
|
-
],
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
return Promise.resolve({});
|
|
45
|
-
}
|
|
46
|
-
// Mock AWS SDK to avoid external dependencies
|
|
47
|
-
vi.mock("@aws-sdk/client-bedrock-agent-runtime", () => ({
|
|
48
|
-
BedrockAgentRuntimeClient: vi.fn().mockImplementation(() => ({
|
|
49
|
-
config: {
|
|
50
|
-
apiVersion: "2023-11-20",
|
|
51
|
-
region: async () => "eu-central-1",
|
|
52
|
-
requestHandler: { handle: vi.fn() },
|
|
53
|
-
},
|
|
54
|
-
destroy: vi.fn(),
|
|
55
|
-
middlewareStack: {},
|
|
56
|
-
send: vi.fn(),
|
|
57
|
-
})),
|
|
58
|
-
RetrieveAndGenerateCommand: vi.fn(),
|
|
59
|
-
RetrieveCommand: vi.fn(),
|
|
60
|
-
}));
|
|
61
|
-
vi.mock("../config/aws.js", () => ({
|
|
62
|
-
createBedrockRuntimeClient: vi.fn(() => ({
|
|
63
|
-
config: {
|
|
64
|
-
apiVersion: "2023-11-20",
|
|
65
|
-
region: async () => "eu-central-1",
|
|
66
|
-
requestHandler: { handle: vi.fn() },
|
|
67
|
-
},
|
|
68
|
-
destroy: vi.fn(),
|
|
69
|
-
middlewareStack: {},
|
|
70
|
-
send: vi.fn().mockImplementation(createMockBedrockResponse),
|
|
71
|
-
})),
|
|
72
|
-
rerankingSupportedRegions: ["us-east-1", "eu-central-1"],
|
|
73
|
-
}));
|
|
74
|
-
// Shared server instance
|
|
75
|
-
let server;
|
|
76
|
-
let baseUrl;
|
|
77
|
-
async function closeTestServer() {
|
|
78
|
-
return new Promise((resolve) => {
|
|
79
|
-
server.close(() => resolve());
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
async function setupTestServer() {
|
|
83
|
-
const testEnv = {
|
|
84
|
-
AWS_BEDROCK_KNOWLEDGE_BASE_ID: "test-kb-id",
|
|
85
|
-
AWS_BEDROCK_MODEL_ARN: "arn:aws:bedrock:test",
|
|
86
|
-
AWS_REGION: "eu-central-1",
|
|
87
|
-
LOG_LEVEL: "error",
|
|
88
|
-
NODE_ENV: "test",
|
|
89
|
-
};
|
|
90
|
-
const config = loadConfig(testEnv);
|
|
91
|
-
const { startHttpServer } = await import("../index.js");
|
|
92
|
-
// Use empty prompts array since we mocked the module
|
|
93
|
-
server = await startHttpServer(config, []);
|
|
94
|
-
const address = server.address();
|
|
95
|
-
const port = typeof address === "object" ? address?.port : 8080;
|
|
96
|
-
baseUrl = `http://localhost:${port}`;
|
|
97
|
-
}
|
|
98
|
-
describe("HTTP Endpoints Integration Tests", () => {
|
|
99
|
-
beforeAll(setupTestServer);
|
|
100
|
-
afterAll(closeTestServer);
|
|
101
|
-
describe("POST /ask", () => {
|
|
102
|
-
it("should return 200 with answer and sources for valid request", async () => {
|
|
103
|
-
const response = await fetch(`${baseUrl}/ask`, {
|
|
104
|
-
body: JSON.stringify({ query: "How do I setup Terraform?" }),
|
|
105
|
-
headers: { "Content-Type": "application/json" },
|
|
106
|
-
method: "POST",
|
|
107
|
-
});
|
|
108
|
-
expect(response.status).toBe(200);
|
|
109
|
-
expect(response.headers.get("content-type")).toContain("application/json");
|
|
110
|
-
const data = (await response.json());
|
|
111
|
-
expect(data).toHaveProperty("answer");
|
|
112
|
-
expect(data).toHaveProperty("sources");
|
|
113
|
-
expect(typeof data.answer).toBe("string");
|
|
114
|
-
expect(Array.isArray(data.sources)).toBe(true);
|
|
115
|
-
});
|
|
116
|
-
it("should return 400 for missing query field", async () => {
|
|
117
|
-
const response = await fetch(`${baseUrl}/ask`, {
|
|
118
|
-
body: JSON.stringify({}),
|
|
119
|
-
headers: { "Content-Type": "application/json" },
|
|
120
|
-
method: "POST",
|
|
121
|
-
});
|
|
122
|
-
expect(response.status).toBe(400);
|
|
123
|
-
const data = (await response.json());
|
|
124
|
-
expect(data.error).toBe("Missing required field: query");
|
|
125
|
-
});
|
|
126
|
-
it("should return 400 for empty query string", async () => {
|
|
127
|
-
const response = await fetch(`${baseUrl}/ask`, {
|
|
128
|
-
body: JSON.stringify({ query: " " }),
|
|
129
|
-
headers: { "Content-Type": "application/json" },
|
|
130
|
-
method: "POST",
|
|
131
|
-
});
|
|
132
|
-
expect(response.status).toBe(400);
|
|
133
|
-
const data = (await response.json());
|
|
134
|
-
expect(data.error).toBe("Missing required field: query");
|
|
135
|
-
});
|
|
136
|
-
it("should return 400 for invalid JSON", async () => {
|
|
137
|
-
const response = await fetch(`${baseUrl}/ask`, {
|
|
138
|
-
body: "not valid json{",
|
|
139
|
-
headers: { "Content-Type": "application/json" },
|
|
140
|
-
method: "POST",
|
|
141
|
-
});
|
|
142
|
-
expect(response.status).toBe(400);
|
|
143
|
-
const data = (await response.json());
|
|
144
|
-
expect(data.error).toBe("Invalid JSON in request body");
|
|
145
|
-
});
|
|
146
|
-
it("should return 400 for non-string query", async () => {
|
|
147
|
-
const response = await fetch(`${baseUrl}/ask`, {
|
|
148
|
-
body: JSON.stringify({ query: 123 }),
|
|
149
|
-
headers: { "Content-Type": "application/json" },
|
|
150
|
-
method: "POST",
|
|
151
|
-
});
|
|
152
|
-
expect(response.status).toBe(400);
|
|
153
|
-
const data = (await response.json());
|
|
154
|
-
expect(data.error).toBe("Missing required field: query");
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
describe("POST /search", () => {
|
|
158
|
-
it("should return 200 with results for valid request", async () => {
|
|
159
|
-
const response = await fetch(`${baseUrl}/search`, {
|
|
160
|
-
body: JSON.stringify({ query: "Azure naming conventions" }),
|
|
161
|
-
headers: { "Content-Type": "application/json" },
|
|
162
|
-
method: "POST",
|
|
163
|
-
});
|
|
164
|
-
expect(response.status).toBe(200);
|
|
165
|
-
expect(response.headers.get("content-type")).toContain("application/json");
|
|
166
|
-
const data = (await response.json());
|
|
167
|
-
expect(data).toHaveProperty("query");
|
|
168
|
-
expect(data).toHaveProperty("results");
|
|
169
|
-
expect(Array.isArray(data.results)).toBe(true);
|
|
170
|
-
});
|
|
171
|
-
it("should use default number_of_results when not provided", async () => {
|
|
172
|
-
const response = await fetch(`${baseUrl}/search`, {
|
|
173
|
-
body: JSON.stringify({ query: "test query" }),
|
|
174
|
-
headers: { "Content-Type": "application/json" },
|
|
175
|
-
method: "POST",
|
|
176
|
-
});
|
|
177
|
-
expect(response.status).toBe(200);
|
|
178
|
-
const data = (await response.json());
|
|
179
|
-
expect(data).toHaveProperty("results");
|
|
180
|
-
});
|
|
181
|
-
it("should accept custom number_of_results", async () => {
|
|
182
|
-
const response = await fetch(`${baseUrl}/search`, {
|
|
183
|
-
body: JSON.stringify({ number_of_results: 10, query: "test query" }),
|
|
184
|
-
headers: { "Content-Type": "application/json" },
|
|
185
|
-
method: "POST",
|
|
186
|
-
});
|
|
187
|
-
expect(response.status).toBe(200);
|
|
188
|
-
});
|
|
189
|
-
it("should return 400 for missing query field", async () => {
|
|
190
|
-
const response = await fetch(`${baseUrl}/search`, {
|
|
191
|
-
body: JSON.stringify({}),
|
|
192
|
-
headers: { "Content-Type": "application/json" },
|
|
193
|
-
method: "POST",
|
|
194
|
-
});
|
|
195
|
-
expect(response.status).toBe(400);
|
|
196
|
-
const data = (await response.json());
|
|
197
|
-
expect(data.error).toBe("Missing required field: query");
|
|
198
|
-
});
|
|
199
|
-
it("should return 400 for empty query string", async () => {
|
|
200
|
-
const response = await fetch(`${baseUrl}/search`, {
|
|
201
|
-
body: JSON.stringify({ query: "" }),
|
|
202
|
-
headers: { "Content-Type": "application/json" },
|
|
203
|
-
method: "POST",
|
|
204
|
-
});
|
|
205
|
-
expect(response.status).toBe(400);
|
|
206
|
-
const data = (await response.json());
|
|
207
|
-
expect(data.error).toBe("Missing required field: query");
|
|
208
|
-
});
|
|
209
|
-
it("should return 400 for invalid JSON", async () => {
|
|
210
|
-
const response = await fetch(`${baseUrl}/search`, {
|
|
211
|
-
body: "{invalid json",
|
|
212
|
-
headers: { "Content-Type": "application/json" },
|
|
213
|
-
method: "POST",
|
|
214
|
-
});
|
|
215
|
-
expect(response.status).toBe(400);
|
|
216
|
-
const data = (await response.json());
|
|
217
|
-
expect(data.error).toBe("Invalid JSON in request body");
|
|
218
|
-
});
|
|
219
|
-
it("should return 400 for number_of_results out of range (too low)", async () => {
|
|
220
|
-
const response = await fetch(`${baseUrl}/search`, {
|
|
221
|
-
body: JSON.stringify({ number_of_results: 0, query: "test" }),
|
|
222
|
-
headers: { "Content-Type": "application/json" },
|
|
223
|
-
method: "POST",
|
|
224
|
-
});
|
|
225
|
-
expect(response.status).toBe(400);
|
|
226
|
-
const data = (await response.json());
|
|
227
|
-
expect(data.error).toBe("number_of_results must be between 1 and 20");
|
|
228
|
-
});
|
|
229
|
-
it("should return 400 for number_of_results out of range (too high)", async () => {
|
|
230
|
-
const response = await fetch(`${baseUrl}/search`, {
|
|
231
|
-
body: JSON.stringify({ number_of_results: 21, query: "test" }),
|
|
232
|
-
headers: { "Content-Type": "application/json" },
|
|
233
|
-
method: "POST",
|
|
234
|
-
});
|
|
235
|
-
expect(response.status).toBe(400);
|
|
236
|
-
const data = (await response.json());
|
|
237
|
-
expect(data.error).toBe("number_of_results must be between 1 and 20");
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
});
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
export const mockTool = {
|
|
4
|
-
annotations: {
|
|
5
|
-
destructiveHint: false,
|
|
6
|
-
idempotentHint: true,
|
|
7
|
-
openWorldHint: true,
|
|
8
|
-
readOnlyHint: true,
|
|
9
|
-
title: "Test Tool",
|
|
10
|
-
},
|
|
11
|
-
description: "A test tool",
|
|
12
|
-
execute: vi.fn(async (args) => {
|
|
13
|
-
const parsedResult = z.object({ input: z.string() }).safeParse(args);
|
|
14
|
-
if (!parsedResult.success) {
|
|
15
|
-
return "Error: Invalid input";
|
|
16
|
-
}
|
|
17
|
-
return `Tool executed with: ${parsedResult.data.input}`;
|
|
18
|
-
}),
|
|
19
|
-
name: "TestTool",
|
|
20
|
-
parameters: z.object({
|
|
21
|
-
input: z.string().min(1, "Input cannot be empty"),
|
|
22
|
-
}),
|
|
23
|
-
};
|
|
24
|
-
export const mockCatalogEntry = {
|
|
25
|
-
category: "test",
|
|
26
|
-
enabled: true,
|
|
27
|
-
id: "test-prompt",
|
|
28
|
-
metadata: {
|
|
29
|
-
description: "A test prompt for unit testing",
|
|
30
|
-
title: "Test Prompt",
|
|
31
|
-
},
|
|
32
|
-
prompt: {
|
|
33
|
-
arguments: [
|
|
34
|
-
{ description: "First argument", name: "arg1", required: true },
|
|
35
|
-
{ description: "Second argument", name: "arg2", required: false },
|
|
36
|
-
],
|
|
37
|
-
description: "A test prompt",
|
|
38
|
-
load: async (args) => `Prompt loaded with args: ${JSON.stringify(args)}`,
|
|
39
|
-
name: "TestPrompt",
|
|
40
|
-
},
|
|
41
|
-
tags: ["test"],
|
|
42
|
-
};
|
|
43
|
-
export const mockPromptEntry = {
|
|
44
|
-
catalogEntry: mockCatalogEntry,
|
|
45
|
-
prompt: {
|
|
46
|
-
load: vi.fn(async (args) => `Prompt loaded with args: ${JSON.stringify(args)}`),
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
describe("MCP Server Handlers", () => {
|
|
50
|
-
describe("Tool Handler Validation", () => {
|
|
51
|
-
it("should validate tool arguments against the Zod schema", async () => {
|
|
52
|
-
// Valid arguments should pass
|
|
53
|
-
const validArgs = { input: "test-value" };
|
|
54
|
-
const validationResult = mockTool.parameters.safeParse(validArgs);
|
|
55
|
-
expect(validationResult.success).toBe(true);
|
|
56
|
-
// Invalid arguments should fail
|
|
57
|
-
const invalidArgs = { input: "" };
|
|
58
|
-
const invalidationResult = mockTool.parameters.safeParse(invalidArgs);
|
|
59
|
-
expect(invalidationResult.success).toBe(false);
|
|
60
|
-
expect(invalidationResult.error?.issues[0].message).toContain("cannot be empty");
|
|
61
|
-
});
|
|
62
|
-
it("should handle tool execution errors gracefully", async () => {
|
|
63
|
-
const errorTool = {
|
|
64
|
-
...mockTool,
|
|
65
|
-
execute: vi.fn(async () => {
|
|
66
|
-
throw new Error("Tool execution failed");
|
|
67
|
-
}),
|
|
68
|
-
};
|
|
69
|
-
await expect(errorTool.execute({ input: "test" }, undefined)).rejects.toThrow("Tool execution failed");
|
|
70
|
-
});
|
|
71
|
-
it("should pass context with session data to tool execute", async () => {
|
|
72
|
-
const context = { session: { id: "session-123" } };
|
|
73
|
-
await mockTool.execute({ input: "test" }, context);
|
|
74
|
-
expect(mockTool.execute).toHaveBeenCalledWith({ input: "test" }, context);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
describe("Prompt Handler Validation", () => {
|
|
78
|
-
it("should validate required prompt arguments", () => {
|
|
79
|
-
const missingRequiredArg = { arg2: "value2" };
|
|
80
|
-
const hasAllRequired = mockCatalogEntry.prompt.arguments
|
|
81
|
-
.filter((arg) => arg.required)
|
|
82
|
-
.every((arg) => arg.name in missingRequiredArg);
|
|
83
|
-
expect(hasAllRequired).toBe(false);
|
|
84
|
-
const withAllRequired = { arg1: "value1", arg2: "value2" };
|
|
85
|
-
const hasAllRequired2 = mockCatalogEntry.prompt.arguments
|
|
86
|
-
.filter((arg) => arg.required)
|
|
87
|
-
.every((arg) => arg.name in withAllRequired);
|
|
88
|
-
expect(hasAllRequired2).toBe(true);
|
|
89
|
-
});
|
|
90
|
-
it("should handle missing required prompt arguments", () => {
|
|
91
|
-
const requiredArgs = mockCatalogEntry.prompt.arguments
|
|
92
|
-
.filter((arg) => arg.required)
|
|
93
|
-
.map((arg) => arg.name);
|
|
94
|
-
const providedArgs = { arg2: "value" };
|
|
95
|
-
const missingArgs = requiredArgs.filter((arg) => !(arg in providedArgs));
|
|
96
|
-
expect(missingArgs).toEqual(["arg1"]);
|
|
97
|
-
});
|
|
98
|
-
it("should handle optional prompt arguments", async () => {
|
|
99
|
-
const argsWithoutOptional = { arg1: "required-value" };
|
|
100
|
-
await mockPromptEntry.prompt.load(argsWithoutOptional);
|
|
101
|
-
expect(mockPromptEntry.prompt.load).toHaveBeenCalledWith(argsWithoutOptional);
|
|
102
|
-
const argsWithOptional = { arg1: "required-value", arg2: "optional" };
|
|
103
|
-
await mockPromptEntry.prompt.load(argsWithOptional);
|
|
104
|
-
expect(mockPromptEntry.prompt.load).toHaveBeenCalledWith(argsWithOptional);
|
|
105
|
-
});
|
|
106
|
-
it("should handle prompt loading errors gracefully", async () => {
|
|
107
|
-
const errorPrompt = {
|
|
108
|
-
...mockPromptEntry,
|
|
109
|
-
prompt: {
|
|
110
|
-
load: vi.fn(async () => {
|
|
111
|
-
throw new Error("Prompt loading failed");
|
|
112
|
-
}),
|
|
113
|
-
},
|
|
114
|
-
};
|
|
115
|
-
await expect(errorPrompt.prompt.load({ arg1: "test" })).rejects.toThrow("Prompt loading failed");
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
describe("Request Handler Integration", () => {
|
|
119
|
-
it("should handle valid tool requests", async () => {
|
|
120
|
-
const args = { input: "test-input" };
|
|
121
|
-
const result = await mockTool.execute(args, undefined);
|
|
122
|
-
expect(result).toContain("Tool executed with: test-input");
|
|
123
|
-
expect(mockTool.execute).toHaveBeenCalledWith(args, undefined);
|
|
124
|
-
});
|
|
125
|
-
it("should reject requests with invalid tool names", () => {
|
|
126
|
-
const toolRegistry = new Map();
|
|
127
|
-
toolRegistry.set("TestTool", mockTool);
|
|
128
|
-
const toolExists = toolRegistry.has("NonExistentTool");
|
|
129
|
-
expect(toolExists).toBe(false);
|
|
130
|
-
});
|
|
131
|
-
it("should handle tool execution with proper error context", async () => {
|
|
132
|
-
const toolWithErrorContext = {
|
|
133
|
-
...mockTool,
|
|
134
|
-
execute: vi.fn(async (args, context) => {
|
|
135
|
-
if (!context?.session) {
|
|
136
|
-
throw new Error("Session context required");
|
|
137
|
-
}
|
|
138
|
-
return "Success with context";
|
|
139
|
-
}),
|
|
140
|
-
};
|
|
141
|
-
// Should fail without context
|
|
142
|
-
await expect(toolWithErrorContext.execute({ input: "test" }, undefined)).rejects.toThrow("Session context required");
|
|
143
|
-
// Should succeed with context
|
|
144
|
-
const result = await toolWithErrorContext.execute({ input: "test" }, { session: { id: "session-1" } });
|
|
145
|
-
expect(result).toBe("Success with context");
|
|
146
|
-
});
|
|
147
|
-
it("should handle batch tool and prompt registration", () => {
|
|
148
|
-
const toolRegistry = new Map();
|
|
149
|
-
const promptRegistry = new Map();
|
|
150
|
-
// Register multiple tools
|
|
151
|
-
toolRegistry.set(mockTool.name, mockTool);
|
|
152
|
-
toolRegistry.set("AnotherTool", { ...mockTool, name: "AnotherTool" });
|
|
153
|
-
// Register multiple prompts
|
|
154
|
-
promptRegistry.set(mockPromptEntry.catalogEntry.prompt.name, mockPromptEntry);
|
|
155
|
-
promptRegistry.set("AnotherPrompt", {
|
|
156
|
-
...mockPromptEntry,
|
|
157
|
-
catalogEntry: {
|
|
158
|
-
...mockPromptEntry.catalogEntry,
|
|
159
|
-
prompt: {
|
|
160
|
-
...mockPromptEntry.catalogEntry.prompt,
|
|
161
|
-
name: "AnotherPrompt",
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
});
|
|
165
|
-
expect(toolRegistry.size).toBe(2);
|
|
166
|
-
expect(promptRegistry.size).toBe(2);
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
});
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { sessionStorage } from "../session.js";
|
|
3
|
-
describe("Session Management", () => {
|
|
4
|
-
it("should maintain session isolation between concurrent contexts", async () => {
|
|
5
|
-
const session1 = { id: "session-1" };
|
|
6
|
-
const session2 = { id: "session-2" };
|
|
7
|
-
const results = [];
|
|
8
|
-
// Run two sessions concurrently
|
|
9
|
-
await Promise.all([
|
|
10
|
-
sessionStorage.run(session1, async () => {
|
|
11
|
-
// Wait a bit to ensure overlap
|
|
12
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
13
|
-
const stored = sessionStorage.getStore();
|
|
14
|
-
if (stored) {
|
|
15
|
-
results.push(stored);
|
|
16
|
-
}
|
|
17
|
-
}),
|
|
18
|
-
sessionStorage.run(session2, async () => {
|
|
19
|
-
const stored = sessionStorage.getStore();
|
|
20
|
-
if (stored) {
|
|
21
|
-
results.push(stored);
|
|
22
|
-
}
|
|
23
|
-
}),
|
|
24
|
-
]);
|
|
25
|
-
// Both sessions should have completed and stored their data
|
|
26
|
-
expect(results).toHaveLength(2);
|
|
27
|
-
// Each session should have its own data
|
|
28
|
-
const ids = results.map((r) => r.id).sort();
|
|
29
|
-
expect(ids).toEqual(["session-1", "session-2"]);
|
|
30
|
-
});
|
|
31
|
-
it("should return undefined when accessed outside of run context", async () => {
|
|
32
|
-
const store = sessionStorage.getStore();
|
|
33
|
-
expect(store).toBeUndefined();
|
|
34
|
-
});
|
|
35
|
-
it("should provide correct session data within run context", async () => {
|
|
36
|
-
const session = {
|
|
37
|
-
id: "test-session-123",
|
|
38
|
-
};
|
|
39
|
-
await sessionStorage.run(session, async () => {
|
|
40
|
-
const stored = sessionStorage.getStore();
|
|
41
|
-
expect(stored).toBeDefined();
|
|
42
|
-
expect(stored?.id).toBe("test-session-123");
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
it("should properly clean up session data after run context completes", async () => {
|
|
46
|
-
const session = { id: "cleanup-456" };
|
|
47
|
-
await sessionStorage.run(session, async () => {
|
|
48
|
-
expect(sessionStorage.getStore()).toBeDefined();
|
|
49
|
-
});
|
|
50
|
-
// After the run completes, store should be undefined
|
|
51
|
-
expect(sessionStorage.getStore()).toBeUndefined();
|
|
52
|
-
});
|
|
53
|
-
it("should handle nested session contexts correctly", async () => {
|
|
54
|
-
const session1 = { id: "outer-1" };
|
|
55
|
-
const session2 = { id: "inner-2" };
|
|
56
|
-
await sessionStorage.run(session1, async () => {
|
|
57
|
-
expect(sessionStorage.getStore()?.id).toBe("outer-1");
|
|
58
|
-
await sessionStorage.run(session2, async () => {
|
|
59
|
-
// Inner context should override outer
|
|
60
|
-
expect(sessionStorage.getStore()?.id).toBe("inner-2");
|
|
61
|
-
});
|
|
62
|
-
// After inner context exits, outer context should be restored
|
|
63
|
-
expect(sessionStorage.getStore()?.id).toBe("outer-1");
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
});
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { createBedrockRuntimeClient, rerankingSupportedRegions, } from "../aws.js";
|
|
3
|
-
describe("aws config", () => {
|
|
4
|
-
it("should export rerankingSupportedRegions as array", () => {
|
|
5
|
-
expect(Array.isArray(rerankingSupportedRegions)).toBe(true);
|
|
6
|
-
expect(rerankingSupportedRegions.length).toBeGreaterThan(0);
|
|
7
|
-
});
|
|
8
|
-
it("should create a Bedrock runtime client", () => {
|
|
9
|
-
const logger = {
|
|
10
|
-
error: vi.fn(),
|
|
11
|
-
};
|
|
12
|
-
const client = createBedrockRuntimeClient("eu-central-1", logger);
|
|
13
|
-
expect(client).toHaveProperty("send");
|
|
14
|
-
});
|
|
15
|
-
});
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { API_TIMEOUT, DEFAULT_PAGE_SIZE, MAX_RESULTS } from "../constants.js";
|
|
3
|
-
describe("Server Constants", () => {
|
|
4
|
-
describe("API_TIMEOUT", () => {
|
|
5
|
-
it("should be a positive number", () => {
|
|
6
|
-
expect(API_TIMEOUT).toBeGreaterThan(0);
|
|
7
|
-
});
|
|
8
|
-
it("should be 30 seconds (30000ms)", () => {
|
|
9
|
-
expect(API_TIMEOUT).toBe(30000);
|
|
10
|
-
});
|
|
11
|
-
});
|
|
12
|
-
describe("MAX_RESULTS", () => {
|
|
13
|
-
it("should be a positive number", () => {
|
|
14
|
-
expect(MAX_RESULTS).toBeGreaterThan(0);
|
|
15
|
-
});
|
|
16
|
-
it("should be 100", () => {
|
|
17
|
-
expect(MAX_RESULTS).toBe(100);
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
describe("DEFAULT_PAGE_SIZE", () => {
|
|
21
|
-
it("should be a positive number", () => {
|
|
22
|
-
expect(DEFAULT_PAGE_SIZE).toBeGreaterThan(0);
|
|
23
|
-
});
|
|
24
|
-
it("should be 20", () => {
|
|
25
|
-
expect(DEFAULT_PAGE_SIZE).toBe(20);
|
|
26
|
-
});
|
|
27
|
-
it("should be less than MAX_RESULTS", () => {
|
|
28
|
-
expect(DEFAULT_PAGE_SIZE).toBeLessThan(MAX_RESULTS);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
});
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { getLogger } from "@logtape/logtape";
|
|
2
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import { retrieveAndGenerate } from "../bedrock-retrieve-and-generate.js";
|
|
4
|
-
// Helper to create mock Bedrock client
|
|
5
|
-
function createMockBedrockClient(sendMock) {
|
|
6
|
-
return {
|
|
7
|
-
config: {
|
|
8
|
-
apiVersion: "2023-11-20",
|
|
9
|
-
region: async () => "eu-central-1",
|
|
10
|
-
requestHandler: { handle: vi.fn() },
|
|
11
|
-
},
|
|
12
|
-
destroy: vi.fn(),
|
|
13
|
-
middlewareStack: {},
|
|
14
|
-
send: sendMock,
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
describe("retrieveAndGenerate", () => {
|
|
18
|
-
let loggerSpy;
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
vi.clearAllMocks();
|
|
21
|
-
const logger = getLogger(["mcpserver", "bedrock", "retrieve-and-generate"]);
|
|
22
|
-
loggerSpy = {
|
|
23
|
-
debug: vi.spyOn(logger, "debug"),
|
|
24
|
-
error: vi.spyOn(logger, "error"),
|
|
25
|
-
info: vi.spyOn(logger, "info"),
|
|
26
|
-
};
|
|
27
|
-
});
|
|
28
|
-
it("should successfully retrieve and generate with valid parameters", async () => {
|
|
29
|
-
const mockResponse = {
|
|
30
|
-
citations: [
|
|
31
|
-
{
|
|
32
|
-
retrievedReferences: [
|
|
33
|
-
{
|
|
34
|
-
content: { text: "Reference content" },
|
|
35
|
-
location: {
|
|
36
|
-
s3Location: { uri: "s3://bucket/doc.md" },
|
|
37
|
-
type: "S3",
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
],
|
|
41
|
-
},
|
|
42
|
-
],
|
|
43
|
-
output: {
|
|
44
|
-
text: "This is the AI-generated answer based on the knowledge base.",
|
|
45
|
-
},
|
|
46
|
-
sessionId: "test-session-123",
|
|
47
|
-
};
|
|
48
|
-
const mockClient = createMockBedrockClient(vi.fn().mockResolvedValue(mockResponse));
|
|
49
|
-
const result = await retrieveAndGenerate("kb-test-id", "arn:aws:bedrock:eu-central-1::foundation-model/amazon.nova-2-lite-v1:0", "How do I setup Terraform?", mockClient, 5);
|
|
50
|
-
expect(result).toEqual(mockResponse);
|
|
51
|
-
expect(result.output?.text).toBe("This is the AI-generated answer based on the knowledge base.");
|
|
52
|
-
expect(result.sessionId).toBe("test-session-123");
|
|
53
|
-
expect(loggerSpy.debug).toHaveBeenCalledWith("Calling RetrieveAndGenerate", {
|
|
54
|
-
knowledgeBaseId: "kb-test-id",
|
|
55
|
-
modelArn: "arn:aws:bedrock:eu-central-1::foundation-model/amazon.nova-2-lite-v1:0",
|
|
56
|
-
numberOfResults: 5,
|
|
57
|
-
query: "How do I setup Terraform?",
|
|
58
|
-
});
|
|
59
|
-
expect(loggerSpy.info).toHaveBeenCalledWith("RetrieveAndGenerate successful", {
|
|
60
|
-
sessionId: "test-session-123",
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
it("should use default numberOfResults when not provided", async () => {
|
|
64
|
-
const mockClient = createMockBedrockClient(vi.fn().mockResolvedValue({
|
|
65
|
-
output: { text: "Answer" },
|
|
66
|
-
sessionId: "session-456",
|
|
67
|
-
}));
|
|
68
|
-
await retrieveAndGenerate("kb-id", "model-arn", "test query", mockClient);
|
|
69
|
-
expect(loggerSpy.debug).toHaveBeenCalledWith("Calling RetrieveAndGenerate", expect.objectContaining({
|
|
70
|
-
numberOfResults: 5,
|
|
71
|
-
}));
|
|
72
|
-
});
|
|
73
|
-
it("should log error details and rethrow on failure", async () => {
|
|
74
|
-
const mockError = {
|
|
75
|
-
$metadata: {
|
|
76
|
-
httpStatusCode: 403,
|
|
77
|
-
},
|
|
78
|
-
__type: "AccessDeniedException",
|
|
79
|
-
message: "User is not authorized to perform: bedrock:RetrieveAndGenerate",
|
|
80
|
-
name: "AccessDeniedException",
|
|
81
|
-
};
|
|
82
|
-
const mockClient = createMockBedrockClient(vi.fn().mockRejectedValue(mockError));
|
|
83
|
-
await expect(retrieveAndGenerate("kb-id", "model-arn", "test query", mockClient)).rejects.toThrow();
|
|
84
|
-
expect(loggerSpy.error).toHaveBeenCalledWith(expect.stringContaining("RetrieveAndGenerate failed"));
|
|
85
|
-
expect(loggerSpy.error).toHaveBeenCalledWith(expect.stringContaining("AccessDeniedException"));
|
|
86
|
-
expect(loggerSpy.error).toHaveBeenCalledWith(expect.stringContaining("kb-id"));
|
|
87
|
-
expect(loggerSpy.error).toHaveBeenCalledWith(expect.stringContaining("model-arn"));
|
|
88
|
-
expect(loggerSpy.error).toHaveBeenCalledWith(expect.stringContaining("403"));
|
|
89
|
-
});
|
|
90
|
-
it("should handle errors without $metadata or __type", async () => {
|
|
91
|
-
const mockError = new Error("Network timeout");
|
|
92
|
-
const mockClient = createMockBedrockClient(vi.fn().mockRejectedValue(mockError));
|
|
93
|
-
await expect(retrieveAndGenerate("kb-id", "model-arn", "test query", mockClient)).rejects.toThrow("Network timeout");
|
|
94
|
-
expect(loggerSpy.error).toHaveBeenCalledWith(expect.stringContaining("RetrieveAndGenerate failed"));
|
|
95
|
-
expect(loggerSpy.error).toHaveBeenCalledWith(expect.stringContaining("Error"));
|
|
96
|
-
expect(loggerSpy.error).toHaveBeenCalledWith(expect.stringContaining("Network timeout"));
|
|
97
|
-
});
|
|
98
|
-
it("should send correct RetrieveAndGenerateCommand configuration", async () => {
|
|
99
|
-
const sendSpy = vi.fn().mockResolvedValue({
|
|
100
|
-
output: { text: "Test answer" },
|
|
101
|
-
sessionId: "test-session",
|
|
102
|
-
});
|
|
103
|
-
const mockClient = createMockBedrockClient(sendSpy);
|
|
104
|
-
await retrieveAndGenerate("kb-test-id", "arn:aws:bedrock:eu-central-1::foundation-model/test-model", "What is the capital of France?", mockClient, 10);
|
|
105
|
-
const commandArg = sendSpy.mock.calls[0][0];
|
|
106
|
-
expect(commandArg.input.input.text).toBe("What is the capital of France?");
|
|
107
|
-
expect(commandArg.input.retrieveAndGenerateConfiguration.type).toBe("KNOWLEDGE_BASE");
|
|
108
|
-
expect(commandArg.input.retrieveAndGenerateConfiguration
|
|
109
|
-
.knowledgeBaseConfiguration.knowledgeBaseId).toBe("kb-test-id");
|
|
110
|
-
expect(commandArg.input.retrieveAndGenerateConfiguration
|
|
111
|
-
.knowledgeBaseConfiguration.modelArn).toBe("arn:aws:bedrock:eu-central-1::foundation-model/test-model");
|
|
112
|
-
expect(commandArg.input.retrieveAndGenerateConfiguration
|
|
113
|
-
.knowledgeBaseConfiguration.retrievalConfiguration
|
|
114
|
-
.vectorSearchConfiguration.numberOfResults).toBe(10);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import { getLogger } from "@logtape/logtape";
|
|
2
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import { queryKnowledgeBase, queryKnowledgeBaseStructured, } from "../bedrock.js";
|
|
4
|
-
// Helper to create mock Bedrock client
|
|
5
|
-
function createMockBedrockClient(sendMock) {
|
|
6
|
-
return {
|
|
7
|
-
config: {
|
|
8
|
-
apiVersion: "2023-11-20",
|
|
9
|
-
region: async () => "eu-central-1",
|
|
10
|
-
requestHandler: { handle: vi.fn() },
|
|
11
|
-
},
|
|
12
|
-
destroy: vi.fn(),
|
|
13
|
-
middlewareStack: {},
|
|
14
|
-
send: sendMock,
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
describe("queryKnowledgeBase", () => {
|
|
18
|
-
let loggerSpy;
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
vi.clearAllMocks();
|
|
21
|
-
// Get logger and spy on its methods - no need for special configuration
|
|
22
|
-
const logger = getLogger(["mcpserver", "bedrock"]);
|
|
23
|
-
loggerSpy = {
|
|
24
|
-
warn: vi.spyOn(logger, "warn"),
|
|
25
|
-
};
|
|
26
|
-
});
|
|
27
|
-
it("should skip images in results", async () => {
|
|
28
|
-
const mockClient = {
|
|
29
|
-
config: {
|
|
30
|
-
apiVersion: "2023-11-20",
|
|
31
|
-
region: async () => "eu-central-1",
|
|
32
|
-
requestHandler: { handle: vi.fn() },
|
|
33
|
-
},
|
|
34
|
-
destroy: vi.fn(),
|
|
35
|
-
middlewareStack: {},
|
|
36
|
-
send: vi.fn().mockResolvedValue({
|
|
37
|
-
retrievalResults: [
|
|
38
|
-
{ content: { text: "doc1", type: "TEXT" } },
|
|
39
|
-
{ content: { type: "IMAGE" } },
|
|
40
|
-
],
|
|
41
|
-
}),
|
|
42
|
-
};
|
|
43
|
-
const result = await queryKnowledgeBase("kbId", "query", mockClient, 2, false);
|
|
44
|
-
expect(result).toContain("doc1");
|
|
45
|
-
expect(loggerSpy.warn).toHaveBeenCalledWith("Images are not supported at this time. Skipping...");
|
|
46
|
-
});
|
|
47
|
-
it("should warn if reranking is not supported in region", async () => {
|
|
48
|
-
const mockClient = {
|
|
49
|
-
config: {
|
|
50
|
-
apiVersion: "2023-11-20",
|
|
51
|
-
region: async () => "unsupported-region",
|
|
52
|
-
requestHandler: { handle: vi.fn() },
|
|
53
|
-
},
|
|
54
|
-
destroy: vi.fn(),
|
|
55
|
-
middlewareStack: {},
|
|
56
|
-
send: vi.fn().mockResolvedValue({ retrievalResults: [] }),
|
|
57
|
-
};
|
|
58
|
-
const result = await queryKnowledgeBase("kbId", "query", mockClient, 2, true);
|
|
59
|
-
expect(typeof result).toBe("string");
|
|
60
|
-
expect(loggerSpy.warn).toHaveBeenCalledWith("Reranking is not supported in region unsupported-region");
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
describe("queryKnowledgeBaseStructured - Basic Functionality", () => {
|
|
64
|
-
let loggerSpy;
|
|
65
|
-
beforeEach(() => {
|
|
66
|
-
vi.clearAllMocks();
|
|
67
|
-
const logger = getLogger(["mcpserver", "bedrock"]);
|
|
68
|
-
loggerSpy = {
|
|
69
|
-
warn: vi.spyOn(logger, "warn"),
|
|
70
|
-
};
|
|
71
|
-
});
|
|
72
|
-
it("should return structured QueryKnowledgeBasesOutput[] instead of a string", async () => {
|
|
73
|
-
const mockClient = createMockBedrockClient(vi.fn().mockResolvedValue({
|
|
74
|
-
retrievalResults: [
|
|
75
|
-
{
|
|
76
|
-
content: { text: "Documentation content", type: "TEXT" },
|
|
77
|
-
location: {
|
|
78
|
-
s3Location: { uri: "s3://bucket/docs/guide.md" },
|
|
79
|
-
type: "S3",
|
|
80
|
-
},
|
|
81
|
-
score: 0.95,
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
}));
|
|
85
|
-
const result = await queryKnowledgeBaseStructured("kbId", "query", mockClient, 5, false);
|
|
86
|
-
expect(Array.isArray(result)).toBe(true);
|
|
87
|
-
expect(result.length).toBe(1);
|
|
88
|
-
expect(typeof result[0]).toBe("object");
|
|
89
|
-
});
|
|
90
|
-
it("should return result objects with content, location, and score properties", async () => {
|
|
91
|
-
const mockClient = createMockBedrockClient(vi.fn().mockResolvedValue({
|
|
92
|
-
retrievalResults: [
|
|
93
|
-
{
|
|
94
|
-
content: { text: "Test content", type: "TEXT" },
|
|
95
|
-
location: {
|
|
96
|
-
s3Location: { uri: "s3://bucket/path/file.md" },
|
|
97
|
-
type: "S3",
|
|
98
|
-
},
|
|
99
|
-
score: 0.87,
|
|
100
|
-
},
|
|
101
|
-
],
|
|
102
|
-
}));
|
|
103
|
-
const result = await queryKnowledgeBaseStructured("kbId", "test query", mockClient);
|
|
104
|
-
expect(result[0]).toHaveProperty("content");
|
|
105
|
-
expect(result[0]).toHaveProperty("location");
|
|
106
|
-
expect(result[0]).toHaveProperty("score");
|
|
107
|
-
expect(result[0].content).toBe("Test content");
|
|
108
|
-
expect(result[0].score).toBe(0.87);
|
|
109
|
-
});
|
|
110
|
-
it("should resolve S3 locations to website URLs via resolveToWebsiteUrl", async () => {
|
|
111
|
-
const mockClient = createMockBedrockClient(vi.fn().mockResolvedValue({
|
|
112
|
-
retrievalResults: [
|
|
113
|
-
{
|
|
114
|
-
content: { text: "Content", type: "TEXT" },
|
|
115
|
-
location: {
|
|
116
|
-
s3Location: {
|
|
117
|
-
uri: "s3://pagopa-dx-documentation/docs/azure/index.md",
|
|
118
|
-
},
|
|
119
|
-
type: "S3",
|
|
120
|
-
},
|
|
121
|
-
score: 0.9,
|
|
122
|
-
},
|
|
123
|
-
],
|
|
124
|
-
}));
|
|
125
|
-
const result = await queryKnowledgeBaseStructured("kbId", "query", mockClient);
|
|
126
|
-
// resolveToWebsiteUrl should transform S3 URI to website URL
|
|
127
|
-
expect(result[0].location).toBeDefined();
|
|
128
|
-
expect(result[0].location?.webLocation?.url).toContain("https://");
|
|
129
|
-
expect(result[0].location?.webLocation?.url).not.toContain("/index");
|
|
130
|
-
});
|
|
131
|
-
it("should skip image content with warning logs", async () => {
|
|
132
|
-
const mockClient = createMockBedrockClient(vi.fn().mockResolvedValue({
|
|
133
|
-
retrievalResults: [
|
|
134
|
-
{
|
|
135
|
-
content: { text: "Valid text content", type: "TEXT" },
|
|
136
|
-
location: {
|
|
137
|
-
s3Location: { uri: "s3://bucket/file.md" },
|
|
138
|
-
type: "S3",
|
|
139
|
-
},
|
|
140
|
-
score: 0.9,
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
content: { type: "IMAGE" },
|
|
144
|
-
location: {
|
|
145
|
-
s3Location: { uri: "s3://bucket/image.png" },
|
|
146
|
-
type: "S3",
|
|
147
|
-
},
|
|
148
|
-
score: 0.85,
|
|
149
|
-
},
|
|
150
|
-
],
|
|
151
|
-
}));
|
|
152
|
-
const result = await queryKnowledgeBaseStructured("kbId", "query", mockClient);
|
|
153
|
-
expect(result.length).toBe(1);
|
|
154
|
-
expect(result[0].content).toBe("Valid text content");
|
|
155
|
-
expect(loggerSpy.warn).toHaveBeenCalledWith("Images are not supported at this time. Skipping...");
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
describe("queryKnowledgeBaseStructured - Reranking", () => {
|
|
159
|
-
let loggerSpy;
|
|
160
|
-
beforeEach(() => {
|
|
161
|
-
vi.clearAllMocks();
|
|
162
|
-
const logger = getLogger(["mcpserver", "bedrock"]);
|
|
163
|
-
loggerSpy = {
|
|
164
|
-
warn: vi.spyOn(logger, "warn"),
|
|
165
|
-
};
|
|
166
|
-
});
|
|
167
|
-
it("should apply reranking configuration when enabled in supported region", async () => {
|
|
168
|
-
const sendSpy = vi.fn().mockResolvedValue({
|
|
169
|
-
retrievalResults: [
|
|
170
|
-
{
|
|
171
|
-
content: { text: "Reranked content", type: "TEXT" },
|
|
172
|
-
location: { s3Location: { uri: "s3://bucket/doc.md" }, type: "S3" },
|
|
173
|
-
score: 0.98,
|
|
174
|
-
},
|
|
175
|
-
],
|
|
176
|
-
});
|
|
177
|
-
const mockClient = createMockBedrockClient(sendSpy);
|
|
178
|
-
// Override region for this test
|
|
179
|
-
mockClient.config.region = async () => "us-east-1"; // Supported region
|
|
180
|
-
await queryKnowledgeBaseStructured("kbId", "query", mockClient, 5, true, // Enable reranking
|
|
181
|
-
"AMAZON");
|
|
182
|
-
// Verify that send was called with reranking configuration
|
|
183
|
-
const commandArg = sendSpy.mock.calls[0][0];
|
|
184
|
-
expect(commandArg.input.retrievalConfiguration).toBeDefined();
|
|
185
|
-
expect(commandArg.input.retrievalConfiguration.vectorSearchConfiguration
|
|
186
|
-
.rerankingConfiguration).toBeDefined();
|
|
187
|
-
expect(commandArg.input.retrievalConfiguration.vectorSearchConfiguration
|
|
188
|
-
.rerankingConfiguration.bedrockRerankingConfiguration.modelConfiguration
|
|
189
|
-
.modelArn).toContain("amazon.rerank-v1:0");
|
|
190
|
-
});
|
|
191
|
-
it("should disable reranking in unsupported region with warning", async () => {
|
|
192
|
-
const sendSpy = vi.fn().mockResolvedValue({
|
|
193
|
-
retrievalResults: [],
|
|
194
|
-
});
|
|
195
|
-
const mockClient = createMockBedrockClient(sendSpy);
|
|
196
|
-
// Override region for this test
|
|
197
|
-
mockClient.config.region = async () => "ap-southeast-1"; // Unsupported region
|
|
198
|
-
await queryKnowledgeBaseStructured("kbId", "query", mockClient, 5, true);
|
|
199
|
-
expect(loggerSpy.warn).toHaveBeenCalledWith("Reranking is not supported in region ap-southeast-1");
|
|
200
|
-
// Verify that send was called WITHOUT reranking configuration
|
|
201
|
-
const commandArg = sendSpy.mock.calls[0][0];
|
|
202
|
-
expect(commandArg.input.retrievalConfiguration.vectorSearchConfiguration
|
|
203
|
-
.rerankingConfiguration).toBeUndefined();
|
|
204
|
-
});
|
|
205
|
-
});
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
it("returns dx.pagopa.it/llms-full.txt for llms-full.txt", () => {
|
|
2
|
-
const location = {
|
|
3
|
-
s3Location: {
|
|
4
|
-
uri: "s3://bucket/llms-full.txt",
|
|
5
|
-
},
|
|
6
|
-
type: "S3",
|
|
7
|
-
};
|
|
8
|
-
const result = resolveToWebsiteUrl(location);
|
|
9
|
-
expect(result?.webLocation?.url).toBe("https://dx.pagopa.it/llms-full.txt");
|
|
10
|
-
});
|
|
11
|
-
it("returns dx.pagopa.it/llms.txt for llms.txt", () => {
|
|
12
|
-
const location = {
|
|
13
|
-
s3Location: {
|
|
14
|
-
uri: "s3://bucket/llms.txt",
|
|
15
|
-
},
|
|
16
|
-
type: "S3",
|
|
17
|
-
};
|
|
18
|
-
const result = resolveToWebsiteUrl(location);
|
|
19
|
-
expect(result?.webLocation?.url).toBe("https://dx.pagopa.it/llms.txt");
|
|
20
|
-
});
|
|
21
|
-
it("returns dx.pagopa.it/blog/ for keys starting with /blog/", () => {
|
|
22
|
-
const location = {
|
|
23
|
-
s3Location: {
|
|
24
|
-
uri: "s3://bucket/blog/some-post.md",
|
|
25
|
-
},
|
|
26
|
-
type: "S3",
|
|
27
|
-
};
|
|
28
|
-
const result = resolveToWebsiteUrl(location);
|
|
29
|
-
expect(result?.webLocation?.url).toBe("https://dx.pagopa.it/blog/");
|
|
30
|
-
});
|
|
31
|
-
import { describe, expect, it } from "vitest";
|
|
32
|
-
import { resolveToWebsiteUrl } from "../bedrock.js";
|
|
33
|
-
describe("resolveToWebsiteUrl", () => {
|
|
34
|
-
it("converte una chiave S3 .md in url dx.pagopa.it/docs", () => {
|
|
35
|
-
const location = {
|
|
36
|
-
s3Location: {
|
|
37
|
-
uri: "s3://bucket/azure/iam.md",
|
|
38
|
-
},
|
|
39
|
-
type: "S3",
|
|
40
|
-
};
|
|
41
|
-
const result = resolveToWebsiteUrl(location);
|
|
42
|
-
expect(result?.webLocation?.url).toBe("https://dx.pagopa.it/docs/azure/iam");
|
|
43
|
-
});
|
|
44
|
-
it("converte una chiave S3 index.md in url dx.pagopa.it/docs con slash finale", () => {
|
|
45
|
-
const location = {
|
|
46
|
-
s3Location: {
|
|
47
|
-
uri: "s3://bucket/pipelines/index.md",
|
|
48
|
-
},
|
|
49
|
-
type: "S3",
|
|
50
|
-
};
|
|
51
|
-
const result = resolveToWebsiteUrl(location);
|
|
52
|
-
expect(result?.webLocation?.url).toBe("https://dx.pagopa.it/docs/pipelines/");
|
|
53
|
-
});
|
|
54
|
-
it("ignora location non S3", () => {
|
|
55
|
-
const location = {
|
|
56
|
-
type: "WEB",
|
|
57
|
-
webLocation: {
|
|
58
|
-
url: "https://example.com",
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
const result = resolveToWebsiteUrl(location);
|
|
62
|
-
expect(result?.webLocation?.url).toBe("https://example.com");
|
|
63
|
-
});
|
|
64
|
-
it("restituisce undefined se location è undefined", () => {
|
|
65
|
-
expect(resolveToWebsiteUrl(undefined)).toBeUndefined();
|
|
66
|
-
});
|
|
67
|
-
});
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { QueryPagoPADXDocumentationInputSchema } from "../query-pagopa-dx-documentation.js";
|
|
3
|
-
// Test the Zod schema validation used in the tools
|
|
4
|
-
describe("Query Validation", () => {
|
|
5
|
-
// Use the actual schema from QueryPagoPADXDocumentationTool
|
|
6
|
-
const queryDocSchema = QueryPagoPADXDocumentationInputSchema;
|
|
7
|
-
describe("QueryPagoPADXDocumentationTool validation", () => {
|
|
8
|
-
it("should accept valid queries", () => {
|
|
9
|
-
const validQueries = [
|
|
10
|
-
{ query: "How do I set up the project?" },
|
|
11
|
-
{ query: "abc" }, // minimum 3 characters
|
|
12
|
-
{ query: "What is the DX CLI?" },
|
|
13
|
-
{ query: "Terraform Azure provider setup" },
|
|
14
|
-
];
|
|
15
|
-
validQueries.forEach((input) => {
|
|
16
|
-
const result = queryDocSchema.safeParse(input);
|
|
17
|
-
expect(result.success).toBe(true);
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
it("should reject queries shorter than 3 characters", () => {
|
|
21
|
-
const shortQueries = [{ query: "" }, { query: "a" }, { query: "ab" }];
|
|
22
|
-
shortQueries.forEach((input) => {
|
|
23
|
-
const result = queryDocSchema.safeParse(input);
|
|
24
|
-
expect(result.success).toBe(false);
|
|
25
|
-
expect(result.error?.issues[0]?.message).toBe("Query must be at least 3 characters");
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
it("should reject queries exceeding 500 characters", () => {
|
|
29
|
-
const longQuery = { query: "a".repeat(501) };
|
|
30
|
-
const result = queryDocSchema.safeParse(longQuery);
|
|
31
|
-
expect(result.success).toBe(false);
|
|
32
|
-
expect(result.error?.issues[0]?.message).toBe("Query must not exceed 500 characters");
|
|
33
|
-
});
|
|
34
|
-
it("should reject missing query field", () => {
|
|
35
|
-
const missingQuery = {};
|
|
36
|
-
const result = queryDocSchema.safeParse(missingQuery);
|
|
37
|
-
expect(result.success).toBe(false);
|
|
38
|
-
});
|
|
39
|
-
it("should reject non-string query values", () => {
|
|
40
|
-
const invalidTypes = [
|
|
41
|
-
{ query: 123 },
|
|
42
|
-
{ query: null },
|
|
43
|
-
{ query: undefined },
|
|
44
|
-
{ query: [] },
|
|
45
|
-
{ query: {} },
|
|
46
|
-
];
|
|
47
|
-
invalidTypes.forEach((input) => {
|
|
48
|
-
const result = queryDocSchema.safeParse(input);
|
|
49
|
-
expect(result.success).toBe(false);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
describe("Edge cases", () => {
|
|
54
|
-
it("should handle queries with special characters", () => {
|
|
55
|
-
const specialCharQueries = [
|
|
56
|
-
{ query: "api/management@v1" },
|
|
57
|
-
{ query: "terraform-module (azure-app-service)" },
|
|
58
|
-
{ query: "query with 'quotes' and \"double quotes\"" },
|
|
59
|
-
];
|
|
60
|
-
specialCharQueries.forEach((input) => {
|
|
61
|
-
const result = queryDocSchema.safeParse(input);
|
|
62
|
-
expect(result.success).toBe(true);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
it("should handle very long query strings within limit", () => {
|
|
66
|
-
const longQuery = { query: "a".repeat(500) }; // exactly at limit
|
|
67
|
-
const result = queryDocSchema.safeParse(longQuery);
|
|
68
|
-
expect(result.success).toBe(true);
|
|
69
|
-
});
|
|
70
|
-
it("should accept whitespace-only strings if they meet min length", () => {
|
|
71
|
-
// Note: The Zod schema doesn't call .trim(), so whitespace-only strings will be accepted
|
|
72
|
-
// if they meet the minimum length requirement (3 characters for queryDoc)
|
|
73
|
-
const whitespaceOnly = { query: " " }; // 3 spaces = 3 characters
|
|
74
|
-
const result = queryDocSchema.safeParse(whitespaceOnly);
|
|
75
|
-
// Whitespace-only string has length >= 3, so it passes
|
|
76
|
-
expect(result.success).toBe(true);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
});
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
vi.mock("../../services/bedrock", () => ({
|
|
3
|
-
queryKnowledgeBase: vi.fn().mockResolvedValue("mocked result"),
|
|
4
|
-
}));
|
|
5
|
-
import { BedrockAgentRuntimeClient } from "@aws-sdk/client-bedrock-agent-runtime";
|
|
6
|
-
import { createQueryPagoPADXDocumentationTool } from "../query-pagopa-dx-documentation.js";
|
|
7
|
-
const QueryPagoPADXDocumentationTool = createQueryPagoPADXDocumentationTool({
|
|
8
|
-
kbRuntimeClient: new BedrockAgentRuntimeClient({ region: "eu-central-1" }),
|
|
9
|
-
knowledgeBaseId: "mockKbId",
|
|
10
|
-
rerankingEnabled: false,
|
|
11
|
-
});
|
|
12
|
-
describe("QueryPagoPADXDocumentationTool", () => {
|
|
13
|
-
it("should return results from the knowledge base", async () => {
|
|
14
|
-
const args = { format: "markdown", query: "test query" };
|
|
15
|
-
const result = await QueryPagoPADXDocumentationTool.execute(args);
|
|
16
|
-
expect(result).toBe("mocked result");
|
|
17
|
-
});
|
|
18
|
-
it("should use default format if not provided", async () => {
|
|
19
|
-
const args = { query: "test query" };
|
|
20
|
-
const result = await QueryPagoPADXDocumentationTool.execute(args);
|
|
21
|
-
expect(result).toBe("mocked result");
|
|
22
|
-
});
|
|
23
|
-
it("should reject unknown parameters due to strict schema", async () => {
|
|
24
|
-
const args = { query: "test query", unknownParam: "value" };
|
|
25
|
-
const result = await QueryPagoPADXDocumentationTool.execute(args);
|
|
26
|
-
expect(result).toContain("Error: Invalid input");
|
|
27
|
-
expect(result).toContain("Unrecognized key");
|
|
28
|
-
});
|
|
29
|
-
it("should reject queries shorter than 3 characters", async () => {
|
|
30
|
-
const args = { query: "ab" };
|
|
31
|
-
const result = await QueryPagoPADXDocumentationTool.execute(args);
|
|
32
|
-
expect(result).toContain("Error: Invalid input");
|
|
33
|
-
expect(result).toContain("Query must be at least 3 characters");
|
|
34
|
-
});
|
|
35
|
-
it("should reject empty queries", async () => {
|
|
36
|
-
const args = { query: "" };
|
|
37
|
-
const result = await QueryPagoPADXDocumentationTool.execute(args);
|
|
38
|
-
expect(result).toContain("Error: Invalid input");
|
|
39
|
-
expect(result).toContain("Query must be at least 3 characters");
|
|
40
|
-
});
|
|
41
|
-
it("should reject queries exceeding 500 characters", async () => {
|
|
42
|
-
const args = { query: "a".repeat(501) };
|
|
43
|
-
const result = await QueryPagoPADXDocumentationTool.execute(args);
|
|
44
|
-
expect(result).toContain("Error: Invalid input");
|
|
45
|
-
expect(result).toContain("Query must not exceed 500 characters");
|
|
46
|
-
});
|
|
47
|
-
});
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { BedrockAgentRuntimeClient } from "@aws-sdk/client-bedrock-agent-runtime";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
import { createToolDefinitions } from "../registry.js";
|
|
4
|
-
const toolDefinitions = createToolDefinitions({
|
|
5
|
-
aws: {
|
|
6
|
-
knowledgeBaseId: "kb-id",
|
|
7
|
-
modelArn: "arn:aws:bedrock:eu-central-1:123456789012:model/model-id",
|
|
8
|
-
region: "eu-central-1",
|
|
9
|
-
rerankingEnabled: false,
|
|
10
|
-
},
|
|
11
|
-
githubSearchOrg: "pagopa",
|
|
12
|
-
kbRuntimeClient: new BedrockAgentRuntimeClient({ region: "eu-central-1" }),
|
|
13
|
-
});
|
|
14
|
-
describe("Tool Registry", () => {
|
|
15
|
-
describe("registry structure", () => {
|
|
16
|
-
it("should contain expected tools", () => {
|
|
17
|
-
expect(toolDefinitions).toHaveLength(1);
|
|
18
|
-
const toolIds = toolDefinitions.map((entry) => entry.id);
|
|
19
|
-
expect(toolIds).toContain("pagopa_query_documentation");
|
|
20
|
-
});
|
|
21
|
-
it("should have unique tool IDs", () => {
|
|
22
|
-
const toolIds = toolDefinitions.map((entry) => entry.id);
|
|
23
|
-
const uniqueIds = new Set(toolIds);
|
|
24
|
-
expect(uniqueIds.size).toBe(toolIds.length);
|
|
25
|
-
});
|
|
26
|
-
it("should follow snake_case naming convention with pagopa prefix", () => {
|
|
27
|
-
for (const entry of toolDefinitions) {
|
|
28
|
-
expect(entry.id).toMatch(/^pagopa_[a-z_]+$/);
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
describe("tool entries", () => {
|
|
33
|
-
it("should have valid tool definitions", () => {
|
|
34
|
-
for (const entry of toolDefinitions) {
|
|
35
|
-
expect(entry.tool).toBeDefined();
|
|
36
|
-
expect(entry.tool.name).toBeTruthy();
|
|
37
|
-
expect(entry.tool.description).toBeTruthy();
|
|
38
|
-
expect(entry.tool.execute).toBeTypeOf("function");
|
|
39
|
-
expect(entry.tool.parameters).toBeDefined();
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
it("should have proper annotations", () => {
|
|
43
|
-
for (const entry of toolDefinitions) {
|
|
44
|
-
expect(entry.tool.annotations).toBeDefined();
|
|
45
|
-
expect(entry.tool.annotations.title).toBeTruthy();
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
describe("session requirements", () => {
|
|
50
|
-
it("should mark documentation tool as not requiring session", () => {
|
|
51
|
-
const docTool = toolDefinitions.find((entry) => entry.id === "pagopa_query_documentation");
|
|
52
|
-
expect(docTool?.requiresSession).toBe(false);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
describe("pagopa_query_documentation", () => {
|
|
56
|
-
const docTool = toolDefinitions.find((entry) => entry.id === "pagopa_query_documentation");
|
|
57
|
-
it("should have correct metadata", () => {
|
|
58
|
-
expect(docTool?.tool.name).toBe("pagopa_query_documentation");
|
|
59
|
-
expect(docTool?.tool.annotations.readOnlyHint).toBe(true);
|
|
60
|
-
expect(docTool?.tool.annotations.destructiveHint).toBe(false);
|
|
61
|
-
expect(docTool?.tool.annotations.idempotentHint).toBe(true);
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
});
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { AxiosError, AxiosHeaders } from "axios";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
import { z, ZodError } from "zod";
|
|
4
|
-
import { handleApiError, isAxiosError, isZodError } from "../error-handling.js";
|
|
5
|
-
/**
|
|
6
|
-
* Helper to create an AxiosError with a response
|
|
7
|
-
*/
|
|
8
|
-
const createAxiosResponseError = (status, statusText) => {
|
|
9
|
-
const config = { headers: new AxiosHeaders() };
|
|
10
|
-
return new AxiosError(statusText, "ERR_BAD_REQUEST", config, undefined, {
|
|
11
|
-
config,
|
|
12
|
-
data: {},
|
|
13
|
-
headers: {},
|
|
14
|
-
status,
|
|
15
|
-
statusText,
|
|
16
|
-
});
|
|
17
|
-
};
|
|
18
|
-
describe("handleApiError - Axios HTTP status codes", () => {
|
|
19
|
-
it("should handle 400 Bad Request", () => {
|
|
20
|
-
const error = createAxiosResponseError(400, "Bad Request");
|
|
21
|
-
expect(handleApiError(error)).toBe("Error: Bad request. Please check your input parameters.");
|
|
22
|
-
});
|
|
23
|
-
it("should handle 401 Unauthorized", () => {
|
|
24
|
-
const error = createAxiosResponseError(401, "Unauthorized");
|
|
25
|
-
expect(handleApiError(error)).toBe("Error: Authentication failed. Please check your credentials.");
|
|
26
|
-
});
|
|
27
|
-
it("should handle 403 Forbidden", () => {
|
|
28
|
-
const error = createAxiosResponseError(403, "Forbidden");
|
|
29
|
-
expect(handleApiError(error)).toBe("Error: Permission denied. You don't have access to this resource.");
|
|
30
|
-
});
|
|
31
|
-
it("should handle 404 Not Found", () => {
|
|
32
|
-
const error = createAxiosResponseError(404, "Not Found");
|
|
33
|
-
expect(handleApiError(error)).toBe("Error: Resource not found. Please check the ID is correct.");
|
|
34
|
-
});
|
|
35
|
-
it("should handle 429 Rate Limit", () => {
|
|
36
|
-
const error = createAxiosResponseError(429, "Too Many Requests");
|
|
37
|
-
expect(handleApiError(error)).toBe("Error: Rate limit exceeded. Please wait before making more requests.");
|
|
38
|
-
});
|
|
39
|
-
it("should handle 500 Internal Server Error", () => {
|
|
40
|
-
const error = createAxiosResponseError(500, "Internal Server Error");
|
|
41
|
-
expect(handleApiError(error)).toBe("Error: Server error. The service is temporarily unavailable.");
|
|
42
|
-
});
|
|
43
|
-
it("should handle 502 Bad Gateway", () => {
|
|
44
|
-
const error = createAxiosResponseError(502, "Bad Gateway");
|
|
45
|
-
expect(handleApiError(error)).toBe("Error: Bad gateway. The service is temporarily unavailable.");
|
|
46
|
-
});
|
|
47
|
-
it("should handle 503 Service Unavailable", () => {
|
|
48
|
-
const error = createAxiosResponseError(503, "Service Unavailable");
|
|
49
|
-
expect(handleApiError(error)).toBe("Error: Service unavailable. Please try again later.");
|
|
50
|
-
});
|
|
51
|
-
it("should handle unknown status codes", () => {
|
|
52
|
-
const error = createAxiosResponseError(418, "I'm a teapot");
|
|
53
|
-
expect(handleApiError(error)).toBe("Error: API request failed with status 418");
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
describe("handleApiError - Axios network errors", () => {
|
|
57
|
-
it("should handle ECONNABORTED timeout errors", () => {
|
|
58
|
-
const error = new AxiosError("Timeout", "ECONNABORTED");
|
|
59
|
-
expect(handleApiError(error)).toBe("Error: Request timed out. Please try again.");
|
|
60
|
-
});
|
|
61
|
-
it("should handle ENOTFOUND errors", () => {
|
|
62
|
-
const error = new AxiosError("Not Found", "ENOTFOUND");
|
|
63
|
-
expect(handleApiError(error)).toBe("Error: Could not connect to the server. Please check your network.");
|
|
64
|
-
});
|
|
65
|
-
it("should handle ECONNREFUSED errors", () => {
|
|
66
|
-
const error = new AxiosError("Connection Refused", "ECONNREFUSED");
|
|
67
|
-
expect(handleApiError(error)).toBe("Error: Connection refused. The server may be down.");
|
|
68
|
-
});
|
|
69
|
-
it("should handle other network errors", () => {
|
|
70
|
-
const error = new AxiosError("Unknown network error");
|
|
71
|
-
expect(handleApiError(error)).toBe("Error: Network error occurred: Unknown network error");
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
describe("handleApiError - Zod validation errors", () => {
|
|
75
|
-
it("should format single validation error", () => {
|
|
76
|
-
const result = z
|
|
77
|
-
.object({
|
|
78
|
-
query: z.string().min(3, "String must contain at least 3 character(s)"),
|
|
79
|
-
})
|
|
80
|
-
.safeParse({ query: "" });
|
|
81
|
-
expect(result.success).toBe(false);
|
|
82
|
-
if (result.success) {
|
|
83
|
-
throw new Error("Expected validation to fail");
|
|
84
|
-
}
|
|
85
|
-
expect(handleApiError(result.error)).toBe("Error: Invalid input - query: String must contain at least 3 character(s)");
|
|
86
|
-
});
|
|
87
|
-
it("should format multiple validation errors", () => {
|
|
88
|
-
const result = z
|
|
89
|
-
.intersection(z.object({
|
|
90
|
-
query: z
|
|
91
|
-
.string()
|
|
92
|
-
.min(3, "String must contain at least 3 character(s)"),
|
|
93
|
-
}), z.object({
|
|
94
|
-
page: z.number({ error: "Expected number, received string" }),
|
|
95
|
-
}))
|
|
96
|
-
.safeParse({ page: "one", query: "" });
|
|
97
|
-
expect(result.success).toBe(false);
|
|
98
|
-
if (result.success) {
|
|
99
|
-
throw new Error("Expected validation to fail");
|
|
100
|
-
}
|
|
101
|
-
expect(handleApiError(result.error)).toBe("Error: Invalid input - query: String must contain at least 3 character(s); page: Expected number, received string");
|
|
102
|
-
});
|
|
103
|
-
it("should handle nested path errors", () => {
|
|
104
|
-
const result = z
|
|
105
|
-
.object({
|
|
106
|
-
data: z.object({
|
|
107
|
-
nested: z.object({
|
|
108
|
-
field: z.string({ error: "Required" }),
|
|
109
|
-
}),
|
|
110
|
-
}),
|
|
111
|
-
})
|
|
112
|
-
.safeParse({ data: { nested: {} } });
|
|
113
|
-
expect(result.success).toBe(false);
|
|
114
|
-
if (result.success) {
|
|
115
|
-
throw new Error("Expected validation to fail");
|
|
116
|
-
}
|
|
117
|
-
expect(handleApiError(result.error)).toBe("Error: Invalid input - data.nested.field: Required");
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
describe("handleApiError - generic errors", () => {
|
|
121
|
-
it("should handle Error instances", () => {
|
|
122
|
-
const error = new Error("Something went wrong");
|
|
123
|
-
expect(handleApiError(error)).toBe("Error: Something went wrong");
|
|
124
|
-
});
|
|
125
|
-
it("should handle string errors", () => {
|
|
126
|
-
expect(handleApiError("Raw string error")).toBe("Error: Unexpected error occurred: Raw string error");
|
|
127
|
-
});
|
|
128
|
-
it("should handle null", () => {
|
|
129
|
-
expect(handleApiError(null)).toBe("Error: Unexpected error occurred: null");
|
|
130
|
-
});
|
|
131
|
-
it("should handle undefined", () => {
|
|
132
|
-
expect(handleApiError(undefined)).toBe("Error: Unexpected error occurred: undefined");
|
|
133
|
-
});
|
|
134
|
-
it("should handle objects", () => {
|
|
135
|
-
expect(handleApiError({ custom: "error" })).toBe("Error: Unexpected error occurred: [object Object]");
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
describe("isAxiosError", () => {
|
|
139
|
-
it("should return true for AxiosError instances", () => {
|
|
140
|
-
const error = new AxiosError("Test error");
|
|
141
|
-
expect(isAxiosError(error)).toBe(true);
|
|
142
|
-
});
|
|
143
|
-
it("should return false for regular Error instances", () => {
|
|
144
|
-
const error = new Error("Test error");
|
|
145
|
-
expect(isAxiosError(error)).toBe(false);
|
|
146
|
-
});
|
|
147
|
-
it("should return false for non-errors", () => {
|
|
148
|
-
expect(isAxiosError("string")).toBe(false);
|
|
149
|
-
expect(isAxiosError(null)).toBe(false);
|
|
150
|
-
expect(isAxiosError(undefined)).toBe(false);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
describe("isZodError", () => {
|
|
154
|
-
it("should return true for ZodError instances", () => {
|
|
155
|
-
const error = new ZodError([]);
|
|
156
|
-
expect(isZodError(error)).toBe(true);
|
|
157
|
-
});
|
|
158
|
-
it("should return false for regular Error instances", () => {
|
|
159
|
-
const error = new Error("Test error");
|
|
160
|
-
expect(isZodError(error)).toBe(false);
|
|
161
|
-
});
|
|
162
|
-
it("should return false for non-errors", () => {
|
|
163
|
-
expect(isZodError("string")).toBe(false);
|
|
164
|
-
expect(isZodError(null)).toBe(false);
|
|
165
|
-
expect(isZodError(undefined)).toBe(false);
|
|
166
|
-
});
|
|
167
|
-
});
|