Skip to content

Commit f084848

Browse files
SavioBS629claude
andcommitted
refactor(testmanagement): share default-field normalization; trim case_type prompt
- Extract normalizeDefaultFields into TCG-utils/api.ts, shared by createTestCase and updateTestCase (removes the duplicated per-file helper; update keeps its automation_status quirk in the shared impl). - Trim the case_type .describe() to ~80 chars, under the tool-design 120 ceiling. - Add a test asserting normalized case_type reaches the v1 create body when template_id is set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ad9d1a1 commit f084848

4 files changed

Lines changed: 104 additions & 79 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: 6 additions & 23 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";
@@ -163,7 +163,7 @@ export const CreateTestCaseSchema = z.object({
163163
.string()
164164
.optional()
165165
.describe(
166-
"Test case type. Accepts either display name (e.g. 'Functional', 'Regression', 'Smoke & Sanity') or internal name (e.g. 'functional', 'smoke_sanity'). If omitted, the project default (usually 'Other') is applied. Valid values are per-project.",
166+
"Test case type display or internal name (per-project). Omit for project default.",
167167
),
168168
template: z
169169
.string()
@@ -204,9 +204,9 @@ export function sanitizeArgs(args: any) {
204204
import { getBrowserStackAuth } from "../../lib/get-auth.js";
205205

206206
/**
207-
* Normalize priority and case_type to the display names the create endpoint
208-
* accepts (it rejects lowercase internal names). Fetches the project's
209-
* form-fields once; on lookup failure, passes raw values 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.
210210
*/
211211
async function normalizeDefaultFields(
212212
projectIdentifier: string,
@@ -219,24 +219,7 @@ async function normalizeDefaultFields(
219219
config,
220220
);
221221
const { default_fields } = await fetchFormFields(numericProjectId, config);
222-
const out: { priority?: string; case_type?: string } = {};
223-
if (fields.priority !== undefined) {
224-
out.priority =
225-
normalizeDefaultFieldValue(
226-
default_fields?.priority?.values ?? [],
227-
fields.priority,
228-
"name",
229-
) ?? fields.priority;
230-
}
231-
if (fields.case_type !== undefined) {
232-
out.case_type =
233-
normalizeDefaultFieldValue(
234-
default_fields?.case_type?.values ?? [],
235-
fields.case_type,
236-
"name",
237-
) ?? fields.case_type;
238-
}
239-
return out;
222+
return normalizeDefaultFieldsFromForm(default_fields, fields);
240223
} catch (err) {
241224
logger.warn(
242225
"Failed to normalize default fields; passing through as given: %s",

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: 38 additions & 14 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
});
@@ -1173,7 +1177,6 @@ describe('createTestCase — priority normalization', () => {
11731177
default_fields: { case_type: { values: caseTypeValues } },
11741178
custom_fields: {},
11751179
});
1176-
normalizeMock.mockReturnValue('Functional');
11771180
(apiClientMock.post as Mock).mockResolvedValueOnce(createSuccess);
11781181

11791182
const result = await createTestCaseReal(
@@ -1182,7 +1185,7 @@ describe('createTestCase — priority normalization', () => {
11821185
);
11831186

11841187
expect(result.isError).toBeFalsy();
1185-
expect(normalizeMock).toHaveBeenCalledWith(caseTypeValues, 'functional', 'name');
1188+
// Real normalizeDefaultFields maps 'functional' to the display name 'Functional'.
11861189
const body = (apiClientMock.post as Mock).mock.calls[0][0].body;
11871190
expect(body.test_case.case_type).toBe('Functional');
11881191
});
@@ -1217,9 +1220,6 @@ describe('createTestCase — priority normalization', () => {
12171220
},
12181221
custom_fields: {},
12191222
});
1220-
normalizeMock.mockImplementation((_values, input) =>
1221-
input === 'critical' ? 'Critical' : 'Functional',
1222-
);
12231223
(apiClientMock.post as Mock).mockResolvedValueOnce(createSuccess);
12241224

12251225
await createTestCaseReal(
@@ -1410,6 +1410,30 @@ describe('createTestCase — template & multi-select custom_fields', () => {
14101410
expect(call.body.test_case.folder_id).toBeUndefined();
14111411
});
14121412

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+
14131437
it('translates custom_fields name→id and option value→id on the v1 path', async () => {
14141438
const api = await import('../../src/tools/testmanagement-utils/TCG-utils/api');
14151439
(api.fetchFormFields as Mock).mockResolvedValueOnce({

0 commit comments

Comments
 (0)