Skip to content

Commit 95fda8e

Browse files
Merge pull request #346 from SavioBS629/fix/create-testcase-case-type
Fix/create testcase case type
2 parents aa87b00 + f084848 commit 95fda8e

4 files changed

Lines changed: 193 additions & 69 deletions

File tree

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,60 @@ export function normalizeDefaultFieldValue(
5858
return match.internal_name ?? match.name;
5959
}
6060

61+
export interface DefaultFieldInputs {
62+
priority?: string;
63+
case_type?: string;
64+
automation_status?: string;
65+
}
66+
67+
/**
68+
* Normalize priority/case_type/automation_status against a project's already
69+
* fetched `default_fields`. priority and case_type resolve to the display name
70+
* (the create/update endpoints reject lowercase internal names); on no match
71+
* the raw value passes through. automation_status is special: its values carry
72+
* a null internal_name and hold the internal name in `value`, so it matches on
73+
* name-or-value and emits `value`. Callers own the fetch and its error path.
74+
*/
75+
export function normalizeDefaultFields(
76+
defaultFields: any,
77+
inputs: DefaultFieldInputs,
78+
): DefaultFieldInputs {
79+
const out: DefaultFieldInputs = {};
80+
81+
if (inputs.priority !== undefined) {
82+
out.priority =
83+
normalizeDefaultFieldValue(
84+
defaultFields?.priority?.values ?? [],
85+
inputs.priority,
86+
"name",
87+
) ?? inputs.priority;
88+
}
89+
if (inputs.case_type !== undefined) {
90+
out.case_type =
91+
normalizeDefaultFieldValue(
92+
defaultFields?.case_type?.values ?? [],
93+
inputs.case_type,
94+
"name",
95+
) ?? inputs.case_type;
96+
}
97+
if (inputs.automation_status !== undefined) {
98+
const values =
99+
(defaultFields?.automation_status?.values as Array<{
100+
name?: string;
101+
value?: string;
102+
}>) ?? [];
103+
const input = inputs.automation_status.toLowerCase().trim();
104+
const match = values.find(
105+
(v) =>
106+
(v.value ?? "").toLowerCase() === input ||
107+
(v.name ?? "").toLowerCase() === input,
108+
);
109+
out.automation_status = match?.value ?? inputs.automation_status;
110+
}
111+
112+
return out;
113+
}
114+
61115
/**
62116
* Trigger AI-based test case generation for a document.
63117
*/

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

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
44
import { formatAxiosError } from "../../lib/error.js";
55
import {
66
fetchFormFields,
7-
normalizeDefaultFieldValue,
7+
normalizeDefaultFields as normalizeDefaultFieldsFromForm,
88
projectIdentifierToId,
99
} from "./TCG-utils/api.js";
1010
import { BrowserStackConfig } from "../../lib/types.js";
@@ -43,6 +43,7 @@ export interface TestCaseCreateRequest {
4343
custom_fields?: Record<string, CustomFieldValue>;
4444
automation_status?: string;
4545
priority?: string;
46+
case_type?: string;
4647
template?: string;
4748
template_id?: number;
4849
}
@@ -158,6 +159,12 @@ export const CreateTestCaseSchema = z.object({
158159
.describe(
159160
"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.",
160161
),
162+
case_type: z
163+
.string()
164+
.optional()
165+
.describe(
166+
"Test case type display or internal name (per-project). Omit for project default.",
167+
),
161168
template: z
162169
.string()
163170
.optional()
@@ -197,33 +204,28 @@ export function sanitizeArgs(args: any) {
197204
import { getBrowserStackAuth } from "../../lib/get-auth.js";
198205

199206
/**
200-
* Normalize priority to the display name the create endpoint accepts (it
201-
* rejects lowercase). On lookup failure, pass the raw value through.
207+
* Fetch the project's form-fields and normalize priority/case_type to the
208+
* display names the create endpoint accepts (it rejects lowercase internal
209+
* names). On lookup failure, passes raw values through.
202210
*/
203-
async function normalizePriority(
211+
async function normalizeDefaultFields(
204212
projectIdentifier: string,
205-
priority: string,
213+
fields: { priority?: string; case_type?: string },
206214
config: BrowserStackConfig,
207-
): Promise<string> {
215+
): Promise<{ priority?: string; case_type?: string }> {
208216
try {
209217
const numericProjectId = await projectIdentifierToId(
210218
projectIdentifier,
211219
config,
212220
);
213221
const { default_fields } = await fetchFormFields(numericProjectId, config);
214-
return (
215-
normalizeDefaultFieldValue(
216-
default_fields?.priority?.values ?? [],
217-
priority,
218-
"name",
219-
) ?? priority
220-
);
222+
return normalizeDefaultFieldsFromForm(default_fields, fields);
221223
} catch (err) {
222224
logger.warn(
223-
"Failed to normalize priority value; passing through as given: %s",
225+
"Failed to normalize default fields; passing through as given: %s",
224226
err instanceof Error ? err.message : String(err),
225227
);
226-
return priority;
228+
return { priority: fields.priority, case_type: fields.case_type };
227229
}
228230
}
229231

@@ -318,12 +320,22 @@ export async function createTestCase(
318320
): Promise<CallToolResult> {
319321
const testCaseParams: TestCaseCreateRequest = { ...params };
320322

321-
if (testCaseParams.priority !== undefined) {
322-
testCaseParams.priority = await normalizePriority(
323+
if (
324+
testCaseParams.priority !== undefined ||
325+
testCaseParams.case_type !== undefined
326+
) {
327+
const normalized = await normalizeDefaultFields(
323328
params.project_identifier,
324-
testCaseParams.priority,
329+
{
330+
priority: testCaseParams.priority,
331+
case_type: testCaseParams.case_type,
332+
},
325333
config,
326334
);
335+
if (normalized.priority !== undefined)
336+
testCaseParams.priority = normalized.priority;
337+
if (normalized.case_type !== undefined)
338+
testCaseParams.case_type = normalized.case_type;
327339
}
328340

329341
const authString = getBrowserStackAuth(config);

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

Lines changed: 6 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
44
import { formatAxiosError } from "../../lib/error.js";
55
import {
66
fetchFormFields,
7-
normalizeDefaultFieldValue,
7+
normalizeDefaultFields as normalizeDefaultFieldsFromForm,
88
projectIdentifierToId,
99
} from "./TCG-utils/api.js";
1010
import { BrowserStackConfig } from "../../lib/types.js";
@@ -146,47 +146,11 @@ async function normalizeDefaultFields(
146146
);
147147
const { default_fields } = await fetchFormFields(numericProjectId, config);
148148

149-
const out: {
150-
priority?: string;
151-
case_type?: string;
152-
automation_status?: string;
153-
} = {};
154-
155-
if (params.priority !== undefined) {
156-
out.priority =
157-
normalizeDefaultFieldValue(
158-
default_fields?.priority?.values ?? [],
159-
params.priority,
160-
"name",
161-
) ?? params.priority;
162-
}
163-
if (params.case_type !== undefined) {
164-
out.case_type =
165-
normalizeDefaultFieldValue(
166-
default_fields?.case_type?.values ?? [],
167-
params.case_type,
168-
"name",
169-
) ?? params.case_type;
170-
}
171-
if (params.automation_status !== undefined) {
172-
// automation_status.values have null internal_name and the internal
173-
// name is actually held in `value` (see API inspection). Accept
174-
// either the display name or the internal snake_case form.
175-
const values =
176-
(default_fields?.automation_status?.values as Array<{
177-
name?: string;
178-
value?: string;
179-
}>) ?? [];
180-
const input = params.automation_status.toLowerCase().trim();
181-
const match = values.find(
182-
(v) =>
183-
(v.value ?? "").toLowerCase() === input ||
184-
(v.name ?? "").toLowerCase() === input,
185-
);
186-
out.automation_status = match?.value ?? params.automation_status;
187-
}
188-
189-
return out;
149+
return normalizeDefaultFieldsFromForm(default_fields, {
150+
priority: params.priority,
151+
case_type: params.case_type,
152+
automation_status: params.automation_status,
153+
});
190154
} catch (err) {
191155
logger.warn(
192156
"Failed to normalize default field values; passing through as given: %s",

tests/tools/testmanagement.test.ts

Lines changed: 103 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,17 @@ vi.mock('../../src/lib/inmemory-store', () => ({ signedUrlMap: new Map() }));
9797
vi.mock('../../src/lib/get-auth', () => ({
9898
getBrowserStackAuth: vi.fn(() => 'fake-user:fake-key')
9999
}));
100-
vi.mock('../../src/tools/testmanagement-utils/TCG-utils/api', () => ({
101-
projectIdentifierToId: vi.fn(() => Promise.resolve('999')),
102-
fetchFormFields: vi.fn(),
103-
normalizeDefaultFieldValue: vi.fn(),
104-
}));
100+
vi.mock('../../src/tools/testmanagement-utils/TCG-utils/api', async (importActual) => {
101+
const actual = await importActual<
102+
typeof import('../../src/tools/testmanagement-utils/TCG-utils/api')
103+
>();
104+
return {
105+
...actual,
106+
projectIdentifierToId: vi.fn(() => Promise.resolve('999')),
107+
fetchFormFields: vi.fn(),
108+
normalizeDefaultFieldValue: vi.fn(),
109+
};
110+
});
105111
vi.mock('form-data', () => {
106112
return {
107113
default: vi.fn().mockImplementation(() => ({
@@ -1077,7 +1083,6 @@ describe('createTestCase — priority normalization', () => {
10771083
let createTestCaseReal: typeof import('../../src/tools/testmanagement-utils/create-testcase').createTestCase;
10781084
let apiClientMock: typeof import('../../src/lib/apiClient').apiClient;
10791085
let fetchFormFieldsMock: Mock;
1080-
let normalizeMock: Mock;
10811086

10821087
beforeAll(async () => {
10831088
const actual = await vi.importActual<
@@ -1087,7 +1092,6 @@ describe('createTestCase — priority normalization', () => {
10871092
apiClientMock = (await import('../../src/lib/apiClient')).apiClient;
10881093
const api = await import('../../src/tools/testmanagement-utils/TCG-utils/api');
10891094
fetchFormFieldsMock = api.fetchFormFields as unknown as Mock;
1090-
normalizeMock = api.normalizeDefaultFieldValue as unknown as Mock;
10911095
});
10921096

10931097
beforeEach(() => {
@@ -1126,7 +1130,6 @@ describe('createTestCase — priority normalization', () => {
11261130

11271131
it('looks up the project form fields and sends the normalized priority in the request body', async () => {
11281132
fetchFormFieldsMock.mockResolvedValue(formFields);
1129-
normalizeMock.mockReturnValue('Critical');
11301133
(apiClientMock.post as Mock).mockResolvedValueOnce(createSuccess);
11311134

11321135
const result = await createTestCaseReal(
@@ -1135,7 +1138,8 @@ describe('createTestCase — priority normalization', () => {
11351138
);
11361139

11371140
expect(result.isError).toBeFalsy();
1138-
expect(normalizeMock).toHaveBeenCalledWith(priorityValues, 'critical', 'name');
1141+
// Real normalizeDefaultFields maps the internal name 'critical' to the
1142+
// display name 'Critical' the create endpoint requires.
11391143
const body = (apiClientMock.post as Mock).mock.calls[0][0].body;
11401144
expect(body.test_case.priority).toBe('Critical');
11411145
});
@@ -1162,6 +1166,72 @@ describe('createTestCase — priority normalization', () => {
11621166
const body = (apiClientMock.post as Mock).mock.calls[0][0].body;
11631167
expect(body.test_case.priority).toBe('critical');
11641168
});
1169+
1170+
const caseTypeValues = [
1171+
{ name: 'Functional', internal_name: 'functional', value: 1 },
1172+
{ name: 'Regression', internal_name: 'regression', value: 2 },
1173+
];
1174+
1175+
it('looks up the project form fields and sends the normalized case_type in the request body', async () => {
1176+
fetchFormFieldsMock.mockResolvedValue({
1177+
default_fields: { case_type: { values: caseTypeValues } },
1178+
custom_fields: {},
1179+
});
1180+
(apiClientMock.post as Mock).mockResolvedValueOnce(createSuccess);
1181+
1182+
const result = await createTestCaseReal(
1183+
{ ...baseArgs, case_type: 'functional' },
1184+
mockConfig as any,
1185+
);
1186+
1187+
expect(result.isError).toBeFalsy();
1188+
// Real normalizeDefaultFields maps 'functional' to the display name 'Functional'.
1189+
const body = (apiClientMock.post as Mock).mock.calls[0][0].body;
1190+
expect(body.test_case.case_type).toBe('Functional');
1191+
});
1192+
1193+
it('omits case_type from the request body when not provided (preserves project default)', async () => {
1194+
(apiClientMock.post as Mock).mockResolvedValueOnce(createSuccess);
1195+
1196+
await createTestCaseReal({ ...baseArgs }, mockConfig as any);
1197+
1198+
const body = (apiClientMock.post as Mock).mock.calls[0][0].body;
1199+
expect(body.test_case).not.toHaveProperty('case_type');
1200+
});
1201+
1202+
it('passes the raw case_type through when the form-fields lookup fails (graceful fallback)', async () => {
1203+
fetchFormFieldsMock.mockRejectedValue(new Error('Network Error'));
1204+
(apiClientMock.post as Mock).mockResolvedValueOnce(createSuccess);
1205+
1206+
await createTestCaseReal(
1207+
{ ...baseArgs, case_type: 'functional' },
1208+
mockConfig as any,
1209+
);
1210+
1211+
const body = (apiClientMock.post as Mock).mock.calls[0][0].body;
1212+
expect(body.test_case.case_type).toBe('functional');
1213+
});
1214+
1215+
it('normalizes both priority and case_type in a single form-fields lookup', async () => {
1216+
fetchFormFieldsMock.mockResolvedValue({
1217+
default_fields: {
1218+
priority: { values: priorityValues },
1219+
case_type: { values: caseTypeValues },
1220+
},
1221+
custom_fields: {},
1222+
});
1223+
(apiClientMock.post as Mock).mockResolvedValueOnce(createSuccess);
1224+
1225+
await createTestCaseReal(
1226+
{ ...baseArgs, priority: 'critical', case_type: 'functional' },
1227+
mockConfig as any,
1228+
);
1229+
1230+
expect(fetchFormFieldsMock).toHaveBeenCalledTimes(1);
1231+
const body = (apiClientMock.post as Mock).mock.calls[0][0].body;
1232+
expect(body.test_case.priority).toBe('Critical');
1233+
expect(body.test_case.case_type).toBe('Functional');
1234+
});
11651235
});
11661236

11671237
// Template slug pass-through + multi-select custom fields.
@@ -1340,6 +1410,30 @@ describe('createTestCase — template & multi-select custom_fields', () => {
13401410
expect(call.body.test_case.folder_id).toBeUndefined();
13411411
});
13421412

1413+
// case_type must survive into the v1 test_case body (spread from
1414+
// testCaseParams), normalized to the display name the endpoint accepts.
1415+
it('carries the normalized case_type into the v1 body when template_id is set', async () => {
1416+
const api = await import('../../src/tools/testmanagement-utils/TCG-utils/api');
1417+
(api.fetchFormFields as Mock).mockResolvedValue({
1418+
default_fields: {
1419+
case_type: {
1420+
values: [{ name: 'Functional', internal_name: 'functional', value: 1 }],
1421+
},
1422+
},
1423+
custom_fields: [],
1424+
});
1425+
(apiClientMock.post as Mock).mockResolvedValueOnce(respWithId(656127));
1426+
1427+
await createTestCaseReal(
1428+
{ ...baseArgs, template_id: 656127, case_type: 'functional' },
1429+
mockConfig as any,
1430+
);
1431+
1432+
const call = (apiClientMock.post as Mock).mock.calls[0][0];
1433+
expect(call.url).toContain('/api/v1/projects/');
1434+
expect(call.body.test_case.case_type).toBe('Functional');
1435+
});
1436+
13431437
it('translates custom_fields name→id and option value→id on the v1 path', async () => {
13441438
const api = await import('../../src/tools/testmanagement-utils/TCG-utils/api');
13451439
(api.fetchFormFields as Mock).mockResolvedValueOnce({

0 commit comments

Comments
 (0)