@aqa-pulse/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +43 -0
  2. package/bin/aqa-pulse.js +49 -0
  3. package/dist/backend/generate-source-facts.d.ts +2 -0
  4. package/dist/backend/generate-source-facts.js +607 -0
  5. package/dist/backend/merge-reports.d.ts +19 -0
  6. package/dist/backend/merge-reports.js +314 -0
  7. package/dist/backend/upload-report-artifacts.d.ts +9 -0
  8. package/dist/backend/upload-report-artifacts.js +772 -0
  9. package/dist/backend/upload-report.d.ts +13 -0
  10. package/dist/backend/upload-report.js +338 -0
  11. package/dist/dashboard-utils.d.ts +437 -0
  12. package/dist/dashboard-utils.js +2627 -0
  13. package/dist/history-utils.d.ts +72 -0
  14. package/dist/history-utils.js +267 -0
  15. package/dist/shared/business-assumptions.d.ts +14 -0
  16. package/dist/shared/business-assumptions.js +61 -0
  17. package/dist/shared/dashboard-helpers.d.ts +63 -0
  18. package/dist/shared/dashboard-helpers.js +429 -0
  19. package/dist/shared/dashboard-metric-info.d.ts +61 -0
  20. package/dist/shared/dashboard-metric-info.js +15 -0
  21. package/dist/shared/error-utils.d.ts +1 -0
  22. package/dist/shared/error-utils.js +6 -0
  23. package/dist/shared/formatting.d.ts +3 -0
  24. package/dist/shared/formatting.js +42 -0
  25. package/dist/shared/i18n/ru.d.ts +558 -0
  26. package/dist/shared/i18n/ru.js +577 -0
  27. package/dist/shared/metric-info.d.ts +5 -0
  28. package/dist/shared/metric-info.js +210 -0
  29. package/dist/shared/navigation.d.ts +31 -0
  30. package/dist/shared/navigation.js +99 -0
  31. package/dist/shared/test-history-helpers.d.ts +51 -0
  32. package/dist/shared/test-history-helpers.js +294 -0
  33. package/dist/shared/test-history-metric-info.d.ts +17 -0
  34. package/dist/shared/test-history-metric-info.js +20 -0
  35. package/dist/shared/text-utils.d.ts +2 -0
  36. package/dist/shared/text-utils.js +15 -0
  37. package/package.json +37 -0
@@ -0,0 +1,772 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.prepareReporterReportForUpload = prepareReporterReportForUpload;
37
+ /**
38
+ * Назначение: подготовка reporter report к backend upload из CI. Здесь собираются локальные Playwright screenshots/markdown, а также preview-артефакты из artifact zip, чтобы ingestion получил самодостаточный payload.
39
+ */
40
+ const crypto = __importStar(require("node:crypto"));
41
+ const fs = __importStar(require("node:fs"));
42
+ const path = __importStar(require("node:path"));
43
+ const INLINE_ATTACHMENT_MAX_SIZE_BYTES = 5 * 1024 * 1024;
44
+ const DEFAULT_INLINE_ATTACHMENTS_TOTAL_MAX_SIZE_BYTES = 12 * 1024 * 1024;
45
+ let playwrightSanitizeForFilePath = fallbackSanitizeForFilePath;
46
+ let playwrightTrimLongString = fallbackTrimLongString;
47
+ let playwrightWindowsFilesystemFriendlyLength = 60;
48
+ initializePlaywrightFileNameUtilities();
49
+ function prepareReporterReportForUpload(report, options) {
50
+ if (!report || typeof report !== 'object' || !Array.isArray(report.tests)) {
51
+ persistPreparedReportIfRequested(report, options.preparedReportPath ?? null);
52
+ return report;
53
+ }
54
+ const reportPath = path.resolve(options.reportPath);
55
+ const debugStats = createAttachmentDebugStats();
56
+ const context = {
57
+ reportPath,
58
+ extractionRoot: path.join(path.dirname(reportPath), '.aqa-pulse-upload-artifacts'),
59
+ extractionCache: new Map(),
60
+ inlineAttachmentBudget: {
61
+ remainingBytes: resolveInlineAttachmentsTotalLimit(options.inlineAttachmentsTotalMaxSizeBytes),
62
+ },
63
+ debugStats,
64
+ };
65
+ const preparedReport = {
66
+ ...report,
67
+ tests: report.tests.map((test) => prepareTestForUpload(test, context)),
68
+ };
69
+ persistPreparedReportIfRequested(preparedReport, options.preparedReportPath ?? null);
70
+ if (options.debugAttachmentsSummary === true) {
71
+ printPreparedReportSnapshot(preparedReport);
72
+ printAttachmentDebugSummary(debugStats);
73
+ }
74
+ return preparedReport;
75
+ }
76
+ function prepareTestForUpload(test, context) {
77
+ const normalizedTest = { ...test };
78
+ const sourceReportPath = pickOptionalText(normalizedTest.aqaPulseSourceReportPath);
79
+ delete normalizedTest.aqaPulseSourceReportPath;
80
+ context.debugStats.testsTotal += 1;
81
+ const baseDirectories = [
82
+ sourceReportPath ? path.dirname(path.resolve(sourceReportPath)) : null,
83
+ path.dirname(context.reportPath),
84
+ process.cwd(),
85
+ ].filter((value) => Boolean(value));
86
+ const preferredArtifactSearchRoots = resolvePreferredArtifactSearchRoots(sourceReportPath ?? context.reportPath);
87
+ const artifactSearchRoots = preferredArtifactSearchRoots.length > 0
88
+ ? preferredArtifactSearchRoots
89
+ : collectArtifactSearchRoots(baseDirectories);
90
+ return {
91
+ ...normalizedTest,
92
+ attempts: Array.isArray(normalizedTest.attempts)
93
+ ? normalizedTest.attempts.map((attempt) => prepareAttemptForUpload(attempt, {
94
+ ...context,
95
+ test: normalizedTest,
96
+ baseDirectories,
97
+ artifactSearchRoots,
98
+ preferExactArtifactMatch: preferredArtifactSearchRoots.length > 0,
99
+ }))
100
+ : normalizedTest.attempts,
101
+ };
102
+ }
103
+ function prepareAttemptForUpload(attempt, context) {
104
+ context.debugStats.attemptsTotal += 1;
105
+ const discoveredAttachments = discoverAttemptAttachments(context.test, attempt, context);
106
+ const existingAttachments = Array.isArray(attempt.attachments) ? attempt.attachments : [];
107
+ if (existingAttachments.length === 0 && discoveredAttachments.length === 0) {
108
+ return attempt;
109
+ }
110
+ const preparedAttachments = [];
111
+ const seenAttachments = new Set();
112
+ for (const attachment of [...existingAttachments, ...discoveredAttachments]) {
113
+ const normalizedAttachment = normalizeAttachmentForUpload(attachment, context);
114
+ if (shouldKeepAttachmentForUpload(normalizedAttachment)) {
115
+ pushUniqueAttachment(preparedAttachments, normalizedAttachment, seenAttachments);
116
+ context.debugStats.attachmentsKept += 1;
117
+ }
118
+ else {
119
+ context.debugStats.attachmentsDroppedAfterNormalization += 1;
120
+ }
121
+ for (const extractedAttachment of extractPreviewArtifactsFromAttachment(normalizedAttachment, context)) {
122
+ const normalizedExtractedAttachment = normalizeAttachmentForUpload(extractedAttachment, context);
123
+ if (shouldKeepAttachmentForUpload(normalizedExtractedAttachment)) {
124
+ pushUniqueAttachment(preparedAttachments, normalizedExtractedAttachment, seenAttachments);
125
+ context.debugStats.attachmentsKept += 1;
126
+ }
127
+ else {
128
+ context.debugStats.attachmentsDroppedAfterNormalization += 1;
129
+ }
130
+ }
131
+ }
132
+ return {
133
+ ...attempt,
134
+ attachments: preparedAttachments,
135
+ };
136
+ }
137
+ function normalizeAttachmentForUpload(attachment, context) {
138
+ if (!attachment || typeof attachment !== 'object') {
139
+ return attachment;
140
+ }
141
+ const attachmentPath = pickOptionalText(attachment.path);
142
+ if (!attachmentPath || isExternalAttachmentPath(attachmentPath)) {
143
+ return attachment;
144
+ }
145
+ const resolvedPath = resolveExistingArtifactPath(attachmentPath, context.baseDirectories);
146
+ if (!resolvedPath) {
147
+ return attachment;
148
+ }
149
+ return enrichAttachmentWithInlineContent({
150
+ ...attachment,
151
+ path: resolvedPath,
152
+ }, context);
153
+ }
154
+ function extractPreviewArtifactsFromAttachment(attachment, context) {
155
+ const attachmentPath = pickOptionalText(attachment.path);
156
+ if (!attachmentPath || !isArtifactZipAttachment(attachmentPath, attachment.name, attachment.contentType)) {
157
+ return [];
158
+ }
159
+ if (!fs.existsSync(attachmentPath) || !fs.statSync(attachmentPath).isFile()) {
160
+ return [];
161
+ }
162
+ const cached = context.extractionCache.get(attachmentPath);
163
+ if (cached) {
164
+ return cached;
165
+ }
166
+ const extractedAttachments = extractPreviewArtifactsFromZip(attachmentPath, context.extractionRoot);
167
+ context.extractionCache.set(attachmentPath, extractedAttachments);
168
+ return extractedAttachments;
169
+ }
170
+ function extractPreviewArtifactsFromZip(zipPath, extractionRoot) {
171
+ const AdmZip = loadAdmZip();
172
+ if (!AdmZip) {
173
+ return [];
174
+ }
175
+ const archiveHash = crypto.createHash('sha1').update(zipPath).digest('hex').slice(0, 12);
176
+ const targetRoot = path.join(extractionRoot, archiveHash);
177
+ const archive = new AdmZip(zipPath);
178
+ const extractedAttachments = [];
179
+ for (const entry of archive.getEntries()) {
180
+ if (entry.isDirectory) {
181
+ continue;
182
+ }
183
+ const normalizedEntryPath = normalizeZipEntryPath(entry.entryName);
184
+ if (!normalizedEntryPath || !shouldExposeExtractedArtifact(normalizedEntryPath)) {
185
+ continue;
186
+ }
187
+ const resolvedTargetRoot = path.resolve(targetRoot);
188
+ const targetPath = path.resolve(targetRoot, normalizedEntryPath);
189
+ if (!targetPath.startsWith(`${resolvedTargetRoot}${path.sep}`) && targetPath !== resolvedTargetRoot) {
190
+ continue;
191
+ }
192
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
193
+ fs.writeFileSync(targetPath, entry.getData());
194
+ extractedAttachments.push({
195
+ name: normalizedEntryPath,
196
+ contentType: detectContentType(normalizedEntryPath),
197
+ path: targetPath,
198
+ });
199
+ }
200
+ return extractedAttachments.sort(compareExtractedArtifacts);
201
+ }
202
+ function normalizeZipEntryPath(entryName) {
203
+ if (typeof entryName !== 'string') {
204
+ return null;
205
+ }
206
+ const normalized = entryName
207
+ .replace(/\\/g, '/')
208
+ .replace(/^\/+/, '')
209
+ .split('/')
210
+ .filter((segment) => segment.length > 0 && segment !== '.' && segment !== '..')
211
+ .join('/');
212
+ return normalized || null;
213
+ }
214
+ function shouldExposeExtractedArtifact(filePath) {
215
+ return isImagePath(filePath)
216
+ || isMarkdownPath(filePath)
217
+ || /(^|\/)error-context\.md$/i.test(filePath);
218
+ }
219
+ function compareExtractedArtifacts(left, right) {
220
+ return getAttachmentPriority(left.name) - getAttachmentPriority(right.name)
221
+ || String(left.name ?? '').localeCompare(String(right.name ?? ''));
222
+ }
223
+ function getAttachmentPriority(name) {
224
+ const normalizedName = String(name ?? '').toLowerCase();
225
+ if (normalizedName.endsWith('error-context.md')) {
226
+ return 0;
227
+ }
228
+ if (isImagePath(normalizedName)) {
229
+ return 1;
230
+ }
231
+ if (isMarkdownPath(normalizedName)) {
232
+ return 2;
233
+ }
234
+ return 3;
235
+ }
236
+ function detectContentType(filePath) {
237
+ const normalizedPath = String(filePath).toLowerCase();
238
+ if (normalizedPath.endsWith('.zip')) {
239
+ return 'application/zip';
240
+ }
241
+ if (normalizedPath.endsWith('.png')) {
242
+ return 'image/png';
243
+ }
244
+ if (normalizedPath.endsWith('.jpg') || normalizedPath.endsWith('.jpeg')) {
245
+ return 'image/jpeg';
246
+ }
247
+ if (normalizedPath.endsWith('.webp')) {
248
+ return 'image/webp';
249
+ }
250
+ if (normalizedPath.endsWith('.gif')) {
251
+ return 'image/gif';
252
+ }
253
+ if (normalizedPath.endsWith('.svg')) {
254
+ return 'image/svg+xml';
255
+ }
256
+ if (normalizedPath.endsWith('.bmp')) {
257
+ return 'image/bmp';
258
+ }
259
+ if (normalizedPath.endsWith('.md') || normalizedPath.endsWith('.markdown') || normalizedPath.endsWith('.mdx')) {
260
+ return 'text/markdown';
261
+ }
262
+ return undefined;
263
+ }
264
+ function enrichAttachmentWithInlineContent(attachment, context) {
265
+ const attachmentPath = pickOptionalText(attachment.path);
266
+ if (!attachmentPath || isExternalAttachmentPath(attachmentPath)) {
267
+ return attachment;
268
+ }
269
+ if (!shouldInlineAttachmentContent(attachment)) {
270
+ return attachment;
271
+ }
272
+ if (!fs.existsSync(attachmentPath) || !fs.statSync(attachmentPath).isFile()) {
273
+ return attachment;
274
+ }
275
+ const attachmentStat = fs.statSync(attachmentPath);
276
+ if (attachmentStat.size > INLINE_ATTACHMENT_MAX_SIZE_BYTES) {
277
+ context.debugStats.inlineSkippedTooLargeCount += 1;
278
+ context.debugStats.inlineSkippedTooLargeBytes += attachmentStat.size;
279
+ return attachment;
280
+ }
281
+ if (context.inlineAttachmentBudget.remainingBytes < attachmentStat.size) {
282
+ context.debugStats.inlineSkippedBudgetCount += 1;
283
+ context.debugStats.inlineSkippedBudgetBytes += attachmentStat.size;
284
+ return attachment;
285
+ }
286
+ context.inlineAttachmentBudget.remainingBytes -= attachmentStat.size;
287
+ return {
288
+ ...attachment,
289
+ inlineContentBase64: fs.readFileSync(attachmentPath).toString('base64'),
290
+ inlineContentEncoding: 'base64',
291
+ inlineContentSizeBytes: attachmentStat.size,
292
+ };
293
+ }
294
+ function shouldKeepAttachmentForUpload(attachment) {
295
+ if (!attachment || typeof attachment !== 'object') {
296
+ return false;
297
+ }
298
+ const attachmentPath = pickOptionalText(attachment.path);
299
+ if (pickOptionalText(attachment.url)) {
300
+ return true;
301
+ }
302
+ if (pickOptionalText(attachment.inlineContentBase64)) {
303
+ return true;
304
+ }
305
+ if (!attachmentPath) {
306
+ return false;
307
+ }
308
+ if (isExternalAttachmentPath(attachmentPath)) {
309
+ return true;
310
+ }
311
+ return isArtifactZipAttachment(attachmentPath, attachment.name, attachment.contentType)
312
+ && isExternalAttachmentPath(attachmentPath);
313
+ }
314
+ function shouldInlineAttachmentContent(attachment) {
315
+ const contentType = String(attachment.contentType ?? '').toLowerCase();
316
+ const reference = pickOptionalText(attachment.name)
317
+ ?? pickOptionalText(attachment.path)
318
+ ?? '';
319
+ return contentType.startsWith('image/')
320
+ || contentType.includes('markdown')
321
+ || isImagePath(reference)
322
+ || isMarkdownPath(reference);
323
+ }
324
+ function isArtifactZipAttachment(attachmentPath, attachmentName, contentType) {
325
+ const normalizedPath = String(attachmentPath).toLowerCase();
326
+ const normalizedName = String(attachmentName ?? '').toLowerCase();
327
+ const normalizedType = String(contentType ?? '').toLowerCase();
328
+ const looksLikeArtifactArchive = /(^|[\\/])artifacts?\.zip$/i.test(normalizedPath)
329
+ || /^artifacts?\.zip$/i.test(normalizedName);
330
+ return looksLikeArtifactArchive && (normalizedType === 'application/zip' || normalizedType === '' || normalizedType === 'binary/octet-stream');
331
+ }
332
+ function resolveExistingArtifactPath(artifactPath, baseDirectories) {
333
+ const candidatePaths = [];
334
+ if (path.isAbsolute(artifactPath)) {
335
+ candidatePaths.push(path.resolve(artifactPath));
336
+ }
337
+ for (const baseDirectory of baseDirectories) {
338
+ candidatePaths.push(path.resolve(baseDirectory, artifactPath));
339
+ }
340
+ for (const candidatePath of candidatePaths) {
341
+ if (fs.existsSync(candidatePath) && fs.statSync(candidatePath).isFile()) {
342
+ return candidatePath;
343
+ }
344
+ }
345
+ return null;
346
+ }
347
+ function pushUniqueAttachment(attachments, attachment, seenAttachments) {
348
+ const uniqueKey = [
349
+ pickOptionalText(attachment.path) ?? '',
350
+ pickOptionalText(attachment.url) ?? '',
351
+ pickOptionalText(attachment.name) ?? '',
352
+ ].join('::');
353
+ if (seenAttachments.has(uniqueKey)) {
354
+ return;
355
+ }
356
+ seenAttachments.add(uniqueKey);
357
+ attachments.push(attachment);
358
+ }
359
+ function isExternalAttachmentPath(value) {
360
+ return /^[a-z][a-z0-9+.-]*:/i.test(value) && !/^[a-z]:[\\/]/i.test(value);
361
+ }
362
+ function isImagePath(value) {
363
+ return /\.(avif|bmp|gif|ico|jpe?g|png|svg|webp)$/i.test(value);
364
+ }
365
+ function isMarkdownPath(value) {
366
+ return /\.(md|markdown|mdx)$/i.test(value);
367
+ }
368
+ function discoverAttemptAttachments(test, attempt, context) {
369
+ const discoveredAttachments = [];
370
+ const seenAttachments = new Set();
371
+ const candidateOutputDirectories = resolveAttemptOutputDirectories(test, attempt, context);
372
+ if (candidateOutputDirectories.length > 0) {
373
+ context.debugStats.attemptsWithResolvedOutputDirectories += 1;
374
+ }
375
+ for (const outputDirectory of candidateOutputDirectories) {
376
+ for (const artifactFile of readAttemptArtifactFiles(outputDirectory)) {
377
+ pushUniqueAttachment(discoveredAttachments, {
378
+ name: path.basename(artifactFile),
379
+ contentType: detectContentType(artifactFile),
380
+ path: artifactFile,
381
+ }, seenAttachments);
382
+ }
383
+ }
384
+ context.debugStats.discoveredAttachments += discoveredAttachments.length;
385
+ if (discoveredAttachments.length > 0) {
386
+ context.debugStats.attemptsWithDiscoveredAttachments += 1;
387
+ }
388
+ return discoveredAttachments.sort(compareExtractedArtifacts);
389
+ }
390
+ function resolveAttemptOutputDirectories(test, attempt, context) {
391
+ const outputDirectoryName = buildPlaywrightOutputDirectoryName(test, attempt);
392
+ if (!outputDirectoryName) {
393
+ context.debugStats.outputDirectoryNameBuildFailures += 1;
394
+ return [];
395
+ }
396
+ const exactMatches = context.artifactSearchRoots
397
+ .map((rootDirectory) => path.join(rootDirectory, outputDirectoryName))
398
+ .filter((candidatePath) => fs.existsSync(candidatePath) && fs.statSync(candidatePath).isDirectory());
399
+ if (exactMatches.length > 0) {
400
+ context.debugStats.outputDirectoryExactMatches += exactMatches.length;
401
+ return uniquePaths(exactMatches);
402
+ }
403
+ if (context.artifactSearchRoots.length > 0 && context.preferExactArtifactMatch) {
404
+ context.debugStats.outputDirectoryMisses += 1;
405
+ return [];
406
+ }
407
+ const fallbackMatches = [];
408
+ for (const rootDirectory of context.artifactSearchRoots) {
409
+ fallbackMatches.push(...findFallbackOutputDirectories(rootDirectory, test, outputDirectoryName));
410
+ }
411
+ if (fallbackMatches.length > 0) {
412
+ context.debugStats.outputDirectoryFallbackMatches += fallbackMatches.length;
413
+ }
414
+ else {
415
+ context.debugStats.outputDirectoryMisses += 1;
416
+ }
417
+ return uniquePaths(fallbackMatches);
418
+ }
419
+ function createAttachmentDebugStats() {
420
+ return {
421
+ testsTotal: 0,
422
+ attemptsTotal: 0,
423
+ attemptsWithResolvedOutputDirectories: 0,
424
+ attemptsWithDiscoveredAttachments: 0,
425
+ discoveredAttachments: 0,
426
+ attachmentsKept: 0,
427
+ attachmentsDroppedAfterNormalization: 0,
428
+ outputDirectoryNameBuildFailures: 0,
429
+ outputDirectoryExactMatches: 0,
430
+ outputDirectoryFallbackMatches: 0,
431
+ outputDirectoryMisses: 0,
432
+ inlineSkippedTooLargeCount: 0,
433
+ inlineSkippedTooLargeBytes: 0,
434
+ inlineSkippedBudgetCount: 0,
435
+ inlineSkippedBudgetBytes: 0,
436
+ };
437
+ }
438
+ function printAttachmentDebugSummary(debugStats) {
439
+ console.log('[aqa-pulse][debug] upload attachment summary');
440
+ console.log(`[aqa-pulse][debug] tests=${debugStats.testsTotal} attempts=${debugStats.attemptsTotal}`);
441
+ console.log(`[aqa-pulse][debug] outputDirs resolvedAttempts=${debugStats.attemptsWithResolvedOutputDirectories} exactMatches=${debugStats.outputDirectoryExactMatches} fallbackMatches=${debugStats.outputDirectoryFallbackMatches} misses=${debugStats.outputDirectoryMisses} buildFailures=${debugStats.outputDirectoryNameBuildFailures}`);
442
+ console.log(`[aqa-pulse][debug] attachments discovered=${debugStats.discoveredAttachments} attemptsWithAttachments=${debugStats.attemptsWithDiscoveredAttachments} kept=${debugStats.attachmentsKept} droppedAfterNormalization=${debugStats.attachmentsDroppedAfterNormalization}`);
443
+ console.log(`[aqa-pulse][debug] inline skippedTooLarge=${debugStats.inlineSkippedTooLargeCount} skippedTooLargeMb=${formatBytesAsMegabytes(debugStats.inlineSkippedTooLargeBytes)} skippedBudget=${debugStats.inlineSkippedBudgetCount} skippedBudgetMb=${formatBytesAsMegabytes(debugStats.inlineSkippedBudgetBytes)}`);
444
+ }
445
+ function printPreparedReportSnapshot(report) {
446
+ const snapshot = buildPreparedReportSnapshot(report);
447
+ console.log(`[aqa-pulse][debug] prepared report snapshot tests=${snapshot.tests} attempts=${snapshot.attempts} attachments=${snapshot.attachments} attemptsWithAttachments=${snapshot.attemptsWithAttachments}`);
448
+ console.log(`[aqa-pulse][debug] prepared report failedTests=${snapshot.failedTests} failedTestsWithAttachments=${snapshot.failedTestsWithAttachments} failedTestsWithoutAttachments=${snapshot.failedTestsWithoutAttachments} failedAttempts=${snapshot.failedAttempts} failedAttemptsWithAttachments=${snapshot.failedAttemptsWithAttachments}`);
449
+ for (const sampleTitle of snapshot.failedTestsWithoutAttachmentsSamples) {
450
+ console.log(`[aqa-pulse][debug] failed without attachments :: ${sampleTitle}`);
451
+ }
452
+ }
453
+ function buildPreparedReportSnapshot(report) {
454
+ if (!report || typeof report !== 'object' || !Array.isArray(report.tests)) {
455
+ return {
456
+ tests: 0,
457
+ attempts: 0,
458
+ attachments: 0,
459
+ attemptsWithAttachments: 0,
460
+ failedTests: 0,
461
+ failedTestsWithAttachments: 0,
462
+ failedTestsWithoutAttachments: 0,
463
+ failedAttempts: 0,
464
+ failedAttemptsWithAttachments: 0,
465
+ failedTestsWithoutAttachmentsSamples: [],
466
+ };
467
+ }
468
+ let attempts = 0;
469
+ let attachments = 0;
470
+ let attemptsWithAttachments = 0;
471
+ let failedTests = 0;
472
+ let failedTestsWithAttachments = 0;
473
+ let failedAttempts = 0;
474
+ let failedAttemptsWithAttachments = 0;
475
+ const failedTestsWithoutAttachmentsSamples = [];
476
+ for (const test of report.tests) {
477
+ const testAttempts = Array.isArray(test.attempts) ? test.attempts : [];
478
+ let testHasAttachments = false;
479
+ let testHasFailedAttempt = false;
480
+ for (const attempt of testAttempts) {
481
+ attempts += 1;
482
+ const attemptAttachments = Array.isArray(attempt.attachments) ? attempt.attachments.length : 0;
483
+ attachments += attemptAttachments;
484
+ if (attemptAttachments > 0) {
485
+ attemptsWithAttachments += 1;
486
+ testHasAttachments = true;
487
+ }
488
+ if (normalizeAttemptStatus(attempt.status) === 'failed') {
489
+ failedAttempts += 1;
490
+ testHasFailedAttempt = true;
491
+ if (attemptAttachments > 0) {
492
+ failedAttemptsWithAttachments += 1;
493
+ }
494
+ }
495
+ }
496
+ if (normalizeAttemptStatus(test.status) === 'failed' || testHasFailedAttempt) {
497
+ failedTests += 1;
498
+ if (testHasAttachments) {
499
+ failedTestsWithAttachments += 1;
500
+ }
501
+ else if (failedTestsWithoutAttachmentsSamples.length < 10) {
502
+ failedTestsWithoutAttachmentsSamples.push(describeTestForDebug(test));
503
+ }
504
+ }
505
+ }
506
+ return {
507
+ tests: report.tests.length,
508
+ attempts,
509
+ attachments,
510
+ attemptsWithAttachments,
511
+ failedTests,
512
+ failedTestsWithAttachments,
513
+ failedTestsWithoutAttachments: failedTests - failedTestsWithAttachments,
514
+ failedAttempts,
515
+ failedAttemptsWithAttachments,
516
+ failedTestsWithoutAttachmentsSamples,
517
+ };
518
+ }
519
+ function normalizeAttemptStatus(value) {
520
+ return typeof value === 'string' ? value.trim().toLowerCase() : '';
521
+ }
522
+ function describeTestForDebug(test) {
523
+ const title = pickOptionalText(test.title) ?? '<unknown-title>';
524
+ const file = pickOptionalText(test.location?.file) ?? '<unknown-file>';
525
+ const project = pickOptionalText(test.project) ?? '<unknown-project>';
526
+ return `${project} :: ${file} :: ${title}`;
527
+ }
528
+ function formatBytesAsMegabytes(value) {
529
+ return (Math.round((Number(value) / (1024 * 1024)) * 100) / 100).toFixed(2);
530
+ }
531
+ function buildPlaywrightOutputDirectoryName(test, attempt) {
532
+ const locationFile = pickOptionalText(test.location?.file);
533
+ const projectName = pickOptionalText(test.project);
534
+ const fullTitle = pickOptionalText(test.title);
535
+ if (!locationFile || !projectName || !fullTitle) {
536
+ return null;
537
+ }
538
+ const normalizedFile = locationFile.replace(/\\/g, '/');
539
+ const relativeTestFilePath = normalizedFile
540
+ .replace(buildProjectTestDirPrefixPattern(projectName), '')
541
+ .replace(/\.(spec|test)\.(js|ts|jsx|tsx|mjs|mts|cjs|cts)$/i, '');
542
+ if (!relativeTestFilePath) {
543
+ return null;
544
+ }
545
+ const sanitizedRelativePath = relativeTestFilePath.replace(/\//g, '-');
546
+ const fullTitleWithoutSpec = fullTitle.replace(/\s*>\s*/g, ' ');
547
+ let outputDirectoryName = playwrightTrimLongString(`${sanitizedRelativePath}-${playwrightSanitizeForFilePath(fullTitleWithoutSpec)}`, playwrightWindowsFilesystemFriendlyLength);
548
+ outputDirectoryName += `-${playwrightSanitizeForFilePath(projectName)}`;
549
+ const attemptNumber = typeof attempt.attempt === 'number' && Number.isFinite(attempt.attempt)
550
+ ? attempt.attempt
551
+ : 1;
552
+ if (attemptNumber > 1) {
553
+ outputDirectoryName += `-retry${attemptNumber - 1}`;
554
+ }
555
+ return outputDirectoryName;
556
+ }
557
+ function buildProjectTestDirPrefixPattern(projectName) {
558
+ const normalizedProjectName = projectName.trim().toLowerCase();
559
+ if (normalizedProjectName === 'ui') {
560
+ return /^.*?tests\/ui\//i;
561
+ }
562
+ if (normalizedProjectName === 'api' || normalizedProjectName === 'smoke') {
563
+ return /^.*?tests\/api\//i;
564
+ }
565
+ return /^.*?tests\//i;
566
+ }
567
+ function resolvePreferredArtifactSearchRoots(reportPathLike) {
568
+ const reportPath = pickOptionalText(reportPathLike);
569
+ if (!reportPath) {
570
+ return [];
571
+ }
572
+ const resolvedReportPath = path.resolve(reportPath);
573
+ const reportDirectory = path.dirname(resolvedReportPath);
574
+ const workspaceRoot = path.resolve(reportDirectory, '..', '..');
575
+ const reportBaseName = path.basename(resolvedReportPath, path.extname(resolvedReportPath)).toLowerCase();
576
+ const explicitOutputDirectoryName = mapReportBaseNameToOutputDirectory(reportBaseName);
577
+ if (!explicitOutputDirectoryName) {
578
+ return [];
579
+ }
580
+ const explicitOutputDirectoryPath = path.join(workspaceRoot, explicitOutputDirectoryName);
581
+ return fs.existsSync(explicitOutputDirectoryPath) && fs.statSync(explicitOutputDirectoryPath).isDirectory()
582
+ ? [explicitOutputDirectoryPath]
583
+ : [];
584
+ }
585
+ function mapReportBaseNameToOutputDirectory(reportBaseName) {
586
+ const normalizedReportBaseName = String(reportBaseName).trim().toLowerCase();
587
+ if (normalizedReportBaseName === 'ui-purchase' || normalizedReportBaseName === 'ui-purchase-merged') {
588
+ return 'test-results-ui-purchase';
589
+ }
590
+ if (normalizedReportBaseName === 'ui-cpu' || normalizedReportBaseName === 'ui-cpu-merged') {
591
+ return 'test-results-ui-cpu';
592
+ }
593
+ if (normalizedReportBaseName === 'ui-first' || normalizedReportBaseName === 'ui-first-merged') {
594
+ return 'test-results-ui-first';
595
+ }
596
+ if (normalizedReportBaseName === 'ui-second' || normalizedReportBaseName === 'ui-second-merged') {
597
+ return 'test-results-ui-second';
598
+ }
599
+ if (normalizedReportBaseName === 'ui-dev' || normalizedReportBaseName === 'ui-development') {
600
+ return 'test-results-ui-dev';
601
+ }
602
+ if (normalizedReportBaseName === 'api' || normalizedReportBaseName === 'api-merged') {
603
+ return 'test-results-api';
604
+ }
605
+ return null;
606
+ }
607
+ function findFallbackOutputDirectories(rootDirectory, test, expectedOutputDirectoryName) {
608
+ if (!fs.existsSync(rootDirectory) || !fs.statSync(rootDirectory).isDirectory()) {
609
+ return [];
610
+ }
611
+ const projectSuffix = `-${playwrightSanitizeForFilePath(pickOptionalText(test.project) ?? 'unknown')}`;
612
+ const fileStem = pickOptionalText(test.location?.file)
613
+ ?.replace(/\\/g, '/')
614
+ .split('/')
615
+ .pop()
616
+ ?.replace(/\.(spec|test)\.(js|ts|jsx|tsx|mjs|mts|cjs|cts)$/i, '');
617
+ const fileStemToken = fileStem ? playwrightSanitizeForFilePath(fileStem).slice(0, 12) : '';
618
+ const expectedPrefix = expectedOutputDirectoryName.slice(0, 24);
619
+ return fs.readdirSync(rootDirectory, { withFileTypes: true })
620
+ .filter((entry) => entry.isDirectory())
621
+ .map((entry) => path.join(rootDirectory, entry.name))
622
+ .filter((candidatePath) => {
623
+ const candidateName = path.basename(candidatePath);
624
+ if (candidateName === expectedOutputDirectoryName) {
625
+ return true;
626
+ }
627
+ if (!candidateName.endsWith(projectSuffix)) {
628
+ return false;
629
+ }
630
+ if (expectedPrefix && candidateName.startsWith(expectedPrefix)) {
631
+ return true;
632
+ }
633
+ return fileStemToken.length > 0 && candidateName.includes(fileStemToken);
634
+ });
635
+ }
636
+ function readAttemptArtifactFiles(outputDirectory) {
637
+ if (!fs.existsSync(outputDirectory) || !fs.statSync(outputDirectory).isDirectory()) {
638
+ return [];
639
+ }
640
+ return fs.readdirSync(outputDirectory, { withFileTypes: true })
641
+ .filter((entry) => entry.isFile())
642
+ .map((entry) => path.join(outputDirectory, entry.name))
643
+ .filter((artifactPath) => shouldAttachArtifactFile(path.basename(artifactPath)));
644
+ }
645
+ function shouldAttachArtifactFile(fileName) {
646
+ const normalizedName = String(fileName).toLowerCase();
647
+ return normalizedName === 'error-context.md'
648
+ || /^test-failed-\d+\.(png|jpg|jpeg|webp|gif|bmp|svg)$/i.test(normalizedName)
649
+ || normalizedName === 'trace.zip';
650
+ }
651
+ function collectArtifactSearchRoots(baseDirectories) {
652
+ const searchRoots = [];
653
+ const seenRoots = new Set();
654
+ for (const anchorDirectory of collectSearchAnchorDirectories(baseDirectories)) {
655
+ pushUniqueDirectory(searchRoots, anchorDirectory, seenRoots);
656
+ for (const childDirectory of listMatchingDirectories(anchorDirectory, isArtifactRootName)) {
657
+ pushUniqueDirectory(searchRoots, childDirectory, seenRoots);
658
+ if (/^test-results($|-)/i.test(path.basename(childDirectory))) {
659
+ for (const nestedDirectory of listMatchingDirectories(childDirectory, isArtifactRootName)) {
660
+ pushUniqueDirectory(searchRoots, nestedDirectory, seenRoots);
661
+ }
662
+ }
663
+ }
664
+ }
665
+ return searchRoots;
666
+ }
667
+ function collectSearchAnchorDirectories(baseDirectories) {
668
+ const anchors = [];
669
+ const seenAnchors = new Set();
670
+ for (const baseDirectory of baseDirectories) {
671
+ let currentDirectory = path.resolve(baseDirectory);
672
+ for (let depth = 0; depth < 3; depth += 1) {
673
+ if (seenAnchors.has(currentDirectory)) {
674
+ break;
675
+ }
676
+ seenAnchors.add(currentDirectory);
677
+ anchors.push(currentDirectory);
678
+ const parentDirectory = path.dirname(currentDirectory);
679
+ if (parentDirectory === currentDirectory) {
680
+ break;
681
+ }
682
+ currentDirectory = parentDirectory;
683
+ }
684
+ }
685
+ return anchors;
686
+ }
687
+ function listMatchingDirectories(rootDirectory, predicate) {
688
+ if (!fs.existsSync(rootDirectory) || !fs.statSync(rootDirectory).isDirectory()) {
689
+ return [];
690
+ }
691
+ return fs.readdirSync(rootDirectory, { withFileTypes: true })
692
+ .filter((entry) => entry.isDirectory() && predicate(entry.name))
693
+ .map((entry) => path.join(rootDirectory, entry.name));
694
+ }
695
+ function isArtifactRootName(directoryName) {
696
+ return /^artifacts($|-)/i.test(directoryName) || /^test-results($|-)/i.test(directoryName);
697
+ }
698
+ function pushUniqueDirectory(directories, directoryPath, seenDirectories) {
699
+ const resolvedDirectoryPath = path.resolve(directoryPath);
700
+ if (seenDirectories.has(resolvedDirectoryPath)) {
701
+ return;
702
+ }
703
+ if (!fs.existsSync(resolvedDirectoryPath) || !fs.statSync(resolvedDirectoryPath).isDirectory()) {
704
+ return;
705
+ }
706
+ seenDirectories.add(resolvedDirectoryPath);
707
+ directories.push(resolvedDirectoryPath);
708
+ }
709
+ function uniquePaths(paths) {
710
+ return Array.from(new Set(paths.map((value) => path.resolve(value))));
711
+ }
712
+ function fallbackSanitizeForFilePath(value) {
713
+ return String(value)
714
+ .replace(/[<>:"/\\|?*\x00-\x1F]/g, '-')
715
+ .replace(/\s+/g, '-')
716
+ .replace(/-+/g, '-')
717
+ .replace(/^-+|-+$/g, '') || 'artifact';
718
+ }
719
+ function fallbackTrimLongString(value, length = 60) {
720
+ const normalizedValue = String(value);
721
+ if (normalizedValue.length <= length) {
722
+ return normalizedValue;
723
+ }
724
+ const hash = crypto.createHash('sha1').update(normalizedValue).digest('hex').slice(0, 5);
725
+ const middle = `-${hash}-`;
726
+ const startLength = Math.floor((length - middle.length) / 2);
727
+ const endLength = length - middle.length - startLength;
728
+ return normalizedValue.slice(0, startLength) + middle + normalizedValue.slice(-endLength);
729
+ }
730
+ function initializePlaywrightFileNameUtilities() {
731
+ try {
732
+ const playwrightCoreUtils = require('playwright-core/lib/utils');
733
+ const playwrightUtils = require('playwright/lib/util');
734
+ if (typeof playwrightCoreUtils.sanitizeForFilePath === 'function') {
735
+ playwrightSanitizeForFilePath = playwrightCoreUtils.sanitizeForFilePath;
736
+ }
737
+ if (typeof playwrightUtils.trimLongString === 'function') {
738
+ playwrightTrimLongString = playwrightUtils.trimLongString;
739
+ }
740
+ if (typeof playwrightUtils.windowsFilesystemFriendlyLength === 'number' && Number.isFinite(playwrightUtils.windowsFilesystemFriendlyLength)) {
741
+ playwrightWindowsFilesystemFriendlyLength = playwrightUtils.windowsFilesystemFriendlyLength;
742
+ }
743
+ }
744
+ catch {
745
+ // Используем запасные реализации, если внутренние утилиты Playwright недоступны во время выполнения.
746
+ }
747
+ }
748
+ function loadAdmZip() {
749
+ try {
750
+ return require('adm-zip');
751
+ }
752
+ catch {
753
+ return null;
754
+ }
755
+ }
756
+ function persistPreparedReportIfRequested(report, preparedReportPath) {
757
+ if (!preparedReportPath) {
758
+ return;
759
+ }
760
+ const resolvedPreparedReportPath = path.resolve(process.cwd(), preparedReportPath);
761
+ fs.mkdirSync(path.dirname(resolvedPreparedReportPath), { recursive: true });
762
+ fs.writeFileSync(resolvedPreparedReportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
763
+ console.log(`[aqa-pulse][debug] Prepared upload report saved: ${resolvedPreparedReportPath}`);
764
+ }
765
+ function resolveInlineAttachmentsTotalLimit(configuredValue) {
766
+ return typeof configuredValue === 'number' && Number.isInteger(configuredValue) && configuredValue > 0
767
+ ? configuredValue
768
+ : DEFAULT_INLINE_ATTACHMENTS_TOTAL_MAX_SIZE_BYTES;
769
+ }
770
+ function pickOptionalText(value) {
771
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
772
+ }