@uarlouski/testrail-mcp-server 1.3.0 → 1.4.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/README.md +178 -48
- package/dist/client/testrail.js +184 -0
- package/{src/index.ts → dist/index.js} +25 -35
- package/{src/tools/add_attachment_to_run.ts → dist/tools/add_attachment_to_run.js} +10 -20
- package/{src/tools/add_results.ts → dist/tools/add_results.js} +3 -7
- package/{src/tools/add_run.ts → dist/tools/add_run.js} +5 -10
- package/{src/tools/create_case.ts → dist/tools/create_case.js} +2 -8
- package/{src/tools/get_case.ts → dist/tools/get_case.js} +7 -14
- package/{src/tools/get_case_fields.ts → dist/tools/get_case_fields.js} +8 -37
- package/{src/tools/get_cases.ts → dist/tools/get_cases.js} +18 -22
- package/{src/tools/get_projects.ts → dist/tools/get_projects.js} +1 -6
- package/{src/tools/get_sections.ts → dist/tools/get_sections.js} +2 -7
- package/{src/tools/get_statuses.ts → dist/tools/get_statuses.js} +2 -7
- package/{src/tools/get_templates.ts → dist/tools/get_templates.js} +2 -7
- package/{src/tools/get_tests.ts → dist/tools/get_tests.js} +5 -10
- package/{src/tools/update_case.ts → dist/tools/update_case.js} +3 -8
- package/{src/tools/update_cases.ts → dist/tools/update_cases.js} +2 -7
- package/dist/types/custom.js +1 -0
- package/{src/types/testrail.ts → dist/types/testrail.js} +6 -43
- package/{src/utils/mapper.ts → dist/utils/mapper.js} +16 -36
- package/{src/utils/sanitizer.ts → dist/utils/sanitizer.js} +4 -8
- package/package.json +8 -2
- package/.env.example +0 -3
- package/.github/workflows/ci.yml +0 -46
- package/.github/workflows/publish.yml +0 -35
- package/CHANGELOG.md +0 -38
- package/jest.config.js +0 -24
- package/src/client/testrail.ts +0 -227
- package/src/types/custom.ts +0 -19
- package/test/client/testrail.test.ts +0 -545
- package/test/tools/add_results.test.ts +0 -94
- package/test/tools/add_run.test.ts +0 -83
- package/test/tools/create_case.test.ts +0 -101
- package/test/tools/get_case.test.ts +0 -185
- package/test/tools/get_case_fields.test.ts +0 -132
- package/test/tools/get_cases.test.ts +0 -104
- package/test/tools/get_projects.test.ts +0 -38
- package/test/tools/get_sections.test.ts +0 -39
- package/test/tools/get_statuses.test.ts +0 -56
- package/test/tools/get_templates.test.ts +0 -38
- package/test/tools/get_tests.test.ts +0 -47
- package/test/tools/update_case.test.ts +0 -102
- package/test/tools/update_cases.test.ts +0 -89
- package/test/utils/mapper.test.ts +0 -112
- package/test/utils/sanitizer.test.ts +0 -143
- package/tsconfig.json +0 -24
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { TestRailClient } from "../client/testrail.js";
|
|
3
|
-
import { ToolDefinition } from "../types/custom.js";
|
|
4
|
-
|
|
5
2
|
const parameters = {
|
|
6
|
-
section_id: z.
|
|
3
|
+
section_id: z.number().describe("The ID of the section where the case should be created. Use get_sections to find available sections"),
|
|
7
4
|
title: z.string().describe("The title of the test case"),
|
|
8
5
|
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
6
|
};
|
|
10
|
-
|
|
11
|
-
export const createCaseTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
7
|
+
export const createCaseTool = {
|
|
12
8
|
name: "create_case",
|
|
13
9
|
description: "Create a new test case in TestRail",
|
|
14
10
|
parameters,
|
|
@@ -17,9 +13,7 @@ export const createCaseTool: ToolDefinition<typeof parameters, TestRailClient> =
|
|
|
17
13
|
title,
|
|
18
14
|
...fields,
|
|
19
15
|
};
|
|
20
|
-
|
|
21
16
|
const createdCase = await client.createCase(section_id, caseData);
|
|
22
|
-
|
|
23
17
|
return {
|
|
24
18
|
success: true,
|
|
25
19
|
case_id: createdCase.id,
|
|
@@ -1,44 +1,37 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { processCustomFields } from "../utils/mapper.js";
|
|
3
|
-
import {
|
|
4
|
-
import { TestCaseResponse, ToolDefinition } from "../types/custom.js";
|
|
5
|
-
|
|
3
|
+
import { LabelSchema } from "../types/testrail.js";
|
|
6
4
|
const parameters = {
|
|
7
5
|
case_id: z.string().describe("The ID of the test case (e.g. '123' or 'C123')"),
|
|
8
6
|
};
|
|
9
|
-
|
|
10
|
-
export const getCaseTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
7
|
+
export const getCaseTool = {
|
|
11
8
|
name: "get_case",
|
|
12
9
|
description: "Get detailed information about a test case including its custom fields",
|
|
13
10
|
parameters,
|
|
14
11
|
handler: async ({ case_id }, client) => {
|
|
15
|
-
const
|
|
12
|
+
const idString = case_id.toUpperCase().startsWith("C") ? case_id.substring(1) : case_id;
|
|
13
|
+
const id = Number(idString);
|
|
16
14
|
const testCase = await client.getCase(id);
|
|
17
|
-
|
|
18
15
|
const [section, caseTypes, priorities, caseFields] = await Promise.all([
|
|
19
|
-
client.getSection(testCase.section_id
|
|
16
|
+
client.getSection(Number(testCase.section_id)),
|
|
20
17
|
client.getCaseTypes(),
|
|
21
18
|
client.getPriorities(),
|
|
22
19
|
client.getCaseFields()
|
|
23
20
|
]);
|
|
24
|
-
|
|
25
21
|
const caseType = caseTypes.find(t => t.id === testCase.type_id);
|
|
26
22
|
const priority = priorities.find(p => p.id === testCase.priority_id);
|
|
27
|
-
|
|
28
23
|
const customFields = processCustomFields(testCase, caseFields);
|
|
29
|
-
|
|
30
|
-
const response: TestCaseResponse = {
|
|
24
|
+
const response = {
|
|
31
25
|
id: testCase.id,
|
|
32
26
|
title: testCase.title,
|
|
33
27
|
section: section.name,
|
|
34
28
|
type: caseType ? caseType.name : "Unknown",
|
|
35
29
|
priority: priority ? priority.name : "Unknown",
|
|
36
|
-
labels: testCase.labels || [],
|
|
30
|
+
labels: (testCase.labels || []).map((label) => LabelSchema.parse(label)),
|
|
37
31
|
references: testCase.refs,
|
|
38
32
|
updated_on: testCase.updated_on,
|
|
39
33
|
...customFields
|
|
40
34
|
};
|
|
41
|
-
|
|
42
35
|
return response;
|
|
43
36
|
}
|
|
44
37
|
};
|
|
@@ -1,24 +1,6 @@
|
|
|
1
|
-
import { TestRailClient } from "../client/testrail.js";
|
|
2
|
-
import { ToolDefinition } from "../types/custom.js";
|
|
3
|
-
import { CaseField } from "../types/testrail.js";
|
|
4
1
|
import { parseDropdownOptions } from "../utils/mapper.js";
|
|
5
|
-
|
|
6
2
|
const parameters = {};
|
|
7
|
-
|
|
8
|
-
interface FieldSchema {
|
|
9
|
-
system_name: string;
|
|
10
|
-
label: string;
|
|
11
|
-
type: string;
|
|
12
|
-
is_required: boolean;
|
|
13
|
-
template_ids?: number[];
|
|
14
|
-
options?: string[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface CaseFieldsResponse {
|
|
18
|
-
fields: FieldSchema[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const FIELD_TYPE_NAMES: Record<number, string> = {
|
|
3
|
+
const FIELD_TYPE_NAMES = {
|
|
22
4
|
1: "String",
|
|
23
5
|
2: "Integer",
|
|
24
6
|
3: "Text",
|
|
@@ -33,35 +15,28 @@ const FIELD_TYPE_NAMES: Record<number, string> = {
|
|
|
33
15
|
12: "Multi-select",
|
|
34
16
|
13: "Scenarios",
|
|
35
17
|
};
|
|
36
|
-
|
|
37
|
-
function getFieldTypeName(typeId: number): string {
|
|
18
|
+
function getFieldTypeName(typeId) {
|
|
38
19
|
return FIELD_TYPE_NAMES[typeId] || `Unknown (${typeId})`;
|
|
39
20
|
}
|
|
40
|
-
|
|
41
|
-
function isFieldRequired(field: CaseField): boolean {
|
|
21
|
+
function isFieldRequired(field) {
|
|
42
22
|
return field.configs.some(config => config.options?.is_required === true);
|
|
43
23
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const schema: FieldSchema = {
|
|
24
|
+
function mapToFieldSchema(field) {
|
|
25
|
+
const schema = {
|
|
47
26
|
system_name: field.system_name,
|
|
48
27
|
label: field.label,
|
|
49
28
|
type: getFieldTypeName(field.type_id),
|
|
50
29
|
is_required: isFieldRequired(field),
|
|
51
30
|
};
|
|
52
|
-
|
|
53
31
|
if (!field.include_all && field.template_ids.length > 0) {
|
|
54
32
|
schema.template_ids = field.template_ids;
|
|
55
33
|
}
|
|
56
|
-
|
|
57
34
|
if (field.type_id === 6 || field.type_id === 12) {
|
|
58
35
|
schema.options = Array.from(parseDropdownOptions(field).values());
|
|
59
36
|
}
|
|
60
|
-
|
|
61
37
|
return schema;
|
|
62
38
|
}
|
|
63
|
-
|
|
64
|
-
const SYSTEM_FIELDS: FieldSchema[] = [
|
|
39
|
+
const SYSTEM_FIELDS = [
|
|
65
40
|
{ system_name: "title", label: "Title", type: "String", is_required: true },
|
|
66
41
|
{ system_name: "section_id", label: "Section", type: "Integer", is_required: true },
|
|
67
42
|
{ system_name: "template_id", label: "Template", type: "Integer", is_required: false },
|
|
@@ -71,20 +46,16 @@ const SYSTEM_FIELDS: FieldSchema[] = [
|
|
|
71
46
|
{ system_name: "milestone_id", label: "Milestone", type: "Integer", is_required: false },
|
|
72
47
|
{ system_name: "refs", label: "References", type: "String", is_required: false },
|
|
73
48
|
];
|
|
74
|
-
|
|
75
|
-
export const getCaseFieldsTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
49
|
+
export const getCaseFieldsTool = {
|
|
76
50
|
name: "get_case_fields",
|
|
77
51
|
description: "Get the field schema for test cases. Returns all available fields with their types, descriptions, and options (for dropdown fields). Use this to understand what fields can be set when creating or updating test cases.",
|
|
78
52
|
parameters,
|
|
79
53
|
handler: async (_args, client) => {
|
|
80
54
|
const caseFields = await client.getCaseFields();
|
|
81
|
-
|
|
82
55
|
const customFieldSchemas = caseFields.filter(field => field.is_active).map(mapToFieldSchema);
|
|
83
|
-
|
|
84
|
-
const response: CaseFieldsResponse = {
|
|
56
|
+
const response = {
|
|
85
57
|
fields: [...SYSTEM_FIELDS, ...customFieldSchemas],
|
|
86
58
|
};
|
|
87
|
-
|
|
88
59
|
return response;
|
|
89
60
|
}
|
|
90
61
|
};
|
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { TestRailClient } from "../client/testrail.js";
|
|
3
|
-
import { ToolDefinition } from "../types/custom.js";
|
|
4
|
-
import { Case } from "../types/testrail.js";
|
|
5
|
-
|
|
6
2
|
const parameters = {
|
|
7
|
-
project_id: z.
|
|
8
|
-
|
|
3
|
+
project_id: z.number().describe("The ID of the project. Use get_projects to find available projects"),
|
|
4
|
+
section: z.object({
|
|
5
|
+
id: z.number(),
|
|
6
|
+
recursive: z.boolean().optional().default(false).describe("If true, fetches cases from the section and all its child sections"),
|
|
7
|
+
excludes: z.array(z.string()).optional().describe("List of section names to exclude from the recursive search"),
|
|
8
|
+
}).optional().describe("Section filter configuration. Use get_sections to find available sections"),
|
|
9
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
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
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
|
-
|
|
14
|
-
interface CasesResponse {
|
|
15
|
-
cases: Record<string, any>[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const getCasesTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
13
|
+
export const getCasesTool = {
|
|
19
14
|
name: "get_cases",
|
|
20
15
|
description: "Get all test cases for a project. Filter by section, API params (priority, type), or any field including custom fields via 'where'. Returns case IDs, titles, and any additional requested fields.",
|
|
21
16
|
parameters,
|
|
22
|
-
handler: async ({ project_id,
|
|
23
|
-
let cases
|
|
24
|
-
|
|
17
|
+
handler: async ({ project_id, section, filter, where, fields }, client) => {
|
|
18
|
+
let cases;
|
|
19
|
+
if (section?.recursive) {
|
|
20
|
+
cases = await client.getCasesRecursively(project_id, section.id, filter, section.excludes);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
cases = await client.getCases(project_id, section?.id, filter);
|
|
24
|
+
}
|
|
25
25
|
// Client-side filtering for custom fields and other unsupported API filters
|
|
26
26
|
if (where) {
|
|
27
27
|
cases = cases.filter(c => {
|
|
28
28
|
for (const [key, value] of Object.entries(where)) {
|
|
29
|
-
const caseValue =
|
|
29
|
+
const caseValue = c[key];
|
|
30
30
|
if (caseValue !== value) {
|
|
31
31
|
return false;
|
|
32
32
|
}
|
|
@@ -34,15 +34,13 @@ export const getCasesTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
|
34
34
|
return true;
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
|
-
|
|
38
|
-
const response: CasesResponse = {
|
|
37
|
+
const response = {
|
|
39
38
|
cases: cases.map(c => {
|
|
40
|
-
const result
|
|
39
|
+
const result = {
|
|
41
40
|
id: c.id,
|
|
42
41
|
title: c.title,
|
|
43
42
|
suite_id: c.suite_id,
|
|
44
43
|
};
|
|
45
|
-
|
|
46
44
|
if (fields) {
|
|
47
45
|
for (const field of fields) {
|
|
48
46
|
if (field in c) {
|
|
@@ -50,11 +48,9 @@ export const getCasesTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
|
50
48
|
}
|
|
51
49
|
}
|
|
52
50
|
}
|
|
53
|
-
|
|
54
51
|
return result;
|
|
55
52
|
}),
|
|
56
53
|
};
|
|
57
|
-
|
|
58
54
|
return response;
|
|
59
55
|
}
|
|
60
56
|
};
|
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
import { TestRailClient } from "../client/testrail.js";
|
|
2
|
-
import { ToolDefinition } from "../types/custom.js";
|
|
3
1
|
import { ProjectSchema } from "../types/testrail.js";
|
|
4
|
-
|
|
5
2
|
const parameters = {};
|
|
6
|
-
|
|
7
|
-
export const getProjectsTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
3
|
+
export const getProjectsTool = {
|
|
8
4
|
name: "get_projects",
|
|
9
5
|
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
6
|
parameters,
|
|
11
7
|
handler: async (_args, client) => {
|
|
12
8
|
const projects = await client.getProjects();
|
|
13
|
-
|
|
14
9
|
return {
|
|
15
10
|
projects: projects
|
|
16
11
|
.filter(p => !p.is_completed)
|
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { TestRailClient } from "../client/testrail.js";
|
|
3
|
-
import { ToolDefinition } from "../types/custom.js";
|
|
4
2
|
import { SectionSchema } from "../types/testrail.js";
|
|
5
|
-
|
|
6
3
|
const parameters = {
|
|
7
|
-
project_id: z.
|
|
4
|
+
project_id: z.number().describe("The ID of the project. Use get_projects to find available projects"),
|
|
8
5
|
};
|
|
9
|
-
|
|
10
|
-
export const getSectionsTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
6
|
+
export const getSectionsTool = {
|
|
11
7
|
name: "get_sections",
|
|
12
8
|
description: "Get all sections for a project. Returns section IDs and names that can be used with create_case",
|
|
13
9
|
parameters,
|
|
14
10
|
handler: async ({ project_id }, client) => {
|
|
15
11
|
const sections = await client.getSections(project_id);
|
|
16
|
-
|
|
17
12
|
return {
|
|
18
13
|
sections: sections.map(s => SectionSchema.parse(s)),
|
|
19
14
|
};
|
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import { TestRailClient } from "../client/testrail.js";
|
|
2
|
-
import { z } from "zod";
|
|
3
1
|
import { StatusSchema } from "../types/testrail.js";
|
|
4
|
-
import { ToolDefinition } from "../types/custom.js";
|
|
5
|
-
|
|
6
2
|
const parameters = {};
|
|
7
|
-
|
|
8
|
-
export const getStatusesTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
3
|
+
export const getStatusesTool = {
|
|
9
4
|
name: "get_statuses",
|
|
10
5
|
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
6
|
parameters,
|
|
12
|
-
handler: async (args
|
|
7
|
+
handler: async (args, client) => {
|
|
13
8
|
const statuses = await client.getStatuses();
|
|
14
9
|
return {
|
|
15
10
|
statuses: statuses.map(s => StatusSchema.parse(s)),
|
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { TestRailClient } from "../client/testrail.js";
|
|
3
|
-
import { ToolDefinition } from "../types/custom.js";
|
|
4
2
|
import { TemplateSchema } from "../types/testrail.js";
|
|
5
|
-
|
|
6
3
|
const parameters = {
|
|
7
|
-
project_id: z.
|
|
4
|
+
project_id: z.number().describe("The ID of the project. Use get_projects to find available projects"),
|
|
8
5
|
};
|
|
9
|
-
|
|
10
|
-
export const getTemplatesTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
6
|
+
export const getTemplatesTool = {
|
|
11
7
|
name: "get_templates",
|
|
12
8
|
description: "Get available test case templates for a project. Template IDs determine which fields are available when creating or updating test cases",
|
|
13
9
|
parameters,
|
|
14
10
|
handler: async ({ project_id }, client) => {
|
|
15
11
|
const templates = await client.getTemplates(project_id);
|
|
16
|
-
|
|
17
12
|
return {
|
|
18
13
|
templates: templates.map(t => TemplateSchema.parse(t)),
|
|
19
14
|
};
|
|
@@ -1,20 +1,15 @@
|
|
|
1
|
-
import { TestRailClient } from "../client/testrail.js";
|
|
2
1
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
4
|
-
import { ToolDefinition } from "../types/custom.js";
|
|
5
|
-
|
|
2
|
+
import { TestSchema } from "../types/testrail.js";
|
|
6
3
|
const parameters = {
|
|
7
4
|
run_id: z.number().describe("The ID of the test run"),
|
|
8
5
|
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> = {
|
|
6
|
+
};
|
|
7
|
+
export const getTestsTool = {
|
|
12
8
|
name: "get_tests",
|
|
13
9
|
description: "Get tests for a test run, optionally filtered by status",
|
|
14
10
|
parameters,
|
|
15
|
-
handler: async ({ run_id, status_id }, client
|
|
16
|
-
const tests
|
|
17
|
-
|
|
11
|
+
handler: async ({ run_id, status_id }, client) => {
|
|
12
|
+
const tests = await client.getTests(run_id, status_id);
|
|
18
13
|
return {
|
|
19
14
|
tests: tests.map(test => TestSchema.parse(test)),
|
|
20
15
|
};
|
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { TestRailClient } from "../client/testrail.js";
|
|
3
|
-
import { ToolDefinition } from "../types/custom.js";
|
|
4
|
-
|
|
5
2
|
const parameters = {
|
|
6
3
|
case_id: z.string().describe("The ID of the test case to update (e.g. '123' or 'C123')"),
|
|
7
4
|
fields: z.record(z.string(), z.any()).describe("Object containing fields to update. Use get_case_fields to see available fields. Example: {\"title\": \"New title\", \"priority_id\": 2, \"custom_automation_priority\": 1}"),
|
|
8
5
|
};
|
|
9
|
-
|
|
10
|
-
export const updateCaseTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
6
|
+
export const updateCaseTool = {
|
|
11
7
|
name: "update_case",
|
|
12
8
|
description: "Update a test case in TestRail. Supports partial updates - only specify the fields you want to change",
|
|
13
9
|
parameters,
|
|
14
10
|
handler: async ({ case_id, fields }, client) => {
|
|
15
|
-
const
|
|
16
|
-
|
|
11
|
+
const idString = case_id.toUpperCase().startsWith("C") ? case_id.substring(1) : case_id;
|
|
12
|
+
const id = Number(idString);
|
|
17
13
|
const updatedCase = await client.updateCase(id, fields);
|
|
18
|
-
|
|
19
14
|
return {
|
|
20
15
|
success: true,
|
|
21
16
|
case_id: updatedCase.id,
|
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { TestRailClient } from "../client/testrail.js";
|
|
3
|
-
import { ToolDefinition } from "../types/custom.js";
|
|
4
|
-
|
|
5
2
|
const parameters = {
|
|
6
3
|
case_ids: z.array(z.number()).describe("Array of case IDs to update (e.g. [123, 456, 789])"),
|
|
7
4
|
fields: z.record(z.string(), z.any()).describe("Object containing fields to update for ALL specified cases. Use get_case_fields to see available fields. Example: {\"priority_id\": 2, \"type_id\": 1}"),
|
|
8
5
|
};
|
|
9
|
-
|
|
10
|
-
export const updateCasesTool: ToolDefinition<typeof parameters, TestRailClient> = {
|
|
6
|
+
export const updateCasesTool = {
|
|
11
7
|
name: "update_cases",
|
|
12
8
|
description: "Bulk update multiple test cases with the same field values. More efficient than calling update_case multiple times",
|
|
13
9
|
parameters,
|
|
14
10
|
handler: async ({ case_ids, fields }, client) => {
|
|
15
|
-
const caseData = await client.getCase(
|
|
11
|
+
const caseData = await client.getCase(case_ids[0]);
|
|
16
12
|
const updatedCases = await client.updateCases(caseData.suite_id, case_ids, fields);
|
|
17
|
-
|
|
18
13
|
return {
|
|
19
14
|
success: true,
|
|
20
15
|
updated_count: updatedCases.length,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
|
|
3
2
|
export const CaseFieldConfigSchema = z.object({
|
|
4
3
|
options: z.object({
|
|
5
4
|
default_value: z.string().optional(),
|
|
@@ -7,9 +6,6 @@ export const CaseFieldConfigSchema = z.object({
|
|
|
7
6
|
items: z.string().optional(),
|
|
8
7
|
}),
|
|
9
8
|
});
|
|
10
|
-
|
|
11
|
-
export type CaseFieldConfig = z.infer<typeof CaseFieldConfigSchema>;
|
|
12
|
-
|
|
13
9
|
export const CaseFieldSchema = z.object({
|
|
14
10
|
id: z.number(),
|
|
15
11
|
name: z.string(),
|
|
@@ -22,9 +18,9 @@ export const CaseFieldSchema = z.object({
|
|
|
22
18
|
include_all: z.boolean(),
|
|
23
19
|
configs: z.array(CaseFieldConfigSchema),
|
|
24
20
|
});
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
export const LabelSchema = z.object({
|
|
22
|
+
title: z.string(),
|
|
23
|
+
});
|
|
28
24
|
export const CaseSchema = z.object({
|
|
29
25
|
id: z.number(),
|
|
30
26
|
title: z.string(),
|
|
@@ -38,11 +34,8 @@ export const CaseSchema = z.object({
|
|
|
38
34
|
updated_on: z.number(),
|
|
39
35
|
estimate: z.string().nullable(),
|
|
40
36
|
suite_id: z.number(),
|
|
41
|
-
labels: z.array(
|
|
37
|
+
labels: z.array(LabelSchema),
|
|
42
38
|
}).loose();
|
|
43
|
-
|
|
44
|
-
export type Case = z.infer<typeof CaseSchema>;
|
|
45
|
-
|
|
46
39
|
export const SectionSchema = z.object({
|
|
47
40
|
id: z.number(),
|
|
48
41
|
name: z.string(),
|
|
@@ -50,42 +43,27 @@ export const SectionSchema = z.object({
|
|
|
50
43
|
parent_id: z.number().nullish(),
|
|
51
44
|
suite_id: z.number(),
|
|
52
45
|
});
|
|
53
|
-
|
|
54
|
-
export type Section = z.infer<typeof SectionSchema>;
|
|
55
|
-
|
|
56
46
|
export const PrioritySchema = z.object({
|
|
57
47
|
id: z.number(),
|
|
58
48
|
is_default: z.boolean(),
|
|
59
49
|
name: z.string(),
|
|
60
50
|
});
|
|
61
|
-
|
|
62
|
-
export type Priority = z.infer<typeof PrioritySchema>;
|
|
63
|
-
|
|
64
51
|
export const CaseTypeSchema = z.object({
|
|
65
52
|
id: z.number(),
|
|
66
53
|
is_default: z.boolean(),
|
|
67
54
|
name: z.string(),
|
|
68
55
|
});
|
|
69
|
-
|
|
70
|
-
export type CaseType = z.infer<typeof CaseTypeSchema>;
|
|
71
|
-
|
|
72
56
|
export const TemplateSchema = z.object({
|
|
73
57
|
id: z.number(),
|
|
74
58
|
name: z.string(),
|
|
75
59
|
is_default: z.boolean(),
|
|
76
60
|
});
|
|
77
|
-
|
|
78
|
-
export type Template = z.infer<typeof TemplateSchema>;
|
|
79
|
-
|
|
80
61
|
export const ProjectSchema = z.object({
|
|
81
62
|
id: z.number(),
|
|
82
63
|
name: z.string(),
|
|
83
64
|
is_completed: z.boolean(),
|
|
84
65
|
suite_mode: z.number(),
|
|
85
66
|
});
|
|
86
|
-
|
|
87
|
-
export type Project = z.infer<typeof ProjectSchema>;
|
|
88
|
-
|
|
89
67
|
export const RunSchema = z.object({
|
|
90
68
|
id: z.number(),
|
|
91
69
|
name: z.string(),
|
|
@@ -100,29 +78,19 @@ export const RunSchema = z.object({
|
|
|
100
78
|
failed_count: z.number(),
|
|
101
79
|
url: z.string(),
|
|
102
80
|
});
|
|
103
|
-
|
|
104
|
-
export type Run = z.infer<typeof RunSchema>;
|
|
105
|
-
|
|
106
81
|
export const StatusSchema = z.object({
|
|
107
82
|
id: z.number(),
|
|
108
83
|
name: z.string(),
|
|
109
84
|
label: z.string(),
|
|
110
85
|
});
|
|
111
|
-
|
|
112
|
-
export type Status = z.infer<typeof StatusSchema>;
|
|
113
|
-
|
|
114
86
|
export const TestSchema = z.object({
|
|
115
87
|
id: z.number(),
|
|
116
88
|
case_id: z.number(),
|
|
117
89
|
status_id: z.number(),
|
|
118
90
|
title: z.string(),
|
|
119
91
|
run_id: z.number(),
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
export const TestsSchema = z.array(TestSchema)
|
|
123
|
-
|
|
124
|
-
export type Test = z.infer<typeof TestSchema>
|
|
125
|
-
|
|
92
|
+
});
|
|
93
|
+
export const TestsSchema = z.array(TestSchema);
|
|
126
94
|
export const ResultSchema = z.object({
|
|
127
95
|
id: z.number(),
|
|
128
96
|
test_id: z.number(),
|
|
@@ -130,11 +98,6 @@ export const ResultSchema = z.object({
|
|
|
130
98
|
comment: z.string().nullable(),
|
|
131
99
|
defects: z.string().nullable(),
|
|
132
100
|
});
|
|
133
|
-
|
|
134
|
-
export type Result = z.infer<typeof ResultSchema>
|
|
135
|
-
|
|
136
101
|
export const AttachmentSchema = z.object({
|
|
137
102
|
attachment_id: z.number(),
|
|
138
103
|
});
|
|
139
|
-
|
|
140
|
-
export type Attachment = z.infer<typeof AttachmentSchema>
|
|
@@ -1,73 +1,55 @@
|
|
|
1
|
-
import { Case, CaseField } from "../types/testrail.js";
|
|
2
1
|
import { sanitizeValue } from "./sanitizer.js";
|
|
3
|
-
|
|
4
2
|
/**
|
|
5
3
|
* Processes custom fields from a test case, converting system names to human-readable labels
|
|
6
4
|
* and mapping dropdown values to their display text.
|
|
7
5
|
*/
|
|
8
|
-
export function processCustomFields(
|
|
9
|
-
testCase: Case,
|
|
10
|
-
caseFields: CaseField[]
|
|
11
|
-
): Record<string, any> {
|
|
6
|
+
export function processCustomFields(testCase, caseFields) {
|
|
12
7
|
if (!testCase) {
|
|
13
8
|
throw new Error("Test case is undefined or null");
|
|
14
9
|
}
|
|
15
|
-
|
|
16
10
|
const templateId = testCase.template_id;
|
|
17
|
-
const result
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
const fieldNameMap = new Map<string, string>();
|
|
24
|
-
const dropdownOptionsMap = new Map<string, Map<string, string>>();
|
|
25
|
-
|
|
11
|
+
const result = {};
|
|
12
|
+
const applicableFields = caseFields.filter(field => field.include_all || field.template_ids.includes(templateId));
|
|
13
|
+
const fieldNameMap = new Map();
|
|
14
|
+
const dropdownOptionsMap = new Map();
|
|
26
15
|
for (const field of applicableFields) {
|
|
27
16
|
fieldNameMap.set(field.system_name, toSnakeCase(field.label));
|
|
28
|
-
|
|
29
17
|
const optionsMap = parseDropdownOptions(field);
|
|
30
18
|
if (optionsMap.size > 0) {
|
|
31
19
|
dropdownOptionsMap.set(field.system_name, optionsMap);
|
|
32
20
|
}
|
|
33
21
|
}
|
|
34
|
-
|
|
35
22
|
for (const [key, value] of Object.entries(testCase)) {
|
|
36
|
-
if (!key.startsWith("custom_"))
|
|
37
|
-
|
|
38
|
-
|
|
23
|
+
if (!key.startsWith("custom_"))
|
|
24
|
+
continue;
|
|
25
|
+
if (value === null || value === undefined)
|
|
26
|
+
continue;
|
|
39
27
|
if (!fieldNameMap.has(key)) {
|
|
40
28
|
console.error(`No field mapping found for: ${key}`);
|
|
41
29
|
result[key] = value;
|
|
42
30
|
continue;
|
|
43
31
|
}
|
|
44
|
-
|
|
45
|
-
const outputKey = fieldNameMap.get(key)!;
|
|
32
|
+
const outputKey = fieldNameMap.get(key);
|
|
46
33
|
let finalValue = value;
|
|
47
|
-
|
|
48
34
|
if (dropdownOptionsMap.has(key)) {
|
|
49
|
-
const options = dropdownOptionsMap.get(key)
|
|
35
|
+
const options = dropdownOptionsMap.get(key);
|
|
50
36
|
const optionKey = String(value);
|
|
51
37
|
finalValue = options.get(optionKey) || value;
|
|
52
38
|
}
|
|
53
|
-
|
|
54
39
|
result[outputKey] = sanitizeValue(finalValue);
|
|
55
40
|
}
|
|
56
|
-
|
|
57
41
|
return result;
|
|
58
42
|
}
|
|
59
|
-
|
|
60
43
|
/**
|
|
61
44
|
* Parse dropdown options from field configs.
|
|
62
45
|
* Format: "1, First\n2, Second\n3, Third"
|
|
63
46
|
*/
|
|
64
|
-
export function parseDropdownOptions(field
|
|
65
|
-
const options = new Map
|
|
66
|
-
|
|
47
|
+
export function parseDropdownOptions(field) {
|
|
48
|
+
const options = new Map();
|
|
67
49
|
for (const config of field.configs) {
|
|
68
50
|
const items = config.options?.items;
|
|
69
|
-
if (!items)
|
|
70
|
-
|
|
51
|
+
if (!items)
|
|
52
|
+
continue;
|
|
71
53
|
const lines = items.split('\n');
|
|
72
54
|
for (const line of lines) {
|
|
73
55
|
const commaIndex = line.indexOf(',');
|
|
@@ -78,11 +60,9 @@ export function parseDropdownOptions(field: CaseField): Map<string, string> {
|
|
|
78
60
|
}
|
|
79
61
|
}
|
|
80
62
|
}
|
|
81
|
-
|
|
82
63
|
return options;
|
|
83
64
|
}
|
|
84
|
-
|
|
85
|
-
function toSnakeCase(str: string): string {
|
|
65
|
+
function toSnakeCase(str) {
|
|
86
66
|
return str
|
|
87
67
|
.toLowerCase()
|
|
88
68
|
.replace(/[^a-z0-9]+/g, '_')
|