@uarlouski/testrail-mcp-server 1.2.0 → 1.3.0
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 +11 -0
- package/README.md +12 -2
- package/package.json +3 -1
- package/src/client/testrail.ts +78 -12
- package/src/index.ts +12 -2
- package/src/tools/add_attachment_to_run.ts +62 -0
- package/src/tools/add_results.ts +28 -0
- package/src/tools/add_run.ts +24 -0
- package/src/tools/create_case.ts +4 -4
- package/src/tools/get_case.ts +1 -1
- package/src/tools/get_cases.ts +5 -5
- package/src/tools/get_projects.ts +2 -4
- package/src/tools/get_sections.ts +3 -5
- package/src/tools/get_statuses.ts +18 -0
- package/src/tools/get_templates.ts +3 -5
- package/src/tools/get_tests.ts +22 -0
- package/src/tools/update_case.ts +1 -1
- package/src/tools/update_cases.ts +3 -2
- package/src/types/testrail.ts +53 -0
- package/test/client/testrail.test.ts +135 -20
- package/test/tools/add_results.test.ts +94 -0
- package/test/tools/add_run.test.ts +83 -0
- package/test/tools/get_case.test.ts +1 -1
- package/test/tools/get_statuses.test.ts +56 -0
- package/test/tools/get_tests.test.ts +47 -0
- package/test/tools/update_cases.test.ts +30 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.3.0] - 2026-02-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Test run and result management tools**
|
|
8
|
+
- `add_run` - Create a new test run in TestRail
|
|
9
|
+
- `get_tests` - Get tests for a test run, optionally filtered by status
|
|
10
|
+
- `add_results` - Add one or more test results to a test run
|
|
11
|
+
- `get_statuses` - Get all available test statuses (e.g. Passed, Failed, Blocked)
|
|
12
|
+
- Add `add_attachment_to_run` tool to attach files to test runs
|
|
13
|
+
|
|
3
14
|
## [1.2.0] - 2026-02-14
|
|
4
15
|
|
|
5
16
|
### Changed
|
package/README.md
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
# TestRail MCP Server
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@uarlouski/testrail-mcp-server)
|
|
4
|
+
[](https://www.npmjs.com/package/@uarlouski/testrail-mcp-server)
|
|
5
|
+
[](https://github.com/uarlouski/testrail-mcp-server/actions/workflows/ci.yml)
|
|
6
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://github.com/uarlouski/testrail-mcp-server)
|
|
9
|
+
|
|
5
10
|
|
|
6
11
|
A powerful **Model Context Protocol (MCP)** server for seamless integration between **Large Language Models (LLMs)** and **TestRail** test case management.
|
|
7
12
|
|
|
@@ -63,6 +68,11 @@ This MCP server exposes the following tools to the LLM:
|
|
|
63
68
|
| `create_case` | **Authoring**: Create new test cases. *Tip: The model should check templates and fields first to ensure validity.* |
|
|
64
69
|
| `update_case` | **Maintenance**: Modify existing test case fields, title, or steps. |
|
|
65
70
|
| `update_cases` | **Bulk Edit**: Update multiple cases simultaneously for efficient refactoring. |
|
|
71
|
+
| `add_run` | **Test Execution**: Create a new test run in TestRail. |
|
|
72
|
+
| `get_tests` | **Test Execution**: Get tests for a test run, optionally filtered by status. |
|
|
73
|
+
| `add_results` | **Test Execution**: Add one or more test results to a test run. |
|
|
74
|
+
| `add_attachment_to_run` | **Test Execution**: Add an attachment (file or zipped folder) to a test run. |
|
|
75
|
+
| `get_statuses` | **Metadata**: Get all available test statuses (e.g. Passed, Failed, Blocked). |
|
|
66
76
|
| `get_case_fields` | **Metadata**: Retrieve definitions of custom fields to understand available inputs. |
|
|
67
77
|
| `get_templates` | **Metadata**: List available case templates to ensure correct case creation structure. |
|
|
68
78
|
| `get_priorities` | **Metadata**: List available priority levels. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uarlouski/testrail-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Model Context Protocol (MCP) server for TestRail integration, enabling LLMs to fetch test cases and metadata.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -34,10 +34,12 @@
|
|
|
34
34
|
"license": "Apache-2.0",
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
37
|
+
"archiver": "^7.0.1",
|
|
37
38
|
"dotenv": "^17.2.3",
|
|
38
39
|
"zod": "^4.3.5"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
42
|
+
"@types/archiver": "^7.0.0",
|
|
41
43
|
"@types/jest": "^30.0.0",
|
|
42
44
|
"@types/node": "^25.0.9",
|
|
43
45
|
"jest": "^30.2.0",
|
package/src/client/testrail.ts
CHANGED
|
@@ -1,28 +1,38 @@
|
|
|
1
|
-
import { Case, Section, Priority, CaseType, CaseField, Template, Project } from "../types/testrail.js";
|
|
1
|
+
import { Case, Section, Priority, CaseType, CaseField, Template, Project, Run, Status, Test, Result, Attachment } from "../types/testrail.js";
|
|
2
|
+
|
|
3
|
+
import * as fs from "fs";
|
|
2
4
|
|
|
3
5
|
interface PaginatedCasesResponse {
|
|
4
6
|
cases: Case[];
|
|
5
7
|
_links: { next: string | null };
|
|
6
8
|
}
|
|
7
9
|
|
|
10
|
+
interface PaginatedTestsResponse {
|
|
11
|
+
tests: Test[];
|
|
12
|
+
_links: { next: string | null };
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
const API_INDEX = '/index.php?';
|
|
9
16
|
const API_BASE_V2 = `${API_INDEX}/api/v2`;
|
|
10
17
|
|
|
11
18
|
export class TestRailClient {
|
|
12
19
|
private baseUrl: string;
|
|
13
20
|
private headers: HeadersInit;
|
|
21
|
+
private auth: string;
|
|
14
22
|
private prioritiesPromise: Promise<Priority[]> | null = null;
|
|
15
23
|
private caseTypesPromise: Promise<CaseType[]> | null = null;
|
|
16
24
|
private caseFieldsPromise: Promise<CaseField[]> | null = null;
|
|
25
|
+
private statusesPromise: Promise<Status[]> | null = null;
|
|
17
26
|
private projectsPromise: Promise<Project[]> | null = null;
|
|
18
27
|
private templatesPromiseMap: Map<string, Promise<Template[]>> = new Map();
|
|
19
28
|
|
|
20
29
|
constructor(baseUrl: string, email: string, apiKey: string) {
|
|
21
30
|
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
22
31
|
const auth = Buffer.from(`${email}:${apiKey}`).toString('base64');
|
|
32
|
+
this.auth = `Basic ${auth}`;
|
|
23
33
|
this.headers = {
|
|
24
34
|
"Content-Type": "application/json",
|
|
25
|
-
"Authorization":
|
|
35
|
+
"Authorization": this.auth,
|
|
26
36
|
};
|
|
27
37
|
}
|
|
28
38
|
|
|
@@ -85,6 +95,13 @@ export class TestRailClient {
|
|
|
85
95
|
return this.caseFieldsPromise;
|
|
86
96
|
}
|
|
87
97
|
|
|
98
|
+
async getStatuses(): Promise<Status[]> {
|
|
99
|
+
if (!this.statusesPromise) {
|
|
100
|
+
this.statusesPromise = this.get<Status[]>(`${API_BASE_V2}/get_statuses`);
|
|
101
|
+
}
|
|
102
|
+
return this.statusesPromise;
|
|
103
|
+
}
|
|
104
|
+
|
|
88
105
|
async getTemplates(projectId: string): Promise<Template[]> {
|
|
89
106
|
if (!this.templatesPromiseMap.has(projectId)) {
|
|
90
107
|
this.templatesPromiseMap.set(
|
|
@@ -99,11 +116,8 @@ export class TestRailClient {
|
|
|
99
116
|
return this.post<Case>(`${API_BASE_V2}/update_case/${caseId}`, fields);
|
|
100
117
|
}
|
|
101
118
|
|
|
102
|
-
async updateCases(caseIds: number[], fields: Record<string, any>): Promise<Case[]> {
|
|
103
|
-
|
|
104
|
-
const caseData = await this.getCase(String(caseIds[0]));
|
|
105
|
-
|
|
106
|
-
const response = await this.post<{ updated_cases: Case[] }>(`${API_BASE_V2}/update_cases/${caseData.suite_id}`, {
|
|
119
|
+
async updateCases(suiteId: number, caseIds: number[], fields: Record<string, any>): Promise<Case[]> {
|
|
120
|
+
const response = await this.post<{ updated_cases: Case[] }>(`${API_BASE_V2}/update_cases/${suiteId}`, {
|
|
107
121
|
case_ids: caseIds,
|
|
108
122
|
...fields,
|
|
109
123
|
});
|
|
@@ -115,6 +129,10 @@ export class TestRailClient {
|
|
|
115
129
|
return this.post<Case>(`${API_BASE_V2}/add_case/${sectionId}`, fields);
|
|
116
130
|
}
|
|
117
131
|
|
|
132
|
+
async addRun(projectId: string, fields: Record<string, any>): Promise<Run> {
|
|
133
|
+
return this.post<Run>(`${API_BASE_V2}/add_run/${projectId}`, fields);
|
|
134
|
+
}
|
|
135
|
+
|
|
118
136
|
async getSections(projectId: string): Promise<Section[]> {
|
|
119
137
|
const response = await this.get<{ sections: Section[] }>(`${API_BASE_V2}/get_sections/${projectId}`);
|
|
120
138
|
return response.sections;
|
|
@@ -128,6 +146,48 @@ export class TestRailClient {
|
|
|
128
146
|
return this.projectsPromise;
|
|
129
147
|
}
|
|
130
148
|
|
|
149
|
+
|
|
150
|
+
async getTests(runId: number, statusId?: number[]): Promise<Test[]> {
|
|
151
|
+
let url = `${API_BASE_V2}/get_tests/${runId}`;
|
|
152
|
+
|
|
153
|
+
if (statusId && statusId.length > 0) {
|
|
154
|
+
url += `&status_id=${statusId.join(',')}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const allTests: Test[] = [];
|
|
158
|
+
let nextUrl: string | null = url;
|
|
159
|
+
|
|
160
|
+
while (nextUrl) {
|
|
161
|
+
const response: PaginatedTestsResponse = await this.get<PaginatedTestsResponse>(nextUrl);
|
|
162
|
+
allTests.push(...response.tests);
|
|
163
|
+
|
|
164
|
+
nextUrl = response._links?.next;
|
|
165
|
+
if (nextUrl) {
|
|
166
|
+
nextUrl = `${API_INDEX}${nextUrl}`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return allTests;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
async addResults(runId: number, results: Array<Record<string, any>>): Promise<Result[]> {
|
|
175
|
+
return this.post<Result[]>(`${API_BASE_V2}/add_results/${runId}`, { results });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async addAttachmentToRun(runId: number, filePath: string, filename: string): Promise<Attachment> {
|
|
179
|
+
const fileBuffer = await fs.promises.readFile(filePath);
|
|
180
|
+
const blob = new Blob([fileBuffer]);
|
|
181
|
+
const formData = new FormData();
|
|
182
|
+
formData.append('attachment', blob, filename);
|
|
183
|
+
|
|
184
|
+
const headers: HeadersInit = {
|
|
185
|
+
'Authorization': this.auth,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
return this._executeRequest<Attachment>('POST', `${API_BASE_V2}/add_attachment_to_run/${runId}`, headers, formData);
|
|
189
|
+
}
|
|
190
|
+
|
|
131
191
|
private async get<T>(endpoint: string): Promise<T> {
|
|
132
192
|
return this.executeRequest<T>('GET', endpoint);
|
|
133
193
|
}
|
|
@@ -137,16 +197,22 @@ export class TestRailClient {
|
|
|
137
197
|
}
|
|
138
198
|
|
|
139
199
|
private async executeRequest<T>(method: 'GET' | 'POST', endpoint: string, data?: Record<string, any>): Promise<T> {
|
|
200
|
+
let jsonData = undefined;
|
|
201
|
+
if (data) {
|
|
202
|
+
jsonData = JSON.stringify(data);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return this._executeRequest<T>(method, endpoint, this.headers, jsonData);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async _executeRequest<T>(method: 'GET' | 'POST', endpoint: string, headers: HeadersInit, body?: any): Promise<T> {
|
|
140
209
|
const url = `${this.baseUrl}${endpoint}`;
|
|
141
210
|
const params: RequestInit = {
|
|
142
211
|
method,
|
|
143
|
-
headers
|
|
212
|
+
headers,
|
|
213
|
+
body,
|
|
144
214
|
};
|
|
145
215
|
|
|
146
|
-
if (data) {
|
|
147
|
-
params.body = JSON.stringify(data);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
216
|
const response = await fetch(url, params);
|
|
151
217
|
|
|
152
218
|
if (!response.ok) {
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,11 @@ import { updateCasesTool } from "./tools/update_cases.js";
|
|
|
11
11
|
import { createCaseTool } from "./tools/create_case.js";
|
|
12
12
|
import { getSectionsTool } from "./tools/get_sections.js";
|
|
13
13
|
import { getProjectsTool } from "./tools/get_projects.js";
|
|
14
|
+
import { addRunTool } from "./tools/add_run.js";
|
|
15
|
+
import { getStatusesTool } from "./tools/get_statuses.js";
|
|
16
|
+
import { getTestsTool } from "./tools/get_tests.js";
|
|
17
|
+
import { addResultsTool } from "./tools/add_results.js";
|
|
18
|
+
import { addAttachmentToRunTool } from "./tools/add_attachment_to_run.js";
|
|
14
19
|
import { removeNullish } from "./utils/sanitizer.js";
|
|
15
20
|
|
|
16
21
|
const TESTRAIL_INSTANCE_URL = process.env.TESTRAIL_INSTANCE_URL;
|
|
@@ -24,7 +29,7 @@ if (!TESTRAIL_INSTANCE_URL || !TESTRAIL_USERNAME || !TESTRAIL_API_KEY) {
|
|
|
24
29
|
|
|
25
30
|
const server = new McpServer({
|
|
26
31
|
name: "TestRail MCP Server",
|
|
27
|
-
version: "1.
|
|
32
|
+
version: "1.3.0",
|
|
28
33
|
});
|
|
29
34
|
|
|
30
35
|
const client = new TestRailClient(TESTRAIL_INSTANCE_URL, TESTRAIL_USERNAME, TESTRAIL_API_KEY);
|
|
@@ -39,6 +44,11 @@ const tools = [
|
|
|
39
44
|
updateCaseTool,
|
|
40
45
|
updateCasesTool,
|
|
41
46
|
createCaseTool,
|
|
47
|
+
addRunTool,
|
|
48
|
+
getStatusesTool,
|
|
49
|
+
getTestsTool,
|
|
50
|
+
addResultsTool,
|
|
51
|
+
addAttachmentToRunTool,
|
|
42
52
|
]
|
|
43
53
|
|
|
44
54
|
for (const tool of tools) {
|
|
@@ -57,7 +67,7 @@ for (const tool of tools) {
|
|
|
57
67
|
content: [
|
|
58
68
|
{
|
|
59
69
|
type: "text" as const,
|
|
60
|
-
text: JSON.stringify(sanitized
|
|
70
|
+
text: JSON.stringify(sanitized),
|
|
61
71
|
},
|
|
62
72
|
],
|
|
63
73
|
} as any;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { TestRailClient } from "../client/testrail.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { AttachmentSchema } from "../types/testrail.js";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import * as os from "os";
|
|
7
|
+
import archiver from "archiver";
|
|
8
|
+
import { ToolDefinition } from "../types/custom.js";
|
|
9
|
+
|
|
10
|
+
const parameters = {
|
|
11
|
+
run_id: z.string().describe("The ID of the test run to attach the file to"),
|
|
12
|
+
file_path: z.string().describe("The path to the file or directory to attach. Directories will be automatically zipped."),
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const addAttachmentToRunTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
16
|
+
name: "add_attachment_to_run",
|
|
17
|
+
description: "Add an attachment to a test run in TestRail. If the file_path points to a directory, it will be automatically zipped before uploading. Maximum upload size is 256MB.",
|
|
18
|
+
parameters,
|
|
19
|
+
handler: async ({ run_id, file_path }, client: TestRailClient) => {
|
|
20
|
+
if (!fs.existsSync(file_path)) {
|
|
21
|
+
throw new Error(`File or directory not found: ${file_path}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const stats = fs.statSync(file_path);
|
|
25
|
+
let uploadPath: string = file_path;
|
|
26
|
+
let filename: string = path.basename(file_path);
|
|
27
|
+
let isTemporary = false;
|
|
28
|
+
|
|
29
|
+
if (stats.isDirectory()) {
|
|
30
|
+
const basename = path.basename(file_path);
|
|
31
|
+
const tempDir = os.tmpdir();
|
|
32
|
+
const zipPath = path.join(tempDir, `${basename}-${Date.now()}.zip`);
|
|
33
|
+
|
|
34
|
+
await new Promise<void>((resolve, reject) => {
|
|
35
|
+
const output = fs.createWriteStream(zipPath);
|
|
36
|
+
const archive = archiver('zip', {
|
|
37
|
+
zlib: { level: 9 }
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
output.on('close', () => resolve());
|
|
41
|
+
archive.on('error', (err) => reject(err));
|
|
42
|
+
|
|
43
|
+
archive.pipe(output);
|
|
44
|
+
archive.directory(file_path, basename);
|
|
45
|
+
archive.finalize();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
uploadPath = zipPath;
|
|
49
|
+
filename = `${basename}.zip`;
|
|
50
|
+
isTemporary = true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const result = await client.addAttachmentToRun(Number(run_id), uploadPath, filename);
|
|
55
|
+
return AttachmentSchema.parse(result);
|
|
56
|
+
} finally {
|
|
57
|
+
if (isTemporary && fs.existsSync(uploadPath)) {
|
|
58
|
+
fs.unlinkSync(uploadPath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { TestRailClient } from "../client/testrail.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { ResultSchema } from "../types/testrail.js";
|
|
4
|
+
import { ToolDefinition } from "../types/custom.js";
|
|
5
|
+
|
|
6
|
+
const parameters = {
|
|
7
|
+
run_id: z.number().describe("The ID of the test run"),
|
|
8
|
+
results: z.array(z.object({
|
|
9
|
+
test_id: z.number().describe("The ID of the test. Use get_tests with a run_id to retrieve available test IDs"),
|
|
10
|
+
status_id: z.number().describe("The ID of the test status (e.g. Passed, Failed). Use get_statuses to retrieve available status IDs"),
|
|
11
|
+
comment: z.string().optional().describe("Optional comment/description for the result"),
|
|
12
|
+
defects: z.string().optional().describe("Optional comma-separated list of defect IDs"),
|
|
13
|
+
})).describe("Array of results to add. Each result must have test_id and status_id"),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const addResultsTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
17
|
+
name: "add_results",
|
|
18
|
+
description: "Add one or more test results to a test run",
|
|
19
|
+
parameters,
|
|
20
|
+
handler: async ({ run_id, results }, client: TestRailClient) => {
|
|
21
|
+
const response = await client.addResults(run_id, results);
|
|
22
|
+
return {
|
|
23
|
+
success: true,
|
|
24
|
+
added_count: response.length,
|
|
25
|
+
results: response.map(r => ResultSchema.parse(r)),
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { TestRailClient } from "../client/testrail.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { RunSchema } from "../types/testrail.js";
|
|
4
|
+
import { ToolDefinition } from "../types/custom.js";
|
|
5
|
+
|
|
6
|
+
const parameters = {
|
|
7
|
+
project_id: z.string().describe("The ID of the project. Use get_projects to find available projects"),
|
|
8
|
+
name: z.string().optional().describe("The name of the test run"),
|
|
9
|
+
description: z.string().optional().describe("The description of the test run"),
|
|
10
|
+
case_ids: z.array(z.number()).describe("Array of case IDs to include in the run. After creating the run, use get_tests with the returned run_id to retrieve test IDs for result submission")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const addRunTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
14
|
+
name: "add_run",
|
|
15
|
+
description: "Create a new test run in TestRail",
|
|
16
|
+
parameters,
|
|
17
|
+
handler: async ({ project_id, ...fields }, client: TestRailClient) => {
|
|
18
|
+
const caseData = await client.getCase(String(fields.case_ids[0]));
|
|
19
|
+
const params = { suite_id: caseData.suite_id, include_all: false, ...fields };
|
|
20
|
+
const run = await client.addRun(project_id, params);
|
|
21
|
+
|
|
22
|
+
return RunSchema.parse(run);
|
|
23
|
+
},
|
|
24
|
+
};
|
package/src/tools/create_case.ts
CHANGED
|
@@ -3,14 +3,14 @@ import { TestRailClient } from "../client/testrail.js";
|
|
|
3
3
|
import { ToolDefinition } from "../types/custom.js";
|
|
4
4
|
|
|
5
5
|
const parameters = {
|
|
6
|
-
section_id: z.string().describe("The ID of the section where the case should be created"),
|
|
7
|
-
title: z.string().describe("The title of the test case
|
|
8
|
-
fields: z.record(z.string(), z.any()).optional().describe("Optional
|
|
6
|
+
section_id: z.string().describe("The ID of the section where the case should be created. Use get_sections to find available sections"),
|
|
7
|
+
title: z.string().describe("The title of the test case"),
|
|
8
|
+
fields: z.record(z.string(), z.any()).optional().describe("Optional fields for the test case. First use get_templates to discover available templates, then get_case_fields to see available fields for your template. Example: {\"priority_id\": 2, \"template_id\": 1, \"custom_automation_priority\": 1}"),
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
export const createCaseTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
12
12
|
name: "create_case",
|
|
13
|
-
description: "Create a new test case in TestRail
|
|
13
|
+
description: "Create a new test case in TestRail",
|
|
14
14
|
parameters,
|
|
15
15
|
handler: async ({ section_id, title, fields }, client) => {
|
|
16
16
|
const caseData = {
|
package/src/tools/get_case.ts
CHANGED
|
@@ -9,7 +9,7 @@ const parameters = {
|
|
|
9
9
|
|
|
10
10
|
export const getCaseTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
11
11
|
name: "get_case",
|
|
12
|
-
description: "Get a test case
|
|
12
|
+
description: "Get detailed information about a test case including its custom fields",
|
|
13
13
|
parameters,
|
|
14
14
|
handler: async ({ case_id }, client) => {
|
|
15
15
|
const id = case_id.toUpperCase().startsWith("C") ? case_id.substring(1) : case_id;
|
package/src/tools/get_cases.ts
CHANGED
|
@@ -4,11 +4,11 @@ import { ToolDefinition } from "../types/custom.js";
|
|
|
4
4
|
import { Case } from "../types/testrail.js";
|
|
5
5
|
|
|
6
6
|
const parameters = {
|
|
7
|
-
project_id: z.string().describe("The ID of the project"),
|
|
8
|
-
section_id: z.string().optional().describe("Optional section ID to filter cases by section"),
|
|
9
|
-
filter: z.record(z.string(), z.string()).optional().describe("Optional API
|
|
10
|
-
where: z.record(z.string(), z.any()).optional().describe("Optional client-side filter for any field including custom fields
|
|
11
|
-
fields: z.array(z.string()).optional().describe("
|
|
7
|
+
project_id: z.string().describe("The ID of the project. Use get_projects to find available projects"),
|
|
8
|
+
section_id: z.string().optional().describe("Optional section ID to filter cases by section. Use get_sections to find available sections"),
|
|
9
|
+
filter: z.record(z.string(), z.string()).optional().describe("Optional API-side filters (more efficient for large datasets). Supported: priority_id, type_id, created_by, updated_by, milestone_id, refs, created_after, created_before, updated_after, updated_before. Use comma-separated values for IDs. Example: {\"priority_id\": \"1,2\", \"type_id\": \"3\"}"),
|
|
10
|
+
where: z.record(z.string(), z.any()).optional().describe("Optional client-side filter for any field including custom fields (filters after fetching all cases). Supports exact value matching. Example: {\"custom_automation_status\": 1, \"priority_id\": 2}"),
|
|
11
|
+
fields: z.array(z.string()).optional().describe("Additional fields to include in response beyond id, title, and suite_id. Use get_case_fields to see available fields. Example: [\"priority_id\", \"type_id\", \"custom_automation_status\"]"),
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
interface CasesResponse {
|
|
@@ -6,17 +6,15 @@ const parameters = {};
|
|
|
6
6
|
|
|
7
7
|
export const getProjectsTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
8
8
|
name: "get_projects",
|
|
9
|
-
description: "Get all available projects in TestRail. Returns project IDs and names
|
|
9
|
+
description: "Get all available projects in TestRail. Returns project IDs and names that can be used with get_sections, get_templates, get_cases, and add_run",
|
|
10
10
|
parameters,
|
|
11
11
|
handler: async (_args, client) => {
|
|
12
12
|
const projects = await client.getProjects();
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
return {
|
|
15
15
|
projects: projects
|
|
16
16
|
.filter(p => !p.is_completed)
|
|
17
17
|
.map(p => ProjectSchema.parse(p)),
|
|
18
18
|
};
|
|
19
|
-
|
|
20
|
-
return response;
|
|
21
19
|
}
|
|
22
20
|
};
|
|
@@ -4,20 +4,18 @@ import { ToolDefinition } from "../types/custom.js";
|
|
|
4
4
|
import { SectionSchema } from "../types/testrail.js";
|
|
5
5
|
|
|
6
6
|
const parameters = {
|
|
7
|
-
project_id: z.string().describe("The ID of the project"),
|
|
7
|
+
project_id: z.string().describe("The ID of the project. Use get_projects to find available projects"),
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
export const getSectionsTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
11
11
|
name: "get_sections",
|
|
12
|
-
description: "Get all sections for a project. Returns section IDs and names
|
|
12
|
+
description: "Get all sections for a project. Returns section IDs and names that can be used with create_case",
|
|
13
13
|
parameters,
|
|
14
14
|
handler: async ({ project_id }, client) => {
|
|
15
15
|
const sections = await client.getSections(project_id);
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
return {
|
|
18
18
|
sections: sections.map(s => SectionSchema.parse(s)),
|
|
19
19
|
};
|
|
20
|
-
|
|
21
|
-
return response;
|
|
22
20
|
}
|
|
23
21
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { TestRailClient } from "../client/testrail.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { StatusSchema } from "../types/testrail.js";
|
|
4
|
+
import { ToolDefinition } from "../types/custom.js";
|
|
5
|
+
|
|
6
|
+
const parameters = {};
|
|
7
|
+
|
|
8
|
+
export const getStatusesTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
9
|
+
name: "get_statuses",
|
|
10
|
+
description: "Get all available test statuses (e.g. Passed, Failed, Blocked). Returns status IDs and names that can be used with add_result and get_tests",
|
|
11
|
+
parameters,
|
|
12
|
+
handler: async (args: any, client: TestRailClient) => {
|
|
13
|
+
const statuses = await client.getStatuses();
|
|
14
|
+
return {
|
|
15
|
+
statuses: statuses.map(s => StatusSchema.parse(s)),
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -4,20 +4,18 @@ import { ToolDefinition } from "../types/custom.js";
|
|
|
4
4
|
import { TemplateSchema } from "../types/testrail.js";
|
|
5
5
|
|
|
6
6
|
const parameters = {
|
|
7
|
-
project_id: z.string().describe("The ID of the project"),
|
|
7
|
+
project_id: z.string().describe("The ID of the project. Use get_projects to find available projects"),
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
export const getTemplatesTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
11
11
|
name: "get_templates",
|
|
12
|
-
description: "Get available test case templates for a project.
|
|
12
|
+
description: "Get available test case templates for a project. Template IDs determine which fields are available when creating or updating test cases",
|
|
13
13
|
parameters,
|
|
14
14
|
handler: async ({ project_id }, client) => {
|
|
15
15
|
const templates = await client.getTemplates(project_id);
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
return {
|
|
18
18
|
templates: templates.map(t => TemplateSchema.parse(t)),
|
|
19
19
|
};
|
|
20
|
-
|
|
21
|
-
return response;
|
|
22
20
|
}
|
|
23
21
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { TestRailClient } from "../client/testrail.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { Test, TestSchema } from "../types/testrail.js";
|
|
4
|
+
import { ToolDefinition } from "../types/custom.js";
|
|
5
|
+
|
|
6
|
+
const parameters = {
|
|
7
|
+
run_id: z.number().describe("The ID of the test run"),
|
|
8
|
+
status_id: z.array(z.number()).optional().describe("Optional array of status IDs to filter by. Use get_statuses to retrieve available status IDs"),
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const getTestsTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
12
|
+
name: "get_tests",
|
|
13
|
+
description: "Get tests for a test run, optionally filtered by status",
|
|
14
|
+
parameters,
|
|
15
|
+
handler: async ({ run_id, status_id }, client: TestRailClient) => {
|
|
16
|
+
const tests: Test[] = await client.getTests(run_id, status_id);
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
tests: tests.map(test => TestSchema.parse(test)),
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
};
|
package/src/tools/update_case.ts
CHANGED
|
@@ -9,7 +9,7 @@ const parameters = {
|
|
|
9
9
|
|
|
10
10
|
export const updateCaseTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
11
11
|
name: "update_case",
|
|
12
|
-
description: "Update a test case in TestRail. Supports partial updates - only specify the fields you want to change
|
|
12
|
+
description: "Update a test case in TestRail. Supports partial updates - only specify the fields you want to change",
|
|
13
13
|
parameters,
|
|
14
14
|
handler: async ({ case_id, fields }, client) => {
|
|
15
15
|
const id = case_id.toUpperCase().startsWith("C") ? case_id.substring(1) : case_id;
|
|
@@ -9,10 +9,11 @@ const parameters = {
|
|
|
9
9
|
|
|
10
10
|
export const updateCasesTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
11
11
|
name: "update_cases",
|
|
12
|
-
description: "Bulk update multiple test cases with the same field values. More efficient than calling update_case multiple times
|
|
12
|
+
description: "Bulk update multiple test cases with the same field values. More efficient than calling update_case multiple times",
|
|
13
13
|
parameters,
|
|
14
14
|
handler: async ({ case_ids, fields }, client) => {
|
|
15
|
-
const
|
|
15
|
+
const caseData = await client.getCase(String(case_ids[0]));
|
|
16
|
+
const updatedCases = await client.updateCases(caseData.suite_id, case_ids, fields);
|
|
16
17
|
|
|
17
18
|
return {
|
|
18
19
|
success: true,
|
package/src/types/testrail.ts
CHANGED
|
@@ -85,3 +85,56 @@ export const ProjectSchema = z.object({
|
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
export type Project = z.infer<typeof ProjectSchema>;
|
|
88
|
+
|
|
89
|
+
export const RunSchema = z.object({
|
|
90
|
+
id: z.number(),
|
|
91
|
+
name: z.string(),
|
|
92
|
+
description: z.string().nullable(),
|
|
93
|
+
suite_id: z.number().nullable(),
|
|
94
|
+
project_id: z.number(),
|
|
95
|
+
is_completed: z.boolean(),
|
|
96
|
+
passed_count: z.number(),
|
|
97
|
+
blocked_count: z.number(),
|
|
98
|
+
untested_count: z.number(),
|
|
99
|
+
retest_count: z.number(),
|
|
100
|
+
failed_count: z.number(),
|
|
101
|
+
url: z.string(),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
export type Run = z.infer<typeof RunSchema>;
|
|
105
|
+
|
|
106
|
+
export const StatusSchema = z.object({
|
|
107
|
+
id: z.number(),
|
|
108
|
+
name: z.string(),
|
|
109
|
+
label: z.string(),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export type Status = z.infer<typeof StatusSchema>;
|
|
113
|
+
|
|
114
|
+
export const TestSchema = z.object({
|
|
115
|
+
id: z.number(),
|
|
116
|
+
case_id: z.number(),
|
|
117
|
+
status_id: z.number(),
|
|
118
|
+
title: z.string(),
|
|
119
|
+
run_id: z.number(),
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
export const TestsSchema = z.array(TestSchema)
|
|
123
|
+
|
|
124
|
+
export type Test = z.infer<typeof TestSchema>
|
|
125
|
+
|
|
126
|
+
export const ResultSchema = z.object({
|
|
127
|
+
id: z.number(),
|
|
128
|
+
test_id: z.number(),
|
|
129
|
+
status_id: z.number(),
|
|
130
|
+
comment: z.string().nullable(),
|
|
131
|
+
defects: z.string().nullable(),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
export type Result = z.infer<typeof ResultSchema>
|
|
135
|
+
|
|
136
|
+
export const AttachmentSchema = z.object({
|
|
137
|
+
attachment_id: z.number(),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
export type Attachment = z.infer<typeof AttachmentSchema>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jest, describe, test, expect, beforeEach, afterEach } from '@jest/globals';
|
|
2
2
|
import { TestRailClient } from '../../src/client/testrail.js';
|
|
3
|
+
import * as fs from 'fs';
|
|
3
4
|
|
|
4
5
|
describe('TestRailClient', () => {
|
|
5
6
|
let client: TestRailClient;
|
|
@@ -315,26 +316,16 @@ describe('TestRailClient', () => {
|
|
|
315
316
|
);
|
|
316
317
|
});
|
|
317
318
|
|
|
318
|
-
test('updateCases
|
|
319
|
-
const mockCase = { id: 1, suite_id: 10 };
|
|
319
|
+
test('updateCases uses passed suiteId', async () => {
|
|
320
320
|
const mockUpdated = [{ id: 1, title: 'Updated' }, { id: 2, title: 'Updated' }];
|
|
321
321
|
|
|
322
|
-
fetchMock
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
})
|
|
327
|
-
.mockResolvedValueOnce({
|
|
328
|
-
ok: true,
|
|
329
|
-
json: async () => ({ updated_cases: mockUpdated })
|
|
330
|
-
});
|
|
322
|
+
fetchMock.mockResolvedValue({
|
|
323
|
+
ok: true,
|
|
324
|
+
json: async () => ({ updated_cases: mockUpdated })
|
|
325
|
+
});
|
|
331
326
|
|
|
332
|
-
const result = await client.updateCases([1, 2], { title: 'Updated' });
|
|
327
|
+
const result = await client.updateCases(10, [1, 2], { title: 'Updated' });
|
|
333
328
|
expect(result).toEqual(mockUpdated);
|
|
334
|
-
expect(fetchMock).toHaveBeenCalledWith(
|
|
335
|
-
'https://testrail.io/index.php?/api/v2/get_case/1',
|
|
336
|
-
expect.any(Object)
|
|
337
|
-
);
|
|
338
329
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
339
330
|
'https://testrail.io/index.php?/api/v2/update_cases/10',
|
|
340
331
|
expect.objectContaining({
|
|
@@ -420,11 +411,135 @@ describe('TestRailClient', () => {
|
|
|
420
411
|
);
|
|
421
412
|
});
|
|
422
413
|
|
|
423
|
-
test('
|
|
424
|
-
|
|
414
|
+
test('addRun posts fields directly', async () => {
|
|
415
|
+
const mockRun = { id: 1, name: 'New Run' };
|
|
425
416
|
|
|
426
|
-
|
|
427
|
-
|
|
417
|
+
fetchMock.mockResolvedValue({
|
|
418
|
+
ok: true,
|
|
419
|
+
json: async () => mockRun
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const result = await client.addRun('1', { name: 'New Run', suite_id: 20 });
|
|
423
|
+
|
|
424
|
+
expect(result).toEqual(mockRun);
|
|
425
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
426
|
+
'https://testrail.io/index.php?/api/v2/add_run/1',
|
|
427
|
+
expect.objectContaining({
|
|
428
|
+
method: 'POST',
|
|
429
|
+
body: JSON.stringify({ name: 'New Run', suite_id: 20 })
|
|
430
|
+
})
|
|
431
|
+
);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test('getTests handles pagination', async () => {
|
|
435
|
+
fetchMock
|
|
436
|
+
.mockImplementationOnce(() => Promise.resolve({
|
|
437
|
+
ok: true,
|
|
438
|
+
json: () => Promise.resolve({
|
|
439
|
+
tests: [{ id: 1, case_id: 1, status_id: 1, title: 'Test 1', run_id: 1 }],
|
|
440
|
+
_links: { next: '/api/v2/get_tests/1&offset=1' }
|
|
441
|
+
}),
|
|
442
|
+
} as Response))
|
|
443
|
+
.mockImplementationOnce(() => Promise.resolve({
|
|
444
|
+
ok: true,
|
|
445
|
+
json: () => Promise.resolve({
|
|
446
|
+
tests: [{ id: 2, case_id: 2, status_id: 1, title: 'Test 2', run_id: 1 }],
|
|
447
|
+
_links: { next: null }
|
|
448
|
+
}),
|
|
449
|
+
} as Response));
|
|
450
|
+
|
|
451
|
+
const tests = await client.getTests(1);
|
|
452
|
+
expect(tests).toHaveLength(2);
|
|
453
|
+
expect(tests[0].id).toBe(1);
|
|
454
|
+
expect(tests[1].id).toBe(2);
|
|
455
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test('getTests with status_id adds filter', async () => {
|
|
459
|
+
fetchMock.mockResolvedValue({
|
|
460
|
+
ok: true,
|
|
461
|
+
json: async () => ({
|
|
462
|
+
tests: [],
|
|
463
|
+
_links: { next: null }
|
|
464
|
+
})
|
|
465
|
+
} as Response);
|
|
466
|
+
|
|
467
|
+
await client.getTests(1, [1, 5]);
|
|
468
|
+
|
|
469
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
470
|
+
expect.stringContaining('/get_tests/1&status_id=1,5'),
|
|
471
|
+
expect.any(Object)
|
|
472
|
+
);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test('getStatuses caches result', async () => {
|
|
476
|
+
const mockStatuses = [{ id: 1, name: 'passed', label: 'Passed' }];
|
|
477
|
+
fetchMock.mockResolvedValue({
|
|
478
|
+
ok: true,
|
|
479
|
+
json: async () => mockStatuses
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
await client.getStatuses();
|
|
483
|
+
await client.getStatuses();
|
|
484
|
+
|
|
485
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test('addResults posts results array', async () => {
|
|
489
|
+
const mockResults = [
|
|
490
|
+
{ id: 1, test_id: 100, status_id: 1, comment: 'Passed', defects: null },
|
|
491
|
+
{ id: 2, test_id: 101, status_id: 5, comment: 'Failed', defects: 'BUG-123' }
|
|
492
|
+
];
|
|
493
|
+
|
|
494
|
+
fetchMock.mockResolvedValue({
|
|
495
|
+
ok: true,
|
|
496
|
+
json: async () => mockResults
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const results = await client.addResults(50, [
|
|
500
|
+
{ test_id: 100, status_id: 1, comment: 'Passed' },
|
|
501
|
+
{ test_id: 101, status_id: 5, comment: 'Failed', defects: 'BUG-123' }
|
|
502
|
+
]);
|
|
503
|
+
|
|
504
|
+
expect(results).toEqual(mockResults);
|
|
505
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
506
|
+
'https://testrail.io/index.php?/api/v2/add_results/50',
|
|
507
|
+
expect.objectContaining({
|
|
508
|
+
method: 'POST',
|
|
509
|
+
body: JSON.stringify({
|
|
510
|
+
results: [
|
|
511
|
+
{ test_id: 100, status_id: 1, comment: 'Passed' },
|
|
512
|
+
{ test_id: 101, status_id: 5, comment: 'Failed', defects: 'BUG-123' }
|
|
513
|
+
]
|
|
514
|
+
})
|
|
515
|
+
})
|
|
516
|
+
);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test('addAttachmentToRun uploads file using FormData', async () => {
|
|
520
|
+
const mockAttachment = { attachment_id: 443 };
|
|
521
|
+
const mockFileBuffer = Buffer.from('file content');
|
|
522
|
+
|
|
523
|
+
jest.spyOn(fs.promises, 'readFile').mockResolvedValue(mockFileBuffer);
|
|
524
|
+
|
|
525
|
+
fetchMock.mockResolvedValue({
|
|
526
|
+
ok: true,
|
|
527
|
+
json: async () => mockAttachment
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const result = await client.addAttachmentToRun(100, '/path/to/file.txt', 'file.txt');
|
|
531
|
+
|
|
532
|
+
expect(result).toEqual(mockAttachment);
|
|
533
|
+
expect(fs.promises.readFile).toHaveBeenCalledWith('/path/to/file.txt');
|
|
534
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
535
|
+
'https://testrail.io/index.php?/api/v2/add_attachment_to_run/100',
|
|
536
|
+
expect.objectContaining({
|
|
537
|
+
method: 'POST',
|
|
538
|
+
headers: expect.objectContaining({
|
|
539
|
+
'Authorization': expect.stringContaining('Basic')
|
|
540
|
+
}),
|
|
541
|
+
body: expect.any(FormData)
|
|
542
|
+
})
|
|
428
543
|
);
|
|
429
544
|
});
|
|
430
545
|
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import { addResultsTool } from '../../src/tools/add_results.js';
|
|
3
|
+
import { TestRailClient } from '../../src/client/testrail.js';
|
|
4
|
+
import { Result } from '../../src/types/testrail.js';
|
|
5
|
+
|
|
6
|
+
describe('add_results tool', () => {
|
|
7
|
+
let mockClient: jest.Mocked<TestRailClient>;
|
|
8
|
+
let addResultsMock: jest.Mock<(runId: number, results: Array<Record<string, any>>) => Promise<Result[]>>;
|
|
9
|
+
|
|
10
|
+
const mockResults: Result[] = [
|
|
11
|
+
{
|
|
12
|
+
id: 1,
|
|
13
|
+
test_id: 100,
|
|
14
|
+
status_id: 1,
|
|
15
|
+
comment: 'Test passed',
|
|
16
|
+
defects: null
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 2,
|
|
20
|
+
test_id: 101,
|
|
21
|
+
status_id: 5,
|
|
22
|
+
comment: 'Test failed',
|
|
23
|
+
defects: 'BUG-123'
|
|
24
|
+
}
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
addResultsMock = jest.fn<(runId: number, results: Array<Record<string, any>>) => Promise<Result[]>>().mockResolvedValue(mockResults);
|
|
29
|
+
|
|
30
|
+
mockClient = {
|
|
31
|
+
addResults: addResultsMock
|
|
32
|
+
} as unknown as jest.Mocked<TestRailClient>;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('exports correct tool definition', () => {
|
|
36
|
+
expect(addResultsTool.name).toBe('add_results');
|
|
37
|
+
expect(addResultsTool.description).toBeDefined();
|
|
38
|
+
expect(addResultsTool.parameters).toBeDefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('handler calls addResults with correct arguments', async () => {
|
|
42
|
+
const args = {
|
|
43
|
+
run_id: 50,
|
|
44
|
+
results: [
|
|
45
|
+
{
|
|
46
|
+
test_id: 100,
|
|
47
|
+
status_id: 1,
|
|
48
|
+
comment: 'Test passed'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
test_id: 101,
|
|
52
|
+
status_id: 5,
|
|
53
|
+
comment: 'Test failed',
|
|
54
|
+
defects: 'BUG-123'
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const result = await addResultsTool.handler(args, mockClient);
|
|
60
|
+
|
|
61
|
+
expect(result).toBeDefined();
|
|
62
|
+
expect(mockClient.addResults).toHaveBeenCalledWith(50, args.results);
|
|
63
|
+
|
|
64
|
+
expect(result.success).toBe(true);
|
|
65
|
+
expect(result.added_count).toBe(2);
|
|
66
|
+
expect(result.results).toHaveLength(2);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('handler works with single result', async () => {
|
|
70
|
+
const singleResult = [mockResults[0]];
|
|
71
|
+
addResultsMock.mockResolvedValue(singleResult);
|
|
72
|
+
|
|
73
|
+
const args = {
|
|
74
|
+
run_id: 50,
|
|
75
|
+
results: [
|
|
76
|
+
{
|
|
77
|
+
test_id: 100,
|
|
78
|
+
status_id: 1,
|
|
79
|
+
comment: 'Test passed'
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const result = await addResultsTool.handler(args, mockClient);
|
|
85
|
+
|
|
86
|
+
expect(result.success).toBe(true);
|
|
87
|
+
expect(result.added_count).toBe(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('handler returns error on failure', async () => {
|
|
91
|
+
addResultsMock.mockRejectedValue(new Error('API Error'));
|
|
92
|
+
await expect(addResultsTool.handler({ run_id: 50, results: [] }, mockClient)).rejects.toThrow('API Error');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { addRunTool } from '../../src/tools/add_run.js';
|
|
3
|
+
import { TestRailClient } from '../../src/client/testrail.js';
|
|
4
|
+
|
|
5
|
+
describe('add_run tool', () => {
|
|
6
|
+
let client: TestRailClient;
|
|
7
|
+
let addRunMock: any;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
addRunMock = jest.fn();
|
|
11
|
+
client = {
|
|
12
|
+
addRun: addRunMock,
|
|
13
|
+
getCase: jest.fn(),
|
|
14
|
+
} as any as TestRailClient;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('should wrap client result including only required fields and forcing include_all to false', async () => {
|
|
18
|
+
const run = {
|
|
19
|
+
id: 1,
|
|
20
|
+
name: 'Test Run',
|
|
21
|
+
description: null,
|
|
22
|
+
suite_id: 10,
|
|
23
|
+
project_id: 1,
|
|
24
|
+
is_completed: false,
|
|
25
|
+
passed_count: 0,
|
|
26
|
+
blocked_count: 0,
|
|
27
|
+
untested_count: 5,
|
|
28
|
+
retest_count: 0,
|
|
29
|
+
failed_count: 0,
|
|
30
|
+
url: 'https://testrail.io/runs/view/1'
|
|
31
|
+
};
|
|
32
|
+
addRunMock.mockResolvedValue(run);
|
|
33
|
+
|
|
34
|
+
const mockCase = { suite_id: 10 };
|
|
35
|
+
(client.getCase as jest.Mock<any>).mockResolvedValue(mockCase);
|
|
36
|
+
|
|
37
|
+
const result = await addRunTool.handler({ project_id: '1', name: 'Test Run', case_ids: [1] }, client);
|
|
38
|
+
|
|
39
|
+
expect(client.getCase).toHaveBeenCalledWith('1');
|
|
40
|
+
expect(addRunMock).toHaveBeenCalledWith('1', { name: 'Test Run', case_ids: [1], suite_id: 10, include_all: false });
|
|
41
|
+
expect(result).toEqual(run);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('should wrap client result handling optional fields', async () => {
|
|
45
|
+
const run = {
|
|
46
|
+
id: 1,
|
|
47
|
+
name: 'Test Run',
|
|
48
|
+
description: 'Description',
|
|
49
|
+
suite_id: 10,
|
|
50
|
+
project_id: 1,
|
|
51
|
+
is_completed: false,
|
|
52
|
+
passed_count: 0,
|
|
53
|
+
blocked_count: 0,
|
|
54
|
+
untested_count: 3,
|
|
55
|
+
retest_count: 0,
|
|
56
|
+
failed_count: 0,
|
|
57
|
+
url: 'https://testrail.io/runs/view/1'
|
|
58
|
+
};
|
|
59
|
+
addRunMock.mockResolvedValue(run);
|
|
60
|
+
|
|
61
|
+
const mockCase = { suite_id: 10 };
|
|
62
|
+
(client.getCase as jest.Mock<any>).mockResolvedValue(mockCase);
|
|
63
|
+
|
|
64
|
+
const args = {
|
|
65
|
+
project_id: '1',
|
|
66
|
+
name: 'Test Run',
|
|
67
|
+
description: 'Description',
|
|
68
|
+
case_ids: [1, 2, 3]
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const result = await addRunTool.handler(args, client);
|
|
72
|
+
|
|
73
|
+
expect(client.getCase).toHaveBeenCalledWith('1');
|
|
74
|
+
expect(addRunMock).toHaveBeenCalledWith('1', {
|
|
75
|
+
name: 'Test Run',
|
|
76
|
+
description: 'Description',
|
|
77
|
+
case_ids: [1, 2, 3],
|
|
78
|
+
suite_id: 10,
|
|
79
|
+
include_all: false
|
|
80
|
+
});
|
|
81
|
+
expect(result).toEqual(run);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -39,7 +39,7 @@ describe('get_case tool', () => {
|
|
|
39
39
|
|
|
40
40
|
test('exports correct tool definition', () => {
|
|
41
41
|
expect(getCaseTool.name).toBe('get_case');
|
|
42
|
-
expect(getCaseTool.description).toBe('Get a test case
|
|
42
|
+
expect(getCaseTool.description).toBe('Get detailed information about a test case including its custom fields');
|
|
43
43
|
expect(getCaseTool.parameters).toBeDefined();
|
|
44
44
|
});
|
|
45
45
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import { getStatusesTool } from '../../src/tools/get_statuses.js';
|
|
3
|
+
import { TestRailClient } from '../../src/client/testrail.js';
|
|
4
|
+
import { Status } from '../../src/types/testrail.js';
|
|
5
|
+
|
|
6
|
+
describe('get_statuses tool', () => {
|
|
7
|
+
let mockClient: jest.Mocked<TestRailClient>;
|
|
8
|
+
let getStatusesMock: jest.Mock<() => Promise<Status[]>>;
|
|
9
|
+
|
|
10
|
+
const mockStatuses: Status[] = [
|
|
11
|
+
{ id: 1, name: 'passed', label: 'Passed' },
|
|
12
|
+
{ id: 2, name: 'blocked', label: 'Blocked' },
|
|
13
|
+
{ id: 3, name: 'untested', label: 'Untested' },
|
|
14
|
+
{ id: 4, name: 'retest', label: 'Retest' },
|
|
15
|
+
{ id: 5, name: 'failed', label: 'Failed' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
getStatusesMock = jest.fn<() => Promise<Status[]>>().mockResolvedValue(mockStatuses);
|
|
20
|
+
|
|
21
|
+
mockClient = {
|
|
22
|
+
getStatuses: getStatusesMock
|
|
23
|
+
} as unknown as jest.Mocked<TestRailClient>;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('exports correct tool definition', () => {
|
|
27
|
+
expect(getStatusesTool.name).toBe('get_statuses');
|
|
28
|
+
expect(getStatusesTool.description).toContain('statuses');
|
|
29
|
+
expect(getStatusesTool.parameters).toBeDefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('handler fetches and returns statuses', async () => {
|
|
33
|
+
const result = await getStatusesTool.handler({}, mockClient);
|
|
34
|
+
|
|
35
|
+
expect(result).toBeDefined();
|
|
36
|
+
expect(result.statuses).toBeDefined();
|
|
37
|
+
expect(result.statuses).toHaveLength(5);
|
|
38
|
+
expect(mockClient.getStatuses).toHaveBeenCalled();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('returns correct status structure', async () => {
|
|
42
|
+
const result = await getStatusesTool.handler({}, mockClient);
|
|
43
|
+
|
|
44
|
+
expect(result.statuses[0]).toEqual({
|
|
45
|
+
id: 1,
|
|
46
|
+
name: 'passed',
|
|
47
|
+
label: 'Passed'
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('handler returns error on failure', async () => {
|
|
52
|
+
getStatusesMock.mockRejectedValue(new Error('API Error'));
|
|
53
|
+
|
|
54
|
+
await expect(getStatusesTool.handler({}, mockClient)).rejects.toThrow('API Error');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import { getTestsTool } from '../../src/tools/get_tests.js';
|
|
3
|
+
import { TestRailClient } from '../../src/client/testrail.js';
|
|
4
|
+
import { Test } from '../../src/types/testrail.js';
|
|
5
|
+
|
|
6
|
+
describe('get_tests tool', () => {
|
|
7
|
+
let mockClient: jest.Mocked<TestRailClient>;
|
|
8
|
+
let getTestsMock: jest.Mock<(runId: number, statusId?: number[]) => Promise<Test[]>>;
|
|
9
|
+
|
|
10
|
+
const mockTests: Test[] = [
|
|
11
|
+
{ id: 1, case_id: 101, status_id: 1, title: 'Test 1', run_id: 1 },
|
|
12
|
+
{ id: 2, case_id: 102, status_id: 2, title: 'Test 2', run_id: 1 },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
getTestsMock = jest.fn<(runId: number, statusId?: number[]) => Promise<Test[]>>().mockResolvedValue(mockTests);
|
|
17
|
+
|
|
18
|
+
mockClient = {
|
|
19
|
+
getTests: getTestsMock
|
|
20
|
+
} as unknown as jest.Mocked<TestRailClient>;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('exports correct tool definition', () => {
|
|
24
|
+
expect(getTestsTool.name).toBe('get_tests');
|
|
25
|
+
expect(getTestsTool.description).toBeDefined();
|
|
26
|
+
expect(getTestsTool.parameters).toBeDefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('handler fetches and returns tests', async () => {
|
|
30
|
+
const result = await getTestsTool.handler({ run_id: 1 }, mockClient);
|
|
31
|
+
|
|
32
|
+
expect(result).toBeDefined();
|
|
33
|
+
expect(result.tests).toBeDefined();
|
|
34
|
+
expect(result.tests).toHaveLength(2);
|
|
35
|
+
expect(mockClient.getTests).toHaveBeenCalledWith(1, undefined);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('handler passes status_id filter', async () => {
|
|
39
|
+
const result = await getTestsTool.handler({ run_id: 1, status_id: [1, 5] }, mockClient);
|
|
40
|
+
expect(mockClient.getTests).toHaveBeenCalledWith(1, [1, 5]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('handler returns error on failure', async () => {
|
|
44
|
+
getTestsMock.mockRejectedValue(new Error('API Error'));
|
|
45
|
+
await expect(getTestsTool.handler({ run_id: 1 }, mockClient)).rejects.toThrow('API Error');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -33,8 +33,12 @@ describe('update_cases tool', () => {
|
|
|
33
33
|
.mockResolvedValue(mockUpdatedCases);
|
|
34
34
|
|
|
35
35
|
mockClient = {
|
|
36
|
-
updateCases: updateCasesMock
|
|
36
|
+
updateCases: updateCasesMock,
|
|
37
|
+
getCase: jest.fn(),
|
|
37
38
|
} as unknown as jest.Mocked<TestRailClient>;
|
|
39
|
+
|
|
40
|
+
// Default mock for getCase to avoid failures in unrelated tests
|
|
41
|
+
(mockClient.getCase as jest.Mock<any>).mockResolvedValue({ suite_id: 1 });
|
|
38
42
|
});
|
|
39
43
|
|
|
40
44
|
test('exports correct tool definition', () => {
|
|
@@ -56,6 +60,30 @@ describe('update_cases tool', () => {
|
|
|
56
60
|
expect(result.updated_count).toBe(3);
|
|
57
61
|
expect(result.case_ids).toEqual([1, 2, 3]);
|
|
58
62
|
expect(result.message).toContain('3 test cases');
|
|
59
|
-
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('passes case_ids and fields correctly', async () => {
|
|
66
|
+
const fields = { priority_id: 2, type_id: 1 };
|
|
67
|
+
const mockCase = { suite_id: 5 } as Case;
|
|
68
|
+
(mockClient.getCase as jest.Mock<any>).mockResolvedValue(mockCase);
|
|
69
|
+
|
|
70
|
+
await updateCasesTool.handler(
|
|
71
|
+
{ case_ids: [10, 20, 30], fields },
|
|
72
|
+
mockClient
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(mockClient.getCase).toHaveBeenCalledWith('10');
|
|
76
|
+
expect(mockClient.updateCases).toHaveBeenCalledWith(5, [10, 20, 30], fields);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('handler throws error on failure', async () => {
|
|
80
|
+
updateCasesMock.mockRejectedValue(new Error('API Error'));
|
|
81
|
+
|
|
82
|
+
await expect(
|
|
83
|
+
updateCasesTool.handler(
|
|
84
|
+
{ case_ids: [1, 2], fields: { priority_id: 2 } },
|
|
85
|
+
mockClient
|
|
86
|
+
)
|
|
87
|
+
).rejects.toThrow('API Error');
|
|
60
88
|
});
|
|
61
89
|
});
|