common-qa-mcp 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/remoteMcp/adapter.js +1 -1
- package/dist/adapters/remoteMcp/adapter.js.map +1 -1
- package/dist/adapters/simplifyqa/adapter.d.ts +24 -4
- package/dist/adapters/simplifyqa/adapter.d.ts.map +1 -1
- package/dist/adapters/simplifyqa/adapter.js +273 -47
- package/dist/adapters/simplifyqa/adapter.js.map +1 -1
- package/dist/adapters/simplifyqa/mapper.d.ts +6 -1
- package/dist/adapters/simplifyqa/mapper.d.ts.map +1 -1
- package/dist/adapters/simplifyqa/mapper.js +137 -6
- package/dist/adapters/simplifyqa/mapper.js.map +1 -1
- package/dist/adapters/types.d.ts +10 -1
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/domain/types.d.ts +24 -0
- package/dist/domain/types.d.ts.map +1 -1
- package/dist/domain/validate.d.ts.map +1 -1
- package/dist/domain/validate.js +14 -8
- package/dist/domain/validate.js.map +1 -1
- package/dist/drafts/store.d.ts +9 -0
- package/dist/drafts/store.d.ts.map +1 -1
- package/dist/drafts/store.js +12 -0
- package/dist/drafts/store.js.map +1 -1
- package/dist/initAgents.d.ts.map +1 -1
- package/dist/initAgents.js +43 -9
- package/dist/initAgents.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +11 -3
- package/dist/server.js.map +1 -1
- package/dist/tools/actionCatalog.d.ts +24 -0
- package/dist/tools/actionCatalog.d.ts.map +1 -0
- package/dist/tools/actionCatalog.js +50 -0
- package/dist/tools/actionCatalog.js.map +1 -0
- package/dist/tools/recording.tools.d.ts.map +1 -1
- package/dist/tools/recording.tools.js +234 -0
- package/dist/tools/recording.tools.js.map +1 -1
- package/dist/tools/run.tools.d.ts.map +1 -1
- package/dist/tools/run.tools.js +160 -9
- package/dist/tools/run.tools.js.map +1 -1
- package/dist/tools/schemas.d.ts +28 -0
- package/dist/tools/schemas.d.ts.map +1 -1
- package/dist/tools/schemas.js +4 -0
- package/dist/tools/schemas.js.map +1 -1
- package/dist/tools/testcase.tools.d.ts +15 -1
- package/dist/tools/testcase.tools.d.ts.map +1 -1
- package/dist/tools/testcase.tools.js +39 -20
- package/dist/tools/testcase.tools.js.map +1 -1
- package/dist/tools/testdata.tools.d.ts.map +1 -1
- package/dist/tools/testdata.tools.js +60 -6
- package/dist/tools/testdata.tools.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import { makeClient } from '../http.js';
|
|
3
3
|
import { RemoteMcpAdapter } from '../remoteMcp/adapter.js';
|
|
4
|
-
import { applyCollectionSteps, buildTestcasePayload, deriveTcType, extractParamNames, fromSqaStep, looksSensitive, normalizeSuiteEntry, numericId, paramDefault, pickArray, str, SUITE_UPDATE_ALLOWED_KEYS, TECH_BY_STEPTYPE, toDefectSummary, toEpic, toEpicSummary, toFeature, toFeatureSummary, toFunction, toFunctionSummary, toModuleSaved, toModuleSummary, toObject, toReleaseSummary, toSavedRef, toSprintSummary, toStory, toStorySummary, toSuite, toSuiteSummary, toTaskSummary, toTcSummary, toTestCase, toVersionEntries, withTotal, } from './mapper.js';
|
|
4
|
+
import { applyCollectionSteps, buildTestcasePayload, deriveTcType, extractApiParamNames, extractParamNames, fromSqaStep, looksSensitive, normalizeSuiteEntry, numericId, paramDefault, pickArray, str, SUITE_UPDATE_ALLOWED_KEYS, TECH_BY_STEPTYPE, toDefectSummary, toEpic, toEpicSummary, toFeature, toFeatureSummary, toFunction, toFunctionSummary, toModuleSaved, toModuleSummary, toObject, toImageUrl, toReleaseSummary, toSavedRef, toSprintSummary, toStory, toStorySummary, toSuite, toSuiteSummary, toTaskSummary, toTcSummary, toTestCase, toVersionEntries, withTotal, } from './mapper.js';
|
|
5
5
|
/** SQA run-row status → neutral RunStatus.state. */
|
|
6
6
|
function mapRunState(s) {
|
|
7
7
|
switch (s.toUpperCase()) {
|
|
@@ -65,13 +65,17 @@ function normalizeFormula(doc, projectId) {
|
|
|
65
65
|
parameters: params,
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
|
-
/** SQA per-step status → neutral step status.
|
|
68
|
+
/** SQA per-step status → neutral step status. Matches RunState terminals ('passed'/'failed'/
|
|
69
|
+
* 'skipped'/'stopped') so the same icon/formatting logic works for top-level state AND per-step
|
|
70
|
+
* rows (run.tools.ts:180 keys off these values — the earlier 'pass'/'fail' names left every
|
|
71
|
+
* step icon defaulting to '•'). Non-terminal SQA states (INFO, INPROGRESS, NOTEXECUTED) pass
|
|
72
|
+
* through lowercased so the tool can still surface them as diagnostic text. */
|
|
69
73
|
function mapStepStatus(s) {
|
|
70
74
|
switch (s.toUpperCase()) {
|
|
71
|
-
case 'PASSED': return '
|
|
72
|
-
case 'FAILED': return '
|
|
73
|
-
case 'SKIPPED': return '
|
|
74
|
-
case 'STOPPED': return '
|
|
75
|
+
case 'PASSED': return 'passed';
|
|
76
|
+
case 'FAILED': return 'failed';
|
|
77
|
+
case 'SKIPPED': return 'skipped';
|
|
78
|
+
case 'STOPPED': return 'stopped';
|
|
75
79
|
default: return s.toLowerCase();
|
|
76
80
|
}
|
|
77
81
|
}
|
|
@@ -608,7 +612,7 @@ export class SimplifyQaAdapter {
|
|
|
608
612
|
async hydrateObjectNames(tc) {
|
|
609
613
|
const ids = new Set();
|
|
610
614
|
for (const s of tc.steps) {
|
|
611
|
-
if (s.objectRef?.id && !s.objectRef.name)
|
|
615
|
+
if (s.objectRef?.id && (!s.objectRef.name || !s.objectRef.thumbnailUrl))
|
|
612
616
|
ids.add(s.objectRef.id);
|
|
613
617
|
}
|
|
614
618
|
if (!ids.size)
|
|
@@ -623,6 +627,7 @@ export class SimplifyQaAdapter {
|
|
|
623
627
|
const raw = await this.http.postJson('/tm/object/search', body);
|
|
624
628
|
const rows = pickArray(raw, 'data', 'content', 'objects');
|
|
625
629
|
const byId = new Map();
|
|
630
|
+
const imgById = new Map();
|
|
626
631
|
for (const r of rows) {
|
|
627
632
|
const rid = str(r, 'id', '_id');
|
|
628
633
|
if (!rid)
|
|
@@ -630,9 +635,19 @@ export class SimplifyQaAdapter {
|
|
|
630
635
|
const name = str(r, 'disname', 'description', 'placeholder');
|
|
631
636
|
if (name)
|
|
632
637
|
byId.set(rid, name);
|
|
638
|
+
const thumbnailUrl = toImageUrl(r?.thumbnail_url, this.cfg.baseUrl);
|
|
639
|
+
const pageScreenshotUrl = toImageUrl(r?.pageScreenShot_url, this.cfg.baseUrl);
|
|
640
|
+
if (thumbnailUrl || pageScreenshotUrl) {
|
|
641
|
+
imgById.set(rid, { ...(thumbnailUrl ? { thumbnailUrl } : {}), ...(pageScreenshotUrl ? { pageScreenshotUrl } : {}) });
|
|
642
|
+
}
|
|
633
643
|
}
|
|
634
644
|
for (const s of tc.steps) {
|
|
635
|
-
if (!s.objectRef?.id
|
|
645
|
+
if (!s.objectRef?.id)
|
|
646
|
+
continue;
|
|
647
|
+
const img = imgById.get(s.objectRef.id);
|
|
648
|
+
if (img)
|
|
649
|
+
Object.assign(s.objectRef, img);
|
|
650
|
+
if (s.objectRef.name)
|
|
636
651
|
continue;
|
|
637
652
|
const name = byId.get(s.objectRef.id);
|
|
638
653
|
if (name)
|
|
@@ -1016,15 +1031,41 @@ export class SimplifyQaAdapter {
|
|
|
1016
1031
|
return str(rows[0], 'id', '_id', 'featureId') ?? ref;
|
|
1017
1032
|
}
|
|
1018
1033
|
// ---------------------------------------------------------------------- Modules, releases, sprints
|
|
1034
|
+
/** Pages internally until the server returns a short page (or the total is reached) — the tool
|
|
1035
|
+
* has no limit/offset of its own and always wants the WHOLE project tree; `searchBody`'s default
|
|
1036
|
+
* page size (100) previously truncated any project with more modules than that silently. */
|
|
1019
1037
|
async listModules() {
|
|
1020
1038
|
const projectId = this.requireProjectId();
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1039
|
+
const PAGE = 100;
|
|
1040
|
+
const SAFETY_CAP = 5000;
|
|
1041
|
+
const rows = [];
|
|
1042
|
+
let totalCount;
|
|
1043
|
+
let startIndex = 0;
|
|
1044
|
+
for (;;) {
|
|
1045
|
+
const body = this.searchBody([
|
|
1046
|
+
{ column: 'projectId', value: projectId },
|
|
1047
|
+
{ column: 'deleted', value: false },
|
|
1048
|
+
], { selectFields: ['id', 'code', 'name', 'parentId', 'isRoot', 'childIds'], startIndex, limit: PAGE });
|
|
1049
|
+
const raw = await this.http.postJson('/pm/module/search', body);
|
|
1050
|
+
const page = pickArray(raw, 'data', 'content', 'modules');
|
|
1051
|
+
if (totalCount === undefined) {
|
|
1052
|
+
const t = raw?.totalCount ?? raw?.total ?? raw?.totalRecords;
|
|
1053
|
+
if (typeof t === 'number')
|
|
1054
|
+
totalCount = t;
|
|
1055
|
+
}
|
|
1056
|
+
rows.push(...page);
|
|
1057
|
+
if (page.length < PAGE)
|
|
1058
|
+
break;
|
|
1059
|
+
if (rows.length >= SAFETY_CAP)
|
|
1060
|
+
break;
|
|
1061
|
+
if (totalCount !== undefined && rows.length >= totalCount)
|
|
1062
|
+
break;
|
|
1063
|
+
startIndex += PAGE;
|
|
1064
|
+
}
|
|
1065
|
+
const mapped = rows.map(toModuleSummary);
|
|
1066
|
+
if (totalCount !== undefined)
|
|
1067
|
+
mapped.totalCount = totalCount;
|
|
1068
|
+
return mapped;
|
|
1028
1069
|
}
|
|
1029
1070
|
/** GET /pm/module/:id — server returns an array; empty array → NotFound. */
|
|
1030
1071
|
async getModule(id) {
|
|
@@ -1227,23 +1268,73 @@ export class SimplifyQaAdapter {
|
|
|
1227
1268
|
// reaches that branch after emptyIds.push).
|
|
1228
1269
|
return { dryRun: false, deleted: true, dependencies: [], hasBlockers: false };
|
|
1229
1270
|
}
|
|
1271
|
+
/** `list_releases` has no limit/offset of its own — it always wants the WHOLE visible set — so
|
|
1272
|
+
* page internally past `searchBody`'s default 100 instead of silently truncating (same fix as
|
|
1273
|
+
* listModules). */
|
|
1230
1274
|
async listReleases(q) {
|
|
1231
1275
|
const fields = [{ column: 'deleted', value: false }];
|
|
1232
1276
|
if (q.scope === 'project')
|
|
1233
1277
|
fields.push({ column: 'projectIds', value: this.requireProjectId() });
|
|
1234
|
-
const
|
|
1235
|
-
const
|
|
1236
|
-
const rows =
|
|
1237
|
-
|
|
1278
|
+
const PAGE = 100;
|
|
1279
|
+
const SAFETY_CAP = 5000;
|
|
1280
|
+
const rows = [];
|
|
1281
|
+
let totalCount;
|
|
1282
|
+
let startIndex = 0;
|
|
1283
|
+
for (;;) {
|
|
1284
|
+
const body = this.searchBody(fields, { startIndex, limit: PAGE });
|
|
1285
|
+
const raw = await this.http.postJson('/pm/release/search', body);
|
|
1286
|
+
const page = pickArray(raw, 'data', 'content', 'releases');
|
|
1287
|
+
if (totalCount === undefined) {
|
|
1288
|
+
const t = raw?.totalCount ?? raw?.total ?? raw?.totalRecords;
|
|
1289
|
+
if (typeof t === 'number')
|
|
1290
|
+
totalCount = t;
|
|
1291
|
+
}
|
|
1292
|
+
rows.push(...page);
|
|
1293
|
+
if (page.length < PAGE)
|
|
1294
|
+
break;
|
|
1295
|
+
if (rows.length >= SAFETY_CAP)
|
|
1296
|
+
break;
|
|
1297
|
+
if (totalCount !== undefined && rows.length >= totalCount)
|
|
1298
|
+
break;
|
|
1299
|
+
startIndex += PAGE;
|
|
1300
|
+
}
|
|
1301
|
+
const mapped = rows.map(toReleaseSummary);
|
|
1302
|
+
if (totalCount !== undefined)
|
|
1303
|
+
mapped.totalCount = totalCount;
|
|
1304
|
+
return mapped;
|
|
1238
1305
|
}
|
|
1306
|
+
/** Same unconditional-pagination fix as listReleases — `list_sprints` has no limit/offset either. */
|
|
1239
1307
|
async listSprints(q) {
|
|
1240
1308
|
const fields = [{ column: 'deleted', value: false }];
|
|
1241
1309
|
if (q.releaseId)
|
|
1242
1310
|
fields.push({ column: 'releaseId', value: Number.parseInt(q.releaseId, 10) });
|
|
1243
|
-
const
|
|
1244
|
-
const
|
|
1245
|
-
const rows =
|
|
1246
|
-
|
|
1311
|
+
const PAGE = 100;
|
|
1312
|
+
const SAFETY_CAP = 5000;
|
|
1313
|
+
const rows = [];
|
|
1314
|
+
let totalCount;
|
|
1315
|
+
let startIndex = 0;
|
|
1316
|
+
for (;;) {
|
|
1317
|
+
const body = this.searchBody(fields, { startIndex, limit: PAGE });
|
|
1318
|
+
const raw = await this.http.postJson('/pm/sprint/search', body);
|
|
1319
|
+
const page = pickArray(raw, 'data', 'content', 'sprints');
|
|
1320
|
+
if (totalCount === undefined) {
|
|
1321
|
+
const t = raw?.totalCount ?? raw?.total ?? raw?.totalRecords;
|
|
1322
|
+
if (typeof t === 'number')
|
|
1323
|
+
totalCount = t;
|
|
1324
|
+
}
|
|
1325
|
+
rows.push(...page);
|
|
1326
|
+
if (page.length < PAGE)
|
|
1327
|
+
break;
|
|
1328
|
+
if (rows.length >= SAFETY_CAP)
|
|
1329
|
+
break;
|
|
1330
|
+
if (totalCount !== undefined && rows.length >= totalCount)
|
|
1331
|
+
break;
|
|
1332
|
+
startIndex += PAGE;
|
|
1333
|
+
}
|
|
1334
|
+
const mapped = rows.map(toSprintSummary);
|
|
1335
|
+
if (totalCount !== undefined)
|
|
1336
|
+
mapped.totalCount = totalCount;
|
|
1337
|
+
return mapped;
|
|
1247
1338
|
}
|
|
1248
1339
|
/** POST /pm/users/search — the customer's users (router mounted at `/pm/users`, plural — see
|
|
1249
1340
|
* project-management/express-app.js:181). Backend auto-scopes non-admin callers to users in
|
|
@@ -1285,7 +1376,7 @@ export class SimplifyQaAdapter {
|
|
|
1285
1376
|
let doc = raw?.data ?? raw;
|
|
1286
1377
|
if (Array.isArray(doc))
|
|
1287
1378
|
doc = doc.length > 0 ? doc[0] : {};
|
|
1288
|
-
const obj = toObject(doc && typeof doc === 'object' ? doc : {});
|
|
1379
|
+
const obj = toObject(doc && typeof doc === 'object' ? doc : {}, this.cfg.baseUrl);
|
|
1289
1380
|
if (opts?.enrichNames)
|
|
1290
1381
|
await this.enrichObjectNames(obj);
|
|
1291
1382
|
return obj;
|
|
@@ -2328,6 +2419,22 @@ export class SimplifyQaAdapter {
|
|
|
2328
2419
|
paramIdByName.set(name, id);
|
|
2329
2420
|
}
|
|
2330
2421
|
}
|
|
2422
|
+
// API steps keep their `{{name}}` tokens INSIDE the request object (url/headers/body/auth),
|
|
2423
|
+
// not in the description — scan those too so the mapper can rewrite them to `{{<paramId>}}`
|
|
2424
|
+
// and register parameterIds. Without this, an API request containing {{var}} saves unbound.
|
|
2425
|
+
if (s.stepType === 'api') {
|
|
2426
|
+
const apiObj = (s.api ?? s.sourceRaw?.['apiData']);
|
|
2427
|
+
for (const name of extractApiParamNames(apiObj)) {
|
|
2428
|
+
// A purely-numeric token is an ALREADY-BOUND `{{<paramId>}}` (round-tripped from the
|
|
2429
|
+
// backend), not a parameter name — never create a parameter for it.
|
|
2430
|
+
if (/^\d+$/.test(name))
|
|
2431
|
+
continue;
|
|
2432
|
+
if (paramIdByName.has(name))
|
|
2433
|
+
continue;
|
|
2434
|
+
const id = await this.getOrCreateParameter(projectId, moduleId, name, paramDefault(name, tc.data, undefined));
|
|
2435
|
+
paramIdByName.set(name, id);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2331
2438
|
}
|
|
2332
2439
|
return paramIdByName;
|
|
2333
2440
|
}
|
|
@@ -2433,6 +2540,14 @@ export class SimplifyQaAdapter {
|
|
|
2433
2540
|
parameterId: [],
|
|
2434
2541
|
description: '',
|
|
2435
2542
|
};
|
|
2543
|
+
// Backend expects raw base64 PNG (no `data:` prefix) and stores it under /download/recording.
|
|
2544
|
+
const rawImage = (v) => (v ?? '').replace(/^data:image\/\w+;base64,/i, '').trim();
|
|
2545
|
+
const thumb = rawImage(objectRef.thumbnail_url);
|
|
2546
|
+
const pageShot = rawImage(objectRef.pageScreenShot_url);
|
|
2547
|
+
if (thumb)
|
|
2548
|
+
body['thumbnail_url'] = thumb;
|
|
2549
|
+
if (pageShot)
|
|
2550
|
+
body['pageScreenShot_url'] = pageShot;
|
|
2436
2551
|
const raw = await this.http.postJson('/tm/object/createObjectSeq', body);
|
|
2437
2552
|
const flat = raw?.data ?? raw;
|
|
2438
2553
|
const id = str(flat, 'id', '_id', 'objectId');
|
|
@@ -2457,7 +2572,11 @@ export class SimplifyQaAdapter {
|
|
|
2457
2572
|
// ponytail: control-flow / function / switch / function_end steps have no action to resolve.
|
|
2458
2573
|
// Push a null-actionId slot so index alignment survives; mapper checks and skips action/object
|
|
2459
2574
|
// emit for these. Without this, IF/FOR/WHILE/FUNCTION steps throw "missing actionType" on save.
|
|
2460
|
-
|
|
2575
|
+
// API steps are action-less too: Postman import and the SQA UI both persist them with
|
|
2576
|
+
// actionId:null and identify them by stepType:'API' + apiData. Resolving an action name here
|
|
2577
|
+
// would fail for a step that legitimately has none (e.g. the recorder's "API Call" is not a
|
|
2578
|
+
// backend action name — the seeded one is executeRestAPI).
|
|
2579
|
+
if (s.stepType === 'control_flow' || s.stepType === 'function' || s.stepType === 'function_end' || s.stepType === 'switch' || s.stepType === 'manual' || s.stepType === 'api') {
|
|
2461
2580
|
out.push({ actionId: null, objectId: null });
|
|
2462
2581
|
continue;
|
|
2463
2582
|
}
|
|
@@ -2767,6 +2886,25 @@ export class SimplifyQaAdapter {
|
|
|
2767
2886
|
doc = doc.length > 0 ? doc[0] : {};
|
|
2768
2887
|
return doc && typeof doc === 'object' ? doc : {};
|
|
2769
2888
|
}
|
|
2889
|
+
/** Mirrors sqa-ui `buildExecutionPlatform` (suites.component.ts:779) so MCP-added TCs save with
|
|
2890
|
+
* the same per-row platform the UI would set — otherwise the suite trips the "select execution
|
|
2891
|
+
* platform" warning at run time. Special types self-identify; WEB falls back to Chrome so
|
|
2892
|
+
* suite_run doesn't send `platform: []`; mobile with no bound device stays empty (UI does same). */
|
|
2893
|
+
buildExecutionPlatformForEntry(raw, tcType) {
|
|
2894
|
+
const special = new Set(['API', 'MAINFRAME', 'SAP', 'DESKTOP', 'DATABASE']);
|
|
2895
|
+
if (special.has(tcType)) {
|
|
2896
|
+
const p = { id: tcType, name: tcType, technologyType: tcType, type: 'NONE' };
|
|
2897
|
+
return { LOCAL: p, REMOTE: { ...p }, CLOUD: { ...p } };
|
|
2898
|
+
}
|
|
2899
|
+
const own = raw?.['executionPlatform'];
|
|
2900
|
+
if (own && typeof own === 'object' && own.id) {
|
|
2901
|
+
return { LOCAL: own, REMOTE: {}, CLOUD: {} };
|
|
2902
|
+
}
|
|
2903
|
+
if (tcType === 'WEB') {
|
|
2904
|
+
return { LOCAL: { id: 'CHROME', name: 'Chrome', technologyType: 'WEB', type: 'NONE' }, REMOTE: {}, CLOUD: {} };
|
|
2905
|
+
}
|
|
2906
|
+
return { LOCAL: {}, REMOTE: {}, CLOUD: {} };
|
|
2907
|
+
}
|
|
2770
2908
|
/** Assemble the strict allow-listed suite entry for an ADD op. Required fields per the SQA
|
|
2771
2909
|
* validator: testcaseId, code, seq (set by the caller), version, versionId, tcType. */
|
|
2772
2910
|
async buildSuiteEntryFromTestcase(tcNum) {
|
|
@@ -2789,7 +2927,7 @@ export class SimplifyQaAdapter {
|
|
|
2789
2927
|
...(raw?.projectId !== undefined ? { projectId: raw.projectId } : {}),
|
|
2790
2928
|
version: versionName ?? 'v1.0',
|
|
2791
2929
|
versionId: versionId ?? '',
|
|
2792
|
-
executionPlatform:
|
|
2930
|
+
executionPlatform: this.buildExecutionPlatformForEntry(raw, tcType),
|
|
2793
2931
|
};
|
|
2794
2932
|
}
|
|
2795
2933
|
async resolveObjectTemplateId(technology) {
|
|
@@ -2833,7 +2971,7 @@ export class SimplifyQaAdapter {
|
|
|
2833
2971
|
const body = this.searchBody(fields, { startIndex: q.offset ?? 0, limit: q.limit ?? 100 });
|
|
2834
2972
|
const raw = await this.http.postJson('/tm/object/search', body);
|
|
2835
2973
|
const rows = pickArray(raw, 'data', 'content', 'objects');
|
|
2836
|
-
return rows.map(toObject);
|
|
2974
|
+
return rows.map((r) => toObject(r, this.cfg.baseUrl));
|
|
2837
2975
|
}
|
|
2838
2976
|
/**
|
|
2839
2977
|
* Create a new object — `POST /tm/object`, plain JSON (NOT multipart, unlike testcase/suite writes;
|
|
@@ -2882,7 +3020,7 @@ export class SimplifyQaAdapter {
|
|
|
2882
3020
|
};
|
|
2883
3021
|
const raw = await this.http.postJson('/tm/object', body);
|
|
2884
3022
|
const doc = (raw?.data && typeof raw.data === 'object' ? raw.data : raw) ?? {};
|
|
2885
|
-
return toObject(doc);
|
|
3023
|
+
return toObject(doc, this.cfg.baseUrl);
|
|
2886
3024
|
}
|
|
2887
3025
|
/** The current project's module literally named "Default Module" (case-insensitive) — mirrors the
|
|
2888
3026
|
* real sqa-ui `object.component.ts` `openForm()` auto-selection. Best-effort: returns undefined
|
|
@@ -3019,7 +3157,7 @@ export class SimplifyQaAdapter {
|
|
|
3019
3157
|
};
|
|
3020
3158
|
const res = await this.http.putJson(`/tm/object/${numId}`, body);
|
|
3021
3159
|
const updated = (res?.data && typeof res.data === 'object' ? res.data : res) ?? doc;
|
|
3022
|
-
const obj = toObject(updated);
|
|
3160
|
+
const obj = toObject(updated, this.cfg.baseUrl);
|
|
3023
3161
|
if (opts?.enrichNames)
|
|
3024
3162
|
await this.enrichObjectNames(obj);
|
|
3025
3163
|
return obj;
|
|
@@ -3344,7 +3482,24 @@ export class SimplifyQaAdapter {
|
|
|
3344
3482
|
executionId, executionRunId, parentId, projectId,
|
|
3345
3483
|
});
|
|
3346
3484
|
const rawSteps = Array.isArray(stepsResp?.data) ? stepsResp.data : [];
|
|
3347
|
-
|
|
3485
|
+
// Backend logs control-flow markers as INFO rows with `description: null` (see
|
|
3486
|
+
// executionStepResult.service.js insertLoopIterationStart/insertFunctionEnd/… — the same
|
|
3487
|
+
// executionRunId carries LOOP_ITERATION_START/END, FUNCTION_START/END, FUNCTION_ITERATION_*,
|
|
3488
|
+
// WHILE_START/END alongside the real action rows). Left in, they fill the text response
|
|
3489
|
+
// with blank `• step N: (info)` lines that hide the actual failing step. Filter here so
|
|
3490
|
+
// stepResults contains only debuggable rows.
|
|
3491
|
+
const NOISE_STEP_TYPES = new Set([
|
|
3492
|
+
'LOOP_ITERATION_START', 'LOOP_ITERATION_END',
|
|
3493
|
+
'FUNCTION_START', 'FUNCTION_END',
|
|
3494
|
+
'FUNCTION_ITERATION_START', 'FUNCTION_ITERATION_END',
|
|
3495
|
+
'FOR_START', 'FOR_END', 'WHILE_START', 'WHILE_END',
|
|
3496
|
+
'ITERATOR_START', 'ITERATOR_END', 'DO',
|
|
3497
|
+
]);
|
|
3498
|
+
const filtered = rawSteps.filter((s) => {
|
|
3499
|
+
const st = String(s?.stepType ?? '').toUpperCase();
|
|
3500
|
+
return !NOISE_STEP_TYPES.has(st);
|
|
3501
|
+
});
|
|
3502
|
+
return filtered.map((s) => {
|
|
3348
3503
|
const step = {
|
|
3349
3504
|
n: typeof s?.seq === 'number' ? s.seq : 0,
|
|
3350
3505
|
description: String(s?.description ?? ''),
|
|
@@ -3414,15 +3569,15 @@ export class SimplifyQaAdapter {
|
|
|
3414
3569
|
reportUrl: `${base}/user/reports/reports-page?type=TESTCASE&parentId=${parentId}&id=${runIdNum}&projectId=${projectId}`,
|
|
3415
3570
|
};
|
|
3416
3571
|
}
|
|
3417
|
-
/** Most recent execution runId for a SUITE — same `/exec/execution/search` the /reports UI's
|
|
3418
|
-
* current-executions view drives, filtered
|
|
3419
|
-
*
|
|
3420
|
-
*
|
|
3421
|
-
async
|
|
3572
|
+
/** Most recent execution runId for a SUITE or TC — same `/exec/execution/search` the /reports UI's
|
|
3573
|
+
* current-executions view drives, filtered by `type` + the entity's numeric id, sorted id desc,
|
|
3574
|
+
* limit 1. `childExecution:false` + the `parentExecutionCopy nor` clause together drop suite-child
|
|
3575
|
+
* TC rows, so a TC lookup returns its LATEST STANDALONE run, not the child spawned inside a suite. */
|
|
3576
|
+
async getLatestRunFor(type, id) {
|
|
3422
3577
|
const projectId = this.requireProjectId();
|
|
3423
|
-
const
|
|
3424
|
-
if (!Number.isFinite(
|
|
3425
|
-
throw new Error(`
|
|
3578
|
+
const num = Number.parseInt(numericId(id), 10);
|
|
3579
|
+
if (!Number.isFinite(num))
|
|
3580
|
+
throw new Error(`getLatestRunFor(${type}): invalid id ${id}.`);
|
|
3426
3581
|
// Hand-rolled payload — searchBody doesn't emit `sort` or `operator` keys the /reports search wants.
|
|
3427
3582
|
const body = {
|
|
3428
3583
|
searchFields: [
|
|
@@ -3431,16 +3586,83 @@ export class SimplifyQaAdapter {
|
|
|
3431
3586
|
{ column: 'projectId', value: projectId, regEx: false },
|
|
3432
3587
|
{ column: 'childExecution', value: false },
|
|
3433
3588
|
{ column: 'parentExecutionCopy', value: true, operator: 'nor' },
|
|
3434
|
-
{ column: 'type', value:
|
|
3435
|
-
{ column: 'executionTypeId', value:
|
|
3589
|
+
{ column: 'type', value: type, regEx: false },
|
|
3590
|
+
{ column: 'executionTypeId', value: num, regEx: false },
|
|
3436
3591
|
],
|
|
3437
3592
|
startIndex: 0,
|
|
3438
3593
|
limit: 1,
|
|
3439
3594
|
};
|
|
3440
3595
|
const raw = await this.http.postJson('/exec/execution/search', body);
|
|
3441
3596
|
const rows = pickArray(raw, 'data', 'content', 'executions');
|
|
3442
|
-
|
|
3443
|
-
|
|
3597
|
+
return str(rows[0], 'id', '_id') ?? null;
|
|
3598
|
+
}
|
|
3599
|
+
async getLatestSuiteRun(suiteId) { return this.getLatestRunFor('SUITE', suiteId); }
|
|
3600
|
+
async getLatestTestcaseRun(tcId) { return this.getLatestRunFor('TC', tcId); }
|
|
3601
|
+
/** Top-N recent runs — one HTTP call, same standalone-only filters as `getLatestRunFor`. For each
|
|
3602
|
+
* SUITE row the exec doc's `executionItems[]` already carries per-TC status, so the tally is
|
|
3603
|
+
* computed inline (no extra call per run). TC rows omit the tally counters. Sorted newest-first
|
|
3604
|
+
* by `id` (execution ids are monotonic). Bounded — callers should cap `limit` at the tool layer
|
|
3605
|
+
* so a runaway request can't pull thousands of rows. */
|
|
3606
|
+
async listRecentRuns(type, id, limit) {
|
|
3607
|
+
const projectId = this.requireProjectId();
|
|
3608
|
+
const num = Number.parseInt(numericId(id), 10);
|
|
3609
|
+
if (!Number.isFinite(num))
|
|
3610
|
+
throw new Error(`listRecentRuns(${type}): invalid id ${id}.`);
|
|
3611
|
+
const base = (this.cfg.baseUrl ?? '').replace(/\/+$/, '');
|
|
3612
|
+
const body = {
|
|
3613
|
+
searchFields: [
|
|
3614
|
+
{ column: 'id', sort: 'dsc' },
|
|
3615
|
+
{ column: 'deleted', value: false },
|
|
3616
|
+
{ column: 'projectId', value: projectId, regEx: false },
|
|
3617
|
+
{ column: 'childExecution', value: false },
|
|
3618
|
+
{ column: 'parentExecutionCopy', value: true, operator: 'nor' },
|
|
3619
|
+
{ column: 'type', value: type, regEx: false },
|
|
3620
|
+
{ column: 'executionTypeId', value: num, regEx: false },
|
|
3621
|
+
],
|
|
3622
|
+
startIndex: 0,
|
|
3623
|
+
limit,
|
|
3624
|
+
};
|
|
3625
|
+
const raw = await this.http.postJson('/exec/execution/search', body);
|
|
3626
|
+
const rows = pickArray(raw, 'data', 'content', 'executions');
|
|
3627
|
+
return rows.map((r) => {
|
|
3628
|
+
const runId = str(r, 'id', '_id') ?? '';
|
|
3629
|
+
const runIdNum = Number.parseInt(runId, 10);
|
|
3630
|
+
const summary = {
|
|
3631
|
+
runId,
|
|
3632
|
+
state: mapRunState(str(r, 'status') ?? ''),
|
|
3633
|
+
};
|
|
3634
|
+
const startedAt = str(r, 'startTime', 'createdAt');
|
|
3635
|
+
const endedAt = str(r, 'endTime');
|
|
3636
|
+
if (startedAt)
|
|
3637
|
+
summary.startedAt = startedAt;
|
|
3638
|
+
if (endedAt)
|
|
3639
|
+
summary.endedAt = endedAt;
|
|
3640
|
+
if (Number.isFinite(runIdNum)) {
|
|
3641
|
+
const entityNum = Number(r?.executionTypeId ?? num);
|
|
3642
|
+
summary.reportUrl = `${base}/user/reports/reports-page?type=${type === 'SUITE' ? 'SUITE' : 'TESTCASE'}&parentId=${entityNum}&id=${runIdNum}&projectId=${projectId}`;
|
|
3643
|
+
}
|
|
3644
|
+
if (type === 'SUITE') {
|
|
3645
|
+
const items = Array.isArray(r?.executionItems) ? r.executionItems : [];
|
|
3646
|
+
let passed = 0, failed = 0, skipped = 0, notExecuted = 0;
|
|
3647
|
+
for (const it of items) {
|
|
3648
|
+
const s = String(it?.status ?? '').toUpperCase();
|
|
3649
|
+
if (s === 'PASSED')
|
|
3650
|
+
passed++;
|
|
3651
|
+
else if (s === 'FAILED')
|
|
3652
|
+
failed++;
|
|
3653
|
+
else if (s === 'SKIPPED')
|
|
3654
|
+
skipped++;
|
|
3655
|
+
else if (s === 'NOTEXECUTED')
|
|
3656
|
+
notExecuted++;
|
|
3657
|
+
}
|
|
3658
|
+
summary.totalTestCases = items.length;
|
|
3659
|
+
summary.passed = passed;
|
|
3660
|
+
summary.failed = failed;
|
|
3661
|
+
summary.skipped = skipped;
|
|
3662
|
+
summary.notExecuted = notExecuted;
|
|
3663
|
+
}
|
|
3664
|
+
return summary;
|
|
3665
|
+
});
|
|
3444
3666
|
}
|
|
3445
3667
|
/** Targeted name lookup for a single project by numeric id — the whole-list `listProjects`
|
|
3446
3668
|
* is paged (default 100) and API-key scoping can hide the selected project, which left
|
|
@@ -3635,11 +3857,15 @@ export class SimplifyQaAdapter {
|
|
|
3635
3857
|
const executionId = Number.parseInt(numericId(runId), 10);
|
|
3636
3858
|
if (!Number.isFinite(executionId))
|
|
3637
3859
|
throw new Error(`stopRun: invalid runId ${runId}.`);
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3860
|
+
// Mirror sqa-ui reports.component.ts:2041-2054: cloud-parallel needs a dedicated killType so
|
|
3861
|
+
// the backend fans the Redis kill out to every child agent (executions/…/execution.service.js
|
|
3862
|
+
// :4610-4657). Plain STOP_EXECUTION only pushes to the parent's agentId, leaving cloud-parallel
|
|
3863
|
+
// child VMs running even though their DB rows read FAILED.
|
|
3864
|
+
const doc = await this.fetchExecDoc(executionId, projectId);
|
|
3865
|
+
const mode = String(doc?.mode ?? '').toUpperCase();
|
|
3866
|
+
const style = String(doc?.executionStyle ?? '').toUpperCase();
|
|
3867
|
+
const killType = mode === 'CLOUD' && style === 'PARALLEL' ? 'STOP_EXECUTION_PARALLEL_CLOUD' : 'STOP_EXECUTION';
|
|
3868
|
+
await this.http.postJson('/exec/execution/stopExecution', { projectId, executionId, killType });
|
|
3643
3869
|
}
|
|
3644
3870
|
/** SUITE dispatch. Mirrors `startRun` above (which handles TC) — same `/exec/execution` endpoint,
|
|
3645
3871
|
* same LOCAL-agent fast-fail, same project-name / environment resolution. What differs is on the
|