common-qa-mcp 0.2.0 → 0.4.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.
Files changed (110) hide show
  1. package/README.md +6 -0
  2. package/dist/adapters/factory.d.ts +17 -7
  3. package/dist/adapters/factory.d.ts.map +1 -1
  4. package/dist/adapters/factory.js +44 -7
  5. package/dist/adapters/factory.js.map +1 -1
  6. package/dist/adapters/http.d.ts +50 -0
  7. package/dist/adapters/http.d.ts.map +1 -0
  8. package/dist/adapters/http.js +108 -0
  9. package/dist/adapters/http.js.map +1 -0
  10. package/dist/adapters/remoteMcp/adapter.d.ts +16 -2
  11. package/dist/adapters/remoteMcp/adapter.d.ts.map +1 -1
  12. package/dist/adapters/remoteMcp/adapter.js +34 -5
  13. package/dist/adapters/remoteMcp/adapter.js.map +1 -1
  14. package/dist/adapters/simplifyqa/adapter.d.ts +437 -0
  15. package/dist/adapters/simplifyqa/adapter.d.ts.map +1 -0
  16. package/dist/adapters/simplifyqa/adapter.js +3097 -0
  17. package/dist/adapters/simplifyqa/adapter.js.map +1 -0
  18. package/dist/adapters/simplifyqa/mapper.d.ts +95 -0
  19. package/dist/adapters/simplifyqa/mapper.d.ts.map +1 -0
  20. package/dist/adapters/simplifyqa/mapper.js +858 -0
  21. package/dist/adapters/simplifyqa/mapper.js.map +1 -0
  22. package/dist/adapters/types.d.ts +168 -5
  23. package/dist/adapters/types.d.ts.map +1 -1
  24. package/dist/adapters/walnut/adapter.d.ts +235 -0
  25. package/dist/adapters/walnut/adapter.d.ts.map +1 -0
  26. package/dist/adapters/walnut/adapter.js +1114 -0
  27. package/dist/adapters/walnut/adapter.js.map +1 -0
  28. package/dist/adapters/walnut/mapper.d.ts +30 -9
  29. package/dist/adapters/walnut/mapper.d.ts.map +1 -1
  30. package/dist/adapters/walnut/mapper.js +149 -24
  31. package/dist/adapters/walnut/mapper.js.map +1 -1
  32. package/dist/config.d.ts +38 -14
  33. package/dist/config.d.ts.map +1 -1
  34. package/dist/config.js +73 -19
  35. package/dist/config.js.map +1 -1
  36. package/dist/connection.d.ts +3 -2
  37. package/dist/connection.d.ts.map +1 -1
  38. package/dist/connection.js +44 -15
  39. package/dist/connection.js.map +1 -1
  40. package/dist/domain/types.d.ts +680 -5
  41. package/dist/domain/types.d.ts.map +1 -1
  42. package/dist/drafts/store.d.ts +4 -1
  43. package/dist/drafts/store.d.ts.map +1 -1
  44. package/dist/drafts/store.js +7 -2
  45. package/dist/drafts/store.js.map +1 -1
  46. package/dist/initAgents.d.ts.map +1 -1
  47. package/dist/initAgents.js +48 -18
  48. package/dist/initAgents.js.map +1 -1
  49. package/dist/requestConnection.d.ts +5 -3
  50. package/dist/requestConnection.d.ts.map +1 -1
  51. package/dist/requestConnection.js +11 -5
  52. package/dist/requestConnection.js.map +1 -1
  53. package/dist/server.d.ts.map +1 -1
  54. package/dist/server.js +47 -1
  55. package/dist/server.js.map +1 -1
  56. package/dist/tools/backlog.tools.d.ts +13 -0
  57. package/dist/tools/backlog.tools.d.ts.map +1 -0
  58. package/dist/tools/backlog.tools.js +399 -0
  59. package/dist/tools/backlog.tools.js.map +1 -0
  60. package/dist/tools/connection.tools.js +5 -5
  61. package/dist/tools/connection.tools.js.map +1 -1
  62. package/dist/tools/defects.tools.d.ts +7 -0
  63. package/dist/tools/defects.tools.d.ts.map +1 -0
  64. package/dist/tools/defects.tools.js +82 -0
  65. package/dist/tools/defects.tools.js.map +1 -0
  66. package/dist/tools/discovery.tools.d.ts.map +1 -1
  67. package/dist/tools/discovery.tools.js +951 -43
  68. package/dist/tools/discovery.tools.js.map +1 -1
  69. package/dist/tools/formulas.tools.d.ts +7 -0
  70. package/dist/tools/formulas.tools.d.ts.map +1 -0
  71. package/dist/tools/formulas.tools.js +56 -0
  72. package/dist/tools/formulas.tools.js.map +1 -0
  73. package/dist/tools/heal.tools.d.ts +17 -0
  74. package/dist/tools/heal.tools.d.ts.map +1 -0
  75. package/dist/tools/heal.tools.js +76 -0
  76. package/dist/tools/heal.tools.js.map +1 -0
  77. package/dist/tools/markdown.d.ts +3 -3
  78. package/dist/tools/markdown.d.ts.map +1 -1
  79. package/dist/tools/markdown.js +89 -18
  80. package/dist/tools/markdown.js.map +1 -1
  81. package/dist/tools/objects.tools.d.ts +7 -0
  82. package/dist/tools/objects.tools.d.ts.map +1 -0
  83. package/dist/tools/objects.tools.js +201 -0
  84. package/dist/tools/objects.tools.js.map +1 -0
  85. package/dist/tools/recording.tools.d.ts.map +1 -1
  86. package/dist/tools/recording.tools.js +92 -12
  87. package/dist/tools/recording.tools.js.map +1 -1
  88. package/dist/tools/run.tools.d.ts.map +1 -1
  89. package/dist/tools/run.tools.js +464 -39
  90. package/dist/tools/run.tools.js.map +1 -1
  91. package/dist/tools/schemas.d.ts +21 -4
  92. package/dist/tools/schemas.d.ts.map +1 -1
  93. package/dist/tools/schemas.js +14 -1
  94. package/dist/tools/schemas.js.map +1 -1
  95. package/dist/tools/storySync.tools.d.ts +66 -0
  96. package/dist/tools/storySync.tools.d.ts.map +1 -0
  97. package/dist/tools/storySync.tools.js +213 -0
  98. package/dist/tools/storySync.tools.js.map +1 -0
  99. package/dist/tools/tasks.tools.d.ts +7 -0
  100. package/dist/tools/tasks.tools.d.ts.map +1 -0
  101. package/dist/tools/tasks.tools.js +75 -0
  102. package/dist/tools/tasks.tools.js.map +1 -0
  103. package/dist/tools/testcase.tools.d.ts.map +1 -1
  104. package/dist/tools/testcase.tools.js +162 -101
  105. package/dist/tools/testcase.tools.js.map +1 -1
  106. package/dist/tools/testdata.tools.d.ts +7 -0
  107. package/dist/tools/testdata.tools.d.ts.map +1 -0
  108. package/dist/tools/testdata.tools.js +249 -0
  109. package/dist/tools/testdata.tools.js.map +1 -0
  110. package/package.json +3 -3
@@ -0,0 +1,858 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Shared JSON pluckers. SQA endpoints wrap responses inconsistently -- one
3
+ // controller returns `{ status, data: [...] }`, another returns the bare
4
+ // array, another wraps it under `content` or the entity plural. `pickArray`
5
+ // and `str` walk a list of candidate keys and return the first hit, matching
6
+ // the tolerance the Java BackendClient built up against real responses.
7
+ // ---------------------------------------------------------------------------
8
+ export function pickArray(n, ...keys) {
9
+ for (const k of keys) {
10
+ const v = n?.[k];
11
+ if (Array.isArray(v))
12
+ return v;
13
+ }
14
+ return Array.isArray(n) ? n : [];
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
+ }
25
+ export function str(n, ...keys) {
26
+ if (n == null)
27
+ return undefined;
28
+ for (const k of keys) {
29
+ const v = n[k];
30
+ if (typeof v === 'string' && v.length)
31
+ return v;
32
+ if (typeof v === 'number')
33
+ return String(v);
34
+ }
35
+ return undefined;
36
+ }
37
+ /** Accept a numeric id ("523") or a business code ("TC_523" / "US-897") — SQA REST paths
38
+ * parseInt the segment, so a raw business key yields NaN → 404. Strips to the numeric run. */
39
+ export function numericId(id) {
40
+ if (!id)
41
+ return '';
42
+ const m = id.match(/(\d+)/);
43
+ return m ? m[1] : id;
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
+ }
70
+ /** SQA priority is UPPERCASE on the wire; neutral is lowercase. Unknown → 'medium'. */
71
+ export function mapPriority(p) {
72
+ const s = typeof p === 'string' ? p.toLowerCase() : '';
73
+ return s === 'critical' || s === 'high' || s === 'medium' || s === 'low'
74
+ ? s : 'medium';
75
+ }
76
+ // ---------------------------------------------------------------------------
77
+ // Steps. SQA stores step docs in two shapes with mostly-overlapping fields:
78
+ // • the `/tm/testcase/steps` COLLECTION (source of truth today), and
79
+ // • the INLINE `steps.manualSteps` / `steps.automationSteps` array on the TC doc
80
+ // (legacy, drifts against the collection).
81
+ // Both use the same field names, so ONE mapper covers both. See adapter.ts
82
+ // getTestCase for the read strategy (collection wins; inline is a fallback).
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
+ }
123
+ export function fromSqaStep(s, fallbackSeq) {
124
+ const sid = str(s, 'id', '_id', 'stepId');
125
+ const description = s?.description ?? '';
126
+ const expectedResult = s?.expectedResult ?? undefined;
127
+ const actionType = str(s, 'actionId', 'actionType') ?? '';
128
+ const seq = typeof s?.seq === 'number' ? s.seq : fallbackSeq;
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';
147
+ // Object linkage: SQA stores it as either scalar `objectId` OR nested `object.id`.
148
+ // Preserving the FK is what lets tc_heal / object_update_locators find the shared object.
149
+ let objectId = str(s, 'objectId');
150
+ if (!objectId && s?.object && typeof s.object === 'object') {
151
+ objectId = str(s.object, 'id');
152
+ }
153
+ const objectName = s?.object && typeof s.object === 'object' ? str(s.object, 'name') : undefined;
154
+ const step = {
155
+ n: seq,
156
+ description,
157
+ actionType,
158
+ stepType,
159
+ ...(sid ? { sourceId: sid } : {}),
160
+ ...(expectedResult ? { expectedResult } : {}),
161
+ };
162
+ if (objectId && objectId.toLowerCase() !== 'null') {
163
+ step.objectRef = { id: objectId, ...(objectName ? { name: objectName } : {}) };
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;
197
+ return step;
198
+ }
199
+ // ---------------------------------------------------------------------------
200
+ // Test cases.
201
+ // `toTestCase` is the projection off the RAW /tm/testcase/getById doc — it
202
+ // reads inline steps as a fallback. `applyCollectionSteps` replaces those with
203
+ // the steps-collection view when that fetch succeeds, matching what the SQA UI
204
+ // + execution + save paths use today.
205
+ // ---------------------------------------------------------------------------
206
+ export function toTestCase(doc) {
207
+ const id = str(doc, 'id', '_id', 'testcaseId');
208
+ const execType = str(doc, 'executionType') ?? '';
209
+ const category = execType.toUpperCase() === 'MANUAL' ? 'manual' : 'automated';
210
+ const priority = mapPriority(doc?.priority);
211
+ const description = str(doc, 'description');
212
+ const preconditions = str(doc, 'prerequisite', 'preconditions');
213
+ // storyRef + folderOrModule come off SQA as raw ints (userstoryId / moduleId). Project into
214
+ // neutral strings so `tc_automate` (SQA branch) can pre-fill the recording guide.
215
+ const userstoryId = typeof doc?.userstoryId === 'number' && doc.userstoryId > 0
216
+ ? String(doc.userstoryId) : undefined;
217
+ const moduleId = typeof doc?.moduleId === 'number' && doc.moduleId > 0
218
+ ? String(doc.moduleId) : undefined;
219
+ // Inline steps as fallback: prefer manualSteps for manual TCs, else automationSteps.
220
+ const stepsNode = doc?.steps;
221
+ let rawSteps = [];
222
+ if (stepsNode && typeof stepsNode === 'object') {
223
+ if (Array.isArray(stepsNode?.manualSteps) && stepsNode.manualSteps.length)
224
+ rawSteps = stepsNode.manualSteps;
225
+ else if (Array.isArray(stepsNode?.automationSteps))
226
+ rawSteps = stepsNode.automationSteps;
227
+ else if (Array.isArray(stepsNode))
228
+ rawSteps = stepsNode;
229
+ }
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);
239
+ return {
240
+ ...(id ? { id } : {}),
241
+ title: str(doc, 'name', 'title') ?? '',
242
+ ...(description ? { description } : {}),
243
+ priority,
244
+ ...(str(doc, 'status') ? { status: str(doc, 'status') } : {}),
245
+ category,
246
+ ...(preconditions ? { preconditions } : {}),
247
+ ...(userstoryId ? { storyRef: userstoryId } : {}),
248
+ ...(moduleId ? { folderOrModule: moduleId } : {}),
249
+ steps,
250
+ ...(versions.length ? { versions } : {}),
251
+ ...(currentVersionId ? { currentVersionId } : {}),
252
+ };
253
+ }
254
+ /** Overwrite `tc.steps` with the mapped collection-endpoint rows when non-empty. */
255
+ export function applyCollectionSteps(tc, rows) {
256
+ if (!Array.isArray(rows) || rows.length === 0)
257
+ return tc;
258
+ const mapped = rows.map((s, i) => fromSqaStep(s, i + 1));
259
+ return { ...tc, steps: mapped };
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
+ }
292
+ export function toTcSummary(row) {
293
+ const id = str(row, 'id', '_id', 'testcaseId') ?? '';
294
+ const code = str(row, 'code');
295
+ const title = str(row, 'name', 'title') ?? '';
296
+ const priority = (str(row, 'priority') ?? 'medium').toLowerCase();
297
+ const status = str(row, 'status');
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;
302
+ return {
303
+ id,
304
+ ...(code ? { code } : {}),
305
+ title,
306
+ priority,
307
+ ...(status ? { status } : {}),
308
+ ...(stepCount !== undefined ? { stepCount } : {}),
309
+ ...(technologyType ? { technologyType } : {}),
310
+ ...(executionType ? { executionType } : {}),
311
+ ...(obsolete !== undefined ? { obsolete } : {}),
312
+ };
313
+ }
314
+ // ---------------------------------------------------------------------------
315
+ // Suites.
316
+ // ---------------------------------------------------------------------------
317
+ export function toSuiteSummary(row) {
318
+ const id = str(row, 'id', '_id', 'suiteId') ?? '';
319
+ const name = str(row, 'name', 'suiteName') ?? '';
320
+ const memberCount = Array.isArray(row?.testCases) ? row.testCases.length : undefined;
321
+ const suiteType = str(row, 'suiteType');
322
+ return {
323
+ id,
324
+ name,
325
+ ...(memberCount !== undefined ? { memberCount } : {}),
326
+ ...(suiteType ? { suiteType } : {}),
327
+ };
328
+ }
329
+ export function toSuite(doc) {
330
+ const id = str(doc, 'id', '_id', 'suiteId');
331
+ const description = str(doc, 'description');
332
+ const rawMembers = Array.isArray(doc?.testCases) ? doc.testCases : [];
333
+ const members = rawMembers.map((tc, i) => ({
334
+ tcId: str(tc, 'testcaseId', 'id', '_id') ?? '',
335
+ order: typeof tc?.seq === 'number' ? tc.seq : i + 1,
336
+ }));
337
+ return {
338
+ ...(id ? { id } : {}),
339
+ name: str(doc, 'name', 'suiteName') ?? '',
340
+ ...(description ? { description } : {}),
341
+ members,
342
+ };
343
+ }
344
+ // ---------------------------------------------------------------------------
345
+ // User stories (`pm/userstory`). Business key comes back as `code` on SQA
346
+ // (US_123 / US-897), so `key` in the neutral shape is populated from `code`.
347
+ // ---------------------------------------------------------------------------
348
+ export function toStorySummary(row) {
349
+ const id = str(row, 'id', '_id', 'userstoryId') ?? '';
350
+ const key = str(row, 'code', 'key', 'userStoryCode');
351
+ const title = str(row, 'name', 'title', 'storyName') ?? '';
352
+ const status = str(row, 'status', 'state');
353
+ const priority = str(row, 'priority');
354
+ return {
355
+ id,
356
+ ...(key ? { key } : {}),
357
+ title,
358
+ ...(status ? { status } : {}),
359
+ ...(priority ? { priority } : {}),
360
+ };
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
+ }
412
+ export function toStory(doc) {
413
+ const base = toStorySummary(doc);
414
+ const description = str(doc, 'description', 'storyDescription');
415
+ // Acceptance criteria may be an array (of strings or {description}) or a newline-joined string.
416
+ const rawAc = doc?.acceptanceCriteria ?? doc?.acceptance_criteria;
417
+ let ac;
418
+ if (Array.isArray(rawAc)) {
419
+ ac = rawAc.map((c) => typeof c === 'string' ? c : String(c?.description ?? ''))
420
+ .filter((s) => s.length > 0);
421
+ }
422
+ else if (typeof rawAc === 'string') {
423
+ ac = rawAc.split(/\r?\n/).map((s) => s.trim()).filter((s) => s.length > 0);
424
+ }
425
+ return {
426
+ ...base,
427
+ ...(description ? { description } : {}),
428
+ ...(ac && ac.length ? { acceptanceCriteria: ac } : {}),
429
+ };
430
+ }
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
+ // ---------------------------------------------------------------------------
496
+ // Modules, releases, sprints.
497
+ // ---------------------------------------------------------------------------
498
+ export function toModuleSummary(row) {
499
+ const id = str(row, 'id', 'moduleId', '_id') ?? '';
500
+ const name = str(row, 'name', 'moduleName') ?? '';
501
+ const parentId = str(row, 'parentId', 'parent', 'parentModuleId');
502
+ return {
503
+ id,
504
+ name,
505
+ ...(parentId && parentId !== '0' ? { parentId } : {}),
506
+ };
507
+ }
508
+ export function toReleaseSummary(row) {
509
+ const id = str(row, 'id', '_id', 'releaseId') ?? '';
510
+ const name = str(row, 'name', 'releaseName') ?? '';
511
+ const state = str(row, 'state', 'status');
512
+ const plannedStartDate = str(row, 'plannedStartDate');
513
+ const plannedEndDate = str(row, 'plannedEndDate');
514
+ return {
515
+ id,
516
+ name,
517
+ ...(state ? { state } : {}),
518
+ ...(plannedStartDate ? { plannedStartDate } : {}),
519
+ ...(plannedEndDate ? { plannedEndDate } : {}),
520
+ };
521
+ }
522
+ export function toSprintSummary(row) {
523
+ const id = str(row, 'id', '_id', 'sprintId') ?? '';
524
+ const name = str(row, 'name', 'sprintName') ?? '';
525
+ const releaseId = str(row, 'releaseId');
526
+ const state = str(row, 'state', 'status');
527
+ const plannedStartDate = str(row, 'plannedStartDate');
528
+ const plannedEndDate = str(row, 'plannedEndDate');
529
+ return {
530
+ id,
531
+ name,
532
+ ...(releaseId ? { releaseId } : {}),
533
+ ...(state ? { state } : {}),
534
+ ...(plannedStartDate ? { plannedStartDate } : {}),
535
+ ...(plannedEndDate ? { plannedEndDate } : {}),
536
+ };
537
+ }
538
+ // ---------------------------------------------------------------------------
539
+ // Objects — the shared object (locator) library. `attributes[]` is the SQA
540
+ // wire shape; the neutral QaObject exposes it as `locators`.
541
+ // ---------------------------------------------------------------------------
542
+ export function toObject(doc) {
543
+ const id = str(doc, 'id', '_id');
544
+ const rawAttrs = Array.isArray(doc?.attributes) ? doc.attributes : [];
545
+ const locators = rawAttrs
546
+ .filter((a) => typeof a?.name === 'string' && typeof a?.value === 'string')
547
+ .map((a) => ({
548
+ name: String(a.name),
549
+ value: String(a.value),
550
+ ...(a.unique === true ? { unique: true } : {}),
551
+ }));
552
+ const name = str(doc, 'disname', 'description', 'placeholder') ?? '';
553
+ const objectType = str(doc, 'technology');
554
+ return {
555
+ ...(id ? { id } : {}),
556
+ name,
557
+ ...(objectType ? { objectType } : {}),
558
+ locators,
559
+ };
560
+ }
561
+ // ---------------------------------------------------------------------------
562
+ // Write-side helpers. `${name}` token extraction + secret detection + payload
563
+ // builders for POST/PUT /tm/testcase (`testcaseObj`).
564
+ // ---------------------------------------------------------------------------
565
+ const PARAM_TOKEN = /\$\{([A-Za-z0-9_]+)\}/g;
566
+ /** Unique `${name}` tokens in a step description, in first-seen order. */
567
+ export function extractParamNames(description) {
568
+ if (!description)
569
+ return [];
570
+ const seen = [];
571
+ for (const m of description.matchAll(PARAM_TOKEN)) {
572
+ const name = m[1];
573
+ if (!seen.includes(name))
574
+ seen.push(name);
575
+ }
576
+ return seen;
577
+ }
578
+ /** Auto-mask heuristic: names containing password/secret/token/apikey/auth/credential get
579
+ * `protected:true` on parameter creation. A missed real secret is worse than a false positive
580
+ * the user can uncheck in the UI. */
581
+ export function looksSensitive(name) {
582
+ if (!name)
583
+ return false;
584
+ const n = name.toLowerCase();
585
+ return n.includes('password') || n.includes('passwd') || n.includes('secret')
586
+ || n.includes('token') || n.includes('apikey') || n.includes('api_key')
587
+ || n.includes('auth') || n.includes('credential');
588
+ }
589
+ /** Pick a default value for a param: `tc.data[name]` wins; else the step's own `value`; else "". */
590
+ export function paramDefault(name, data, stepValue) {
591
+ if (data && Object.prototype.hasOwnProperty.call(data, name))
592
+ return String(data[name] ?? '');
593
+ return stepValue ?? '';
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: [] };
618
+ /**
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:
631
+ * - `tcfnObj` — TC-level meta. `deleted` is required on UPDATE and rejected on CREATE.
632
+ * - `versionsDetails` — a minimal `{name:'v1.0', id:''}` stub matches what the SQA UI sends
633
+ * for a fresh TC; UPDATE also carries `deleted:false` because the validator requires it.
634
+ * - `steps.manualSteps` — one entry per neutral step; `automationSteps` is emitted empty.
635
+ * - Every step gets `parameterIds[]` for the `${name}` tokens in its description.
636
+ */
637
+ function buildManualPayload(inputs) {
638
+ const { projectId, moduleId, userstoryId, existingId, tc, paramIdByName } = inputs;
639
+ const tcfnObj = {
640
+ ...(existingId ? { id: Number.parseInt(existingId, 10) } : {}),
641
+ name: tc.title ?? '',
642
+ projectId,
643
+ moduleId,
644
+ ...(userstoryId != null ? { userstoryId } : {}),
645
+ obsolete: false,
646
+ ...(existingId ? { deleted: false } : {}),
647
+ tcType: 'WEB',
648
+ executionType: 'MANUAL',
649
+ customFields: {},
650
+ ...(tc.description ? { description: tc.description } : {}),
651
+ ...(tc.preconditions ? { prerequisite: tc.preconditions } : {}),
652
+ };
653
+ const versionsDetails = {
654
+ name: 'v1.0',
655
+ id: '',
656
+ ...(existingId ? { deleted: false } : {}),
657
+ };
658
+ const manualSteps = [];
659
+ const steps = Array.isArray(tc.steps) ? tc.steps : [];
660
+ steps.forEach((s, i) => {
661
+ const desc = s.description ?? '';
662
+ const step = {
663
+ projectId,
664
+ moduleId,
665
+ type: 'TC',
666
+ stepType: 'NORMAL_STATEMENT',
667
+ seq: i + 1,
668
+ description: desc,
669
+ expectedResult: s.expectedResult ?? '',
670
+ executionType: 'MANUAL',
671
+ };
672
+ const paramIds = [];
673
+ const recordedParam = {};
674
+ for (const name of extractParamNames(desc)) {
675
+ const id = paramIdByName.get(name);
676
+ if (id == null)
677
+ continue;
678
+ paramIds.push(id);
679
+ recordedParam[String(id)] = paramDefault(name, tc.data, undefined);
680
+ }
681
+ if (paramIds.length > 0) {
682
+ step['parameterIds'] = paramIds;
683
+ step['recordedParam'] = recordedParam;
684
+ }
685
+ manualSteps.push(step);
686
+ });
687
+ return {
688
+ tcfnObj,
689
+ versionsDetails,
690
+ steps: { automationSteps: [], manualSteps },
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',
820
+ type: 'TC',
821
+ };
822
+ }
823
+ /** Neutral SavedRef from a POST/PUT /tm/testcase response. Tolerates a bare doc or a `data`-wrapped
824
+ * response; prefers the business `code` (TC_xxx) for productUrl deep links but uses the numeric id
825
+ * when only that is present. */
826
+ export function toSavedRef(raw, created) {
827
+ const flat = (raw?.data ?? raw) ?? {};
828
+ const id = str(flat, 'id', '_id', 'testcaseId') ?? '';
829
+ return {
830
+ id,
831
+ created,
832
+ };
833
+ }
834
+ // ---------------------------------------------------------------------------
835
+ // Suites (write). The update PUT reshapes the members list under a strict
836
+ // allow-list; anything else on the raw doc is rejected. Shared helpers are:
837
+ // • normalizeSuiteEntry — copy an existing members[] row into the allow-list.
838
+ // • Adapter builds each ADD entry by fetching the target TC, so those helpers
839
+ // stay in the adapter (they need IO).
840
+ // ---------------------------------------------------------------------------
841
+ /** Copy an existing suite testCases[] row into the strict allow-list; drop `id`, audit fields, etc. */
842
+ export function normalizeSuiteEntry(m) {
843
+ const out = {};
844
+ const keep = ['testcaseId', 'code', 'seq', 'stepId', 'version', 'versionId', 'tcType', 'projectId', 'executionPlatform'];
845
+ for (const k of keep)
846
+ if (m?.[k] !== undefined)
847
+ out[k] = m[k];
848
+ return out;
849
+ }
850
+ /** The 15+ allow-listed suite-level keys the update validator accepts. */
851
+ export const SUITE_UPDATE_ALLOWED_KEYS = [
852
+ 'id', 'name', 'description', 'state', 'suiteType', 'projectId', 'autoDefectLog',
853
+ 'autoReExecCount', 'autoReExecTriggerOn', 'newBrowserSession', 'terminateOnFailure',
854
+ 'minimumContainerCount', 'maximumContainerCount', 'copySuiteName', 'copiedFrom',
855
+ 'mailersList', 'tags', 'customFields', 'agentId', 'cloudAgentId',
856
+ 'lastSelectedExecutionType', 'updateVersion',
857
+ ];
858
+ //# sourceMappingURL=mapper.js.map