Skip to content

Commit 909c231

Browse files
Merge pull request #303 from SavioBS629/PMAA-126_priority
fix: PMAA-126 ticket
2 parents 06a3435 + 5174b51 commit 909c231

4 files changed

Lines changed: 217 additions & 34 deletions

File tree

src/tools/testmanagement-utils/TCG-utils/api.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,30 @@ export async function fetchFormFields(
3333
return res.data;
3434
}
3535

36+
/**
37+
* Resolve a default-field input (priority/case_type) to the form's display or
38+
* internal name, matching case-insensitively. Returns undefined if no match.
39+
*/
40+
export function normalizeDefaultFieldValue(
41+
fieldValues: Array<{
42+
internal_name?: string | null;
43+
name?: string;
44+
value: any;
45+
}>,
46+
input: string,
47+
emit: "name" | "internal_name",
48+
): string | undefined {
49+
const normalized = input.toLowerCase().trim();
50+
const match = fieldValues.find(
51+
(v) =>
52+
(v.internal_name ?? "").toLowerCase() === normalized ||
53+
(v.name ?? "").toLowerCase() === normalized,
54+
);
55+
if (!match) return undefined;
56+
if (emit === "name") return match.name;
57+
return match.internal_name ?? match.name;
58+
}
59+
3660
/**
3761
* Trigger AI-based test case generation for a document.
3862
*/

src/tools/testmanagement-utils/create-testcase.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@ import { apiClient } from "../../lib/apiClient.js";
22
import { z } from "zod";
33
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
44
import { formatAxiosError } from "../../lib/error.js";
5-
import { projectIdentifierToId } from "./TCG-utils/api.js";
5+
import {
6+
fetchFormFields,
7+
normalizeDefaultFieldValue,
8+
projectIdentifierToId,
9+
} from "./TCG-utils/api.js";
610
import { BrowserStackConfig } from "../../lib/types.js";
711
import { getTMBaseURL } from "../../lib/tm-base-url.js";
12+
import logger from "../../logger.js";
813

914
interface TestCaseStep {
1015
step: string;
@@ -29,6 +34,7 @@ export interface TestCaseCreateRequest {
2934
tags?: string[];
3035
custom_fields?: Record<string, string>;
3136
automation_status?: string;
37+
priority?: string;
3238
}
3339

3440
export interface TestCaseResponse {
@@ -125,6 +131,12 @@ export const CreateTestCaseSchema = z.object({
125131
.describe(
126132
"Automation status of the test case. Common values include 'not_automated', 'automated', 'automation_not_required'.",
127133
),
134+
priority: z
135+
.string()
136+
.optional()
137+
.describe(
138+
"Priority of the test case. Accepts either display name (e.g. 'Critical', 'High', 'Medium', 'Low') or internal name (e.g. 'medium'). If omitted, the project default (usually 'Medium') is applied. Valid values are per-project and discoverable via the form-fields endpoint.",
139+
),
128140
});
129141

130142
export function sanitizeArgs(args: any) {
@@ -149,11 +161,52 @@ export function sanitizeArgs(args: any) {
149161

150162
import { getBrowserStackAuth } from "../../lib/get-auth.js";
151163

164+
/**
165+
* Normalize priority to the display name the create endpoint accepts (it
166+
* rejects lowercase). On lookup failure, pass the raw value through.
167+
*/
168+
async function normalizePriority(
169+
projectIdentifier: string,
170+
priority: string,
171+
config: BrowserStackConfig,
172+
): Promise<string> {
173+
try {
174+
const numericProjectId = await projectIdentifierToId(
175+
projectIdentifier,
176+
config,
177+
);
178+
const { default_fields } = await fetchFormFields(numericProjectId, config);
179+
return (
180+
normalizeDefaultFieldValue(
181+
default_fields?.priority?.values ?? [],
182+
priority,
183+
"name",
184+
) ?? priority
185+
);
186+
} catch (err) {
187+
logger.warn(
188+
"Failed to normalize priority value; passing through as given: %s",
189+
err instanceof Error ? err.message : String(err),
190+
);
191+
return priority;
192+
}
193+
}
194+
152195
export async function createTestCase(
153196
params: TestCaseCreateRequest,
154197
config: BrowserStackConfig,
155198
): Promise<CallToolResult> {
156-
const body = { test_case: params };
199+
const testCaseParams: TestCaseCreateRequest = { ...params };
200+
201+
if (testCaseParams.priority !== undefined) {
202+
testCaseParams.priority = await normalizePriority(
203+
params.project_identifier,
204+
testCaseParams.priority,
205+
config,
206+
);
207+
}
208+
209+
const body = { test_case: testCaseParams };
157210
const authString = getBrowserStackAuth(config);
158211
const [username, password] = authString.split(":");
159212

src/tools/testmanagement-utils/update-testcase.ts

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import { apiClient } from "../../lib/apiClient.js";
22
import { z } from "zod";
33
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
44
import { formatAxiosError } from "../../lib/error.js";
5-
import { fetchFormFields, projectIdentifierToId } from "./TCG-utils/api.js";
5+
import {
6+
fetchFormFields,
7+
normalizeDefaultFieldValue,
8+
projectIdentifierToId,
9+
} from "./TCG-utils/api.js";
610
import { BrowserStackConfig } from "../../lib/types.js";
711
import { getTMBaseURL } from "../../lib/tm-base-url.js";
812
import { getBrowserStackAuth } from "../../lib/get-auth.js";
@@ -104,36 +108,6 @@ export const UpdateTestCaseSchema = z.object({
104108
),
105109
});
106110

107-
/**
108-
* Build a normalizer for a default field's accepted value.
109-
* The TM PATCH endpoint accepts different casings for different default
110-
* fields (Title-Case display name for priority/case_type, snake_case
111-
* internal_name for automation_status). We accept either from the caller
112-
* and emit the form the API actually wants.
113-
*
114-
* Returns undefined when no matching option is found — callers should
115-
* pass the raw value through so the backend can surface its own error.
116-
*/
117-
function normalizeDefaultFieldValue(
118-
fieldValues: Array<{
119-
internal_name?: string | null;
120-
name?: string;
121-
value: any;
122-
}>,
123-
input: string,
124-
emit: "name" | "internal_name",
125-
): string | undefined {
126-
const normalized = input.toLowerCase().trim();
127-
const match = fieldValues.find(
128-
(v) =>
129-
(v.internal_name ?? "").toLowerCase() === normalized ||
130-
(v.name ?? "").toLowerCase() === normalized,
131-
);
132-
if (!match) return undefined;
133-
if (emit === "name") return match.name;
134-
return match.internal_name ?? match.name;
135-
}
136-
137111
/**
138112
* Normalise default-field inputs (priority/case_type/automation_status) to
139113
* what the TM PATCH endpoint accepts. Fetches the project's form-fields

tests/tools/testmanagement.test.ts

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@ vi.mock('../../src/lib/get-auth', () => ({
9898
getBrowserStackAuth: vi.fn(() => 'fake-user:fake-key')
9999
}));
100100
vi.mock('../../src/tools/testmanagement-utils/TCG-utils/api', () => ({
101-
projectIdentifierToId: vi.fn(() => Promise.resolve('999'))
101+
projectIdentifierToId: vi.fn(() => Promise.resolve('999')),
102+
fetchFormFields: vi.fn(),
103+
normalizeDefaultFieldValue: vi.fn(),
102104
}));
103105
vi.mock('form-data', () => {
104106
return {
@@ -193,6 +195,9 @@ vi.mock('../../src/lib/apiClient', () => ({
193195
vi.mock('../../src/lib/tm-base-url', () => ({
194196
getTMBaseURL: vi.fn(async () => 'https://test-management.browserstack.com'),
195197
}));
198+
vi.mock('../../src/logger', () => ({
199+
default: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() },
200+
}));
196201

197202

198203
const mockedAxios = axios as Mocked<typeof axios>;
@@ -1031,3 +1036,130 @@ describe('getSubTestPlan util — fail-soft enrichment', () => {
10311036
expect(result.content?.[0]?.text).toContain('Failed to fetch sub-test-plan');
10321037
});
10331038
});
1039+
1040+
// Real function via importActual (the module is mocked at the top).
1041+
describe('normalizeDefaultFieldValue', () => {
1042+
let normalize: typeof import('../../src/tools/testmanagement-utils/TCG-utils/api').normalizeDefaultFieldValue;
1043+
1044+
beforeAll(async () => {
1045+
const actual = await vi.importActual<
1046+
typeof import('../../src/tools/testmanagement-utils/TCG-utils/api')
1047+
>('../../src/tools/testmanagement-utils/TCG-utils/api');
1048+
normalize = actual.normalizeDefaultFieldValue;
1049+
});
1050+
1051+
const values = [
1052+
{ name: 'Critical', internal_name: 'critical', value: 1 },
1053+
{ name: 'Medium', internal_name: 'medium', value: 2 },
1054+
];
1055+
1056+
it('resolves a lowercase internal name to the Title-Case display name', () => {
1057+
expect(normalize(values, 'critical', 'name')).toBe('Critical');
1058+
});
1059+
1060+
it('matches case-insensitively regardless of input casing', () => {
1061+
expect(normalize(values, 'CRITICAL', 'name')).toBe('Critical');
1062+
expect(normalize(values, ' Critical ', 'name')).toBe('Critical');
1063+
});
1064+
1065+
it('resolves a display name to the internal name when emit is internal_name', () => {
1066+
expect(normalize(values, 'Critical', 'internal_name')).toBe('critical');
1067+
});
1068+
1069+
it('returns undefined when no option matches (caller passes raw value through)', () => {
1070+
expect(normalize(values, 'urgent', 'name')).toBeUndefined();
1071+
expect(normalize([], 'critical', 'name')).toBeUndefined();
1072+
});
1073+
});
1074+
1075+
// Real createTestCase via importActual; collaborators are mocked at module scope.
1076+
describe('createTestCase — priority normalization', () => {
1077+
let createTestCaseReal: typeof import('../../src/tools/testmanagement-utils/create-testcase').createTestCase;
1078+
let apiClientMock: typeof import('../../src/lib/apiClient').apiClient;
1079+
let fetchFormFieldsMock: Mock;
1080+
let normalizeMock: Mock;
1081+
1082+
beforeAll(async () => {
1083+
const actual = await vi.importActual<
1084+
typeof import('../../src/tools/testmanagement-utils/create-testcase')
1085+
>('../../src/tools/testmanagement-utils/create-testcase');
1086+
createTestCaseReal = actual.createTestCase;
1087+
apiClientMock = (await import('../../src/lib/apiClient')).apiClient;
1088+
const api = await import('../../src/tools/testmanagement-utils/TCG-utils/api');
1089+
fetchFormFieldsMock = api.fetchFormFields as unknown as Mock;
1090+
normalizeMock = api.normalizeDefaultFieldValue as unknown as Mock;
1091+
});
1092+
1093+
beforeEach(() => {
1094+
vi.clearAllMocks();
1095+
});
1096+
1097+
// Priority options as returned by fetchFormFields.
1098+
const priorityValues = [
1099+
{ name: 'Critical', internal_name: 'critical', value: 1 },
1100+
{ name: 'Medium', internal_name: 'medium', value: 2 },
1101+
];
1102+
const formFields = {
1103+
default_fields: { priority: { values: priorityValues } },
1104+
custom_fields: {},
1105+
};
1106+
const createSuccess = {
1107+
data: {
1108+
data: {
1109+
success: true,
1110+
test_case: {
1111+
identifier: 'TC-1',
1112+
title: 'Sample',
1113+
priority: 'Critical',
1114+
folder_id: 1,
1115+
},
1116+
},
1117+
},
1118+
};
1119+
1120+
const baseArgs = {
1121+
project_identifier: 'PR-1',
1122+
folder_id: 'F-1',
1123+
name: 'Sample',
1124+
test_case_steps: [{ step: 'a', result: 'b' }],
1125+
};
1126+
1127+
it('looks up the project form fields and sends the normalized priority in the request body', async () => {
1128+
fetchFormFieldsMock.mockResolvedValue(formFields);
1129+
normalizeMock.mockReturnValue('Critical');
1130+
(apiClientMock.post as Mock).mockResolvedValueOnce(createSuccess);
1131+
1132+
const result = await createTestCaseReal(
1133+
{ ...baseArgs, priority: 'critical' },
1134+
mockConfig as any,
1135+
);
1136+
1137+
expect(result.isError).toBeFalsy();
1138+
expect(normalizeMock).toHaveBeenCalledWith(priorityValues, 'critical', 'name');
1139+
const body = (apiClientMock.post as Mock).mock.calls[0][0].body;
1140+
expect(body.test_case.priority).toBe('Critical');
1141+
});
1142+
1143+
it('omits priority from the request body when not provided (preserves project default)', async () => {
1144+
(apiClientMock.post as Mock).mockResolvedValueOnce(createSuccess);
1145+
1146+
await createTestCaseReal({ ...baseArgs }, mockConfig as any);
1147+
1148+
expect(fetchFormFieldsMock).not.toHaveBeenCalled();
1149+
const body = (apiClientMock.post as Mock).mock.calls[0][0].body;
1150+
expect(body.test_case).not.toHaveProperty('priority');
1151+
});
1152+
1153+
it('passes the raw priority through when the form-fields lookup fails (graceful fallback)', async () => {
1154+
fetchFormFieldsMock.mockRejectedValue(new Error('Network Error'));
1155+
(apiClientMock.post as Mock).mockResolvedValueOnce(createSuccess);
1156+
1157+
await createTestCaseReal(
1158+
{ ...baseArgs, priority: 'critical' },
1159+
mockConfig as any,
1160+
);
1161+
1162+
const body = (apiClientMock.post as Mock).mock.calls[0][0].body;
1163+
expect(body.test_case.priority).toBe('critical');
1164+
});
1165+
});

0 commit comments

Comments
 (0)