common-qa-mcp 0.11.0 → 0.13.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/simplifyqa/adapter.d.ts +42 -1
- package/dist/adapters/simplifyqa/adapter.d.ts.map +1 -1
- package/dist/adapters/simplifyqa/adapter.js +201 -18
- package/dist/adapters/simplifyqa/adapter.js.map +1 -1
- package/dist/adapters/simplifyqa/executionFailures.d.ts +59 -0
- package/dist/adapters/simplifyqa/executionFailures.d.ts.map +1 -0
- package/dist/adapters/simplifyqa/executionFailures.js +638 -0
- package/dist/adapters/simplifyqa/executionFailures.js.map +1 -0
- package/dist/adapters/simplifyqa/failureCategory.d.ts +26 -0
- package/dist/adapters/simplifyqa/failureCategory.d.ts.map +1 -0
- package/dist/adapters/simplifyqa/failureCategory.js +208 -0
- package/dist/adapters/simplifyqa/failureCategory.js.map +1 -0
- package/dist/adapters/simplifyqa/failureReport.d.ts +50 -0
- package/dist/adapters/simplifyqa/failureReport.d.ts.map +1 -0
- package/dist/adapters/simplifyqa/failureReport.js +353 -0
- package/dist/adapters/simplifyqa/failureReport.js.map +1 -0
- package/dist/adapters/simplifyqa/failureTypes.d.ts +193 -0
- package/dist/adapters/simplifyqa/failureTypes.d.ts.map +1 -0
- package/dist/adapters/simplifyqa/failureTypes.js +8 -0
- package/dist/adapters/simplifyqa/failureTypes.js.map +1 -0
- package/dist/adapters/simplifyqa/flow.d.ts +55 -0
- package/dist/adapters/simplifyqa/flow.d.ts.map +1 -0
- package/dist/adapters/simplifyqa/flow.js +325 -0
- package/dist/adapters/simplifyqa/flow.js.map +1 -0
- package/dist/adapters/simplifyqa/mapLimit.d.ts +3 -0
- package/dist/adapters/simplifyqa/mapLimit.d.ts.map +1 -0
- package/dist/adapters/simplifyqa/mapLimit.js +14 -0
- package/dist/adapters/simplifyqa/mapLimit.js.map +1 -0
- package/dist/adapters/simplifyqa/mapper.d.ts +3 -0
- package/dist/adapters/simplifyqa/mapper.d.ts.map +1 -1
- package/dist/adapters/simplifyqa/mapper.js +6 -7
- package/dist/adapters/simplifyqa/mapper.js.map +1 -1
- package/dist/adapters/simplifyqa/planTree.d.ts +31 -0
- package/dist/adapters/simplifyqa/planTree.d.ts.map +1 -0
- package/dist/adapters/simplifyqa/planTree.js +110 -0
- package/dist/adapters/simplifyqa/planTree.js.map +1 -0
- package/dist/adapters/simplifyqa/searchEngine.d.ts +237 -0
- package/dist/adapters/simplifyqa/searchEngine.d.ts.map +1 -0
- package/dist/adapters/simplifyqa/searchEngine.js +1206 -0
- package/dist/adapters/simplifyqa/searchEngine.js.map +1 -0
- package/dist/adapters/simplifyqa/searchRegistry.d.ts +112 -0
- package/dist/adapters/simplifyqa/searchRegistry.d.ts.map +1 -0
- package/dist/adapters/simplifyqa/searchRegistry.js +1413 -0
- package/dist/adapters/simplifyqa/searchRegistry.js.map +1 -0
- package/dist/adapters/simplifyqa/searchResolve.d.ts +84 -0
- package/dist/adapters/simplifyqa/searchResolve.d.ts.map +1 -0
- package/dist/adapters/simplifyqa/searchResolve.js +319 -0
- package/dist/adapters/simplifyqa/searchResolve.js.map +1 -0
- package/dist/adapters/simplifyqa/stepRules.d.ts +91 -0
- package/dist/adapters/simplifyqa/stepRules.d.ts.map +1 -0
- package/dist/adapters/simplifyqa/stepRules.js +228 -0
- package/dist/adapters/simplifyqa/stepRules.js.map +1 -0
- package/dist/adapters/types.d.ts +14 -1
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/domain/types.d.ts +14 -2
- package/dist/domain/types.d.ts.map +1 -1
- package/dist/initAgents.d.ts.map +1 -1
- package/dist/initAgents.js +32 -9
- package/dist/initAgents.js.map +1 -1
- package/dist/mcpResult.d.ts +19 -0
- package/dist/mcpResult.d.ts.map +1 -1
- package/dist/mcpResult.js +11 -0
- package/dist/mcpResult.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +70 -16
- package/dist/server.js.map +1 -1
- package/dist/tools/discovery.tools.d.ts.map +1 -1
- package/dist/tools/discovery.tools.js +36 -10
- package/dist/tools/discovery.tools.js.map +1 -1
- package/dist/tools/failure.tools.d.ts +14 -0
- package/dist/tools/failure.tools.d.ts.map +1 -0
- package/dist/tools/failure.tools.js +65 -0
- package/dist/tools/failure.tools.js.map +1 -0
- package/dist/tools/query.tools.d.ts +20 -0
- package/dist/tools/query.tools.d.ts.map +1 -0
- package/dist/tools/query.tools.js +310 -0
- package/dist/tools/query.tools.js.map +1 -0
- package/dist/tools/testcase.tools.d.ts.map +1 -1
- package/dist/tools/testcase.tools.js +87 -10
- package/dist/tools/testcase.tools.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whole-execution failure analysis for SimplifyQA (`failure_analysis`). Reads exactly ONE
|
|
3
|
+
* execution — the named one, or the latest standalone (TC) / parent (SU, PL) execution of a code —
|
|
4
|
+
* analyses every FAILED run (= iteration), groups iterations by cause, and builds the FLOW for each
|
|
5
|
+
* cause's representative. The pure rules live in stepRules.ts / flow.ts / failureCategory.ts; this
|
|
6
|
+
* module is the I/O around them. It does not assert a root cause.
|
|
7
|
+
*/
|
|
8
|
+
import { notDeleted, pickArray, str } from './mapper.js';
|
|
9
|
+
import { mapLimit } from './mapLimit.js';
|
|
10
|
+
import { causeSignature, currentRuns, EXECUTION_TYPE_BY_KIND, failureState, groupBy, iterationLabel, lastTouchedStep, numberSteps, parseEntityCode, realFailures, reportStepLabel, runKey, runLabel, } from './stepRules.js';
|
|
11
|
+
import { buildFlow, functionCallsLine, notRunLines, stepData } from './flow.js';
|
|
12
|
+
import { categoryOf, cleanLogLines, extractCause, isNoiseLine, trimTrace } from './failureCategory.js';
|
|
13
|
+
/** Log lines kept per failed step by default — the tail nearest the failure (ai-assistant FAILURE_LOG_TAIL). */
|
|
14
|
+
export const FAILURE_LOG_TAIL = 30;
|
|
15
|
+
/** Distinct error lines kept per failed step. */
|
|
16
|
+
const MAX_ERROR_LINES = 5;
|
|
17
|
+
/** Failed iterations analysed per execution (the assistant's MAX_RUNS); the rest are counted only. */
|
|
18
|
+
const MAX_FAILURE_RUNS = 300;
|
|
19
|
+
/** Step-result reads in flight at once. */
|
|
20
|
+
const FAILURE_CONCURRENCY = 5;
|
|
21
|
+
/** Cause groups whose representative keeps full logs — the output goes into the client's context. */
|
|
22
|
+
const MAX_DETAILED_GROUPS = 5;
|
|
23
|
+
const FAILURE_LOG_RE = /(\bERROR\b|exception|validation failed|\bfailed\b|no such element|timeout|not found|assert|unable to)/i;
|
|
24
|
+
/** Control-flow bookkeeping rows SQA logs with an empty description — not real steps. */
|
|
25
|
+
const FAILURE_NOISE_STEP_TYPES = new Set([
|
|
26
|
+
'LOOP_ITERATION_START', 'LOOP_ITERATION_END',
|
|
27
|
+
'FUNCTION_START', 'FUNCTION_END',
|
|
28
|
+
'FUNCTION_ITERATION_START', 'FUNCTION_ITERATION_END',
|
|
29
|
+
'FOR_START', 'FOR_END', 'WHILE_START', 'WHILE_END',
|
|
30
|
+
'ITERATOR_START', 'ITERATOR_END', 'DO',
|
|
31
|
+
]);
|
|
32
|
+
/** Image type from the file's magic bytes — SQA may serve screenshots as application/octet-stream. */
|
|
33
|
+
function imageMimeType(b) {
|
|
34
|
+
if (b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47)
|
|
35
|
+
return 'image/png';
|
|
36
|
+
if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff)
|
|
37
|
+
return 'image/jpeg';
|
|
38
|
+
if (b[0] === 0x47 && b[1] === 0x49 && b[2] === 0x46)
|
|
39
|
+
return 'image/gif';
|
|
40
|
+
if (b[0] === 0x52 && b[1] === 0x49 && b[2] === 0x46 && b[3] === 0x46
|
|
41
|
+
&& b[8] === 0x57 && b[9] === 0x45 && b[10] === 0x42 && b[11] === 0x50)
|
|
42
|
+
return 'image/webp';
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/** Drop the SQA log timestamp/level prefix so repeated retries collapse to one message. */
|
|
46
|
+
function normalizeLogMessage(line) {
|
|
47
|
+
return line
|
|
48
|
+
.replace(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+\s*-\s*(INFO|ERROR|WARN|DEBUG)\s*:\s*/i, '')
|
|
49
|
+
.trim();
|
|
50
|
+
}
|
|
51
|
+
/** One line a reader can act on: the failed step and its error, or the run-level reason. */
|
|
52
|
+
function failureCauseText(it) {
|
|
53
|
+
const cut = (s, n) => (s.length > n ? `${s.slice(0, n - 1)}…` : s);
|
|
54
|
+
const f = it.focus;
|
|
55
|
+
if (it.source === 'step' && f) {
|
|
56
|
+
const why = f.cause ?? f.exception ?? f.errorLines[0] ?? f.actual ?? f.status;
|
|
57
|
+
return cut(`Step ${f.reportStep ?? f.displaySeq ?? f.seq} "${f.description}" — ${normalizeLogMessage(String(why))}`, 300);
|
|
58
|
+
}
|
|
59
|
+
const reason = it.exception ?? it.readError ?? 'no reason recorded';
|
|
60
|
+
return cut(`Run-level: ${reason}${f ? ` (last step that ran: ${f.reportStep ?? f.displaySeq ?? f.seq} "${f.description}")` : ''}`, 300);
|
|
61
|
+
}
|
|
62
|
+
/** The step rows failure detection reads: no control-flow bookkeeping rows, none without a
|
|
63
|
+
* description — except FUNCTION rows, which name the function a failure sits in. */
|
|
64
|
+
function keepStepRows(raw) {
|
|
65
|
+
return raw.filter((s) => {
|
|
66
|
+
const st = String(s?.stepType ?? '').toUpperCase();
|
|
67
|
+
if (st === 'FUNCTION')
|
|
68
|
+
return true;
|
|
69
|
+
return !FAILURE_NOISE_STEP_TYPES.has(st) && String(s?.description ?? '').trim().length > 0;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function isFailureLogLine(line) {
|
|
73
|
+
return FAILURE_LOG_RE.test(line);
|
|
74
|
+
}
|
|
75
|
+
function expectedFromTestdata(td) {
|
|
76
|
+
for (const t of td) {
|
|
77
|
+
const name = String(t['paramName'] ?? t['runtimeKey'] ?? '');
|
|
78
|
+
if (/expected/i.test(name))
|
|
79
|
+
return String(t['value'] ?? '');
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
function matchesFirst(logs, patterns) {
|
|
84
|
+
for (const line of logs) {
|
|
85
|
+
for (const re of patterns) {
|
|
86
|
+
const m = re.exec(line);
|
|
87
|
+
if (m)
|
|
88
|
+
return m[1] ?? '';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
function expectedFromLogs(logs) {
|
|
94
|
+
return matchesFirst(logs, [
|
|
95
|
+
/partial text:\s*'([^']*)'/i,
|
|
96
|
+
/expected(?:\s+text|\s+value)?\s*:\s*'([^']*)'/i,
|
|
97
|
+
]);
|
|
98
|
+
}
|
|
99
|
+
function actualFromLogs(logs) {
|
|
100
|
+
return matchesFirst(logs, [
|
|
101
|
+
/actual(?:\s+text|\s+value)?\s*:\s*'([^']*)'/i,
|
|
102
|
+
]);
|
|
103
|
+
}
|
|
104
|
+
function logField(logs, re) {
|
|
105
|
+
for (const line of logs) {
|
|
106
|
+
const m = re.exec(line);
|
|
107
|
+
if (m)
|
|
108
|
+
return (m[1] ?? '').trim();
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
function absUrl(base, value) {
|
|
113
|
+
if (!value)
|
|
114
|
+
return '';
|
|
115
|
+
return value.startsWith('/') ? `${base}${value}` : value;
|
|
116
|
+
}
|
|
117
|
+
/** One raw step-result row → the failure-analysis step (cleaned, tailed logs; cause; category). */
|
|
118
|
+
function normalizeFailureStep(s, base, opts) {
|
|
119
|
+
const status = String(s?.status ?? '').toUpperCase();
|
|
120
|
+
const state = failureState(status, s?.stepType);
|
|
121
|
+
const allLogs = Array.isArray(s?.executionLogData?.executionLogs)
|
|
122
|
+
? s.executionLogData.executionLogs.map((l) => String(l))
|
|
123
|
+
: [];
|
|
124
|
+
// Cleaned (stamp shortened, stack frames dropped, traces cut to their message) and TAILED: the
|
|
125
|
+
// lines nearest the failure are the ones that explain it (ai-assistant log_tail).
|
|
126
|
+
const cleaned = allLogs.flatMap(cleanLogLines);
|
|
127
|
+
const logs = opts.includeLogs ? cleaned.slice(-opts.maxLogLines) : [];
|
|
128
|
+
const failedStep = state === 'failed' || state === 'error';
|
|
129
|
+
const errorLines = failedStep
|
|
130
|
+
? [...new Set(cleaned.filter((l) => isFailureLogLine(l) && !isNoiseLine(l)))].slice(0, MAX_ERROR_LINES)
|
|
131
|
+
: [];
|
|
132
|
+
const evidenceLines = errorLines.length ? errorLines : (state === 'failed' ? cleaned.slice(-3) : []);
|
|
133
|
+
const td = Array.isArray(s?.testdata) ? s.testdata : [];
|
|
134
|
+
const expected = str(s, 'expectedResult') ?? expectedFromTestdata(td) ?? expectedFromLogs(allLogs);
|
|
135
|
+
const actual = str(s, 'actualResult') ?? actualFromLogs(allLogs);
|
|
136
|
+
const screenshots = opts.includeScreenshots && Array.isArray(s?.screenShots)
|
|
137
|
+
? s.screenShots
|
|
138
|
+
.map((a) => ({
|
|
139
|
+
...(a?.name ? { name: String(a.name) } : {}),
|
|
140
|
+
...(a?.original_name ? { originalName: String(a.original_name) } : {}),
|
|
141
|
+
url: String(a?.url ?? ''),
|
|
142
|
+
absoluteUrl: absUrl(base, String(a?.url ?? '')),
|
|
143
|
+
...(typeof a?.size === 'number' ? { size: a.size } : {}),
|
|
144
|
+
...(a?.fileSize ? { fileSize: String(a.fileSize) } : {}),
|
|
145
|
+
...(a?.type ? { type: String(a.type) } : {}),
|
|
146
|
+
}))
|
|
147
|
+
.filter((a) => a.url.length > 0)
|
|
148
|
+
: [];
|
|
149
|
+
const objectName = logField(allLogs, /Object Name:\s*(.+?)\s*$/i);
|
|
150
|
+
const actionName = logField(allLogs, /Action Name\s*:\s*(.+?)\s*$/i);
|
|
151
|
+
const locator = logField(allLogs, /Locator Used\s+LocatorName:\s*(.+?)\s*$/i);
|
|
152
|
+
const fnName = str(s?.functionData ?? {}, 'functionName', 'functionCode');
|
|
153
|
+
const cause = failedStep ? extractCause(s) : undefined;
|
|
154
|
+
const recordedBaseline = absUrl(base, str(s, 'recordedBaseLinePath') ?? '');
|
|
155
|
+
const recordedThumbnail = absUrl(base, str(s, 'recordedThumbnailPath') ?? '');
|
|
156
|
+
const executedThumbnail = absUrl(base, str(s, 'executedThumbnailPath') ?? '');
|
|
157
|
+
return {
|
|
158
|
+
seq: typeof s?.seq === 'number' ? s.seq : 0,
|
|
159
|
+
...(typeof s?.reportSeq === 'number' || typeof s?.reportSeq === 'string' ? { reportSeq: s.reportSeq } : {}),
|
|
160
|
+
...(s?.displaySeq !== undefined ? { displaySeq: String(s.displaySeq) } : {}),
|
|
161
|
+
description: String(s?.description ?? ''),
|
|
162
|
+
status,
|
|
163
|
+
state,
|
|
164
|
+
...(s?.stepType !== undefined ? { stepType: String(s.stepType) } : {}),
|
|
165
|
+
...(s?.type !== undefined ? { type: String(s.type) } : {}),
|
|
166
|
+
...(typeof s?.parentId === 'number' ? { parentId: s.parentId } : {}),
|
|
167
|
+
...(s?.stepId !== undefined ? { stepId: String(s.stepId) } : {}),
|
|
168
|
+
...(str(s, 'id', '_id') ? { rowId: str(s, 'id', '_id') } : {}),
|
|
169
|
+
...(expected !== undefined ? { expected } : {}),
|
|
170
|
+
...(actual !== undefined ? { actual } : {}),
|
|
171
|
+
...(str(s, 'exception') ? { exception: trimTrace(str(s, 'exception')) } : {}),
|
|
172
|
+
errorLines: evidenceLines,
|
|
173
|
+
logs,
|
|
174
|
+
logCount: allLogs.length,
|
|
175
|
+
screenshots,
|
|
176
|
+
...(recordedBaseline ? { recordedBaseline } : {}),
|
|
177
|
+
...(recordedThumbnail ? { recordedThumbnail } : {}),
|
|
178
|
+
...(executedThumbnail ? { executedThumbnail } : {}),
|
|
179
|
+
...(td.length ? { testdata: td } : {}),
|
|
180
|
+
...(s?.networkLogs && (Array.isArray(s.networkLogs) ? s.networkLogs.length > 0 : Object.keys(s.networkLogs).length > 0) ? { networkLogs: s.networkLogs } : {}),
|
|
181
|
+
...(s?.apiData ? { apiData: s.apiData } : {}),
|
|
182
|
+
...(s?.defectIds ? { defectIds: s.defectIds } : {}),
|
|
183
|
+
...(typeof s?.selfHealed === 'boolean' ? { selfHealed: s.selfHealed } : {}),
|
|
184
|
+
...(objectName ? { objectName } : {}),
|
|
185
|
+
...(actionName ? { actionName } : {}),
|
|
186
|
+
...(locator ? { locator } : {}),
|
|
187
|
+
...(fnName ? { functionName: fnName } : {}),
|
|
188
|
+
...(cause ? { cause: cause.text } : {}),
|
|
189
|
+
...(failedStep ? { category: categoryOf(cause?.text, cause?.source) } : {}),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/** A test case entry of an execution: test case, its version and its suite entry (`stepId`). */
|
|
193
|
+
const entryKey = (r) => JSON.stringify([r?.executionItemId ?? null, r?.versionId ?? null, r?.stepId ?? null]);
|
|
194
|
+
/**
|
|
195
|
+
* A failure screenshot as base64, for returning inline. Only artifacts on the connected SQA host
|
|
196
|
+
* are fetched (with the SQA bearer token); anything else, anything over `maxBytes`, or anything
|
|
197
|
+
* that is not a PNG/JPEG/GIF/WEBP returns null so the caller falls back to the link.
|
|
198
|
+
*/
|
|
199
|
+
export async function fetchFailureImage(http, baseUrl, absoluteUrl, maxBytes) {
|
|
200
|
+
if (!baseUrl || !absoluteUrl.startsWith(`${baseUrl}/`) || !http.getBinary)
|
|
201
|
+
return null;
|
|
202
|
+
try {
|
|
203
|
+
const { data } = await http.getBinary(absoluteUrl.slice(baseUrl.length), 30000);
|
|
204
|
+
const mimeType = imageMimeType(data);
|
|
205
|
+
if (!mimeType || data.byteLength === 0 || data.byteLength > maxBytes)
|
|
206
|
+
return null;
|
|
207
|
+
return { data: Buffer.from(data).toString('base64'), mimeType };
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
export class ExecutionFailureAnalysis {
|
|
214
|
+
constructor(deps) {
|
|
215
|
+
this.deps = deps;
|
|
216
|
+
}
|
|
217
|
+
/** Its runs are the parent's current copies (a child execution's runs are history), one per run
|
|
218
|
+
* key; every FAILED run (= iteration) gets its steps read and a failure focus; iterations are
|
|
219
|
+
* then grouped by cause, and only each group's representative keeps full logs. */
|
|
220
|
+
async analyse(input) {
|
|
221
|
+
const { projectId, baseUrl: base } = this.deps;
|
|
222
|
+
const entity = input.code ? parseEntityCode(input.code) : null;
|
|
223
|
+
if (input.code && !entity) {
|
|
224
|
+
throw new Error(`Unrecognised code "${input.code}". Use TC-123, SU-123 or PL-123 (PL = execution plan).`);
|
|
225
|
+
}
|
|
226
|
+
const execRef = input.executionId?.trim() || undefined;
|
|
227
|
+
if (!execRef && !entity)
|
|
228
|
+
throw new Error('Provide a code (TC-123 / SU-123 / PL-123) or an executionId.');
|
|
229
|
+
// ---------------------------------------------------------------- 1. the execution
|
|
230
|
+
let execDoc;
|
|
231
|
+
if (execRef) {
|
|
232
|
+
execDoc = await this.fetchExecDocByRef(execRef);
|
|
233
|
+
if (!execDoc)
|
|
234
|
+
throw new Error(`Execution ${execRef} not found in project ${projectId}.`);
|
|
235
|
+
if (entity && entity.kind !== 'TC') {
|
|
236
|
+
const type = String(execDoc.type ?? '').toUpperCase();
|
|
237
|
+
if (type !== EXECUTION_TYPE_BY_KIND[entity.kind] || Number(execDoc.executionTypeId) !== entity.num) {
|
|
238
|
+
const ran = str(execDoc, 'executionTypeCode') ?? `${type} ${execDoc.executionTypeId ?? '?'}`;
|
|
239
|
+
throw new Error(`Execution ${execRef} ran ${ran}, not ${entity.code}.`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const kind = entity.kind === 'TC' ? 'TC' : entity.kind === 'SU' ? 'SUITE' : 'EXECUTIONPLAN';
|
|
245
|
+
const latest = await this.deps.latestExecutionOf(kind, String(entity.num));
|
|
246
|
+
if (!latest)
|
|
247
|
+
throw new Error(`${entity.code} has no execution in project ${projectId}.`);
|
|
248
|
+
execDoc = await this.deps.fetchExecDoc(Number(latest), projectId);
|
|
249
|
+
if (!execDoc)
|
|
250
|
+
throw new Error(`Execution ${latest} not found in project ${projectId}.`);
|
|
251
|
+
}
|
|
252
|
+
const execNum = Number(execDoc.id);
|
|
253
|
+
const execType = String(execDoc.type ?? '').toUpperCase();
|
|
254
|
+
// ---------------------------------------------------------------- 2. its runs (iterations)
|
|
255
|
+
const rawRuns = await this.deps.http.getJson(`/exec/report/${execNum}/${projectId}`);
|
|
256
|
+
// A re-execution soft-deletes the run row it replaces: only live rows are current results.
|
|
257
|
+
let runs = currentRuns(notDeleted(pickArray(rawRuns, 'data')));
|
|
258
|
+
if (entity?.kind === 'TC' && execRef) {
|
|
259
|
+
runs = runs.filter((r) => Number(r?.executionItemId) === entity.num
|
|
260
|
+
|| String(r?.code ?? '').toUpperCase().replace('_', '-') === entity.code);
|
|
261
|
+
if (!runs.length)
|
|
262
|
+
throw new Error(`${entity.code} has no runs in execution ${execRef}.`);
|
|
263
|
+
}
|
|
264
|
+
const failedRuns = runs.filter((r) => String(r?.status ?? '').toUpperCase() === 'FAILED');
|
|
265
|
+
const toAnalyse = failedRuns.slice(0, MAX_FAILURE_RUNS);
|
|
266
|
+
// ---------------------------------------------------------------- 3. each failed iteration
|
|
267
|
+
const opts = {
|
|
268
|
+
includeLogs: input.includeLogs !== false,
|
|
269
|
+
maxLogLines: Math.max(10, Math.min(input.maxLogLines ?? FAILURE_LOG_TAIL, 2000)),
|
|
270
|
+
includeScreenshots: input.includeScreenshots !== false,
|
|
271
|
+
};
|
|
272
|
+
const analysed = await mapLimit(toAnalyse, FAILURE_CONCURRENCY, async (run) => ({
|
|
273
|
+
run,
|
|
274
|
+
it: await this.analyseFailedIteration(execNum, run, base, opts),
|
|
275
|
+
}));
|
|
276
|
+
// A run that failed with no reason of its own takes the EXECUTION's: a manual stop or a dead
|
|
277
|
+
// container is recorded once, on the execution ("…manually terminated by …"), not on each run.
|
|
278
|
+
const execException = trimTrace(str(execDoc, 'exception'));
|
|
279
|
+
if (execException) {
|
|
280
|
+
for (const { it } of analysed) {
|
|
281
|
+
if (it.source === 'run_level' && !it.exception) {
|
|
282
|
+
it.exception = execException;
|
|
283
|
+
it.category = categoryOf(execException);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// ---------------------------------------------------------------- 4. group by cause
|
|
288
|
+
const grouped = groupBy(analysed, (a) => causeSignature(a.it.focus, a.it.source, a.it.exception));
|
|
289
|
+
const representatives = new Set();
|
|
290
|
+
const groups = grouped.map((g, i) => {
|
|
291
|
+
const id = i + 1;
|
|
292
|
+
for (const a of g.items)
|
|
293
|
+
a.it.group = id;
|
|
294
|
+
// Most evidence wins: a screenshot on the focus step, then logs; the newest on a tie.
|
|
295
|
+
const evidence = (it) => (it.focus?.screenshots.length ? 2 : 0) + (it.focus?.logCount ? 1 : 0);
|
|
296
|
+
const rep = g.items.reduce((best, a) => (evidence(a.it) > evidence(best.it) ? a : best), g.items[0]);
|
|
297
|
+
if (id <= MAX_DETAILED_GROUPS)
|
|
298
|
+
representatives.add(rep.it);
|
|
299
|
+
return {
|
|
300
|
+
id,
|
|
301
|
+
cause: failureCauseText(rep.it),
|
|
302
|
+
kind: rep.it.source === 'step' ? 'step' : 'run_level',
|
|
303
|
+
...(rep.it.category ? { category: rep.it.category } : {}),
|
|
304
|
+
size: g.items.length,
|
|
305
|
+
testcases: [...new Set(g.items.map((a) => String(a.run?.code ?? a.run?.executionItemId ?? '')))].filter(Boolean),
|
|
306
|
+
iterations: g.items.map((a) => a.it.label),
|
|
307
|
+
representative: rep.it.label,
|
|
308
|
+
representativeRunId: rep.it.runId,
|
|
309
|
+
};
|
|
310
|
+
});
|
|
311
|
+
// Full logs only where they explain a group; everyone else keeps the error lines + logCount.
|
|
312
|
+
for (const { it } of analysed) {
|
|
313
|
+
if (representatives.has(it))
|
|
314
|
+
continue;
|
|
315
|
+
for (const s of new Set([...it.failedSteps, ...(it.focus ? [it.focus] : [])]))
|
|
316
|
+
s.logs = [];
|
|
317
|
+
}
|
|
318
|
+
// ---------------------------------------------------------------- 5. per test case entry
|
|
319
|
+
const planItems = Array.isArray(execDoc.executionItems) ? execDoc.executionItems : [];
|
|
320
|
+
const itByRunKey = new Map(analysed.map((a) => [runKey(a.run), a.it]));
|
|
321
|
+
const entries = new Map();
|
|
322
|
+
for (const r of runs) {
|
|
323
|
+
const key = entryKey(r);
|
|
324
|
+
let e = entries.get(key);
|
|
325
|
+
if (!e) {
|
|
326
|
+
const item = planItems.find((p) => p?.id !== undefined && String(p.id) === String(r?.executionItemId));
|
|
327
|
+
e = {
|
|
328
|
+
executionItemId: String(r?.executionItemId ?? ''),
|
|
329
|
+
...(str(r, 'code') ? { code: str(r, 'code') } : {}),
|
|
330
|
+
...(str(r, 'name', 'testcaseName') ? { name: str(r, 'name', 'testcaseName') } : {}),
|
|
331
|
+
...(str(r, 'versionId') ? { versionId: str(r, 'versionId') } : {}),
|
|
332
|
+
...(str(r, 'stepId') ? { stepId: str(r, 'stepId') } : {}),
|
|
333
|
+
...(execType === 'EXECUTIONPLAN' && item?.sourceType ? { source: `${item.sourceType} ${item.sourceId ?? ''}`.trim() } : {}),
|
|
334
|
+
iterations: { total: 0, passed: 0, failed: 0, other: 0 },
|
|
335
|
+
failedIterations: [],
|
|
336
|
+
};
|
|
337
|
+
entries.set(key, e);
|
|
338
|
+
}
|
|
339
|
+
const st = String(r?.status ?? '').toUpperCase();
|
|
340
|
+
e.iterations.total++;
|
|
341
|
+
if (st === 'PASSED')
|
|
342
|
+
e.iterations.passed++;
|
|
343
|
+
else if (st === 'FAILED')
|
|
344
|
+
e.iterations.failed++;
|
|
345
|
+
else
|
|
346
|
+
e.iterations.other++;
|
|
347
|
+
const it = itByRunKey.get(runKey(r));
|
|
348
|
+
if (it)
|
|
349
|
+
e.failedIterations.push(it);
|
|
350
|
+
}
|
|
351
|
+
const allEntries = [...entries.values()];
|
|
352
|
+
await this.nameEntries(allEntries);
|
|
353
|
+
for (const e of allEntries) {
|
|
354
|
+
for (const it of e.failedIterations) {
|
|
355
|
+
if (e.name)
|
|
356
|
+
it.testcaseName = e.name;
|
|
357
|
+
if (e.versionName)
|
|
358
|
+
it.versionName = e.versionName;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// ---------------------------------------------------------------- 6. the flow, per cause
|
|
362
|
+
// The whole-test-case context a tester reads, for each representative: the FLOW, what it never
|
|
363
|
+
// reached, function calls and other iterations compared, warnings from passed steps.
|
|
364
|
+
const writtenCache = new Map();
|
|
365
|
+
await mapLimit(analysed.filter((a) => representatives.has(a.it)), FAILURE_CONCURRENCY, async ({ run, it }) => {
|
|
366
|
+
try {
|
|
367
|
+
it.flow = await this.runFlow(execNum, run, it, {
|
|
368
|
+
siblings: runs.filter((r) => entryKey(r) === entryKey(run) && String(r.id) !== String(run.id)),
|
|
369
|
+
analysed: analysed.filter((a) => entryKey(a.run) === entryKey(run) && a.it !== it).map((a) => a.it),
|
|
370
|
+
written: () => {
|
|
371
|
+
const key = `${run.executionItemId}:${run.versionId ?? ''}`;
|
|
372
|
+
if (!writtenCache.has(key))
|
|
373
|
+
writtenCache.set(key, this.fetchWrittenSteps(String(run.executionItemId), str(run, 'versionId')));
|
|
374
|
+
return writtenCache.get(key);
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
catch { /* the flow is context; the analysis stands without it */ }
|
|
379
|
+
});
|
|
380
|
+
const testcases = allEntries.filter((e) => e.iterations.failed > 0);
|
|
381
|
+
const passedEntries = allEntries.filter((e) => e.iterations.total > 0 && e.iterations.passed === e.iterations.total);
|
|
382
|
+
for (const e of testcases)
|
|
383
|
+
e.failedIterations.sort((a, b) => a.label.localeCompare(b.label, undefined, { numeric: true }));
|
|
384
|
+
// ---------------------------------------------------------------- notes + header
|
|
385
|
+
const notes = [];
|
|
386
|
+
if (!runs.length)
|
|
387
|
+
notes.push('The execution has no runs yet.');
|
|
388
|
+
else if (!failedRuns.length)
|
|
389
|
+
notes.push(`No failed iterations — all ${runs.length} run(s) are ${[...new Set(runs.map((r) => String(r?.status ?? '?')))].join('/')}.`);
|
|
390
|
+
if (failedRuns.length > toAnalyse.length) {
|
|
391
|
+
notes.push(`${failedRuns.length - toAnalyse.length} more failed iterations were not analysed (limit ${MAX_FAILURE_RUNS}).`);
|
|
392
|
+
}
|
|
393
|
+
const unread = analysed.filter((a) => a.it.readError).length;
|
|
394
|
+
if (unread)
|
|
395
|
+
notes.push(`${unread} failed iteration(s) could not be read; they are grouped by their run-level exception.`);
|
|
396
|
+
if (groups.length > MAX_DETAILED_GROUPS) {
|
|
397
|
+
notes.push(`Full logs are kept for the representatives of the first ${MAX_DETAILED_GROUPS} cause groups only.`);
|
|
398
|
+
}
|
|
399
|
+
if (analysed.some((a) => a.it.source === 'run_level' && !a.it.readError)) {
|
|
400
|
+
notes.push('Some iterations failed with no failed step (manual verdict or a stop) — their focus is the last step that ran, and the cause is the run-level exception.');
|
|
401
|
+
}
|
|
402
|
+
if (analysed.some((a) => a.it.failedSteps.some((s) => s.screenshots.length))) {
|
|
403
|
+
notes.push('Screenshot URLs are SQA artifact paths; they need the SQA bearer token to download.');
|
|
404
|
+
}
|
|
405
|
+
const startedAt = str(execDoc, 'startTime', 'createdAt');
|
|
406
|
+
const endedAt = str(execDoc, 'endTime');
|
|
407
|
+
return {
|
|
408
|
+
input: {
|
|
409
|
+
...(entity ? { code: entity.code } : {}),
|
|
410
|
+
...(execRef ? { executionId: execRef } : {}),
|
|
411
|
+
resolvedBy: execRef ? 'executionId' : 'latest',
|
|
412
|
+
},
|
|
413
|
+
execution: {
|
|
414
|
+
id: String(execNum),
|
|
415
|
+
...(str(execDoc, 'code') ? { code: str(execDoc, 'code') } : {}),
|
|
416
|
+
type: execType,
|
|
417
|
+
status: String(execDoc.status ?? ''),
|
|
418
|
+
...(str(execDoc, 'executionTypeCode') ? { executionTypeCode: str(execDoc, 'executionTypeCode') } : {}),
|
|
419
|
+
...(str(execDoc, 'executionTypeName') ? { executionTypeName: str(execDoc, 'executionTypeName') } : {}),
|
|
420
|
+
...(str(execDoc, 'environmentType') ? { environment: str(execDoc, 'environmentType') } : {}),
|
|
421
|
+
...(typeof execDoc.childExecution === 'boolean' ? { childExecution: execDoc.childExecution } : {}),
|
|
422
|
+
...(startedAt ? { startedAt } : {}),
|
|
423
|
+
...(endedAt ? { endedAt } : {}),
|
|
424
|
+
...(typeof execDoc.executionDuration === 'number' ? { durationMs: execDoc.executionDuration } : {}),
|
|
425
|
+
...(str(execDoc, 'exception') ? { exception: str(execDoc, 'exception') } : {}),
|
|
426
|
+
reportUrl: `${base}/user/reports/reports-page?type=${execType}&parentId=${execDoc.executionTypeId ?? ''}&id=${execNum}&projectId=${projectId}`,
|
|
427
|
+
},
|
|
428
|
+
counts: {
|
|
429
|
+
runs: runs.length,
|
|
430
|
+
failedRuns: failedRuns.length,
|
|
431
|
+
analysedRuns: analysed.length,
|
|
432
|
+
testcases: allEntries.length,
|
|
433
|
+
failedTestcases: testcases.length,
|
|
434
|
+
passedTestcases: passedEntries.length,
|
|
435
|
+
},
|
|
436
|
+
...(passedEntries.length ? { passed: passedEntries.map((e) => e.code ?? e.executionItemId) } : {}),
|
|
437
|
+
groups,
|
|
438
|
+
testcases,
|
|
439
|
+
notes,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* One run's whole-test-case context (flow.ts): its FLOW around the failure, the written steps it
|
|
444
|
+
* never reached, how this function call compares with the run's other calls of it, and how the
|
|
445
|
+
* same test case's other iterations did at this step — failed ones from the analysis, and ONE
|
|
446
|
+
* passed iteration read for comparison (its status and data at the same written step).
|
|
447
|
+
*/
|
|
448
|
+
async runFlow(execNum, run, it, ctx) {
|
|
449
|
+
const automated = String(run.type ?? '').toUpperCase() !== 'MANUAL';
|
|
450
|
+
// Read again rather than kept from analyseFailedIteration: holding every failed run's raw rows
|
|
451
|
+
// (full logs) until grouping is done costs far more memory than one read per representative.
|
|
452
|
+
const rows = await this.fetchStepRows(execNum, String(run.id), Number(run.executionItemId ?? 0));
|
|
453
|
+
const focusId = it.focus?.rowId;
|
|
454
|
+
const built = buildFlow(rows, { automated, ...(focusId ? { focusId } : {}) });
|
|
455
|
+
const out = { lines: built.lines, warnings: built.warnings };
|
|
456
|
+
if (built.breakdown)
|
|
457
|
+
out.breakdown = built.breakdown;
|
|
458
|
+
// What the test was going to do next — only when the run stopped at this failure (nothing
|
|
459
|
+
// executed after it).
|
|
460
|
+
const focusIdx = focusId ? rows.findIndex((r) => String(r?.id) === String(focusId)) : -1;
|
|
461
|
+
const stopped = focusIdx >= 0 && !rows.slice(focusIdx + 1).some((r) => /^(PASSED|FAILED)$/i.test(String(r?.status ?? '')));
|
|
462
|
+
if (it.source === 'step' && stopped) {
|
|
463
|
+
const notRun = notRunLines(await ctx.written(), built.furthestStepIds);
|
|
464
|
+
if (notRun.length)
|
|
465
|
+
out.notRun = notRun;
|
|
466
|
+
}
|
|
467
|
+
// This function call against the run's other calls of the same function.
|
|
468
|
+
if (focusIdx > 0) {
|
|
469
|
+
const label = reportStepLabel(rows[focusIdx], automated) ?? '';
|
|
470
|
+
let call = -1;
|
|
471
|
+
for (let j = focusIdx - 1; j >= 0; j--) {
|
|
472
|
+
const l = reportStepLabel(rows[j], automated) ?? '';
|
|
473
|
+
if (String(rows[j]?.stepType ?? '').toUpperCase() === 'FUNCTION' && l && label.startsWith(`${l}.`)) {
|
|
474
|
+
call = j;
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const line = call >= 0 ? functionCallsLine(rows, call) : undefined;
|
|
479
|
+
if (line)
|
|
480
|
+
out.functionCalls = line;
|
|
481
|
+
}
|
|
482
|
+
// The same test case's other iterations at this written step.
|
|
483
|
+
const stepId = it.focus?.stepId;
|
|
484
|
+
const others = [];
|
|
485
|
+
for (const o of ctx.analysed) {
|
|
486
|
+
const at = o.failedSteps.find((s) => stepId && s.stepId === stepId);
|
|
487
|
+
const data = at ? stepData({ testdata: at.testdata }) : '';
|
|
488
|
+
others.push(at
|
|
489
|
+
? `${o.iteration ?? o.label}: failed at this step too${data ? ` with ${data}` : ''}`
|
|
490
|
+
: `${o.iteration ?? o.label}: failed elsewhere (${o.focus?.reportStep ? `report step ${o.focus.reportStep}` : o.source})`);
|
|
491
|
+
}
|
|
492
|
+
const passed = ctx.siblings.filter((r) => String(r?.status ?? '').toUpperCase() === 'PASSED');
|
|
493
|
+
if (passed.length && stepId) {
|
|
494
|
+
const p = passed[0];
|
|
495
|
+
try {
|
|
496
|
+
const prows = await this.fetchStepRows(execNum, String(p.id), Number(p.executionItemId ?? 0));
|
|
497
|
+
const same = prows.find((r) => String(r?.stepId ?? '') === stepId);
|
|
498
|
+
const data = same ? stepData(same) : '';
|
|
499
|
+
others.push(`${iterationLabel(p) ?? p.id}: ${same ? `${String(same.status ?? '').toLowerCase()} at this step${data ? ` with ${data}` : ''}` : 'passed (did not reach this step)'}`);
|
|
500
|
+
}
|
|
501
|
+
catch { /* comparison is best-effort */ }
|
|
502
|
+
if (passed.length > 1)
|
|
503
|
+
others.push(`${passed.length - 1} more iteration(s) passed: ${passed.slice(1, 9).map((r) => iterationLabel(r) ?? r.id).join(', ')}`);
|
|
504
|
+
}
|
|
505
|
+
if (others.length)
|
|
506
|
+
out.otherIterations = others;
|
|
507
|
+
return out;
|
|
508
|
+
}
|
|
509
|
+
/** Test case names and version names for the failure table — one search each, best-effort (a
|
|
510
|
+
* failed lookup leaves the column empty, never the analysis). */
|
|
511
|
+
async nameEntries(entries) {
|
|
512
|
+
const { http, projectId } = this.deps;
|
|
513
|
+
const tcIds = [...new Set(entries.map((e) => Number(e.executionItemId)).filter((n) => Number.isFinite(n) && n > 0))];
|
|
514
|
+
const versionIds = [...new Set(entries.map((e) => e.versionId).filter((v) => !!v))];
|
|
515
|
+
const search = async (path, column, ids, select) => {
|
|
516
|
+
if (!ids.length)
|
|
517
|
+
return [];
|
|
518
|
+
const raw = await http.postJson(path, {
|
|
519
|
+
searchFields: [{ column: 'deleted', value: false }, { column: 'projectId', value: projectId }, { column, value: ids }],
|
|
520
|
+
startIndex: 0, limit: ids.length, skipStartIndex: 1, selectFields: select,
|
|
521
|
+
});
|
|
522
|
+
return pickArray(raw, 'data', 'content');
|
|
523
|
+
};
|
|
524
|
+
const [tcs, versions] = await Promise.all([
|
|
525
|
+
search('/tm/testcase/search', 'id', tcIds, ['id', 'code', 'name']).catch(() => []),
|
|
526
|
+
search('/tm/tcfnversion/search', 'id', versionIds, ['id', 'name']).catch(() => []),
|
|
527
|
+
]);
|
|
528
|
+
const tcName = new Map(tcs.map((t) => [String(t.id), str(t, 'name')]));
|
|
529
|
+
const vName = new Map(versions.map((v) => [String(v.id), str(v, 'name')]));
|
|
530
|
+
for (const e of entries) {
|
|
531
|
+
const n = e.name ?? tcName.get(e.executionItemId);
|
|
532
|
+
if (n)
|
|
533
|
+
e.name = n;
|
|
534
|
+
const v = e.versionId ? vName.get(e.versionId) : undefined;
|
|
535
|
+
if (v)
|
|
536
|
+
e.versionName = v;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
/** One failed run → its failure focus. A run whose steps cannot be read is still returned (with
|
|
540
|
+
* `readError`) so it is counted and grouped by its run-level exception, never dropped. */
|
|
541
|
+
async analyseFailedIteration(execNum, run, base, opts) {
|
|
542
|
+
const itr = iterationLabel(run);
|
|
543
|
+
const it = {
|
|
544
|
+
runId: String(run.id),
|
|
545
|
+
label: runLabel(run) || String(run.id),
|
|
546
|
+
status: String(run.status ?? ''),
|
|
547
|
+
...(itr ? { iteration: itr } : {}),
|
|
548
|
+
...(str(run?.platform, 'name') ? { platform: str(run.platform, 'name') } : {}),
|
|
549
|
+
...(str(run, 'versionId') ? { versionId: str(run, 'versionId') } : {}),
|
|
550
|
+
...(str(run, 'startTime') ? { startedAt: str(run, 'startTime') } : {}),
|
|
551
|
+
...(str(run, 'endTime') ? { endedAt: str(run, 'endTime') } : {}),
|
|
552
|
+
...(typeof run.executionDuration === 'number' ? { durationMs: run.executionDuration } : {}),
|
|
553
|
+
...(str(run, 'exception') ? { exception: str(run, 'exception') } : {}),
|
|
554
|
+
source: 'none',
|
|
555
|
+
failedSteps: [],
|
|
556
|
+
stepCounts: { total: 0, passed: 0, failed: 0, skipped: 0, notExecuted: 0 },
|
|
557
|
+
};
|
|
558
|
+
try {
|
|
559
|
+
const all = await this.fetchStepRows(execNum, String(run.id), Number(run.executionItemId ?? 0));
|
|
560
|
+
const steps = keepStepRows(all).map((s) => normalizeFailureStep(s, base, opts));
|
|
561
|
+
numberSteps(steps, String(run.type ?? '').toUpperCase() !== 'MANUAL');
|
|
562
|
+
for (const s of steps) {
|
|
563
|
+
// A FUNCTION heading carries its child's verdict — counting it says one result twice.
|
|
564
|
+
if (s.state === 'info' || s.state === 'unknown' || String(s.stepType ?? '').toUpperCase() === 'FUNCTION')
|
|
565
|
+
continue;
|
|
566
|
+
it.stepCounts.total++;
|
|
567
|
+
if (s.state === 'passed')
|
|
568
|
+
it.stepCounts.passed++;
|
|
569
|
+
else if (s.state === 'failed' || s.state === 'error')
|
|
570
|
+
it.stepCounts.failed++;
|
|
571
|
+
else if (s.state === 'skipped')
|
|
572
|
+
it.stepCounts.skipped++;
|
|
573
|
+
else if (s.state === 'not_executed')
|
|
574
|
+
it.stepCounts.notExecuted++;
|
|
575
|
+
}
|
|
576
|
+
const failures = realFailures(steps);
|
|
577
|
+
if (failures.length) {
|
|
578
|
+
it.failedSteps = failures;
|
|
579
|
+
it.focus = failures[0];
|
|
580
|
+
it.source = 'step';
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
const last = lastTouchedStep(steps);
|
|
584
|
+
if (last)
|
|
585
|
+
it.focus = last;
|
|
586
|
+
it.source = 'run_level';
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
catch (e) {
|
|
590
|
+
it.readError = e instanceof Error ? e.message : String(e);
|
|
591
|
+
it.source = 'run_level';
|
|
592
|
+
}
|
|
593
|
+
finally {
|
|
594
|
+
it.category = it.source === 'step' && it.focus?.category ? it.focus.category : categoryOf(it.exception);
|
|
595
|
+
if (str(run, 'code'))
|
|
596
|
+
it.testcaseCode = str(run, 'code');
|
|
597
|
+
}
|
|
598
|
+
return it;
|
|
599
|
+
}
|
|
600
|
+
/** Execution doc by numeric id, or by its EX code. */
|
|
601
|
+
async fetchExecDocByRef(ref) {
|
|
602
|
+
const { http, projectId } = this.deps;
|
|
603
|
+
if (/^\d+$/.test(ref))
|
|
604
|
+
return this.deps.fetchExecDoc(Number(ref), projectId);
|
|
605
|
+
const raw = await http.postJson('/exec/execution/search', {
|
|
606
|
+
searchFields: [
|
|
607
|
+
{ column: 'code', value: ref.toUpperCase(), regEx: false },
|
|
608
|
+
{ column: 'projectId', value: projectId, regEx: false },
|
|
609
|
+
{ column: 'deleted', value: false },
|
|
610
|
+
],
|
|
611
|
+
startIndex: 0,
|
|
612
|
+
limit: 1,
|
|
613
|
+
});
|
|
614
|
+
return pickArray(raw, 'data', 'content', 'executions')[0];
|
|
615
|
+
}
|
|
616
|
+
/** Every step-result row of one run, in report order — control-flow and marker rows included
|
|
617
|
+
* (the FLOW needs them; failure detection uses `keepStepRows`). */
|
|
618
|
+
async fetchStepRows(executionId, executionRunId, parentId) {
|
|
619
|
+
const resp = await this.deps.http.postJson('/reports/execution/getstepResult', {
|
|
620
|
+
executionId, executionRunId, parentId, projectId: this.deps.projectId,
|
|
621
|
+
});
|
|
622
|
+
return Array.isArray(resp?.data) ? notDeleted(resp.data) : [];
|
|
623
|
+
}
|
|
624
|
+
/** The test case's written steps for one version (what it was going to do), or [] when unreadable. */
|
|
625
|
+
async fetchWrittenSteps(testcaseId, versionId) {
|
|
626
|
+
const params = new URLSearchParams({ projectId: String(this.deps.projectId), tcfnId: testcaseId, type: 'TC' });
|
|
627
|
+
if (versionId)
|
|
628
|
+
params.set('versionId', versionId);
|
|
629
|
+
try {
|
|
630
|
+
const raw = await this.deps.http.getJson(`/tm/testcase/steps?${params.toString()}`);
|
|
631
|
+
return notDeleted(pickArray(raw, 'data', 'content', 'steps'));
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
return [];
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
//# sourceMappingURL=executionFailures.js.map
|