common-qa-mcp 0.6.0 → 0.7.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 (49) hide show
  1. package/README.md +37 -79
  2. package/dist/adapters/remoteMcp/adapter.d.ts +3 -0
  3. package/dist/adapters/remoteMcp/adapter.d.ts.map +1 -1
  4. package/dist/adapters/remoteMcp/adapter.js +1 -1
  5. package/dist/adapters/remoteMcp/adapter.js.map +1 -1
  6. package/dist/adapters/simplifyqa/adapter.d.ts +23 -16
  7. package/dist/adapters/simplifyqa/adapter.d.ts.map +1 -1
  8. package/dist/adapters/simplifyqa/adapter.js +283 -61
  9. package/dist/adapters/simplifyqa/adapter.js.map +1 -1
  10. package/dist/adapters/simplifyqa/mapper.d.ts.map +1 -1
  11. package/dist/adapters/simplifyqa/mapper.js +22 -1
  12. package/dist/adapters/simplifyqa/mapper.js.map +1 -1
  13. package/dist/adapters/types.d.ts +18 -5
  14. package/dist/adapters/types.d.ts.map +1 -1
  15. package/dist/domain/types.d.ts +24 -1
  16. package/dist/domain/types.d.ts.map +1 -1
  17. package/dist/initAgents.d.ts.map +1 -1
  18. package/dist/initAgents.js +101 -15
  19. package/dist/initAgents.js.map +1 -1
  20. package/dist/server.d.ts.map +1 -1
  21. package/dist/server.js +95 -1
  22. package/dist/server.js.map +1 -1
  23. package/dist/tools/discovery.tools.d.ts.map +1 -1
  24. package/dist/tools/discovery.tools.js +36 -9
  25. package/dist/tools/discovery.tools.js.map +1 -1
  26. package/dist/tools/gapAnalysis.tools.d.ts +31 -0
  27. package/dist/tools/gapAnalysis.tools.d.ts.map +1 -0
  28. package/dist/tools/gapAnalysis.tools.js +287 -0
  29. package/dist/tools/gapAnalysis.tools.js.map +1 -0
  30. package/dist/tools/impactAnalysis.tools.d.ts +17 -0
  31. package/dist/tools/impactAnalysis.tools.d.ts.map +1 -0
  32. package/dist/tools/impactAnalysis.tools.js +352 -0
  33. package/dist/tools/impactAnalysis.tools.js.map +1 -0
  34. package/dist/tools/markdown.d.ts.map +1 -1
  35. package/dist/tools/markdown.js +6 -1
  36. package/dist/tools/markdown.js.map +1 -1
  37. package/dist/tools/objects.tools.d.ts.map +1 -1
  38. package/dist/tools/objects.tools.js +102 -16
  39. package/dist/tools/objects.tools.js.map +1 -1
  40. package/dist/tools/run.tools.d.ts.map +1 -1
  41. package/dist/tools/run.tools.js +41 -5
  42. package/dist/tools/run.tools.js.map +1 -1
  43. package/dist/tools/testcase.tools.d.ts.map +1 -1
  44. package/dist/tools/testcase.tools.js +9 -3
  45. package/dist/tools/testcase.tools.js.map +1 -1
  46. package/dist/tools/testdata.tools.d.ts.map +1 -1
  47. package/dist/tools/testdata.tools.js +43 -4
  48. package/dist/tools/testdata.tools.js.map +1 -1
  49. package/package.json +1 -1
@@ -514,10 +514,6 @@ export class SimplifyQaAdapter {
514
514
  const rows = Array.isArray(body) ? body : Array.isArray(body?.data) ? body.data : [];
515
515
  return rows;
516
516
  }
517
- /** For every FUNCTION step, resolve the referenced function's code + name via /tm/function search
518
- * (batched by distinct functionId). Mutates `tc.steps[i].functionCall` in place with
519
- * `testCaseCode`/`testCaseName`. Non-fatal on failure — the raw id still renders. Mirrors the
520
- * Excel export builder in tcfn.service.js:2696-2703. */
521
517
  async hydrateFunctionNames(tc) {
522
518
  const ids = new Set();
523
519
  for (const s of tc.steps) {
@@ -543,16 +539,33 @@ export class SimplifyQaAdapter {
543
539
  continue;
544
540
  byId.set(rid, { code: str(r, 'code'), name: str(r, 'name', 'title') ?? '' });
545
541
  }
542
+ const versionNamesById = new Map();
543
+ await Promise.all([...ids].map(async (fnId) => {
544
+ try {
545
+ const versionRows = await this.fetchFunctionVersionRows(fnId);
546
+ const versions = toVersionEntries(versionRows);
547
+ versionNamesById.set(fnId, new Map(versions.map((v) => [v.id, v.name])));
548
+ }
549
+ catch {
550
+ // Non-fatal per function: that step's version falls back to the bare uuid.
551
+ }
552
+ }));
546
553
  for (const s of tc.steps) {
547
554
  if (s.stepType !== 'function' || !s.functionCall)
548
555
  continue;
549
556
  const hit = byId.get(s.functionCall.testCaseId);
550
- if (!hit)
551
- continue;
552
- if (hit.code)
553
- s.functionCall.testCaseCode = hit.code;
554
- if (hit.name)
555
- s.functionCall.testCaseName = hit.name;
557
+ if (hit) {
558
+ if (hit.code)
559
+ s.functionCall.testCaseCode = hit.code;
560
+ if (hit.name)
561
+ s.functionCall.testCaseName = hit.name;
562
+ }
563
+ const versionId = s.functionCall.versionId;
564
+ if (versionId) {
565
+ const versionName = versionNamesById.get(s.functionCall.testCaseId)?.get(versionId);
566
+ if (versionName)
567
+ s.functionCall.versionName = versionName;
568
+ }
556
569
  }
557
570
  }
558
571
  catch {
@@ -1023,11 +1036,27 @@ export class SimplifyQaAdapter {
1023
1036
  * every node BEFORE that point is already persisted. */
1024
1037
  async createModule(input) {
1025
1038
  const root = await this.postOneModule(input.name, input.isRoot, input.projectId, input.description, input.parentId);
1039
+ if (!input.isRoot && input.parentId) {
1040
+ root.linkWarning = await this.checkChildLinked(input.parentId, root.id);
1041
+ }
1026
1042
  if (input.children?.length) {
1027
1043
  root.children = await this.createSubtree(input.children, root.id, input.projectId, input.name, root);
1028
1044
  }
1029
1045
  return root;
1030
1046
  }
1047
+ async checkChildLinked(parentId, childId) {
1048
+ try {
1049
+ const parent = await this.getModule(parentId);
1050
+ const numChildId = Number.parseInt(childId, 10);
1051
+ if (parent.childIds?.includes(numChildId))
1052
+ return undefined;
1053
+ return `Created with parentId=${parentId}, but a fresh read of module ${parentId} does not yet list ` +
1054
+ `${childId} in its childIds — it may not appear under its parent in the Module screen tree until that catches up.`;
1055
+ }
1056
+ catch {
1057
+ return undefined;
1058
+ }
1059
+ }
1031
1060
  /** Depth-first walk of a subtree. Stops at the first failure — everything already POSTed stays
1032
1061
  * (no rollback endpoint upstream) and the failure is bubbled back on the root via `failedAt`. */
1033
1062
  async createSubtree(children, parentId, projectId, parentPath, root) {
@@ -1044,6 +1073,7 @@ export class SimplifyQaAdapter {
1044
1073
  root.failedAt = { path, error: e instanceof Error ? e.message : String(e) };
1045
1074
  return out;
1046
1075
  }
1076
+ saved.linkWarning = await this.checkChildLinked(parentId, saved.id);
1047
1077
  if (child.children?.length) {
1048
1078
  saved.children = await this.createSubtree(child.children, saved.id, projectId, path, root);
1049
1079
  }
@@ -1065,23 +1095,56 @@ export class SimplifyQaAdapter {
1065
1095
  const raw = await this.http.postJson('/pm/module', payload);
1066
1096
  return toModuleSaved(raw, name);
1067
1097
  }
1068
- /** PUT /pm/module/:id — partial edit. Controller reads `projectId` off the body for its
1069
- * `add_module` privilege check (module.controller.js:64), so it MUST be sent even when unchanged;
1070
- * we default to the connected project. Only `name` / `description` are exposed — the controller
1071
- * ignores everything else the validator accepts (isRoot, parentId, childIds). */
1072
1098
  async updateModule(id, patch) {
1073
- const projectId = patch.projectId
1074
- ? Number.parseInt(numericId(patch.projectId), 10)
1075
- : this.requireProjectId();
1076
- const body = { projectId };
1077
- if (patch.name !== undefined)
1078
- body['name'] = patch.name;
1079
- if (patch.description !== undefined)
1080
- body['description'] = patch.description;
1081
1099
  const resolvedId = numericId(id);
1082
- const raw = await this.http.putJson(`/pm/module/${resolvedId}`, body);
1100
+ let raw;
1101
+ if (patch.name !== undefined || patch.description !== undefined) {
1102
+ const projectId = patch.projectId
1103
+ ? Number.parseInt(numericId(patch.projectId), 10)
1104
+ : this.requireProjectId();
1105
+ const body = { projectId };
1106
+ if (patch.name !== undefined)
1107
+ body['name'] = patch.name;
1108
+ if (patch.description !== undefined)
1109
+ body['description'] = patch.description;
1110
+ raw = await this.http.putJson(`/pm/module/${resolvedId}`, body);
1111
+ }
1112
+ if (patch.parentId !== undefined) {
1113
+ await this.reparentModule(resolvedId, patch.parentId);
1114
+ }
1083
1115
  return toSavedRef(raw ?? { id: resolvedId }, false);
1084
1116
  }
1117
+ async reparentModule(id, newParentId) {
1118
+ if (newParentId === '') {
1119
+ await this.http.putJson(`/pm/module/bulkupdate/${id}`, { isRoot: true });
1120
+ return;
1121
+ }
1122
+ const targetId = numericId(newParentId);
1123
+ if (targetId === id) {
1124
+ throw new Error(`Cannot set module ${id} as its own parent.`);
1125
+ }
1126
+ const all = await this.listModules();
1127
+ const childrenOf = new Map();
1128
+ for (const m of all)
1129
+ if (m.parentId)
1130
+ childrenOf.set(m.parentId, [...(childrenOf.get(m.parentId) ?? []), m.id]);
1131
+ const descendants = new Set();
1132
+ const stack = [...(childrenOf.get(id) ?? [])];
1133
+ while (stack.length) {
1134
+ const next = stack.pop();
1135
+ if (descendants.has(next))
1136
+ continue;
1137
+ descendants.add(next);
1138
+ stack.push(...(childrenOf.get(next) ?? []));
1139
+ }
1140
+ if (descendants.has(targetId)) {
1141
+ throw new Error(`Cannot move module ${id} under ${targetId} — it is already a descendant of ${id} (would detach the subtree).`);
1142
+ }
1143
+ if (!all.some((m) => m.id === targetId)) {
1144
+ throw new Error(`Parent module ${targetId} not found.`);
1145
+ }
1146
+ await this.http.putJson(`/pm/module/bulkupdate/${id}`, { parentId: Number.parseInt(targetId, 10), isRoot: false });
1147
+ }
1085
1148
  /** PATCH /pm/module — two-phase per module.controller.js:94. `confirm:false` returns the per-id
1086
1149
  * dependency footprint (test cases, functions, params, objects, defects, user stories). `confirm:true`
1087
1150
  * triggers the actual delete: server drops only ids with EMPTY dependencies; ids with references are
@@ -1195,12 +1258,55 @@ export class SimplifyQaAdapter {
1195
1258
  return withTotal(users, raw);
1196
1259
  }
1197
1260
  // ---------------------------------------------------------------------- Objects (read)
1198
- async getObject(id) {
1261
+ async getObject(id, opts) {
1199
1262
  const raw = await this.http.getJson(`/tm/object/${numericId(id)}`);
1200
1263
  let doc = raw?.data ?? raw;
1201
1264
  if (Array.isArray(doc))
1202
1265
  doc = doc.length > 0 ? doc[0] : {};
1203
- return toObject(doc && typeof doc === 'object' ? doc : {});
1266
+ const obj = toObject(doc && typeof doc === 'object' ? doc : {});
1267
+ if (opts?.enrichNames)
1268
+ await this.enrichObjectNames(obj);
1269
+ return obj;
1270
+ }
1271
+ async enrichObjectNames(obj) {
1272
+ if (obj.moduleId) {
1273
+ try {
1274
+ const modules = await this.listModules();
1275
+ const match = modules.find((m) => m.id === obj.moduleId);
1276
+ if (match)
1277
+ obj.moduleName = match.name;
1278
+ }
1279
+ catch { /* best-effort */ }
1280
+ }
1281
+ if (obj.parameterIds?.length) {
1282
+ const names = await this.resolveParameterNames(obj.parameterIds);
1283
+ const resolved = obj.parameterIds.map((pid) => names.get(pid)).filter((n) => !!n);
1284
+ if (resolved.length)
1285
+ obj.parameterNames = resolved;
1286
+ }
1287
+ }
1288
+ async resolveParameterNames(ids) {
1289
+ const map = new Map();
1290
+ const numIds = ids.map((i) => Number.parseInt(i, 10)).filter(Number.isFinite);
1291
+ if (!numIds.length)
1292
+ return map;
1293
+ try {
1294
+ const projectId = this.requireProjectId();
1295
+ const body = this.searchBody([
1296
+ { column: 'projectId', value: projectId },
1297
+ { column: 'id', value: numIds, regEx: false },
1298
+ ]);
1299
+ const raw = await this.http.postJson('/tm/parameter/search', body);
1300
+ const rows = pickArray(raw, 'data', 'content');
1301
+ for (const row of rows) {
1302
+ const rid = str(row, 'id', '_id', 'parameterId');
1303
+ const name = str(row, 'name');
1304
+ if (rid && name)
1305
+ map.set(rid, name);
1306
+ }
1307
+ }
1308
+ catch { /* best-effort */ }
1309
+ return map;
1204
1310
  }
1205
1311
  // ---------------------------------------------------------------------- Deep links
1206
1312
  productUrlFor(entity) {
@@ -2249,15 +2355,30 @@ export class SimplifyQaAdapter {
2249
2355
  })().catch((e) => { this.actionCatalogCache = null; throw e; });
2250
2356
  return this.actionCatalogCache;
2251
2357
  }
2252
- /** POST /tm/object/createObjectSeq — get-or-create the shared object row by locator match. The
2253
- * backend does the dedup (`object.controller.js:268-370`) — same-locators reuses, same-disname-
2254
- * different-locators creates a suffixed one. Returns the numeric object id.
2255
- *
2256
- * ponytail: minimum body — no thumbnail_url / pageScreenShot_url / parameterId. The recording
2257
- * path enriches these; the tc_save path doesn't have screenshots to attach, and the object
2258
- * works for execution without them. */
2259
2358
  async resolveObjectId(objectRef, projectId, moduleId, technology, objectTemplateId) {
2260
- const attributes = (objectRef.locators ?? []).map((l) => ({
2359
+ const name = (objectRef.name ?? '').trim();
2360
+ const incomingLocators = objectRef.locators ?? [];
2361
+ if (name) {
2362
+ try {
2363
+ const candidates = await this.searchObjects({ query: name, limit: 20 });
2364
+ const existing = candidates.find((c) => c.name.trim().toLowerCase() === name.toLowerCase());
2365
+ if (existing?.id) {
2366
+ const key = (l) => `${l.name}::${l.value}`;
2367
+ const existingKeys = new Set(existing.locators.map(key));
2368
+ const incomingKeys = new Set(incomingLocators.map(key));
2369
+ const sameLocators = existingKeys.size === incomingKeys.size
2370
+ && [...existingKeys].every((k) => incomingKeys.has(k));
2371
+ if (!sameLocators && incomingLocators.length) {
2372
+ await this.updateObject(existing.id, { locators: incomingLocators });
2373
+ }
2374
+ return Number.parseInt(existing.id, 10);
2375
+ }
2376
+ }
2377
+ catch {
2378
+ // Lookup failed — fall through to create rather than block the save over this.
2379
+ }
2380
+ }
2381
+ const attributes = incomingLocators.map((l) => ({
2261
2382
  name: l.name,
2262
2383
  value: l.value,
2263
2384
  ...(l.unique === true ? { unique: true } : {}),
@@ -2267,7 +2388,7 @@ export class SimplifyQaAdapter {
2267
2388
  moduleId,
2268
2389
  objecttemplateId: objectTemplateId,
2269
2390
  technology,
2270
- disname: objectRef.name ?? '',
2391
+ disname: name,
2271
2392
  attributes,
2272
2393
  parameterId: [],
2273
2394
  description: '',
@@ -2276,7 +2397,7 @@ export class SimplifyQaAdapter {
2276
2397
  const flat = raw?.data ?? raw;
2277
2398
  const id = str(flat, 'id', '_id', 'objectId');
2278
2399
  if (!id)
2279
- throw new Error(`createObjectSeq returned no id for object "${objectRef.name}": ${JSON.stringify(raw)}`);
2400
+ throw new Error(`createObjectSeq returned no id for object "${name}": ${JSON.stringify(raw)}`);
2280
2401
  return Number.parseInt(id, 10);
2281
2402
  }
2282
2403
  /** For every automation step, resolve `actionId` (from action name + technology) and
@@ -2586,6 +2707,26 @@ export class SimplifyQaAdapter {
2586
2707
  throw new Error('createObject: objectType is required (e.g. "WEB", "ANDROID") — it selects the object template ' +
2587
2708
  'and cannot be changed after the object is created.');
2588
2709
  }
2710
+ const wantedName = (o.name ?? '').trim();
2711
+ if (wantedName) {
2712
+ try {
2713
+ const candidates = await this.searchObjects({ query: wantedName, limit: 20 });
2714
+ const existing = candidates.find((c) => c.name.trim().toLowerCase() === wantedName.toLowerCase());
2715
+ if (existing?.id) {
2716
+ const key = (l) => `${l.name}::${l.value}`;
2717
+ const existingKeys = new Set(existing.locators.map(key));
2718
+ const incomingKeys = new Set((o.locators ?? []).map(key));
2719
+ const sameLocators = existingKeys.size === incomingKeys.size
2720
+ && [...existingKeys].every((k) => incomingKeys.has(k));
2721
+ return sameLocators || !o.locators?.length
2722
+ ? existing
2723
+ : await this.updateObject(existing.id, { locators: o.locators });
2724
+ }
2725
+ }
2726
+ catch {
2727
+ // Lookup failed — fall through to create rather than block the save over this.
2728
+ }
2729
+ }
2589
2730
  const projectId = this.requireProjectId();
2590
2731
  const objecttemplateId = await this.resolveObjectTemplateId(o.objectType);
2591
2732
  const moduleId = await this.resolveDefaultModuleId();
@@ -2673,7 +2814,7 @@ export class SimplifyQaAdapter {
2673
2814
  * attribute set is even valid for this object), so editing them here — outside the create flow — is
2674
2815
  * out of scope; changing them means creating a new object, not "updating" this one.
2675
2816
  */
2676
- async updateObject(id, changes) {
2817
+ async updateObject(id, changes, opts) {
2677
2818
  const numId = numericId(id);
2678
2819
  const raw = await this.http.getJson(`/tm/object/${numId}`);
2679
2820
  const doc = (raw?.data && typeof raw.data === 'object' ? raw.data : raw) ?? {};
@@ -2695,12 +2836,41 @@ export class SimplifyQaAdapter {
2695
2836
  else
2696
2837
  attributes.push(merged);
2697
2838
  }
2839
+ let moduleId = doc.moduleId;
2840
+ if (changes.moduleName !== undefined) {
2841
+ const modules = await this.listModules();
2842
+ const wanted = changes.moduleName.trim().toLowerCase();
2843
+ const match = modules.find((m) => m.name.toLowerCase() === wanted);
2844
+ if (!match) {
2845
+ const options = modules.map((m) => m.name).join(', ') || '(no modules configured in this project)';
2846
+ throw new Error(`Module "${changes.moduleName}" not found on object ${id}. Valid modules: ${options}`);
2847
+ }
2848
+ moduleId = Number.parseInt(match.id, 10);
2849
+ }
2850
+ let parameterId = doc.parameterId ?? [];
2851
+ if (changes.parameterNames !== undefined) {
2852
+ const projectId = this.requireProjectId();
2853
+ const resolved = [];
2854
+ const unresolved = [];
2855
+ for (const name of changes.parameterNames) {
2856
+ const pid = await this.findParameterId(projectId, name);
2857
+ if (pid != null)
2858
+ resolved.push(pid);
2859
+ else
2860
+ unresolved.push(name);
2861
+ }
2862
+ if (unresolved.length) {
2863
+ throw new Error(`Parameter(s) not found on object ${id}: ${unresolved.join(', ')}. Check the exact name ` +
2864
+ '(case-insensitive), or create the parameter first in the Parameters screen.');
2865
+ }
2866
+ parameterId = resolved;
2867
+ }
2698
2868
  const body = {
2699
2869
  disname: changes.name ?? doc.disname,
2700
- description: doc.description,
2870
+ description: changes.description !== undefined ? changes.description : doc.description,
2701
2871
  objecttemplateId: doc.objecttemplateId,
2702
- parameterId: doc.parameterId ?? [],
2703
- moduleId: doc.moduleId,
2872
+ parameterId,
2873
+ moduleId,
2704
2874
  technology: doc.technology,
2705
2875
  attributes,
2706
2876
  projectId: this.requireProjectId(),
@@ -2708,7 +2878,10 @@ export class SimplifyQaAdapter {
2708
2878
  };
2709
2879
  const res = await this.http.putJson(`/tm/object/${numId}`, body);
2710
2880
  const updated = (res?.data && typeof res.data === 'object' ? res.data : res) ?? doc;
2711
- return toObject(updated);
2881
+ const obj = toObject(updated);
2882
+ if (opts?.enrichNames)
2883
+ await this.enrichObjectNames(obj);
2884
+ return obj;
2712
2885
  }
2713
2886
  // ---------------------------------------------------------------------- Actions catalog
2714
2887
  async actionsCatalog(platform) {
@@ -2762,8 +2935,9 @@ export class SimplifyQaAdapter {
2762
2935
  const meta = await this.fetchParameterMeta(pid);
2763
2936
  if (!meta)
2764
2937
  continue; // parameter deleted — skip
2765
- const seed = meta.protected ? (rawValue ? '*****' : '(empty)') : rawValue;
2766
- parameters.push({ paramId: pid, name: meta.name, seed, ...(meta.protected ? { protected: true } : {}) });
2938
+ const isProtected = meta.protected || looksSensitive(meta.name);
2939
+ const seed = isProtected ? (rawValue ? '*****' : '(empty)') : rawValue;
2940
+ parameters.push({ paramId: pid, name: meta.name, seed, ...(isProtected ? { protected: true } : {}) });
2767
2941
  }
2768
2942
  }
2769
2943
  return { tcId: q.tcId, versionId: versionId, environmentType: env, parameters };
@@ -2942,23 +3116,19 @@ export class SimplifyQaAdapter {
2942
3116
  // Top-level execution doc (carries `type` = 'TC' or 'SUITE', and `executionItems[]` for a suite).
2943
3117
  // One extra POST per getRun call; a suite short-circuits here so no /exec/report is needed.
2944
3118
  if (Number.isFinite(runIdNum)) {
2945
- const execBody = {
2946
- searchFields: [
2947
- { column: 'id', value: runIdNum, regEx: false },
2948
- { column: 'projectId', value: projectId, regEx: false },
2949
- { column: 'deleted', value: false },
2950
- ],
2951
- startIndex: 0,
2952
- limit: 1,
2953
- };
2954
- const execRaw = await this.http.postJson('/exec/execution/search', execBody);
2955
- const execDoc = pickArray(execRaw, 'data', 'content', 'executions')[0];
3119
+ const execDoc = await this.fetchExecDoc(runIdNum, projectId);
2956
3120
  if (execDoc && String(execDoc.type ?? '').toUpperCase() === 'SUITE') {
2957
3121
  const items = Array.isArray(execDoc.executionItems) ? execDoc.executionItems : [];
2958
3122
  const children = items.map((it) => {
3123
+ const testcaseId = str(it, 'testcaseId');
3124
+ const versionId = str(it, 'versionId');
3125
+ const stepId = str(it, 'stepId');
2959
3126
  const child = {
2960
- id: str(it, 'code') ?? String(it?.id ?? ''),
3127
+ id: testcaseId ?? String(it?.id ?? ''),
2961
3128
  state: mapRunState(str(it, 'status') ?? ''),
3129
+ ...(testcaseId ? { testcaseId } : {}),
3130
+ ...(versionId ? { versionId } : {}),
3131
+ ...(stepId ? { stepId } : {}),
2962
3132
  };
2963
3133
  const name = str(it, 'name');
2964
3134
  if (name)
@@ -2991,14 +3161,33 @@ export class SimplifyQaAdapter {
2991
3161
  }
2992
3162
  const executionRunId = str(chosen, 'id', '_id') ?? '';
2993
3163
  const parentId = Number(chosen?.executionItemId ?? 0);
3164
+ const stepResults = await this.fetchStepResults(runIdNum, executionRunId, parentId, projectId);
3165
+ return {
3166
+ runId,
3167
+ state: mapRunState(str(chosen, 'status') ?? ''),
3168
+ stepResults,
3169
+ reportUrl: `${base}/user/reports/reports-page?type=TESTCASE&parentId=${parentId}&id=${runIdNum}&projectId=${projectId}`,
3170
+ };
3171
+ }
3172
+ async fetchExecDoc(runIdNum, projectId) {
3173
+ const execBody = {
3174
+ searchFields: [
3175
+ { column: 'id', value: runIdNum, regEx: false },
3176
+ { column: 'projectId', value: projectId, regEx: false },
3177
+ { column: 'deleted', value: false },
3178
+ ],
3179
+ startIndex: 0,
3180
+ limit: 1,
3181
+ };
3182
+ const execRaw = await this.http.postJson('/exec/execution/search', execBody);
3183
+ return pickArray(execRaw, 'data', 'content', 'executions')[0];
3184
+ }
3185
+ async fetchStepResults(executionId, executionRunId, parentId, projectId) {
2994
3186
  const stepsResp = await this.http.postJson('/reports/execution/getstepResult', {
2995
- executionId: Number.parseInt(numericId(runId), 10),
2996
- executionRunId,
2997
- parentId,
2998
- projectId,
3187
+ executionId, executionRunId, parentId, projectId,
2999
3188
  });
3000
3189
  const rawSteps = Array.isArray(stepsResp?.data) ? stepsResp.data : [];
3001
- const stepResults = rawSteps.map((s) => {
3190
+ return rawSteps.map((s) => {
3002
3191
  const step = {
3003
3192
  n: typeof s?.seq === 'number' ? s.seq : 0,
3004
3193
  description: String(s?.description ?? ''),
@@ -3028,11 +3217,44 @@ export class SimplifyQaAdapter {
3028
3217
  }
3029
3218
  return step;
3030
3219
  });
3220
+ }
3221
+ async getSuiteChildRun(suiteRunId, child) {
3222
+ const projectId = this.requireProjectId();
3223
+ const runIdNum = Number.parseInt(numericId(suiteRunId), 10);
3224
+ const execDoc = await this.fetchExecDoc(runIdNum, projectId);
3225
+ if (!execDoc)
3226
+ throw new Error(`Suite run ${suiteRunId} not found.`);
3227
+ const suiteNum = Number(execDoc.executionTypeId ?? 0);
3228
+ const body = {
3229
+ type: 'SU',
3230
+ projectId,
3231
+ parentId: suiteNum,
3232
+ executionId: runIdNum,
3233
+ tcId: Number.parseInt(numericId(child.testcaseId), 10),
3234
+ ...(child.versionId ? { tcVersionId: child.versionId } : {}),
3235
+ ...(child.stepId ? { stepId: child.stepId } : {}),
3236
+ };
3237
+ const raw = await this.http.postJson('/reports/execution/getExecutionRuns', body);
3238
+ const runs = Array.isArray(raw?.data) ? raw.data : [];
3239
+ const base = (this.cfg.baseUrl ?? '').replace(/\/+$/, '');
3240
+ if (!runs.length) {
3241
+ return { runId: suiteRunId, state: 'queued' };
3242
+ }
3243
+ let chosen = runs[0];
3244
+ for (const r of runs) {
3245
+ if (typeof r?.status === 'string' && r.status.toUpperCase() === 'FAILED') {
3246
+ chosen = r;
3247
+ break;
3248
+ }
3249
+ }
3250
+ const executionRunId = str(chosen, 'id', '_id') ?? '';
3251
+ const parentId = Number(chosen?.executionItemId ?? 0);
3252
+ const stepResults = await this.fetchStepResults(runIdNum, executionRunId, parentId, projectId);
3031
3253
  return {
3032
- runId,
3254
+ runId: suiteRunId,
3033
3255
  state: mapRunState(str(chosen, 'status') ?? ''),
3034
3256
  stepResults,
3035
- reportUrl: `${base}/user/reports/reports-page?type=TESTCASE&parentId=${parentId}&id=${Number.parseInt(numericId(runId), 10)}&projectId=${projectId}`,
3257
+ reportUrl: `${base}/user/reports/reports-page?type=TESTCASE&parentId=${parentId}&id=${runIdNum}&projectId=${projectId}`,
3036
3258
  };
3037
3259
  }
3038
3260
  /** Most recent execution runId for a SUITE — same `/exec/execution/search` the /reports UI's