common-qa-mcp 0.4.1 → 0.6.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/dist/adapters/remoteMcp/adapter.d.ts +4 -1
- package/dist/adapters/remoteMcp/adapter.d.ts.map +1 -1
- package/dist/adapters/remoteMcp/adapter.js +8 -3
- package/dist/adapters/remoteMcp/adapter.js.map +1 -1
- package/dist/adapters/simplifyqa/adapter.d.ts +130 -36
- package/dist/adapters/simplifyqa/adapter.d.ts.map +1 -1
- package/dist/adapters/simplifyqa/adapter.js +833 -265
- package/dist/adapters/simplifyqa/adapter.js.map +1 -1
- package/dist/adapters/simplifyqa/mapper.d.ts +5 -1
- package/dist/adapters/simplifyqa/mapper.d.ts.map +1 -1
- package/dist/adapters/simplifyqa/mapper.js +35 -1
- package/dist/adapters/simplifyqa/mapper.js.map +1 -1
- package/dist/adapters/types.d.ts +47 -10
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/adapters/walnut/actionTypes.d.ts +1 -1
- package/dist/adapters/walnut/actionTypes.d.ts.map +1 -1
- package/dist/adapters/walnut/actionTypes.js +37 -0
- package/dist/adapters/walnut/actionTypes.js.map +1 -1
- package/dist/adapters/walnut/mapper.d.ts.map +1 -1
- package/dist/adapters/walnut/mapper.js +12 -1
- package/dist/adapters/walnut/mapper.js.map +1 -1
- package/dist/domain/types.d.ts +98 -17
- package/dist/domain/types.d.ts.map +1 -1
- package/dist/initAgents.d.ts.map +1 -1
- package/dist/initAgents.js +31 -11
- package/dist/initAgents.js.map +1 -1
- package/dist/server.js +1 -1
- package/dist/tools/backlog.tools.d.ts.map +1 -1
- package/dist/tools/backlog.tools.js +154 -26
- package/dist/tools/backlog.tools.js.map +1 -1
- package/dist/tools/connection.tools.d.ts.map +1 -1
- package/dist/tools/connection.tools.js +15 -3
- package/dist/tools/connection.tools.js.map +1 -1
- package/dist/tools/defects.tools.d.ts.map +1 -1
- package/dist/tools/defects.tools.js +29 -3
- package/dist/tools/defects.tools.js.map +1 -1
- package/dist/tools/discovery.tools.d.ts.map +1 -1
- package/dist/tools/discovery.tools.js +304 -137
- package/dist/tools/discovery.tools.js.map +1 -1
- package/dist/tools/markdown.d.ts.map +1 -1
- package/dist/tools/markdown.js +9 -5
- package/dist/tools/markdown.js.map +1 -1
- package/dist/tools/objects.tools.d.ts.map +1 -1
- package/dist/tools/objects.tools.js +111 -6
- package/dist/tools/objects.tools.js.map +1 -1
- package/dist/tools/recording.tools.d.ts.map +1 -1
- package/dist/tools/recording.tools.js +108 -10
- package/dist/tools/recording.tools.js.map +1 -1
- package/dist/tools/run.tools.d.ts.map +1 -1
- package/dist/tools/run.tools.js +263 -63
- package/dist/tools/run.tools.js.map +1 -1
- package/dist/tools/tasks.tools.d.ts.map +1 -1
- package/dist/tools/tasks.tools.js +29 -3
- package/dist/tools/tasks.tools.js.map +1 -1
- package/dist/tools/testcase.tools.d.ts.map +1 -1
- package/dist/tools/testcase.tools.js +5 -2
- package/dist/tools/testcase.tools.js.map +1 -1
- package/package.json +1 -1
|
@@ -17,6 +17,30 @@ function totalOf(rows) {
|
|
|
17
17
|
const t = rows.totalCount;
|
|
18
18
|
return typeof t === 'number' ? t : undefined;
|
|
19
19
|
}
|
|
20
|
+
/** Render a `ModuleSaved` (single node or subtree) as human-readable text so the model sees the
|
|
21
|
+
* full tree shape it just created — id, code, isRoot flag, parent, and nested children indented.
|
|
22
|
+
* `failedAt` (if set on root) is surfaced at the bottom so a partial-success is never hidden. */
|
|
23
|
+
function formatModuleSaved(saved, indent = 0) {
|
|
24
|
+
const pad = ' '.repeat(indent);
|
|
25
|
+
const code = saved.code ? ` [${saved.code}]` : '';
|
|
26
|
+
const parent = saved.parentId ? `, parent=${saved.parentId}` : '';
|
|
27
|
+
const root = saved.isRoot ? ', isRoot=true' : '';
|
|
28
|
+
const head = `${pad}- ${saved.name}${code} (id ${saved.id}${root}${parent})`;
|
|
29
|
+
const childLines = (saved.children ?? []).map((c) => formatModuleSaved(c, indent + 1));
|
|
30
|
+
const parts = [head, ...childLines];
|
|
31
|
+
if (indent === 0 && saved.failedAt) {
|
|
32
|
+
parts.push(`\n⚠ Subtree create stopped at "${saved.failedAt.path}": ${saved.failedAt.error}`);
|
|
33
|
+
parts.push(' Nodes listed above ARE persisted. Fix the error, then create the remaining nodes.');
|
|
34
|
+
}
|
|
35
|
+
if (indent === 0) {
|
|
36
|
+
const created = countCreated(saved);
|
|
37
|
+
parts.unshift(`Created ${created} module${created === 1 ? '' : 's'}:`);
|
|
38
|
+
}
|
|
39
|
+
return parts.join('\n');
|
|
40
|
+
}
|
|
41
|
+
function countCreated(saved) {
|
|
42
|
+
return 1 + (saved.children ?? []).reduce((n, c) => n + countCreated(c), 0);
|
|
43
|
+
}
|
|
20
44
|
/** Header line for a list tool: "Showing 100 of 350 test cases" when the backend returned a
|
|
21
45
|
* totalCount larger than the page, else "N test cases". Pluralises via `plural` (defaults to `s`
|
|
22
46
|
* suffix — pass explicit forms for "stor y/ies", etc). */
|
|
@@ -88,7 +112,9 @@ export function registerDiscoveryTools(server, deps) {
|
|
|
88
112
|
'use `list_functions` instead. Trigger words for THIS tool: "test case", "TC", "TC_", ' +
|
|
89
113
|
'"scenario", "script". Requires a live connection (qa_connect). Returns a summary list — id, ' +
|
|
90
114
|
'title, technology, execution type, status, step count. Use `tc_get` to read one in full. ' +
|
|
91
|
-
'Read-only, product-neutral.'
|
|
115
|
+
'Read-only, product-neutral. When presenting this as a table, label the columns "ID" and ' +
|
|
116
|
+
'"Name" — the underlying fields are literally named `code`/`title`, but that\'s internal ' +
|
|
117
|
+
'wire terminology, not what to show the user.',
|
|
92
118
|
inputSchema: {
|
|
93
119
|
search: z.string().optional().describe('Free-text search over title / id.'),
|
|
94
120
|
status: z.string().optional().describe('Filter by status.'),
|
|
@@ -126,7 +152,7 @@ export function registerDiscoveryTools(server, deps) {
|
|
|
126
152
|
const obsoleteCount = rows.filter((r) => r.obsolete === true).length;
|
|
127
153
|
const obsoleteNote = obsoleteCount ? ` · ${obsoleteCount} obsolete` : '';
|
|
128
154
|
const text = `${countHeader(rows, 'test case')}${obsoleteNote}:\n\n` +
|
|
129
|
-
'|
|
|
155
|
+
'| ID | Name | technology | execution | status | steps | obsolete |\n|---|---|---|---|---|---|---|\n' +
|
|
130
156
|
rows
|
|
131
157
|
.map((r) => `| ${escCell(r.code)} | ${escCell(r.title)} | ${r.technologyType ?? '-'} | ${r.executionType ?? '-'} | ${r.status ?? '-'} | ${r.stepCount ?? '-'} | ${r.obsolete === true ? 'yes' : ''} |`)
|
|
132
158
|
.join('\n');
|
|
@@ -232,69 +258,55 @@ export function registerDiscoveryTools(server, deps) {
|
|
|
232
258
|
}
|
|
233
259
|
});
|
|
234
260
|
server.registerTool('tc_clone', {
|
|
235
|
-
title: '
|
|
261
|
+
title: 'Save a test case as a new test case (same project + module)',
|
|
236
262
|
description: 'FOR TEST CASES ONLY. NOT for reusable functions: if the user names an `FN_*` key or asks to ' +
|
|
237
263
|
'clone a function, use `fn_clone` instead. Trigger words for THIS tool: "test case", "TC", ' +
|
|
238
264
|
'"TC_", "scenario", "script".\n\n' +
|
|
239
|
-
'Save an existing test case as a NEW
|
|
240
|
-
'
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
'
|
|
244
|
-
'
|
|
245
|
-
'
|
|
246
|
-
'
|
|
247
|
-
'
|
|
248
|
-
'
|
|
249
|
-
'
|
|
250
|
-
|
|
251
|
-
'
|
|
265
|
+
'Save an existing test case as a NEW test case (new TC id) in the SAME project + module. ' +
|
|
266
|
+
'Mirrors the FE in-editor "Save as new TC" dialog (operation: SAVEASNEWTC). Clones ONE ' +
|
|
267
|
+
'source version — pass `sourceVersionId` to pick which; defaults to the TC\'s currently ' +
|
|
268
|
+
'selected version. `tcId` accepts the numeric id or business key (`TC_*`).\n\n' +
|
|
269
|
+
'`newName` is REQUIRED — backend rejects an empty name on SAVEASNEWTC.\n\n' +
|
|
270
|
+
'TESTDATA — TWO-STEP: call once WITHOUT `tdCopy` to get the "Copy Test Data" prompt and ' +
|
|
271
|
+
'confirm with the user; then call again with `tdCopy` set. Mirrors the FE\'s "COPY TEST ' +
|
|
272
|
+
'DATA" modal (Cancel = false, Confirm = true). The tool REFUSES to clone until `tdCopy` ' +
|
|
273
|
+
'is passed — the prompt step is not skippable.\n\n' +
|
|
274
|
+
'`tdCopy:true` clones the source version\'s testdata iterations across every environment. ' +
|
|
275
|
+
'`tdCopy:false` copies only the column STRUCTURE — one empty iteration (`ITR-1`) per ' +
|
|
276
|
+
'environment; cells fall back to each parameter\'s project-level `defaultValue` if any.\n\n' +
|
|
277
|
+
'Requires a live connection.',
|
|
252
278
|
inputSchema: {
|
|
253
279
|
tcId: z.string().describe('The source test case id or business key (e.g. TC_123).'),
|
|
254
|
-
newName: z.string().
|
|
255
|
-
|
|
280
|
+
newName: z.string().min(1).describe('Name for the new test case (required by backend).'),
|
|
281
|
+
tdCopy: z.boolean().optional().describe('REQUIRED to actually clone. Whether to clone the source version\'s testdata iterations. Omit on the first call to get the confirmation prompt; ask the user, then call again with the chosen value.'),
|
|
282
|
+
sourceVersionId: z.string().optional().describe('Which version of the source TC to clone; defaults to the TC\'s current version.'),
|
|
256
283
|
},
|
|
257
284
|
annotations: { readOnlyHint: false },
|
|
258
|
-
}, async ({ tcId, newName,
|
|
285
|
+
}, async ({ tcId, newName, tdCopy, sourceVersionId }) => {
|
|
259
286
|
const adapter = deps.getAdapter();
|
|
260
287
|
if (!adapter)
|
|
261
288
|
return err(NOT_CONNECTED);
|
|
262
289
|
if (!adapter.cloneTestCase)
|
|
263
|
-
return err('This product does not support
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const names = (tc.versions ?? []).map((v) => v.name);
|
|
272
|
-
if (names.length === 0) {
|
|
273
|
-
return err(`Cannot clone ${tcId}: source has no versions to copy.`);
|
|
274
|
-
}
|
|
275
|
-
return ok(`**${tcId}** has ${names.length} version${names.length === 1 ? '' : 's'}: ` +
|
|
276
|
-
`${names.map((n) => `\`${n}\``).join(', ')}.\n\n` +
|
|
277
|
-
`**Ask the user which version(s) to clone**, then call \`tc_clone\` again:\n` +
|
|
278
|
-
`- \`versions: ["*"]\` → clone ALL versions\n` +
|
|
279
|
-
`- \`versions: ${JSON.stringify([names[0]])}\` → clone only that version\n` +
|
|
280
|
-
`- \`versions: [name1, name2]\` → clone a specific subset`, { preview: true, versions: names, totalCount: names.length });
|
|
281
|
-
}
|
|
282
|
-
catch (e) {
|
|
283
|
-
return err(describeError(e));
|
|
284
|
-
}
|
|
290
|
+
return err('This product does not support save-as-new-testcase.');
|
|
291
|
+
// Prompt step: no `tdCopy` → hand back the "Copy Test Data" question.
|
|
292
|
+
if (tdCopy === undefined) {
|
|
293
|
+
return ok(`**COPY TEST DATA** — Do you want to copy test data to new test case **${newName}** (from ${tcId})?\n\n` +
|
|
294
|
+
`If you do not want to copy the test data, only the column structure will be carried over (one empty ITR-1 per environment).\n\n` +
|
|
295
|
+
`**Ask the user**, then call \`tc_clone\` again:\n` +
|
|
296
|
+
`- \`tdCopy: true\` → copy iterations across all environments (Confirm)\n` +
|
|
297
|
+
`- \`tdCopy: false\` → structure only, empty values (Cancel)`, { preview: true, needs: 'tdCopy' });
|
|
285
298
|
}
|
|
286
|
-
// Explicit sentinel ["*"] = "all versions" — pass through as undefined to the adapter.
|
|
287
|
-
const isAll = versions.length === 1 && versions[0] === '*';
|
|
288
|
-
const effectiveVersions = isAll ? undefined : versions;
|
|
289
299
|
try {
|
|
290
|
-
const ref = await adapter.cloneTestCase(tcId, newName,
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
300
|
+
const ref = await adapter.cloneTestCase(tcId, newName, {
|
|
301
|
+
tdCopy,
|
|
302
|
+
...(sourceVersionId !== undefined ? { sourceVersionId } : {}),
|
|
303
|
+
});
|
|
304
|
+
const tdNote = tdCopy
|
|
305
|
+
? ' Testdata iterations were copied across environments.'
|
|
306
|
+
: ' Testdata structure was carried over but values are empty (tdCopy:false).';
|
|
307
|
+
return ok(`Cloned ${tcId} → **${newName}**. New id: **${ref.id}**` +
|
|
296
308
|
(ref.productUrl ? ` · ${ref.productUrl}` : '') +
|
|
297
|
-
`.${
|
|
309
|
+
`.${tdNote}`, { savedRef: ref });
|
|
298
310
|
}
|
|
299
311
|
catch (e) {
|
|
300
312
|
return err(describeError(e));
|
|
@@ -309,16 +321,19 @@ export function registerDiscoveryTools(server, deps) {
|
|
|
309
321
|
'new version row with cloned steps. Mirrors the SimplifyQA UI "Save as new version" dialog ' +
|
|
310
322
|
'(operation: SAVENEWVERSION). `tcId` accepts the numeric id or business key (`TC_*`).\n\n' +
|
|
311
323
|
'`versionName` must be unique for this TC — the backend rejects duplicates.\n\n' +
|
|
312
|
-
'`tdCopy`
|
|
313
|
-
'
|
|
314
|
-
'
|
|
315
|
-
'
|
|
324
|
+
'TESTDATA — TWO-STEP: call once WITHOUT `tdCopy` to get the "Copy Test Data" prompt and ' +
|
|
325
|
+
'confirm with the user; then call again with `tdCopy` set. Mirrors the FE\'s "COPY TEST ' +
|
|
326
|
+
'DATA" modal (Cancel = false, Confirm = true). The tool REFUSES to snapshot until `tdCopy` ' +
|
|
327
|
+
'is passed — the prompt step is not skippable.\n\n' +
|
|
328
|
+
'`tdCopy:true` clones the source version\'s testdata iterations across every environment. ' +
|
|
329
|
+
'`tdCopy:false` copies only the column STRUCTURE — one empty iteration (`ITR-1`) per ' +
|
|
330
|
+
'environment; cells fall back to each parameter\'s project-level `defaultValue` if any.\n\n' +
|
|
316
331
|
'`sourceVersionId` picks which version to snapshot; omit to snapshot the TC\'s currently ' +
|
|
317
332
|
'selected version. Requires a live connection.',
|
|
318
333
|
inputSchema: {
|
|
319
334
|
tcId: z.string().describe('The test case id or business key (e.g. TC_123).'),
|
|
320
335
|
versionName: z.string().min(1).describe('Name for the new version (must be unique for this TC).'),
|
|
321
|
-
tdCopy: z.boolean().optional().describe('Clone testdata iterations from the source version.
|
|
336
|
+
tdCopy: z.boolean().optional().describe('REQUIRED to actually snapshot. Clone testdata iterations from the source version. Omit on the first call to get the confirmation prompt; ask the user, then call again with the chosen value.'),
|
|
322
337
|
sourceVersionId: z.string().optional().describe('Version to snapshot; defaults to the TC\'s current version.'),
|
|
323
338
|
},
|
|
324
339
|
annotations: { readOnlyHint: false },
|
|
@@ -328,14 +343,22 @@ export function registerDiscoveryTools(server, deps) {
|
|
|
328
343
|
return err(NOT_CONNECTED);
|
|
329
344
|
if (!adapter.saveAsNewVersion)
|
|
330
345
|
return err('This product does not support save-as-new-version.');
|
|
331
|
-
|
|
346
|
+
// Prompt step: no `tdCopy` → hand back the "Copy Test Data" question so the caller can
|
|
347
|
+
// confirm with the user before writing. Mirrors the FE's COPY TEST DATA modal.
|
|
348
|
+
if (tdCopy === undefined) {
|
|
349
|
+
return ok(`**COPY TEST DATA** — Do you want to copy test data to new version **${versionName}** of ${tcId}?\n\n` +
|
|
350
|
+
`If you do not want to copy the test data, only the column structure will be carried over (one empty ITR-1 per environment).\n\n` +
|
|
351
|
+
`**Ask the user**, then call \`tc_save_as_new_version\` again:\n` +
|
|
352
|
+
`- \`tdCopy: true\` → copy iterations across all environments (Confirm)\n` +
|
|
353
|
+
`- \`tdCopy: false\` → structure only, empty values (Cancel)`, { preview: true, needs: 'tdCopy' });
|
|
354
|
+
}
|
|
332
355
|
try {
|
|
333
356
|
const ref = await adapter.saveAsNewVersion(tcId, {
|
|
334
357
|
versionName,
|
|
335
|
-
tdCopy
|
|
358
|
+
tdCopy,
|
|
336
359
|
...(sourceVersionId !== undefined ? { sourceVersionId } : {}),
|
|
337
360
|
});
|
|
338
|
-
const tdNote =
|
|
361
|
+
const tdNote = tdCopy
|
|
339
362
|
? ' Testdata iterations were copied across environments.'
|
|
340
363
|
: ' Testdata structure was carried over but values are empty (tdCopy:false).';
|
|
341
364
|
return ok(`Snapshotted ${tcId} as new version **${versionName}**` +
|
|
@@ -746,69 +769,55 @@ export function registerDiscoveryTools(server, deps) {
|
|
|
746
769
|
}
|
|
747
770
|
});
|
|
748
771
|
server.registerTool('fn_clone', {
|
|
749
|
-
title: '
|
|
772
|
+
title: 'Save a reusable function as a new function (same project + module)',
|
|
750
773
|
description: 'FOR REUSABLE FUNCTIONS ONLY. NOT for test cases: if the user names a `TC_*` key or asks to ' +
|
|
751
774
|
'clone a test case / scenario / script, use `tc_clone` instead. Trigger words for THIS tool: ' +
|
|
752
775
|
'"function", "reusable function", "FN_", "FN-".\n\n' +
|
|
753
|
-
'Save an existing reusable FUNCTION as a NEW
|
|
754
|
-
'
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
'
|
|
758
|
-
'
|
|
759
|
-
'
|
|
760
|
-
'
|
|
761
|
-
'
|
|
762
|
-
'
|
|
763
|
-
'
|
|
764
|
-
|
|
765
|
-
'
|
|
776
|
+
'Save an existing reusable FUNCTION as a NEW function (new FN id) in the SAME project + ' +
|
|
777
|
+
'module. Mirrors the FE in-editor "Save as new function" dialog (operation: SAVEASNEWFN). ' +
|
|
778
|
+
'Clones ONE source version — pass `sourceVersionId` to pick which; defaults to the FN\'s ' +
|
|
779
|
+
'newest version. `fnId` accepts the numeric id or business key (`FN_*`).\n\n' +
|
|
780
|
+
'`newName` is REQUIRED — backend rejects an empty name on SAVEASNEWFN.\n\n' +
|
|
781
|
+
'TESTDATA — TWO-STEP: call once WITHOUT `tdCopy` to get the "Copy Test Data" prompt and ' +
|
|
782
|
+
'confirm with the user; then call again with `tdCopy` set. Mirrors the FE\'s "COPY TEST ' +
|
|
783
|
+
'DATA" modal (Cancel = false, Confirm = true). The tool REFUSES to clone until `tdCopy` ' +
|
|
784
|
+
'is passed — the prompt step is not skippable.\n\n' +
|
|
785
|
+
'`tdCopy:true` clones the source version\'s testdata iterations across every environment. ' +
|
|
786
|
+
'`tdCopy:false` copies only the column STRUCTURE — one empty iteration (`ITR-1`) per ' +
|
|
787
|
+
'environment; cells fall back to each parameter\'s project-level `defaultValue` if any.\n\n' +
|
|
788
|
+
'Requires a live connection.',
|
|
766
789
|
inputSchema: {
|
|
767
790
|
fnId: z.string().describe('The source function id or business key (e.g. FN_123).'),
|
|
768
|
-
newName: z.string().
|
|
769
|
-
|
|
791
|
+
newName: z.string().min(1).describe('Name for the new function (required by backend).'),
|
|
792
|
+
tdCopy: z.boolean().optional().describe('REQUIRED to actually clone. Whether to clone the source version\'s testdata iterations. Omit on the first call to get the confirmation prompt; ask the user, then call again with the chosen value.'),
|
|
793
|
+
sourceVersionId: z.string().optional().describe('Which version of the source FN to clone; defaults to the FN\'s newest version.'),
|
|
770
794
|
},
|
|
771
795
|
annotations: { readOnlyHint: false },
|
|
772
|
-
}, async ({ fnId, newName,
|
|
796
|
+
}, async ({ fnId, newName, tdCopy, sourceVersionId }) => {
|
|
773
797
|
const adapter = deps.getAdapter();
|
|
774
798
|
if (!adapter)
|
|
775
799
|
return err(NOT_CONNECTED);
|
|
776
800
|
if (!adapter.cloneFunction)
|
|
777
|
-
return err('This product does not support function
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
const names = (fn.versions ?? []).map((v) => v.name);
|
|
786
|
-
if (names.length === 0) {
|
|
787
|
-
return err(`Cannot clone ${fnId}: source has no versions to copy.`);
|
|
788
|
-
}
|
|
789
|
-
return ok(`**${fnId}** has ${names.length} version${names.length === 1 ? '' : 's'}: ` +
|
|
790
|
-
`${names.map((n) => `\`${n}\``).join(', ')}.\n\n` +
|
|
791
|
-
`**Ask the user which version(s) to clone**, then call \`fn_clone\` again:\n` +
|
|
792
|
-
`- \`versions: ["*"]\` → clone ALL versions\n` +
|
|
793
|
-
`- \`versions: ${JSON.stringify([names[0]])}\` → clone only that version\n` +
|
|
794
|
-
`- \`versions: [name1, name2]\` → clone a specific subset`, { preview: true, versions: names, totalCount: names.length });
|
|
795
|
-
}
|
|
796
|
-
catch (e) {
|
|
797
|
-
return err(describeError(e));
|
|
798
|
-
}
|
|
801
|
+
return err('This product does not support save-as-new-function.');
|
|
802
|
+
// Prompt step: no `tdCopy` → hand back the "Copy Test Data" question.
|
|
803
|
+
if (tdCopy === undefined) {
|
|
804
|
+
return ok(`**COPY TEST DATA** — Do you want to copy test data to new function **${newName}** (from ${fnId})?\n\n` +
|
|
805
|
+
`If you do not want to copy the test data, only the column structure will be carried over (one empty ITR-1 per environment).\n\n` +
|
|
806
|
+
`**Ask the user**, then call \`fn_clone\` again:\n` +
|
|
807
|
+
`- \`tdCopy: true\` → copy iterations across all environments (Confirm)\n` +
|
|
808
|
+
`- \`tdCopy: false\` → structure only, empty values (Cancel)`, { preview: true, needs: 'tdCopy' });
|
|
799
809
|
}
|
|
800
|
-
// Explicit sentinel ["*"] = "all versions" — pass through as undefined to the adapter.
|
|
801
|
-
const isAll = versions.length === 1 && versions[0] === '*';
|
|
802
|
-
const effectiveVersions = isAll ? undefined : versions;
|
|
803
810
|
try {
|
|
804
|
-
const ref = await adapter.cloneFunction(fnId, newName,
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
811
|
+
const ref = await adapter.cloneFunction(fnId, newName, {
|
|
812
|
+
tdCopy,
|
|
813
|
+
...(sourceVersionId !== undefined ? { sourceVersionId } : {}),
|
|
814
|
+
});
|
|
815
|
+
const tdNote = tdCopy
|
|
816
|
+
? ' Testdata iterations were copied across environments.'
|
|
817
|
+
: ' Testdata structure was carried over but values are empty (tdCopy:false).';
|
|
818
|
+
return ok(`Cloned ${fnId} → **${newName}**. New id: **${ref.id}**` +
|
|
810
819
|
(ref.productUrl ? ` · ${ref.productUrl}` : '') +
|
|
811
|
-
`.${
|
|
820
|
+
`.${tdNote}`, { savedRef: ref });
|
|
812
821
|
}
|
|
813
822
|
catch (e) {
|
|
814
823
|
return err(describeError(e));
|
|
@@ -823,15 +832,19 @@ export function registerDiscoveryTools(server, deps) {
|
|
|
823
832
|
'new version row with cloned steps. Mirrors the SimplifyQA UI "Save as new version" dialog ' +
|
|
824
833
|
'(operation: SAVENEWVERSION, type: FN). `fnId` accepts the numeric id or business key (`FN_*`).\n\n' +
|
|
825
834
|
'`versionName` must be unique for this FN — the backend rejects duplicates.\n\n' +
|
|
826
|
-
'`tdCopy`
|
|
827
|
-
'
|
|
828
|
-
'
|
|
835
|
+
'TESTDATA — TWO-STEP: call once WITHOUT `tdCopy` to get the "Copy Test Data" prompt and ' +
|
|
836
|
+
'confirm with the user; then call again with `tdCopy` set. Mirrors the FE\'s "COPY TEST ' +
|
|
837
|
+
'DATA" modal (Cancel = false, Confirm = true). The tool REFUSES to snapshot until `tdCopy` ' +
|
|
838
|
+
'is passed — the prompt step is not skippable.\n\n' +
|
|
839
|
+
'`tdCopy:true` clones the source version\'s testdata iterations across every environment. ' +
|
|
840
|
+
'`tdCopy:false` copies only the column STRUCTURE — one empty iteration (`ITR-1`) per ' +
|
|
841
|
+
'environment; cells fall back to each parameter\'s project-level `defaultValue` if any.\n\n' +
|
|
829
842
|
'`sourceVersionId` picks which version to snapshot; omit to snapshot the FN\'s newest ' +
|
|
830
843
|
'version (the FN doc has no inline current-version marker). Requires a live connection.',
|
|
831
844
|
inputSchema: {
|
|
832
845
|
fnId: z.string().describe('The function id or business key (e.g. FN_123).'),
|
|
833
846
|
versionName: z.string().min(1).describe('Name for the new version (must be unique for this FN).'),
|
|
834
|
-
tdCopy: z.boolean().optional().describe('Clone testdata iterations from the source version.
|
|
847
|
+
tdCopy: z.boolean().optional().describe('REQUIRED to actually snapshot. Clone testdata iterations from the source version. Omit on the first call to get the confirmation prompt; ask the user, then call again with the chosen value.'),
|
|
835
848
|
sourceVersionId: z.string().optional().describe('Version to snapshot; defaults to the FN\'s newest version.'),
|
|
836
849
|
},
|
|
837
850
|
annotations: { readOnlyHint: false },
|
|
@@ -841,14 +854,22 @@ export function registerDiscoveryTools(server, deps) {
|
|
|
841
854
|
return err(NOT_CONNECTED);
|
|
842
855
|
if (!adapter.saveAsNewFunctionVersion)
|
|
843
856
|
return err('This product does not support save-as-new-version for functions.');
|
|
844
|
-
|
|
857
|
+
// Prompt step: no `tdCopy` → hand back the "Copy Test Data" question so the caller can
|
|
858
|
+
// confirm with the user before writing. Mirrors the FE's COPY TEST DATA modal.
|
|
859
|
+
if (tdCopy === undefined) {
|
|
860
|
+
return ok(`**COPY TEST DATA** — Do you want to copy test data to new version **${versionName}** of ${fnId}?\n\n` +
|
|
861
|
+
`If you do not want to copy the test data, only the column structure will be carried over (one empty ITR-1 per environment).\n\n` +
|
|
862
|
+
`**Ask the user**, then call \`fn_save_as_new_version\` again:\n` +
|
|
863
|
+
`- \`tdCopy: true\` → copy iterations across all environments (Confirm)\n` +
|
|
864
|
+
`- \`tdCopy: false\` → structure only, empty values (Cancel)`, { preview: true, needs: 'tdCopy' });
|
|
865
|
+
}
|
|
845
866
|
try {
|
|
846
867
|
const ref = await adapter.saveAsNewFunctionVersion(fnId, {
|
|
847
868
|
versionName,
|
|
848
|
-
tdCopy
|
|
869
|
+
tdCopy,
|
|
849
870
|
...(sourceVersionId !== undefined ? { sourceVersionId } : {}),
|
|
850
871
|
});
|
|
851
|
-
const tdNote =
|
|
872
|
+
const tdNote = tdCopy
|
|
852
873
|
? ' Testdata iterations were copied across environments.'
|
|
853
874
|
: ' Testdata structure was carried over but values are empty (tdCopy:false).';
|
|
854
875
|
return ok(`Snapshotted ${fnId} as new version **${versionName}**` +
|
|
@@ -878,8 +899,12 @@ export function registerDiscoveryTools(server, deps) {
|
|
|
878
899
|
if (!rows.length)
|
|
879
900
|
return ok('No modules found.', { modules: [], totalCount: totalOf(rows) ?? 0 });
|
|
880
901
|
const text = `${countHeader(rows, 'module')}:\n\n` +
|
|
881
|
-
'| id | name | parent |\n
|
|
882
|
-
rows.map((m) =>
|
|
902
|
+
'| id | name | type | parent | childIds |\n|---|---|---|---|---|\n' +
|
|
903
|
+
rows.map((m) => {
|
|
904
|
+
const type = m.isRoot ? 'root' : m.parentId ? 'sub' : '-';
|
|
905
|
+
const children = m.childIds?.length ? m.childIds.join(', ') : '-';
|
|
906
|
+
return `| ${escCell(m.id)} | ${escCell(m.name)} | ${type} | ${escCell(m.parentId ?? '-')} | ${escCell(children)} |`;
|
|
907
|
+
}).join('\n');
|
|
883
908
|
return ok(text, { modules: rows, totalCount: totalOf(rows) ?? rows.length });
|
|
884
909
|
}
|
|
885
910
|
catch (e) {
|
|
@@ -916,19 +941,29 @@ export function registerDiscoveryTools(server, deps) {
|
|
|
916
941
|
return err(describeError(e));
|
|
917
942
|
}
|
|
918
943
|
});
|
|
944
|
+
const zModuleChild = z.lazy(() => z.object({
|
|
945
|
+
name: z.string().min(1),
|
|
946
|
+
description: z.string().optional(),
|
|
947
|
+
children: z.array(zModuleChild).optional(),
|
|
948
|
+
}));
|
|
919
949
|
server.registerTool('create_module', {
|
|
920
|
-
title: 'Create a module (folder) in the test-case tree',
|
|
950
|
+
title: 'Create a module (folder), optionally with a nested subtree, in the test-case tree',
|
|
921
951
|
description: 'Create a new MODULE under the connected project. Set `isRoot:true` for a top-level folder; ' +
|
|
922
|
-
'otherwise pass `parentId` (resolve via list_modules).
|
|
923
|
-
'
|
|
952
|
+
'otherwise pass `parentId` (resolve via list_modules). To create a WHOLE HIERARCHY in one call ' +
|
|
953
|
+
'(root + submodules + nested submodules), pass a `children` array — each child gets ' +
|
|
954
|
+
'`isRoot:false` and its parentId auto-wired from the tree position (do NOT set `isRoot`/`parentId` ' +
|
|
955
|
+
'on child nodes). Duplicate name in the same project is rejected server-side. On mid-subtree ' +
|
|
956
|
+
'failure the response carries `failedAt`; every node before it is already persisted (no ' +
|
|
957
|
+
'transactional rollback upstream). SimplifyQA-only. Requires a live connection.',
|
|
924
958
|
inputSchema: {
|
|
925
959
|
name: z.string().min(1).describe('Module name. Must be unique within the project.'),
|
|
926
960
|
projectId: z.string().describe('Target project id (see list_projects).'),
|
|
927
961
|
isRoot: z.boolean().describe('True → top-level folder; false → child of `parentId`.'),
|
|
928
|
-
parentId: z.string().optional().describe('Parent module id (required when isRoot is false).'),
|
|
962
|
+
parentId: z.string().optional().describe('Parent module id (required when isRoot is false; MUST be omitted when isRoot is true).'),
|
|
929
963
|
description: z.string().optional().describe('Free-text description.'),
|
|
964
|
+
children: z.array(zModuleChild).optional().describe('Optional subtree under this module. Each child = { name, description?, children? } — no isRoot/parentId (derived).'),
|
|
930
965
|
},
|
|
931
|
-
}, async ({ name, projectId, isRoot, parentId, description }) => {
|
|
966
|
+
}, async ({ name, projectId, isRoot, parentId, description, children }) => {
|
|
932
967
|
const adapter = deps.getAdapter();
|
|
933
968
|
if (!adapter)
|
|
934
969
|
return err(NOT_CONNECTED);
|
|
@@ -937,6 +972,9 @@ export function registerDiscoveryTools(server, deps) {
|
|
|
937
972
|
if (!isRoot && !parentId) {
|
|
938
973
|
return err('parentId is required when isRoot is false.');
|
|
939
974
|
}
|
|
975
|
+
if (isRoot && parentId) {
|
|
976
|
+
return err('parentId must be omitted when isRoot is true — backend silently drops it and the module would land at root, not under the intended parent.');
|
|
977
|
+
}
|
|
940
978
|
try {
|
|
941
979
|
const saved = await adapter.createModule({
|
|
942
980
|
name,
|
|
@@ -944,8 +982,41 @@ export function registerDiscoveryTools(server, deps) {
|
|
|
944
982
|
isRoot,
|
|
945
983
|
...(parentId ? { parentId } : {}),
|
|
946
984
|
...(description ? { description } : {}),
|
|
985
|
+
...(children?.length ? { children } : {}),
|
|
986
|
+
});
|
|
987
|
+
return ok(formatModuleSaved(saved), { saved });
|
|
988
|
+
}
|
|
989
|
+
catch (e) {
|
|
990
|
+
return err(describeError(e));
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
server.registerTool('update_module', {
|
|
994
|
+
title: 'Update a module (rename / edit description)',
|
|
995
|
+
description: 'Edit an existing MODULE — pass `id` plus any of `name`, `description`. Name must remain ' +
|
|
996
|
+
'unique within the project (duplicate rejected server-side). SimplifyQA-only. Requires a ' +
|
|
997
|
+
'live connection.',
|
|
998
|
+
inputSchema: {
|
|
999
|
+
id: z.string().describe('Module id (numeric or business key, e.g. MO_12).'),
|
|
1000
|
+
name: z.string().min(1).optional().describe('New module name.'),
|
|
1001
|
+
description: z.string().optional().describe('New description (pass "" to clear).'),
|
|
1002
|
+
projectId: z.string().optional().describe('Project id; defaults to the connected project.'),
|
|
1003
|
+
},
|
|
1004
|
+
}, async ({ id, name, description, projectId }) => {
|
|
1005
|
+
const adapter = deps.getAdapter();
|
|
1006
|
+
if (!adapter)
|
|
1007
|
+
return err(NOT_CONNECTED);
|
|
1008
|
+
if (!adapter.updateModule)
|
|
1009
|
+
return err('This product has no module tree.');
|
|
1010
|
+
if (name === undefined && description === undefined) {
|
|
1011
|
+
return err('Nothing to update — pass at least `name` or `description`.');
|
|
1012
|
+
}
|
|
1013
|
+
try {
|
|
1014
|
+
const saved = await adapter.updateModule(id, {
|
|
1015
|
+
...(name !== undefined ? { name } : {}),
|
|
1016
|
+
...(description !== undefined ? { description } : {}),
|
|
1017
|
+
...(projectId ? { projectId } : {}),
|
|
947
1018
|
});
|
|
948
|
-
return ok(`
|
|
1019
|
+
return ok(`Updated module ${saved.id || id}.`, { saved });
|
|
949
1020
|
}
|
|
950
1021
|
catch (e) {
|
|
951
1022
|
return err(describeError(e));
|
|
@@ -1084,9 +1155,10 @@ export function registerDiscoveryTools(server, deps) {
|
|
|
1084
1155
|
server.registerTool('suite_list', {
|
|
1085
1156
|
title: 'List test suites',
|
|
1086
1157
|
description: 'List the test suites in the connected product — id, name, type (Manual/Automation, where the ' +
|
|
1087
|
-
'provider has a suite-level type — SimplifyQA does, Walnut doesn\'t), and
|
|
1088
|
-
'
|
|
1089
|
-
'Requires a live connection (qa_connect). Read-only and product-neutral.'
|
|
1158
|
+
'provider has a suite-level type — SimplifyQA does, Walnut doesn\'t), and number of test ' +
|
|
1159
|
+
'cases. Use suite_get to read one with its ordered test cases, or suite_run to execute one. ' +
|
|
1160
|
+
'Requires a live connection (qa_connect). Read-only and product-neutral. When presenting this ' +
|
|
1161
|
+
'as a table, label the count column "Number of testcases", not "Members".',
|
|
1090
1162
|
inputSchema: {},
|
|
1091
1163
|
annotations: { readOnlyHint: true },
|
|
1092
1164
|
}, async () => {
|
|
@@ -1098,7 +1170,7 @@ export function registerDiscoveryTools(server, deps) {
|
|
|
1098
1170
|
if (!rows.length)
|
|
1099
1171
|
return ok('No test suites found.', { suites: [], totalCount: totalOf(rows) ?? 0 });
|
|
1100
1172
|
const text = `${countHeader(rows, 'suite')}:\n\n` +
|
|
1101
|
-
'| id | name | type |
|
|
1173
|
+
'| id | name | type | Number of testcases |\n|---|---|---|---|\n' +
|
|
1102
1174
|
rows.map((s) => `| ${escCell(s.id)} | ${escCell(s.name)} | ${escCell(s.suiteType ?? '-')} | ${s.memberCount ?? '-'} |`).join('\n');
|
|
1103
1175
|
return ok(text, { suites: rows, totalCount: totalOf(rows) ?? rows.length });
|
|
1104
1176
|
}
|
|
@@ -1145,8 +1217,8 @@ export function registerDiscoveryTools(server, deps) {
|
|
|
1145
1217
|
}
|
|
1146
1218
|
});
|
|
1147
1219
|
server.registerTool('suite_get', {
|
|
1148
|
-
title: 'Read one test suite (with
|
|
1149
|
-
description: 'Read a single test suite — its name, description, and ORDERED
|
|
1220
|
+
title: 'Read one test suite (with test cases)',
|
|
1221
|
+
description: 'Read a single test suite — its name, description, and ORDERED test cases — from the ' +
|
|
1150
1222
|
'connected product. Requires a live connection (qa_connect). `id` is the suite id. ' +
|
|
1151
1223
|
'Product-neutral: same shape on any connected product.',
|
|
1152
1224
|
inputSchema: {
|
|
@@ -1172,10 +1244,105 @@ export function registerDiscoveryTools(server, deps) {
|
|
|
1172
1244
|
return err(describeError(e));
|
|
1173
1245
|
}
|
|
1174
1246
|
});
|
|
1247
|
+
server.registerTool('suite_create', {
|
|
1248
|
+
title: 'Create a new suite from a few test cases',
|
|
1249
|
+
description: 'Create a NEW test suite in the connected project from a small set of existing test cases. ' +
|
|
1250
|
+
'`tcIds` accepts numeric ids, business keys (TC_123), OR plain test case NAMES — a name is ' +
|
|
1251
|
+
'resolved by searching the project (tc_list under the hood); an ambiguous name (multiple ' +
|
|
1252
|
+
'matches) or one with zero matches is reported back rather than guessed. NOTE: a suite can ' +
|
|
1253
|
+
'only hold test cases of ONE category (all manual OR all automated) — a mixed set is refused.\n\n' +
|
|
1254
|
+
'NAME: if `name` is omitted, this tool does NOT create anything yet — it resolves the test ' +
|
|
1255
|
+
'cases, SUGGESTS a suite name, and returns a preview. Show the user the suggested name and the ' +
|
|
1256
|
+
'resolved test cases, ask them to confirm it or give their own, then call suite_create again ' +
|
|
1257
|
+
'with `name` set (and the same `tcIds`) to actually create it. Requires a live connection.',
|
|
1258
|
+
inputSchema: {
|
|
1259
|
+
tcIds: z.array(z.string()).min(1).describe('Test case ids, business keys (TC_123), or plain names to include.'),
|
|
1260
|
+
name: z.string().optional().describe('Suite name. Omit to get a suggested name + preview first (does not create).'),
|
|
1261
|
+
description: z.string().optional().describe('Optional suite description.'),
|
|
1262
|
+
},
|
|
1263
|
+
annotations: { readOnlyHint: false },
|
|
1264
|
+
}, async ({ tcIds, name, description }) => {
|
|
1265
|
+
const adapter = deps.getAdapter();
|
|
1266
|
+
if (!adapter)
|
|
1267
|
+
return err(NOT_CONNECTED);
|
|
1268
|
+
if (!adapter.createSuite)
|
|
1269
|
+
return err('This product does not support creating suites.');
|
|
1270
|
+
if (!adapter.getTestCase)
|
|
1271
|
+
return err('This product does not support test-case read.');
|
|
1272
|
+
// Resolve each ref: try it as an id/business key first, else search by name.
|
|
1273
|
+
const resolved = [];
|
|
1274
|
+
const unresolved = [];
|
|
1275
|
+
for (const ref of tcIds) {
|
|
1276
|
+
try {
|
|
1277
|
+
const tc = await adapter.getTestCase(ref);
|
|
1278
|
+
resolved.push({ tcId: tc.id ?? ref, title: tc.title || ref, category: tc.category === 'manual' ? 'manual' : 'automated' });
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
catch {
|
|
1282
|
+
// Not a direct id/key hit — fall through to a name search below.
|
|
1283
|
+
}
|
|
1284
|
+
if (adapter.listTestCases) {
|
|
1285
|
+
try {
|
|
1286
|
+
const matches = await adapter.listTestCases({ search: ref, limit: 5 });
|
|
1287
|
+
if (matches.length === 1) {
|
|
1288
|
+
const m = matches[0];
|
|
1289
|
+
const category = String(m.executionType ?? '').toUpperCase() === 'MANUAL' ? 'manual' : 'automated';
|
|
1290
|
+
resolved.push({ tcId: m.id, title: m.title, category });
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
if (matches.length > 1) {
|
|
1294
|
+
return err(`"${ref}" matches ${matches.length} test cases — be more specific or pass the id: ` +
|
|
1295
|
+
matches.map((m) => `${m.title} (${m.id})`).join(', '));
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
catch {
|
|
1299
|
+
// fall through to unresolved
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
unresolved.push(ref);
|
|
1303
|
+
}
|
|
1304
|
+
if (unresolved.length) {
|
|
1305
|
+
return err(`Could not find a test case for: ${unresolved.join(', ')} — check the id/name, or use ` +
|
|
1306
|
+
'tc_list to search first.');
|
|
1307
|
+
}
|
|
1308
|
+
// Same rule the backend enforces: a suite can only hold ONE category. Catch a mismatch here,
|
|
1309
|
+
// before suggesting a name / asking the user to confirm — no point previewing a suite that
|
|
1310
|
+
// createSuite would refuse anyway.
|
|
1311
|
+
const categories = new Set(resolved.map((r) => r.category));
|
|
1312
|
+
if (categories.size > 1) {
|
|
1313
|
+
const byCategory = (cat) => resolved.filter((r) => r.category === cat).map((r) => `${r.title} (${r.tcId})`).join(', ') || '(none)';
|
|
1314
|
+
return err('Cannot create suite: the test cases are a mix of manual and automated — a suite can only ' +
|
|
1315
|
+
`hold ONE category. Manual: ${byCategory('manual')}. Automated: ${byCategory('automated')}. ` +
|
|
1316
|
+
'Split them into two suites, or pick only same-category test cases.');
|
|
1317
|
+
}
|
|
1318
|
+
if (!name) {
|
|
1319
|
+
const suggested = resolved.length === 1
|
|
1320
|
+
? `${resolved[0].title} Suite`
|
|
1321
|
+
: `${resolved[0].title} + ${resolved.length - 1} more Suite`;
|
|
1322
|
+
return ok(`Resolved ${resolved.length} test case${resolved.length === 1 ? '' : 's'}: ` +
|
|
1323
|
+
`${resolved.map((r) => `${r.title} (${r.tcId})`).join(', ')}.\n\n` +
|
|
1324
|
+
`Suggested suite name: "${suggested}" — ASK the user to confirm this or give their own ` +
|
|
1325
|
+
`name, then call suite_create again with { name, tcIds } (reuse the same tcIds, or the ` +
|
|
1326
|
+
`resolved ids: ${resolved.map((r) => r.tcId).join(', ')}).`, { preview: true, suggestedName: suggested, testCases: resolved });
|
|
1327
|
+
}
|
|
1328
|
+
try {
|
|
1329
|
+
const ref = await adapter.createSuite({
|
|
1330
|
+
name,
|
|
1331
|
+
...(description !== undefined ? { description } : {}),
|
|
1332
|
+
members: resolved.map((r, i) => ({ tcId: r.tcId, order: i + 1 })),
|
|
1333
|
+
});
|
|
1334
|
+
return ok(`Created suite **${name}**${ref.id ? ` · ${ref.id}` : ''} with ${resolved.length} test ` +
|
|
1335
|
+
`case${resolved.length === 1 ? '' : 's'}: ${resolved.map((r) => r.title).join(', ')}.` +
|
|
1336
|
+
(ref.productUrl ? ` ${ref.productUrl}` : ''), { savedRef: ref });
|
|
1337
|
+
}
|
|
1338
|
+
catch (e) {
|
|
1339
|
+
return err(describeError(e));
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1175
1342
|
// --- Suite membership edits (writes) --------------------------------------
|
|
1176
1343
|
// Kept beside suite_list/suite_get so all suite tools live together; they mutate
|
|
1177
1344
|
// via the adapter's updateSuiteMembers (→ update_suite_members provider-op).
|
|
1178
|
-
const
|
|
1345
|
+
const testCasesTable = (suite) => suite.members.length
|
|
1179
1346
|
? '\n\n| order | test case |\n|---|---|\n' +
|
|
1180
1347
|
suite.members.map((m) => `| ${m.order} | ${escCell(m.tcId)} |`).join('\n')
|
|
1181
1348
|
: '';
|
|
@@ -1184,7 +1351,7 @@ export function registerDiscoveryTools(server, deps) {
|
|
|
1184
1351
|
description: 'Add one or more EXISTING test cases to a suite (appended in order). `tcIds` accepts ids or ' +
|
|
1185
1352
|
'business keys (e.g. TC_123) — use suite_list / tc_list to find them. NOTE: a suite can only ' +
|
|
1186
1353
|
'hold test cases of ONE category (all manual OR all automated) — the product rejects a mix. ' +
|
|
1187
|
-
'Requires a live connection. Returns the updated suite with its
|
|
1354
|
+
'Requires a live connection. Returns the updated suite with its test cases.',
|
|
1188
1355
|
inputSchema: {
|
|
1189
1356
|
suiteId: z.string().describe('The suite id to add to (from suite_list).'),
|
|
1190
1357
|
tcIds: z.array(z.string()).min(1).describe('Test case ids / business keys to add.'),
|
|
@@ -1209,8 +1376,8 @@ export function registerDiscoveryTools(server, deps) {
|
|
|
1209
1376
|
}
|
|
1210
1377
|
}));
|
|
1211
1378
|
return ok(`Added ${added.length} test case${added.length === 1 ? '' : 's'} to **${suite.name}**: ` +
|
|
1212
|
-
`${added.join(', ')} — now ${n}
|
|
1213
|
-
|
|
1379
|
+
`${added.join(', ')} — now ${n} test case${n === 1 ? '' : 's'} total.` +
|
|
1380
|
+
testCasesTable(suite), { suite });
|
|
1214
1381
|
}
|
|
1215
1382
|
catch (e) {
|
|
1216
1383
|
return err(describeError(e));
|
|
@@ -1219,8 +1386,8 @@ export function registerDiscoveryTools(server, deps) {
|
|
|
1219
1386
|
server.registerTool('suite_remove_testcases', {
|
|
1220
1387
|
title: 'Remove test cases from a suite',
|
|
1221
1388
|
description: 'Remove one or more test cases from a suite. `tcIds` accepts ids or business keys; every ' +
|
|
1222
|
-
'occurrence of each is removed and the remaining
|
|
1223
|
-
'connection. Returns the updated suite with its
|
|
1389
|
+
'occurrence of each is removed and the remaining test cases are reordered. Requires a live ' +
|
|
1390
|
+
'connection. Returns the updated suite with its test cases.',
|
|
1224
1391
|
inputSchema: {
|
|
1225
1392
|
suiteId: z.string().describe('The suite id to remove from.'),
|
|
1226
1393
|
tcIds: z.array(z.string()).min(1).describe('Test case ids / business keys to remove.'),
|
|
@@ -1234,8 +1401,8 @@ export function registerDiscoveryTools(server, deps) {
|
|
|
1234
1401
|
const ops = tcIds.map((tcId) => ({ op: 'remove', tcId }));
|
|
1235
1402
|
const suite = await adapter.updateSuiteMembers(suiteId, ops);
|
|
1236
1403
|
const n = suite.members.length;
|
|
1237
|
-
return ok(`Removed ${tcIds.length} test case${tcIds.length === 1 ? '' : 's'} from **${suite.name}** — now ${n}
|
|
1238
|
-
|
|
1404
|
+
return ok(`Removed ${tcIds.length} test case${tcIds.length === 1 ? '' : 's'} from **${suite.name}** — now ${n} test case${n === 1 ? '' : 's'} total.` +
|
|
1405
|
+
testCasesTable(suite), { suite });
|
|
1239
1406
|
}
|
|
1240
1407
|
catch (e) {
|
|
1241
1408
|
return err(describeError(e));
|