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.
Files changed (83) hide show
  1. package/dist/adapters/factory.d.ts +1 -1
  2. package/dist/adapters/factory.d.ts.map +1 -1
  3. package/dist/adapters/factory.js +12 -29
  4. package/dist/adapters/factory.js.map +1 -1
  5. package/dist/adapters/http.js +2 -3
  6. package/dist/adapters/http.js.map +1 -1
  7. package/dist/adapters/remoteMcp/adapter.d.ts +7 -2
  8. package/dist/adapters/remoteMcp/adapter.d.ts.map +1 -1
  9. package/dist/adapters/remoteMcp/adapter.js +9 -3
  10. package/dist/adapters/remoteMcp/adapter.js.map +1 -1
  11. package/dist/adapters/simplifyqa/adapter.d.ts +309 -23
  12. package/dist/adapters/simplifyqa/adapter.d.ts.map +1 -1
  13. package/dist/adapters/simplifyqa/adapter.js +2042 -86
  14. package/dist/adapters/simplifyqa/adapter.js.map +1 -1
  15. package/dist/adapters/simplifyqa/mapper.d.ts +43 -11
  16. package/dist/adapters/simplifyqa/mapper.d.ts.map +1 -1
  17. package/dist/adapters/simplifyqa/mapper.js +470 -21
  18. package/dist/adapters/simplifyqa/mapper.js.map +1 -1
  19. package/dist/adapters/types.d.ts +126 -5
  20. package/dist/adapters/types.d.ts.map +1 -1
  21. package/dist/adapters/walnut/adapter.d.ts +6 -2
  22. package/dist/adapters/walnut/adapter.d.ts.map +1 -1
  23. package/dist/adapters/walnut/adapter.js +33 -2
  24. package/dist/adapters/walnut/adapter.js.map +1 -1
  25. package/dist/adapters/walnut/mapper.d.ts +2 -1
  26. package/dist/adapters/walnut/mapper.d.ts.map +1 -1
  27. package/dist/adapters/walnut/mapper.js +13 -0
  28. package/dist/adapters/walnut/mapper.js.map +1 -1
  29. package/dist/config.d.ts +10 -0
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +36 -0
  32. package/dist/config.js.map +1 -1
  33. package/dist/connection.d.ts.map +1 -1
  34. package/dist/connection.js +15 -7
  35. package/dist/connection.js.map +1 -1
  36. package/dist/domain/types.d.ts +485 -4
  37. package/dist/domain/types.d.ts.map +1 -1
  38. package/dist/initAgents.d.ts.map +1 -1
  39. package/dist/initAgents.js +22 -9
  40. package/dist/initAgents.js.map +1 -1
  41. package/dist/server.d.ts.map +1 -1
  42. package/dist/server.js +23 -1
  43. package/dist/server.js.map +1 -1
  44. package/dist/tools/backlog.tools.d.ts +13 -0
  45. package/dist/tools/backlog.tools.d.ts.map +1 -0
  46. package/dist/tools/backlog.tools.js +399 -0
  47. package/dist/tools/backlog.tools.js.map +1 -0
  48. package/dist/tools/defects.tools.d.ts +7 -0
  49. package/dist/tools/defects.tools.d.ts.map +1 -0
  50. package/dist/tools/defects.tools.js +82 -0
  51. package/dist/tools/defects.tools.js.map +1 -0
  52. package/dist/tools/discovery.tools.d.ts.map +1 -1
  53. package/dist/tools/discovery.tools.js +912 -55
  54. package/dist/tools/discovery.tools.js.map +1 -1
  55. package/dist/tools/markdown.d.ts +3 -3
  56. package/dist/tools/markdown.d.ts.map +1 -1
  57. package/dist/tools/markdown.js +89 -18
  58. package/dist/tools/markdown.js.map +1 -1
  59. package/dist/tools/objects.tools.d.ts +7 -0
  60. package/dist/tools/objects.tools.d.ts.map +1 -0
  61. package/dist/tools/objects.tools.js +201 -0
  62. package/dist/tools/objects.tools.js.map +1 -0
  63. package/dist/tools/recording.tools.d.ts.map +1 -1
  64. package/dist/tools/recording.tools.js +64 -5
  65. package/dist/tools/recording.tools.js.map +1 -1
  66. package/dist/tools/run.tools.d.ts.map +1 -1
  67. package/dist/tools/run.tools.js +102 -18
  68. package/dist/tools/run.tools.js.map +1 -1
  69. package/dist/tools/schemas.d.ts +21 -4
  70. package/dist/tools/schemas.d.ts.map +1 -1
  71. package/dist/tools/schemas.js +14 -1
  72. package/dist/tools/schemas.js.map +1 -1
  73. package/dist/tools/tasks.tools.d.ts +7 -0
  74. package/dist/tools/tasks.tools.d.ts.map +1 -0
  75. package/dist/tools/tasks.tools.js +75 -0
  76. package/dist/tools/tasks.tools.js.map +1 -0
  77. package/dist/tools/testcase.tools.d.ts.map +1 -1
  78. package/dist/tools/testcase.tools.js +146 -100
  79. package/dist/tools/testcase.tools.js.map +1 -1
  80. package/dist/tools/testdata.tools.d.ts.map +1 -1
  81. package/dist/tools/testdata.tools.js +99 -11
  82. package/dist/tools/testdata.tools.js.map +1 -1
  83. 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
- // SQA `executionType` is "MANUAL" for manual steps in a manual (or DUO) TC and "AUTOMATION"
57
- // otherwise. Angular's step-card routing branches on 'manual' vs anything-else, so a manual
58
- // step should really land as stepType:'manual' or the row renders as an empty automation card.
59
- // ponytail: `Step['stepType']` on main does not yet include 'manual' — that addition is queued
60
- // by the in-flight tc-edit branch. Until it lands, default to 'web' here (matches the Java
61
- // toTestCase inline path's current behavior). Upgrade this line the day 'manual' is on the enum.
62
- const stepType = 'web';
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
- return { id, name, ...(memberCount !== undefined ? { memberCount } : {}) };
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, 'description', 'disname', 'placeholder') ?? '';
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). Mirrors Java's
312
- * `buildTestcasePayload` exactly on wire shape:
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
- * because this common-mcp save is the manual-authoring flow (agent handles automation).
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
- export function buildTestcasePayload(inputs) {
322
- const { projectId, moduleId, userstoryId, existingId, tc, paramIdByName, operation } = inputs;
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 != null)
360
- paramIds.push(id);
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
  }