common-qa-mcp 0.3.0 → 0.4.1
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/factory.d.ts +1 -1
- package/dist/adapters/factory.d.ts.map +1 -1
- package/dist/adapters/factory.js +12 -29
- package/dist/adapters/factory.js.map +1 -1
- package/dist/adapters/http.js +2 -3
- package/dist/adapters/http.js.map +1 -1
- package/dist/adapters/remoteMcp/adapter.d.ts +7 -2
- package/dist/adapters/remoteMcp/adapter.d.ts.map +1 -1
- package/dist/adapters/remoteMcp/adapter.js +9 -3
- package/dist/adapters/remoteMcp/adapter.js.map +1 -1
- package/dist/adapters/simplifyqa/adapter.d.ts +309 -23
- package/dist/adapters/simplifyqa/adapter.d.ts.map +1 -1
- package/dist/adapters/simplifyqa/adapter.js +2042 -86
- package/dist/adapters/simplifyqa/adapter.js.map +1 -1
- package/dist/adapters/simplifyqa/mapper.d.ts +43 -11
- package/dist/adapters/simplifyqa/mapper.d.ts.map +1 -1
- package/dist/adapters/simplifyqa/mapper.js +470 -21
- package/dist/adapters/simplifyqa/mapper.js.map +1 -1
- package/dist/adapters/types.d.ts +126 -5
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/adapters/walnut/adapter.d.ts +6 -2
- package/dist/adapters/walnut/adapter.d.ts.map +1 -1
- package/dist/adapters/walnut/adapter.js +33 -2
- package/dist/adapters/walnut/adapter.js.map +1 -1
- package/dist/adapters/walnut/mapper.d.ts +2 -1
- package/dist/adapters/walnut/mapper.d.ts.map +1 -1
- package/dist/adapters/walnut/mapper.js +13 -0
- package/dist/adapters/walnut/mapper.js.map +1 -1
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +36 -0
- package/dist/config.js.map +1 -1
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +15 -7
- package/dist/connection.js.map +1 -1
- package/dist/domain/types.d.ts +485 -4
- package/dist/domain/types.d.ts.map +1 -1
- package/dist/initAgents.d.ts.map +1 -1
- package/dist/initAgents.js +22 -9
- package/dist/initAgents.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +23 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/backlog.tools.d.ts +13 -0
- package/dist/tools/backlog.tools.d.ts.map +1 -0
- package/dist/tools/backlog.tools.js +399 -0
- package/dist/tools/backlog.tools.js.map +1 -0
- package/dist/tools/defects.tools.d.ts +7 -0
- package/dist/tools/defects.tools.d.ts.map +1 -0
- package/dist/tools/defects.tools.js +82 -0
- package/dist/tools/defects.tools.js.map +1 -0
- package/dist/tools/discovery.tools.d.ts.map +1 -1
- package/dist/tools/discovery.tools.js +912 -55
- package/dist/tools/discovery.tools.js.map +1 -1
- package/dist/tools/markdown.d.ts +3 -3
- package/dist/tools/markdown.d.ts.map +1 -1
- package/dist/tools/markdown.js +89 -18
- package/dist/tools/markdown.js.map +1 -1
- package/dist/tools/objects.tools.d.ts +7 -0
- package/dist/tools/objects.tools.d.ts.map +1 -0
- package/dist/tools/objects.tools.js +201 -0
- package/dist/tools/objects.tools.js.map +1 -0
- package/dist/tools/recording.tools.d.ts.map +1 -1
- package/dist/tools/recording.tools.js +64 -5
- 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 +102 -18
- package/dist/tools/run.tools.js.map +1 -1
- package/dist/tools/schemas.d.ts +21 -4
- package/dist/tools/schemas.d.ts.map +1 -1
- package/dist/tools/schemas.js +14 -1
- package/dist/tools/schemas.js.map +1 -1
- package/dist/tools/tasks.tools.d.ts +7 -0
- package/dist/tools/tasks.tools.d.ts.map +1 -0
- package/dist/tools/tasks.tools.js +75 -0
- package/dist/tools/tasks.tools.js.map +1 -0
- package/dist/tools/testcase.tools.d.ts.map +1 -1
- package/dist/tools/testcase.tools.js +146 -100
- package/dist/tools/testcase.tools.js.map +1 -1
- package/dist/tools/testdata.tools.d.ts.map +1 -1
- package/dist/tools/testdata.tools.js +99 -11
- package/dist/tools/testdata.tools.js.map +1 -1
- package/package.json +3 -3
|
@@ -13,6 +13,15 @@ export function pickArray(n, ...keys) {
|
|
|
13
13
|
}
|
|
14
14
|
return Array.isArray(n) ? n : [];
|
|
15
15
|
}
|
|
16
|
+
/** Hang `totalCount` off a mapped array as a side-band prop — SQA search responses expose it
|
|
17
|
+
* at the top level as `totalCount` (with `total`/`totalRecords` as tolerant fallbacks). Undefined
|
|
18
|
+
* when the raw response omits it (callers show `rows.length` as the total in that case). */
|
|
19
|
+
export function withTotal(rows, raw) {
|
|
20
|
+
const t = raw?.totalCount ?? raw?.total ?? raw?.totalRecords;
|
|
21
|
+
if (typeof t === 'number')
|
|
22
|
+
rows.totalCount = t;
|
|
23
|
+
return rows;
|
|
24
|
+
}
|
|
16
25
|
export function str(n, ...keys) {
|
|
17
26
|
if (n == null)
|
|
18
27
|
return undefined;
|
|
@@ -33,6 +42,31 @@ export function numericId(id) {
|
|
|
33
42
|
const m = id.match(/(\d+)/);
|
|
34
43
|
return m ? m[1] : id;
|
|
35
44
|
}
|
|
45
|
+
/** Map raw `tcfn_version` rows → neutral `TcVersion[]`. Shared between TC (versions inlined on
|
|
46
|
+
* the `/tm/testcase/getById` doc) and FN (versions come from `/tm/function/version/:id/:pid`).
|
|
47
|
+
* `currentVersionId` stamps `isCurrent:true` on the matching entry — pass undefined if unknown. */
|
|
48
|
+
export function toVersionEntries(rawVersions, currentVersionId) {
|
|
49
|
+
return (Array.isArray(rawVersions) ? rawVersions : [])
|
|
50
|
+
.map((v) => {
|
|
51
|
+
const vid = str(v, 'id', '_id');
|
|
52
|
+
if (!vid)
|
|
53
|
+
return null;
|
|
54
|
+
const name = str(v, 'name') ?? vid;
|
|
55
|
+
const entry = { id: vid, name };
|
|
56
|
+
if (typeof v?.updateVersion === 'number')
|
|
57
|
+
entry.updateVersion = v.updateVersion;
|
|
58
|
+
const rev = str(v, 'reviewApproveStatus');
|
|
59
|
+
if (rev)
|
|
60
|
+
entry.reviewApproveStatus = rev;
|
|
61
|
+
const ex = str(v, 'executionType');
|
|
62
|
+
if (ex)
|
|
63
|
+
entry.executionType = ex;
|
|
64
|
+
if (currentVersionId && vid === currentVersionId)
|
|
65
|
+
entry.isCurrent = true;
|
|
66
|
+
return entry;
|
|
67
|
+
})
|
|
68
|
+
.filter((v) => v !== null);
|
|
69
|
+
}
|
|
36
70
|
/** SQA priority is UPPERCASE on the wire; neutral is lowercase. Unknown → 'medium'. */
|
|
37
71
|
export function mapPriority(p) {
|
|
38
72
|
const s = typeof p === 'string' ? p.toLowerCase() : '';
|
|
@@ -47,19 +81,69 @@ export function mapPriority(p) {
|
|
|
47
81
|
// Both use the same field names, so ONE mapper covers both. See adapter.ts
|
|
48
82
|
// getTestCase for the read strategy (collection wins; inline is a fallback).
|
|
49
83
|
// ---------------------------------------------------------------------------
|
|
84
|
+
export const STEP_BLOCK_END_TYPES = new Set([
|
|
85
|
+
'ELSE', 'ELSE_IF', 'IF_END', 'FOR_END', 'WHILE', 'WHILE_END', 'ITERATOR_END',
|
|
86
|
+
'LOOP_ITERATION_END', 'FUNCTION_END',
|
|
87
|
+
]);
|
|
88
|
+
export const STEP_LOOP_START_TYPES = new Set([
|
|
89
|
+
'FOR_START', 'WHILE_START', 'ITERATOR_START', 'LOOP_ITERATION_START', 'DO',
|
|
90
|
+
]);
|
|
91
|
+
// SQA raw `stepType` → neutral Step.stepType + Step.controlFlow.kind. Enumerated from the
|
|
92
|
+
// backend's own union (tcfn.service.js:2176 stepTypeMapping + validators). Everything not in
|
|
93
|
+
// SIMPLE / CF_KINDS falls through as a plain automation step.
|
|
94
|
+
const SIMPLE_STEP_KINDS = {
|
|
95
|
+
NORMAL_STATEMENT: 'web',
|
|
96
|
+
API: 'api',
|
|
97
|
+
COMPARE: 'compare',
|
|
98
|
+
};
|
|
99
|
+
const CF_KINDS = new Set([
|
|
100
|
+
'IF', 'ELSE', 'ELSE_IF', 'IF_END',
|
|
101
|
+
'WHILE_START', 'WHILE', 'WHILE_END',
|
|
102
|
+
'DO', 'DO_WHILE',
|
|
103
|
+
'FOR_START', 'FOR_END', 'LOOP',
|
|
104
|
+
'ITERATOR_START', 'ITERATOR_END',
|
|
105
|
+
'LOOP_ITERATION_START', 'LOOP_ITERATION_END',
|
|
106
|
+
]);
|
|
107
|
+
const SWITCH_KINDS = new Set([
|
|
108
|
+
'SWITCH_TO_API', 'SWITCH_TO_DB', 'SWITCH_TO_WEB', 'SWITCH_TO_MAINFRAME',
|
|
109
|
+
'SWITCH_TO_DESKTOP', 'SWITCH_TO_SAP', 'SWITCH_TO_IOS', 'SWITCH_TO_ANDROID',
|
|
110
|
+
]);
|
|
111
|
+
/** Squash a step's `recordedParam: { <paramId>: value }` map into a single readable cell
|
|
112
|
+
* ("packageName=us.zoom, activity=..."). Names would be nicer than ids, but the id→name
|
|
113
|
+
* resolution requires the project parameter dictionary — deferred; the raw id is unambiguous
|
|
114
|
+
* and keeps this pure. */
|
|
115
|
+
function formatRecordedParam(rp) {
|
|
116
|
+
if (!rp || typeof rp !== 'object')
|
|
117
|
+
return undefined;
|
|
118
|
+
const entries = Object.entries(rp).filter(([, v]) => v != null && v !== '');
|
|
119
|
+
if (!entries.length)
|
|
120
|
+
return undefined;
|
|
121
|
+
return entries.map(([k, v]) => `${k}=${String(v)}`).join(', ');
|
|
122
|
+
}
|
|
50
123
|
export function fromSqaStep(s, fallbackSeq) {
|
|
51
124
|
const sid = str(s, 'id', '_id', 'stepId');
|
|
52
125
|
const description = s?.description ?? '';
|
|
53
126
|
const expectedResult = s?.expectedResult ?? undefined;
|
|
54
127
|
const actionType = str(s, 'actionId', 'actionType') ?? '';
|
|
55
128
|
const seq = typeof s?.seq === 'number' ? s.seq : fallbackSeq;
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
129
|
+
const rawStepType = String(s?.stepType ?? '').toUpperCase();
|
|
130
|
+
// Neutral stepType routing. Manual TCs have executionType:'MANUAL' at the STEP level — that's
|
|
131
|
+
// the only reliable signal for the neutral 'manual' bucket (validators show `executionType`
|
|
132
|
+
// is a per-step string). Otherwise SIMPLE_STEP_KINDS wins, then CF, then FUNCTION, then a
|
|
133
|
+
// safe automation fallback.
|
|
134
|
+
let stepType = 'web';
|
|
135
|
+
if (str(s, 'executionType')?.toUpperCase() === 'MANUAL')
|
|
136
|
+
stepType = 'manual';
|
|
137
|
+
else if (SIMPLE_STEP_KINDS[rawStepType])
|
|
138
|
+
stepType = SIMPLE_STEP_KINDS[rawStepType];
|
|
139
|
+
else if (rawStepType === 'FUNCTION')
|
|
140
|
+
stepType = 'function';
|
|
141
|
+
else if (rawStepType === 'FUNCTION_END')
|
|
142
|
+
stepType = 'function_end';
|
|
143
|
+
else if (CF_KINDS.has(rawStepType))
|
|
144
|
+
stepType = 'control_flow';
|
|
145
|
+
else if (SWITCH_KINDS.has(rawStepType))
|
|
146
|
+
stepType = 'switch';
|
|
63
147
|
// Object linkage: SQA stores it as either scalar `objectId` OR nested `object.id`.
|
|
64
148
|
// Preserving the FK is what lets tc_heal / object_update_locators find the shared object.
|
|
65
149
|
let objectId = str(s, 'objectId');
|
|
@@ -78,6 +162,38 @@ export function fromSqaStep(s, fallbackSeq) {
|
|
|
78
162
|
if (objectId && objectId.toLowerCase() !== 'null') {
|
|
79
163
|
step.objectRef = { id: objectId, ...(objectName ? { name: objectName } : {}) };
|
|
80
164
|
}
|
|
165
|
+
// Control-flow: keep the RAW SQA kind on controlFlow.kind so the renderer can label it exactly
|
|
166
|
+
// as the UI does. `conditions.ifstring` is the human-readable expression the SQA UI shows in the
|
|
167
|
+
// condition column — carry it into conditionText.
|
|
168
|
+
if (stepType === 'control_flow' || stepType === 'function_end') {
|
|
169
|
+
const conditionText = str(s?.conditions, 'ifstring');
|
|
170
|
+
step.controlFlow = {
|
|
171
|
+
kind: rawStepType === 'FUNCTION_END' ? 'FUNCTION_END' : rawStepType,
|
|
172
|
+
...(conditionText ? { conditionText } : {}),
|
|
173
|
+
...(typeof s?.loopDuration === 'number' ? { maxIterations: s.loopDuration } : {}),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// FUNCTION step — carry every field the ROUND-TRIP save + a human render need. Backend
|
|
177
|
+
// sends functionId as a number, fnVersionId as a string uuid; the neutral surface is string.
|
|
178
|
+
if (stepType === 'function') {
|
|
179
|
+
const fnId = str(s, 'functionId');
|
|
180
|
+
const fnVersion = str(s, 'fnVersionId');
|
|
181
|
+
const sharedProject = str(s, 'sharedFnProjectId');
|
|
182
|
+
const sharedName = str(s, 'sharedFnProjectName');
|
|
183
|
+
if (fnId) {
|
|
184
|
+
step.functionCall = {
|
|
185
|
+
testCaseId: fnId,
|
|
186
|
+
...(fnVersion ? { versionId: fnVersion } : {}),
|
|
187
|
+
...(sharedProject ? { sharedProjectId: sharedProject } : {}),
|
|
188
|
+
...(sharedName ? { testCaseName: sharedName } : {}),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Value column: prefer recordedParam summary (concrete values) → falls through to parameterIds
|
|
193
|
+
// which surface as `${name}` tokens in the description already.
|
|
194
|
+
const recordedSummary = formatRecordedParam(s?.recordedParam);
|
|
195
|
+
if (recordedSummary)
|
|
196
|
+
step.value = recordedSummary;
|
|
81
197
|
return step;
|
|
82
198
|
}
|
|
83
199
|
// ---------------------------------------------------------------------------
|
|
@@ -112,6 +228,14 @@ export function toTestCase(doc) {
|
|
|
112
228
|
rawSteps = stepsNode;
|
|
113
229
|
}
|
|
114
230
|
const steps = rawSteps.map((s, i) => fromSqaStep(s, i + 1));
|
|
231
|
+
// Versions: `/tm/testcase/getById` hangs a `versions[]` array on the doc (tcfn.service.js:1489),
|
|
232
|
+
// one entry per non-deleted tcfn_version. Current-version resolution mirrors the FE:
|
|
233
|
+
// `versionId` / `selectedVersionId` on the doc wins, else the doc's `versionsDetails.id`.
|
|
234
|
+
const currentVersionId = str(doc, 'versionId', 'selectedVersionId') ??
|
|
235
|
+
(doc?.versionsDetails && typeof doc.versionsDetails === 'object'
|
|
236
|
+
? str(doc.versionsDetails, 'id', '_id', 'versionId')
|
|
237
|
+
: undefined);
|
|
238
|
+
const versions = toVersionEntries(Array.isArray(doc?.versions) ? doc.versions : [], currentVersionId);
|
|
115
239
|
return {
|
|
116
240
|
...(id ? { id } : {}),
|
|
117
241
|
title: str(doc, 'name', 'title') ?? '',
|
|
@@ -123,6 +247,8 @@ export function toTestCase(doc) {
|
|
|
123
247
|
...(userstoryId ? { storyRef: userstoryId } : {}),
|
|
124
248
|
...(moduleId ? { folderOrModule: moduleId } : {}),
|
|
125
249
|
steps,
|
|
250
|
+
...(versions.length ? { versions } : {}),
|
|
251
|
+
...(currentVersionId ? { currentVersionId } : {}),
|
|
126
252
|
};
|
|
127
253
|
}
|
|
128
254
|
/** Overwrite `tc.steps` with the mapped collection-endpoint rows when non-empty. */
|
|
@@ -132,18 +258,57 @@ export function applyCollectionSteps(tc, rows) {
|
|
|
132
258
|
const mapped = rows.map((s, i) => fromSqaStep(s, i + 1));
|
|
133
259
|
return { ...tc, steps: mapped };
|
|
134
260
|
}
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// Functions (reusable). Same `tcfnObj` collection as TCs — type='FN' instead of 'TC'.
|
|
263
|
+
// Payload shape overlaps with TCs (id/code/name/description/executionType/status), plus
|
|
264
|
+
// `fnType` (WEB/API/ANDROID/IOS) in place of `tcType`.
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
export function toFunctionSummary(row) {
|
|
267
|
+
const id = str(row, 'id', '_id', 'functionId') ?? '';
|
|
268
|
+
const code = str(row, 'code');
|
|
269
|
+
const name = str(row, 'name', 'title') ?? '';
|
|
270
|
+
const description = str(row, 'description');
|
|
271
|
+
const fnType = str(row, 'fnType', 'tcType');
|
|
272
|
+
const executionType = str(row, 'executionType');
|
|
273
|
+
const status = str(row, 'reviewApproveStatus', 'status');
|
|
274
|
+
const stepCount = typeof row?.stepCount === 'number' ? row.stepCount : undefined;
|
|
275
|
+
return {
|
|
276
|
+
id,
|
|
277
|
+
...(code ? { code } : {}),
|
|
278
|
+
name,
|
|
279
|
+
...(description ? { description } : {}),
|
|
280
|
+
...(fnType ? { fnType } : {}),
|
|
281
|
+
...(executionType ? { executionType } : {}),
|
|
282
|
+
...(status ? { status } : {}),
|
|
283
|
+
...(stepCount !== undefined ? { stepCount } : {}),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/** Full function doc → neutral QaFunction. Steps are attached separately by the adapter (fetched
|
|
287
|
+
* from /tm/function/steps, same collection endpoint TCs use — just type='FN'). */
|
|
288
|
+
export function toFunction(row) {
|
|
289
|
+
const summary = toFunctionSummary(row);
|
|
290
|
+
return { ...summary, steps: [] };
|
|
291
|
+
}
|
|
135
292
|
export function toTcSummary(row) {
|
|
136
293
|
const id = str(row, 'id', '_id', 'testcaseId') ?? '';
|
|
294
|
+
const code = str(row, 'code');
|
|
137
295
|
const title = str(row, 'name', 'title') ?? '';
|
|
138
296
|
const priority = (str(row, 'priority') ?? 'medium').toLowerCase();
|
|
139
297
|
const status = str(row, 'status');
|
|
140
298
|
const stepCount = typeof row?.stepCount === 'number' ? row.stepCount : undefined;
|
|
299
|
+
const technologyType = str(row, 'tcType');
|
|
300
|
+
const executionType = str(row, 'executionType');
|
|
301
|
+
const obsolete = typeof row?.obsolete === 'boolean' ? row.obsolete : undefined;
|
|
141
302
|
return {
|
|
142
303
|
id,
|
|
304
|
+
...(code ? { code } : {}),
|
|
143
305
|
title,
|
|
144
306
|
priority,
|
|
145
307
|
...(status ? { status } : {}),
|
|
146
308
|
...(stepCount !== undefined ? { stepCount } : {}),
|
|
309
|
+
...(technologyType ? { technologyType } : {}),
|
|
310
|
+
...(executionType ? { executionType } : {}),
|
|
311
|
+
...(obsolete !== undefined ? { obsolete } : {}),
|
|
147
312
|
};
|
|
148
313
|
}
|
|
149
314
|
// ---------------------------------------------------------------------------
|
|
@@ -153,7 +318,13 @@ export function toSuiteSummary(row) {
|
|
|
153
318
|
const id = str(row, 'id', '_id', 'suiteId') ?? '';
|
|
154
319
|
const name = str(row, 'name', 'suiteName') ?? '';
|
|
155
320
|
const memberCount = Array.isArray(row?.testCases) ? row.testCases.length : undefined;
|
|
156
|
-
|
|
321
|
+
const suiteType = str(row, 'suiteType');
|
|
322
|
+
return {
|
|
323
|
+
id,
|
|
324
|
+
name,
|
|
325
|
+
...(memberCount !== undefined ? { memberCount } : {}),
|
|
326
|
+
...(suiteType ? { suiteType } : {}),
|
|
327
|
+
};
|
|
157
328
|
}
|
|
158
329
|
export function toSuite(doc) {
|
|
159
330
|
const id = str(doc, 'id', '_id', 'suiteId');
|
|
@@ -188,6 +359,56 @@ export function toStorySummary(row) {
|
|
|
188
359
|
...(priority ? { priority } : {}),
|
|
189
360
|
};
|
|
190
361
|
}
|
|
362
|
+
export function toDefectSummary(row) {
|
|
363
|
+
const id = str(row, 'id', '_id') ?? '';
|
|
364
|
+
const code = str(row, 'code');
|
|
365
|
+
const title = str(row, 'name', 'title') ?? '';
|
|
366
|
+
const state = str(row, 'state', 'status');
|
|
367
|
+
const severity = str(row, 'severity');
|
|
368
|
+
const priority = str(row, 'priority');
|
|
369
|
+
// `ownerId` is an ARRAY on the defect row (a defect can have several assignees) — see
|
|
370
|
+
// defect-interface.ts `ownerId: number[]` — unlike the single-value `ownerId` on stories/testcases.
|
|
371
|
+
const assignedTo = Array.isArray(row?.ownerId)
|
|
372
|
+
? row.ownerId.map((v) => String(v ?? '')).filter((v) => v.length > 0)
|
|
373
|
+
: undefined;
|
|
374
|
+
const tcIds = Array.isArray(row?.selectedTestCase)
|
|
375
|
+
? row.selectedTestCase
|
|
376
|
+
.map((tc) => (typeof tc === 'object' ? str(tc, 'testcaseId') : String(tc ?? '')))
|
|
377
|
+
.filter((v) => !!v)
|
|
378
|
+
: undefined;
|
|
379
|
+
return {
|
|
380
|
+
id,
|
|
381
|
+
...(code ? { code } : {}),
|
|
382
|
+
title,
|
|
383
|
+
...(state ? { state } : {}),
|
|
384
|
+
...(severity ? { severity } : {}),
|
|
385
|
+
...(priority ? { priority } : {}),
|
|
386
|
+
...(assignedTo?.length ? { assignedTo } : {}),
|
|
387
|
+
...(tcIds?.length ? { tcIds } : {}),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
/** Maps a `/pm/task/search` row (see `Itask` in sqa-ui) to the neutral shape. `ownerId` is an array
|
|
391
|
+
* on the task row, same as on a defect. */
|
|
392
|
+
export function toTaskSummary(row) {
|
|
393
|
+
const id = str(row, 'id', '_id') ?? '';
|
|
394
|
+
const code = str(row, 'code');
|
|
395
|
+
const title = str(row, 'name', 'title') ?? '';
|
|
396
|
+
const state = str(row, 'state', 'status');
|
|
397
|
+
const priority = str(row, 'priority');
|
|
398
|
+
const assignedTo = Array.isArray(row?.ownerId)
|
|
399
|
+
? row.ownerId.map((v) => String(v ?? '')).filter((v) => v.length > 0)
|
|
400
|
+
: undefined;
|
|
401
|
+
const defectId = typeof row?.defectId === 'number' && row.defectId > 0 ? String(row.defectId) : undefined;
|
|
402
|
+
return {
|
|
403
|
+
id,
|
|
404
|
+
...(code ? { code } : {}),
|
|
405
|
+
title,
|
|
406
|
+
...(state ? { state } : {}),
|
|
407
|
+
...(priority ? { priority } : {}),
|
|
408
|
+
...(assignedTo?.length ? { assignedTo } : {}),
|
|
409
|
+
...(defectId ? { defectId } : {}),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
191
412
|
export function toStory(doc) {
|
|
192
413
|
const base = toStorySummary(doc);
|
|
193
414
|
const description = str(doc, 'description', 'storyDescription');
|
|
@@ -208,6 +429,70 @@ export function toStory(doc) {
|
|
|
208
429
|
};
|
|
209
430
|
}
|
|
210
431
|
// ---------------------------------------------------------------------------
|
|
432
|
+
// Epics (`pm/epic`) and features (`pm/feature`). Business keys come back as
|
|
433
|
+
// `code` (EP_XXX / FT_XXX); numeric doc id is `id` (with `_id` as fallback).
|
|
434
|
+
// ---------------------------------------------------------------------------
|
|
435
|
+
export function toEpicSummary(row) {
|
|
436
|
+
const id = str(row, 'id', '_id', 'epicId') ?? '';
|
|
437
|
+
const key = str(row, 'code', 'key');
|
|
438
|
+
const name = str(row, 'name', 'epicName') ?? '';
|
|
439
|
+
const state = str(row, 'state', 'status');
|
|
440
|
+
return {
|
|
441
|
+
id,
|
|
442
|
+
...(key ? { key } : {}),
|
|
443
|
+
name,
|
|
444
|
+
...(state ? { state } : {}),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
export function toEpic(doc) {
|
|
448
|
+
const base = toEpicSummary(doc);
|
|
449
|
+
const description = str(doc, 'description');
|
|
450
|
+
const plannedStartDate = str(doc, 'plannedStartDate');
|
|
451
|
+
const plannedEndDate = str(doc, 'plannedEndDate');
|
|
452
|
+
const rawEffort = doc?.estimatedEffort;
|
|
453
|
+
const rawOwner = doc?.ownerId;
|
|
454
|
+
return {
|
|
455
|
+
...base,
|
|
456
|
+
...(description ? { description } : {}),
|
|
457
|
+
...(typeof rawEffort === 'number' ? { estimatedEffort: rawEffort } : {}),
|
|
458
|
+
...(typeof rawOwner === 'number' ? { ownerId: rawOwner } : {}),
|
|
459
|
+
...(plannedStartDate ? { plannedStartDate } : {}),
|
|
460
|
+
...(plannedEndDate ? { plannedEndDate } : {}),
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
export function toFeatureSummary(row) {
|
|
464
|
+
const id = str(row, 'id', '_id', 'featureId') ?? '';
|
|
465
|
+
const key = str(row, 'code', 'key');
|
|
466
|
+
const name = str(row, 'name', 'featureName') ?? '';
|
|
467
|
+
const state = str(row, 'state', 'status');
|
|
468
|
+
const priority = str(row, 'priority');
|
|
469
|
+
const epicId = str(row, 'epicId');
|
|
470
|
+
return {
|
|
471
|
+
id,
|
|
472
|
+
...(key ? { key } : {}),
|
|
473
|
+
name,
|
|
474
|
+
...(state ? { state } : {}),
|
|
475
|
+
...(priority ? { priority } : {}),
|
|
476
|
+
...(epicId ? { epicId } : {}),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
export function toFeature(doc) {
|
|
480
|
+
const base = toFeatureSummary(doc);
|
|
481
|
+
const description = str(doc, 'description');
|
|
482
|
+
const plannedStartDate = str(doc, 'plannedStartDate');
|
|
483
|
+
const plannedEndDate = str(doc, 'plannedEndDate');
|
|
484
|
+
const rawEffort = doc?.estimatedEffort;
|
|
485
|
+
const rawOwner = doc?.ownerId;
|
|
486
|
+
return {
|
|
487
|
+
...base,
|
|
488
|
+
...(description ? { description } : {}),
|
|
489
|
+
...(typeof rawEffort === 'number' ? { estimatedEffort: rawEffort } : {}),
|
|
490
|
+
...(typeof rawOwner === 'number' ? { ownerId: rawOwner } : {}),
|
|
491
|
+
...(plannedStartDate ? { plannedStartDate } : {}),
|
|
492
|
+
...(plannedEndDate ? { plannedEndDate } : {}),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
// ---------------------------------------------------------------------------
|
|
211
496
|
// Modules, releases, sprints.
|
|
212
497
|
// ---------------------------------------------------------------------------
|
|
213
498
|
export function toModuleSummary(row) {
|
|
@@ -264,7 +549,7 @@ export function toObject(doc) {
|
|
|
264
549
|
value: String(a.value),
|
|
265
550
|
...(a.unique === true ? { unique: true } : {}),
|
|
266
551
|
}));
|
|
267
|
-
const name = str(doc, '
|
|
552
|
+
const name = str(doc, 'disname', 'description', 'placeholder') ?? '';
|
|
268
553
|
const objectType = str(doc, 'technology');
|
|
269
554
|
return {
|
|
270
555
|
...(id ? { id } : {}),
|
|
@@ -307,19 +592,50 @@ export function paramDefault(name, data, stepValue) {
|
|
|
307
592
|
return String(data[name] ?? '');
|
|
308
593
|
return stepValue ?? '';
|
|
309
594
|
}
|
|
595
|
+
/** Neutral Step.stepType → SQA `technologyType` on the step + `tcType` on the TC.
|
|
596
|
+
* Exported so the adapter's per-step resolver keys the action catalog by the same technology
|
|
597
|
+
* labels the mapper writes into the step. */
|
|
598
|
+
export const TECH_BY_STEPTYPE = {
|
|
599
|
+
web: 'WEB',
|
|
600
|
+
android: 'ANDROID',
|
|
601
|
+
api: 'API',
|
|
602
|
+
database: 'DATABASE',
|
|
603
|
+
mainframe: 'MAINFRAME',
|
|
604
|
+
};
|
|
605
|
+
/** Pick the tcType from the first step whose stepType we recognize as an automation technology.
|
|
606
|
+
* Falls back to WEB — matches the SQA UI's default when the user starts a fresh automation TC. */
|
|
607
|
+
export function deriveTcType(tc) {
|
|
608
|
+
for (const s of tc.steps ?? []) {
|
|
609
|
+
const tech = TECH_BY_STEPTYPE[s.stepType];
|
|
610
|
+
if (tech)
|
|
611
|
+
return tech;
|
|
612
|
+
}
|
|
613
|
+
return 'WEB';
|
|
614
|
+
}
|
|
615
|
+
/** SQA-empty `mailersList` shape — the frontend and joi validator expect the three arrays, not
|
|
616
|
+
* a bare `{}`. Verified against the real create payload for TC-584. */
|
|
617
|
+
const EMPTY_MAILERS = { users: [], nonUsers: [], groups: [] };
|
|
310
618
|
/**
|
|
311
|
-
* Assemble the `testcaseObj` payload for POST/PUT /tm/testcase (multipart).
|
|
312
|
-
* `
|
|
619
|
+
* Assemble the `testcaseObj` payload for POST/PUT /tm/testcase (multipart). Branches on
|
|
620
|
+
* `tc.category`: manual → `steps.manualSteps` (legacy behavior), automated → `steps.automationSteps`
|
|
621
|
+
* with resolved actionId/objectId/parameterIds. See `buildManualPayload` and
|
|
622
|
+
* `buildAutomationPayload` below for the per-branch shapes; both share the wrapper.
|
|
623
|
+
*/
|
|
624
|
+
export function buildTestcasePayload(inputs) {
|
|
625
|
+
return inputs.tc.category === 'automated'
|
|
626
|
+
? buildAutomationPayload(inputs)
|
|
627
|
+
: buildManualPayload(inputs);
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Manual TC payload. Mirrors Java's `buildTestcasePayload` exactly on wire shape:
|
|
313
631
|
* - `tcfnObj` — TC-level meta. `deleted` is required on UPDATE and rejected on CREATE.
|
|
314
632
|
* - `versionsDetails` — a minimal `{name:'v1.0', id:''}` stub matches what the SQA UI sends
|
|
315
633
|
* for a fresh TC; UPDATE also carries `deleted:false` because the validator requires it.
|
|
316
|
-
* - `steps.manualSteps` — one entry per neutral step; `automationSteps` is emitted empty
|
|
317
|
-
*
|
|
318
|
-
* - Every step gets `parameterIds[]` for the `${name}` tokens in its description, so the UI's
|
|
319
|
-
* ID-based dropdown lookup resolves them.
|
|
634
|
+
* - `steps.manualSteps` — one entry per neutral step; `automationSteps` is emitted empty.
|
|
635
|
+
* - Every step gets `parameterIds[]` for the `${name}` tokens in its description.
|
|
320
636
|
*/
|
|
321
|
-
|
|
322
|
-
const { projectId, moduleId, userstoryId, existingId, tc, paramIdByName
|
|
637
|
+
function buildManualPayload(inputs) {
|
|
638
|
+
const { projectId, moduleId, userstoryId, existingId, tc, paramIdByName } = inputs;
|
|
323
639
|
const tcfnObj = {
|
|
324
640
|
...(existingId ? { id: Number.parseInt(existingId, 10) } : {}),
|
|
325
641
|
name: tc.title ?? '',
|
|
@@ -354,20 +670,153 @@ export function buildTestcasePayload(inputs) {
|
|
|
354
670
|
executionType: 'MANUAL',
|
|
355
671
|
};
|
|
356
672
|
const paramIds = [];
|
|
673
|
+
const recordedParam = {};
|
|
357
674
|
for (const name of extractParamNames(desc)) {
|
|
358
675
|
const id = paramIdByName.get(name);
|
|
359
|
-
if (id
|
|
360
|
-
|
|
676
|
+
if (id == null)
|
|
677
|
+
continue;
|
|
678
|
+
paramIds.push(id);
|
|
679
|
+
recordedParam[String(id)] = paramDefault(name, tc.data, undefined);
|
|
361
680
|
}
|
|
362
|
-
if (paramIds.length > 0)
|
|
681
|
+
if (paramIds.length > 0) {
|
|
363
682
|
step['parameterIds'] = paramIds;
|
|
683
|
+
step['recordedParam'] = recordedParam;
|
|
684
|
+
}
|
|
364
685
|
manualSteps.push(step);
|
|
365
686
|
});
|
|
366
687
|
return {
|
|
367
688
|
tcfnObj,
|
|
368
689
|
versionsDetails,
|
|
369
690
|
steps: { automationSteps: [], manualSteps },
|
|
370
|
-
operation,
|
|
691
|
+
operation: 'TC',
|
|
692
|
+
type: 'TC',
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Automation TC payload. Mirrors the shape observed on a saved SQA automation TC (TC-584, WEB +
|
|
697
|
+
* ANDROID validation samples):
|
|
698
|
+
* - `tcfnObj.executionType: 'AUTOMATION'` + `originalExecutinType: 'AUTOMATION'`.
|
|
699
|
+
* - `tcfnObj.tcType` derived from the first automation step's `stepType` (web / android / …).
|
|
700
|
+
* - `tcfnObj.executionPlatform` — WEB defaults to Chrome server-side (omitted here), non-WEB
|
|
701
|
+
* REQUIRES `tc.executionPlatform` from the caller (device serial + name).
|
|
702
|
+
* - `steps.automationSteps[]` — one entry per neutral step. Each carries the resolved
|
|
703
|
+
* `actionId` (numeric FK to the SQA `action` collection) + `objectId` (numeric FK to the
|
|
704
|
+
* shared `object` collection, or null when the action doesn't need one) + `parameterIds` +
|
|
705
|
+
* `recordedParam:{<id>:value}` (drives the default iteration on save).
|
|
706
|
+
* - Always-present step defaults (`conditions`, `loop`, `stepLevelActions`, `attachments`)
|
|
707
|
+
* are emitted every time — the joi validator + backend read these unconditionally on
|
|
708
|
+
* execution, and the sample confirms they're present on simple straight-line steps too.
|
|
709
|
+
*
|
|
710
|
+
* Straight-line only in this cut — FUNCTION / IF / FOR / WHILE / API / SWITCH are ponytail-
|
|
711
|
+
* deferred to a follow-up (need a real control-flow sample to validate the `conditions.children[]`
|
|
712
|
+
* shape end-to-end).
|
|
713
|
+
*/
|
|
714
|
+
function buildAutomationPayload(inputs) {
|
|
715
|
+
const { projectId, moduleId, userstoryId, existingId, tc, paramIdByName, stepResolutions } = inputs;
|
|
716
|
+
const tcType = deriveTcType(tc);
|
|
717
|
+
// WEB: backend auto-defaults executionPlatform to Chrome (tcfn.service.js:206-213), so omit
|
|
718
|
+
// when the caller didn't supply one. Non-WEB: refuse the save early rather than land a broken
|
|
719
|
+
// TC — the backend doesn't default a real device for us.
|
|
720
|
+
if (tcType !== 'WEB' && !tc.executionPlatform) {
|
|
721
|
+
throw new Error(`Automation TC "${tc.title}" needs \`executionPlatform\` for tcType=${tcType} — set ` +
|
|
722
|
+
`tc.executionPlatform:{id:<deviceSerial>, name, technologyType:'${tcType}', type:'REAL_DEVICE'|'CLOUD_DEVICE'}. ` +
|
|
723
|
+
`For Android, call android_list_devices to see the available devices.`);
|
|
724
|
+
}
|
|
725
|
+
const tcfnObj = {
|
|
726
|
+
...(existingId ? { id: Number.parseInt(existingId, 10) } : {}),
|
|
727
|
+
name: tc.title ?? '',
|
|
728
|
+
projectId,
|
|
729
|
+
moduleId,
|
|
730
|
+
...(userstoryId != null ? { userstoryId } : {}),
|
|
731
|
+
obsolete: false,
|
|
732
|
+
...(existingId ? { deleted: false } : {}),
|
|
733
|
+
tcType,
|
|
734
|
+
executionType: 'AUTOMATION',
|
|
735
|
+
originalExecutinType: 'AUTOMATION',
|
|
736
|
+
customFields: {},
|
|
737
|
+
mailersList: EMPTY_MAILERS,
|
|
738
|
+
suiteId: [],
|
|
739
|
+
defectId: [],
|
|
740
|
+
tags: [],
|
|
741
|
+
reviewApproveStatus: '',
|
|
742
|
+
...(tc.executionPlatform ? { executionPlatform: tc.executionPlatform } : {}),
|
|
743
|
+
...(tc.description ? { description: tc.description } : {}),
|
|
744
|
+
...(tc.preconditions ? { prerequisite: tc.preconditions } : {}),
|
|
745
|
+
};
|
|
746
|
+
const versionsDetails = {
|
|
747
|
+
name: 'v1.0',
|
|
748
|
+
id: '',
|
|
749
|
+
syncONOFF: '',
|
|
750
|
+
executionType: 'AUTOMATION',
|
|
751
|
+
functionProjectIds: [projectId],
|
|
752
|
+
...(existingId ? { deleted: false } : {}),
|
|
753
|
+
};
|
|
754
|
+
const automationSteps = [];
|
|
755
|
+
const steps = Array.isArray(tc.steps) ? tc.steps : [];
|
|
756
|
+
steps.forEach((s, i) => {
|
|
757
|
+
const desc = s.description ?? '';
|
|
758
|
+
const technologyType = TECH_BY_STEPTYPE[s.stepType] ?? tcType;
|
|
759
|
+
const resolution = stepResolutions?.[i];
|
|
760
|
+
if (!resolution) {
|
|
761
|
+
throw new Error(`Automation TC "${tc.title}" step ${i + 1}: missing action resolution — the adapter did ` +
|
|
762
|
+
'not resolve `actionId` for this step. Check that the neutral Step.actionType maps to a ' +
|
|
763
|
+
'known SQA action for this technology.');
|
|
764
|
+
}
|
|
765
|
+
// parameterIds: union of description ${tokens} + keys of step.recordedParam. `recordedParam`
|
|
766
|
+
// on the wire is keyed by NUMERIC paramId (JSON keys → strings), NOT by name.
|
|
767
|
+
const paramIds = [];
|
|
768
|
+
const recordedParamOut = {};
|
|
769
|
+
const seen = new Set();
|
|
770
|
+
const addParam = (name, value) => {
|
|
771
|
+
if (seen.has(name))
|
|
772
|
+
return;
|
|
773
|
+
seen.add(name);
|
|
774
|
+
const id = paramIdByName.get(name);
|
|
775
|
+
if (id == null)
|
|
776
|
+
return;
|
|
777
|
+
paramIds.push(id);
|
|
778
|
+
if (value !== undefined)
|
|
779
|
+
recordedParamOut[String(id)] = value;
|
|
780
|
+
};
|
|
781
|
+
for (const name of extractParamNames(desc))
|
|
782
|
+
addParam(name, tc.data?.[name] ?? s.value);
|
|
783
|
+
if (s.recordedParam) {
|
|
784
|
+
for (const [name, value] of Object.entries(s.recordedParam))
|
|
785
|
+
addParam(name, value);
|
|
786
|
+
}
|
|
787
|
+
const step = {
|
|
788
|
+
projectId,
|
|
789
|
+
moduleId,
|
|
790
|
+
type: 'TC',
|
|
791
|
+
stepType: 'NORMAL_STATEMENT',
|
|
792
|
+
seq: i + 1,
|
|
793
|
+
description: desc,
|
|
794
|
+
expectedResult: s.expectedResult ?? '',
|
|
795
|
+
executionType: 'AUTOMATION',
|
|
796
|
+
technologyType,
|
|
797
|
+
actionId: resolution.actionId,
|
|
798
|
+
objectId: resolution.objectId,
|
|
799
|
+
parameterIds: paramIds,
|
|
800
|
+
objectParameterIds: [],
|
|
801
|
+
recordedParam: recordedParamOut,
|
|
802
|
+
attachments: [],
|
|
803
|
+
conditions: { ifstring: '', children: [] },
|
|
804
|
+
loop: { loopType: 'for', loopIteration: null },
|
|
805
|
+
stepLevelActions: {
|
|
806
|
+
skip: s.flags?.skip ?? false,
|
|
807
|
+
captureScreenshot: s.flags?.screenshot ?? false,
|
|
808
|
+
continueOnFailure: s.flags?.continueOnFailure ?? false,
|
|
809
|
+
skippedReport: false,
|
|
810
|
+
},
|
|
811
|
+
...(technologyType === 'WEB' ? { browserType: s.browserType ?? 'CHROME' } : {}),
|
|
812
|
+
};
|
|
813
|
+
automationSteps.push(step);
|
|
814
|
+
});
|
|
815
|
+
return {
|
|
816
|
+
tcfnObj,
|
|
817
|
+
versionsDetails,
|
|
818
|
+
steps: { automationSteps, manualSteps: [] },
|
|
819
|
+
operation: 'TC',
|
|
371
820
|
type: 'TC',
|
|
372
821
|
};
|
|
373
822
|
}
|