@appliqation/automation-sdk 2.1.11 → 2.3.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/LICENSE +0 -0
- package/README.md +509 -7
- package/package.json +2 -2
- package/src/AppliqationClient.js +47 -2
- package/src/constants.js +0 -0
- package/src/core/AuthManager.js +0 -0
- package/src/core/HttpClient.js +52 -0
- package/src/index.d.ts +0 -0
- package/src/index.js +0 -0
- package/src/playwright/JwtBrowserAuth.js +0 -0
- package/src/playwright/fixture.js +0 -0
- package/src/playwright/global-setup.js +43 -0
- package/src/playwright/global-teardown.js +0 -0
- package/src/playwright/helpers/jwt-browser-auth.js +0 -0
- package/src/playwright/index.js +0 -0
- package/src/reporters/cypress/CypressReporter.js +49 -2
- package/src/reporters/cypress/UuidExtractor.js +0 -0
- package/src/reporters/cypress/index.js +0 -0
- package/src/reporters/jest/JestReporter.js +49 -2
- package/src/reporters/jest/UuidExtractor.js +0 -0
- package/src/reporters/jest/index.js +0 -0
- package/src/reporters/playwright/AppliqationReporter.js +734 -26
- package/src/reporters/playwright/helpers/DeviceOsDetector.js +0 -0
- package/src/reporters/playwright/helpers/UuidExtractor.js +8 -3
- package/src/reporters/playwright/index.d.ts +0 -0
- package/src/reporters/playwright/index.js +0 -0
- package/src/services/OrphanTestService.js +0 -0
- package/src/services/ResultService.js +193 -24
- package/src/services/RunMatrixService.js +44 -0
- package/src/services/TaggingService.js +241 -0
- package/src/utils/PayloadBuilder.js +0 -0
- package/src/utils/RunDataNormalizer.js +0 -0
- package/src/utils/UuidValidator.js +0 -0
- package/src/utils/errors.js +0 -0
- package/src/utils/index.js +0 -0
- package/src/utils/logger.js +0 -0
- package/src/utils/mapAppqUuid.js +0 -0
- package/src/utils/validator.js +0 -0
|
File without changes
|
|
@@ -120,16 +120,21 @@ class UuidExtractor {
|
|
|
120
120
|
for (const annotation of annotations) {
|
|
121
121
|
if (!annotation || typeof annotation !== 'object') continue;
|
|
122
122
|
|
|
123
|
-
// Check annotation type
|
|
123
|
+
// Check annotation type - STRICT validation for explicit UUID annotations
|
|
124
124
|
if (annotation.type === 'uuid' || annotation.type === 'appliqation' || annotation.type === 'appliqation-uuid') {
|
|
125
125
|
const uuid = annotation.description || annotation.value;
|
|
126
126
|
if (uuid && UuidValidator.validate(uuid)) {
|
|
127
127
|
return uuid;
|
|
128
128
|
}
|
|
129
|
+
// If UUID type annotation exists but invalid, do NOT try pattern extraction
|
|
130
|
+
// This ensures invalid UUIDs are treated as orphans, not auto-corrected
|
|
131
|
+
if (uuid) {
|
|
132
|
+
return null; // Invalid UUID explicitly provided -> treat as orphan
|
|
133
|
+
}
|
|
129
134
|
}
|
|
130
135
|
|
|
131
|
-
// Check annotation description for UUID
|
|
132
|
-
if (annotation.description) {
|
|
136
|
+
// Check annotation description for UUID (only for non-UUID-type annotations)
|
|
137
|
+
if (annotation.description && annotation.type !== 'uuid' && annotation.type !== 'appliqation' && annotation.type !== 'appliqation-uuid') {
|
|
133
138
|
const uuid = this.extractFromTitle(annotation.description);
|
|
134
139
|
if (uuid) return uuid;
|
|
135
140
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -2,8 +2,10 @@ const logger = require('../utils/logger');
|
|
|
2
2
|
const { normalizeBrowser } = require('../utils/RunDataNormalizer');
|
|
3
3
|
|
|
4
4
|
class ResultService {
|
|
5
|
-
constructor(httpClient) {
|
|
5
|
+
constructor(httpClient, taggingService = null, config = { options: {} }) {
|
|
6
6
|
this.http = httpClient;
|
|
7
|
+
this.tagging = taggingService;
|
|
8
|
+
this.config = config;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
/**
|
|
@@ -15,7 +17,15 @@ class ResultService {
|
|
|
15
17
|
*/
|
|
16
18
|
async submitSingle(runId, result, runMetadata = {}) {
|
|
17
19
|
try {
|
|
18
|
-
|
|
20
|
+
if (this.config?.options?.enableAppq === false) {
|
|
21
|
+
logger.info('Appq disabled - skipping single result submission', {
|
|
22
|
+
runId,
|
|
23
|
+
uuid: result.uuid
|
|
24
|
+
});
|
|
25
|
+
return { success: true, skipped: true, reason: 'Appq disabled' };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const payload = this.buildAutomationResultPayload(runId, result, runMetadata);
|
|
19
29
|
|
|
20
30
|
logger.debug('Submitting single result', {
|
|
21
31
|
runId,
|
|
@@ -23,15 +33,38 @@ class ResultService {
|
|
|
23
33
|
status: result.status
|
|
24
34
|
});
|
|
25
35
|
|
|
26
|
-
// Use
|
|
27
|
-
const response = await this.http.post('/api/
|
|
36
|
+
// Use automation endpoint to enable project validation
|
|
37
|
+
const response = await this.http.post('/api/automation/result/submit', payload);
|
|
38
|
+
|
|
39
|
+
// DEBUG: Show response with context
|
|
40
|
+
console.log('\n🔍 ========== SUBMIT RESULT RESPONSE ==========');
|
|
41
|
+
console.log('UUID:', result.uuid);
|
|
42
|
+
if (response.data && response.data.debug_context) {
|
|
43
|
+
console.log('Project ID (from API):', response.data.debug_context.project_id);
|
|
44
|
+
console.log('User ID (from API):', response.data.debug_context.uid);
|
|
45
|
+
}
|
|
46
|
+
console.log('Status:', response.status);
|
|
47
|
+
console.log('Success:', response.success);
|
|
48
|
+
console.log('===========================================\n');
|
|
28
49
|
|
|
29
50
|
// Check if request was successful
|
|
30
51
|
if (!response.success) {
|
|
31
52
|
throw new Error(response.error || 'Result submission failed');
|
|
32
53
|
}
|
|
33
54
|
|
|
34
|
-
|
|
55
|
+
// Auto-tag accepted result (await to ensure completion)
|
|
56
|
+
if (this.tagging && this.tagging.isEnabled()) {
|
|
57
|
+
try {
|
|
58
|
+
await this.tagging.autoTagAcceptedResults([result.uuid]);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
logger.debug('Background tagging failed', {
|
|
61
|
+
error: err.message,
|
|
62
|
+
uuid: result.uuid
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return response.data || response;
|
|
35
68
|
} catch (error) {
|
|
36
69
|
logger.error('Failed to submit result', {
|
|
37
70
|
error: error.message,
|
|
@@ -60,6 +93,16 @@ class ResultService {
|
|
|
60
93
|
runMetadata = {}
|
|
61
94
|
} = options;
|
|
62
95
|
|
|
96
|
+
if (this.config?.options?.enableAppq === false) {
|
|
97
|
+
logger.info(`Appq disabled - skipping batch submission of ${results?.length || 0} result(s)`);
|
|
98
|
+
return {
|
|
99
|
+
success: 0,
|
|
100
|
+
failed: 0,
|
|
101
|
+
total: results?.length || 0,
|
|
102
|
+
skipped: results?.length || 0
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
63
106
|
if (!results || results.length === 0) {
|
|
64
107
|
logger.warn('No results to submit');
|
|
65
108
|
return {
|
|
@@ -71,53 +114,115 @@ class ResultService {
|
|
|
71
114
|
|
|
72
115
|
logger.info(`Starting batch submission of ${results.length} results`);
|
|
73
116
|
|
|
74
|
-
// Group results by
|
|
75
|
-
const
|
|
117
|
+
// Group results by runId for automation endpoint
|
|
118
|
+
const groupedByRun = this.groupByRun(results);
|
|
76
119
|
|
|
77
120
|
const allResults = [];
|
|
78
121
|
const failures = [];
|
|
79
122
|
|
|
80
|
-
for (const [
|
|
81
|
-
const batches = this.chunkArray(
|
|
123
|
+
for (const [runId, runResults] of Object.entries(groupedByRun)) {
|
|
124
|
+
const batches = this.chunkArray(runResults, batchSize);
|
|
82
125
|
|
|
83
|
-
logger.info(`Submitting ${
|
|
126
|
+
logger.info(`Submitting ${runResults.length} results for run ${runId} in ${batches.length} batches`);
|
|
84
127
|
|
|
85
128
|
for (let i = 0; i < batches.length; i++) {
|
|
86
129
|
const batch = batches[i];
|
|
87
|
-
|
|
88
|
-
|
|
130
|
+
const payload = {
|
|
131
|
+
run_id: runId,
|
|
132
|
+
results: batch.map(r => this.buildAutomationResultPayload(runId, r, runMetadata))
|
|
133
|
+
};
|
|
89
134
|
|
|
90
|
-
logger.debug('Batch payload', { payload: JSON.stringify(
|
|
135
|
+
logger.debug('Batch payload', { payload: JSON.stringify(payload, null, 2) });
|
|
136
|
+
|
|
137
|
+
// Show UUIDs being submitted (test execution status shown, NOT backend validation status)
|
|
138
|
+
console.log('\n📤 Submitting batch with UUIDs:');
|
|
139
|
+
payload.results.forEach((r, idx) => {
|
|
140
|
+
console.log(` ${idx + 1}. ${r.test_case_uuid}`);
|
|
141
|
+
});
|
|
91
142
|
|
|
92
143
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
144
|
+
const response = await this.http.post('/api/automation/result/batch', payload);
|
|
145
|
+
|
|
146
|
+
// DEBUG: Show batch response with context
|
|
147
|
+
console.log('\n🔍 ========== BATCH SUBMIT RESPONSE ==========');
|
|
148
|
+
console.log('Run ID:', runId);
|
|
149
|
+
console.log('Batch:', `${i + 1}/${batches.length}`);
|
|
150
|
+
console.log('Results in batch:', batch.length);
|
|
151
|
+
if (response.data && response.data.debug_context) {
|
|
152
|
+
console.log('Project ID (from API):', response.data.debug_context.project_id);
|
|
153
|
+
console.log('User ID (from API):', response.data.debug_context.uid);
|
|
154
|
+
} else {
|
|
155
|
+
console.log('⚠️ No debug_context in response');
|
|
156
|
+
}
|
|
157
|
+
console.log('Status:', response.status);
|
|
158
|
+
console.log('Success:', response.success);
|
|
159
|
+
|
|
160
|
+
// Check for validation failures - response has nested data object
|
|
161
|
+
const failedResults = response.data?.data?.results?.failed || response.data?.results?.failed || response.data?.failed || [];
|
|
162
|
+
const submittedResults = response.data?.data?.results?.submitted || response.data?.results?.submitted || response.data?.submitted || [];
|
|
163
|
+
const submittedCount = typeof submittedResults === 'number' ? submittedResults : (Array.isArray(submittedResults) ? submittedResults.length : 0);
|
|
164
|
+
|
|
165
|
+
if (failedResults.length > 0) {
|
|
166
|
+
console.log('\n❌ ========== VALIDATION FAILURES ==========');
|
|
167
|
+
console.log(` ✅ ACCEPTED: ${submittedCount} result(s)`);
|
|
168
|
+
console.log(` ❌ REJECTED: ${failedResults.length} result(s)\n`);
|
|
169
|
+
failedResults.forEach((fail, idx) => {
|
|
170
|
+
const uuid = fail.test_case_uuid || fail.uuid;
|
|
171
|
+
const error = fail.error || fail.message;
|
|
172
|
+
const code = fail.code;
|
|
173
|
+
|
|
174
|
+
console.log(` ${idx + 1}. UUID: ${uuid}`);
|
|
175
|
+
console.log(` ❌ Reason: ${error}`);
|
|
176
|
+
if (code) {
|
|
177
|
+
console.log(` 🚫 HTTP Code: ${code}`);
|
|
178
|
+
}
|
|
179
|
+
console.log('');
|
|
180
|
+
});
|
|
181
|
+
console.log('===========================================');
|
|
182
|
+
}
|
|
183
|
+
console.log('============================================\n');
|
|
95
184
|
|
|
96
|
-
// Check if request was successful
|
|
97
185
|
if (!response.success) {
|
|
98
186
|
throw new Error(response.error || 'Batch submission failed');
|
|
99
187
|
}
|
|
100
188
|
|
|
101
|
-
//
|
|
102
|
-
|
|
189
|
+
// Only count results that were actually accepted by backend
|
|
190
|
+
// Backend may reject some results due to validation (e.g., project mismatch)
|
|
191
|
+
if (failedResults.length > 0) {
|
|
192
|
+
// Some results were rejected - need to filter based on UUIDs
|
|
193
|
+
const rejectedUuids = new Set(failedResults.map(f => f.test_case_uuid || f.uuid));
|
|
194
|
+
|
|
195
|
+
batch.forEach(result => {
|
|
196
|
+
if (rejectedUuids.has(result.uuid)) {
|
|
197
|
+
// This result was rejected by backend validation
|
|
198
|
+
failures.push(result);
|
|
199
|
+
} else {
|
|
200
|
+
// This result was accepted
|
|
201
|
+
allResults.push(result);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
} else {
|
|
205
|
+
// All results accepted
|
|
206
|
+
allResults.push(...batch);
|
|
207
|
+
}
|
|
103
208
|
|
|
104
|
-
// Call progress callback
|
|
105
209
|
if (onProgress) {
|
|
106
210
|
onProgress({
|
|
107
|
-
|
|
211
|
+
runId,
|
|
108
212
|
batchIndex: i + 1,
|
|
109
213
|
totalBatches: batches.length,
|
|
110
|
-
completed: (i + 1) * batchSize,
|
|
111
|
-
total:
|
|
214
|
+
completed: Math.min((i + 1) * batchSize, runResults.length),
|
|
215
|
+
total: runResults.length
|
|
112
216
|
});
|
|
113
217
|
}
|
|
114
218
|
|
|
115
|
-
logger.debug(`Batch ${i + 1}/${batches.length} submitted for ${
|
|
219
|
+
logger.debug(`Batch ${i + 1}/${batches.length} submitted for run ${runId}`, {
|
|
116
220
|
count: batch.length
|
|
117
221
|
});
|
|
118
222
|
} catch (error) {
|
|
119
|
-
logger.error(`Batch ${i + 1}/${batches.length} failed for ${
|
|
223
|
+
logger.error(`Batch ${i + 1}/${batches.length} failed for run ${runId}`, {
|
|
120
224
|
error: error.message,
|
|
225
|
+
status: error.status,
|
|
121
226
|
count: batch.length
|
|
122
227
|
});
|
|
123
228
|
failures.push(...batch);
|
|
@@ -150,6 +255,28 @@ class ResultService {
|
|
|
150
255
|
|
|
151
256
|
logger.info('Batch submission completed', summary);
|
|
152
257
|
|
|
258
|
+
// Auto-tag accepted results (await to ensure completion before reporter exits)
|
|
259
|
+
if (this.tagging && this.tagging.isEnabled() && allResults.length > 0) {
|
|
260
|
+
const acceptedUuids = allResults.map(r => r.uuid).filter(Boolean);
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const tagResult = await this.tagging.autoTagAcceptedResults(acceptedUuids);
|
|
264
|
+
if (tagResult.tagged > 0) {
|
|
265
|
+
console.log(`✅ Auto-tagged ${tagResult.tagged} test case(s) with "${this.tagging.tagName}"`);
|
|
266
|
+
if (tagResult.skipped > 0) {
|
|
267
|
+
console.log(`ℹ️ Skipped ${tagResult.skipped} test case(s) - already tagged with "${this.tagging.tagName}"`);
|
|
268
|
+
}
|
|
269
|
+
} else if (tagResult.skipped > 0) {
|
|
270
|
+
console.log(`ℹ️ All ${tagResult.skipped} test case(s) already tagged with "${this.tagging.tagName}" - skipping`);
|
|
271
|
+
}
|
|
272
|
+
} catch (err) {
|
|
273
|
+
logger.warn('Auto-tagging failed (non-blocking)', {
|
|
274
|
+
error: err.message,
|
|
275
|
+
count: acceptedUuids.length
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
153
280
|
return summary;
|
|
154
281
|
}
|
|
155
282
|
|
|
@@ -211,6 +338,33 @@ class ResultService {
|
|
|
211
338
|
};
|
|
212
339
|
}
|
|
213
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Build result payload for automation endpoints
|
|
343
|
+
* @private
|
|
344
|
+
*/
|
|
345
|
+
buildAutomationResultPayload(runId, result, runMetadata = {}) {
|
|
346
|
+
const statusMap = {
|
|
347
|
+
passed: 'passed',
|
|
348
|
+
failed: 'failed',
|
|
349
|
+
skipped: 'skipped',
|
|
350
|
+
timedOut: 'failed',
|
|
351
|
+
interrupted: 'skipped'
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
run_id: runId || result.runId,
|
|
356
|
+
test_case_uuid: result.uuid,
|
|
357
|
+
status: statusMap[result.status] || result.status,
|
|
358
|
+
duration: result.duration || 0,
|
|
359
|
+
error_message: result.error || result.comment || '',
|
|
360
|
+
browser: normalizeBrowser(result.browser || 'Unknown Browser'),
|
|
361
|
+
device: result.device || runMetadata.device || 'Desktop',
|
|
362
|
+
os: result.os || runMetadata.os || '',
|
|
363
|
+
environment: runMetadata.environment || 'Local',
|
|
364
|
+
timestamp: result.timestamp || Math.floor(Date.now() / 1000)
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
214
368
|
/**
|
|
215
369
|
* Group results by browser
|
|
216
370
|
* @private
|
|
@@ -238,6 +392,21 @@ class ResultService {
|
|
|
238
392
|
return chunks;
|
|
239
393
|
}
|
|
240
394
|
|
|
395
|
+
/**
|
|
396
|
+
* Group results by runId
|
|
397
|
+
* @private
|
|
398
|
+
*/
|
|
399
|
+
groupByRun(results) {
|
|
400
|
+
return results.reduce((acc, result) => {
|
|
401
|
+
const runId = result.runId || result.run_id || 'unknown';
|
|
402
|
+
if (!acc[runId]) {
|
|
403
|
+
acc[runId] = [];
|
|
404
|
+
}
|
|
405
|
+
acc[runId].push(result);
|
|
406
|
+
return acc;
|
|
407
|
+
}, {});
|
|
408
|
+
}
|
|
409
|
+
|
|
241
410
|
/**
|
|
242
411
|
* Extract NID from UUID
|
|
243
412
|
* @private
|
|
@@ -22,6 +22,22 @@ class RunMatrixService {
|
|
|
22
22
|
*/
|
|
23
23
|
async create(options) {
|
|
24
24
|
try {
|
|
25
|
+
if (this.config?.options?.enableAppq === false) {
|
|
26
|
+
logger.info('Appq disabled - returning local demo run (no Appq entry created)');
|
|
27
|
+
const now = Math.floor(Date.now() / 1000);
|
|
28
|
+
return {
|
|
29
|
+
runId: `demo-run-${now}`,
|
|
30
|
+
token: null,
|
|
31
|
+
timestamp: now,
|
|
32
|
+
metadata: {
|
|
33
|
+
appqEnabled: false,
|
|
34
|
+
mode: 'demo',
|
|
35
|
+
environment: options.environment || 'Local',
|
|
36
|
+
browsers: options.browsers || ['Chrome']
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
25
41
|
// Validate required fields
|
|
26
42
|
this.validateCreateOptions(options);
|
|
27
43
|
|
|
@@ -304,6 +320,34 @@ class RunMatrixService {
|
|
|
304
320
|
};
|
|
305
321
|
return normalizeOS(osMap[platform] || 'Unknown');
|
|
306
322
|
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Delete a test run
|
|
326
|
+
* @param {string} runId - Run ID to delete
|
|
327
|
+
* @param {string} reason - Deletion reason
|
|
328
|
+
* @returns {Promise<Object>} Deletion result
|
|
329
|
+
*/
|
|
330
|
+
async delete(runId, reason = 'orphan_cleanup') {
|
|
331
|
+
try {
|
|
332
|
+
logger.info('Deleting run...', { runId, reason });
|
|
333
|
+
|
|
334
|
+
const response = await this.http.deleteRun(runId, reason);
|
|
335
|
+
|
|
336
|
+
if (!response.success) {
|
|
337
|
+
throw new Error(response.error || 'Failed to delete run');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
logger.info('Run deleted successfully', { runId });
|
|
341
|
+
|
|
342
|
+
return response.data || response;
|
|
343
|
+
} catch (error) {
|
|
344
|
+
logger.error('Failed to delete run', {
|
|
345
|
+
error: error.message,
|
|
346
|
+
runId
|
|
347
|
+
});
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
307
351
|
}
|
|
308
352
|
|
|
309
353
|
module.exports = RunMatrixService;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
const logger = require('../utils/logger');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Service for auto-tagging test cases after successful runs
|
|
5
|
+
*/
|
|
6
|
+
class TaggingService {
|
|
7
|
+
constructor(httpClient, config = { options: {} }) {
|
|
8
|
+
this.http = httpClient;
|
|
9
|
+
this.config = config;
|
|
10
|
+
|
|
11
|
+
// Configuration resolution: Runtime options > Env vars > Defaults
|
|
12
|
+
this.enabled = this.config?.options?.autoTag !== false &&
|
|
13
|
+
process.env.APPLIQATION_AUTO_TAG_ENABLED !== 'false';
|
|
14
|
+
this.tagName = this.config?.options?.autoTagName ||
|
|
15
|
+
process.env.APPLIQATION_AUTO_TAG_NAME ||
|
|
16
|
+
'Appq_automated';
|
|
17
|
+
this.batchSize = this.config?.options?.autoTagBatchSize || 50;
|
|
18
|
+
this.retries = this.config?.options?.autoTagRetries || 2;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if tagging is enabled
|
|
23
|
+
* @returns {boolean}
|
|
24
|
+
*/
|
|
25
|
+
isEnabled() {
|
|
26
|
+
return this.enabled;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check which UUIDs already have the tag
|
|
31
|
+
*
|
|
32
|
+
* @param {string[]} uuids - Array of test case UUIDs
|
|
33
|
+
* @param {string} tagName - Tag name (optional, uses config default)
|
|
34
|
+
* @returns {Promise<Object>} { needsTagging: [], alreadyTagged: [], errors: [] }
|
|
35
|
+
*/
|
|
36
|
+
async checkTagStatus(uuids, tagName = null) {
|
|
37
|
+
if (!uuids || uuids.length === 0) {
|
|
38
|
+
return { needsTagging: [], alreadyTagged: [], errors: [] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const tag = tagName || this.tagName;
|
|
42
|
+
const unique = Array.from(new Set(uuids.filter(Boolean)));
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// Build query string: uuids=123-xxx,124-yyy&tag=Appq_automated
|
|
46
|
+
const uuidsParam = unique.join(',');
|
|
47
|
+
const url = `/api/automation/testcases/tags/check?uuids=${encodeURIComponent(uuidsParam)}&tag=${encodeURIComponent(tag)}`;
|
|
48
|
+
const response = await this.http.get(url);
|
|
49
|
+
|
|
50
|
+
if (response.success && response.data) {
|
|
51
|
+
return {
|
|
52
|
+
needsTagging: response.data.needsTagging || [],
|
|
53
|
+
alreadyTagged: response.data.alreadyTagged || [],
|
|
54
|
+
errors: response.data.errors || []
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
throw new Error(response.error || 'Failed to check tag status');
|
|
59
|
+
} catch (error) {
|
|
60
|
+
logger.error('Failed to check tag status', {
|
|
61
|
+
error: error.message,
|
|
62
|
+
count: unique.length,
|
|
63
|
+
tag
|
|
64
|
+
});
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Tag test cases
|
|
71
|
+
*
|
|
72
|
+
* @param {string[]} uuids - Array of test case UUIDs to tag
|
|
73
|
+
* @param {string} tagName - Tag name (optional, uses config default)
|
|
74
|
+
* @returns {Promise<Object>} { tagged: 0, failed: 0, errors: [] }
|
|
75
|
+
*/
|
|
76
|
+
async tagTestCases(uuids, tagName = null) {
|
|
77
|
+
if (!uuids || uuids.length === 0) {
|
|
78
|
+
return { tagged: 0, failed: 0, errors: [] };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const tag = tagName || this.tagName;
|
|
82
|
+
const unique = Array.from(new Set(uuids.filter(Boolean)));
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const response = await this.http.post('/api/automation/testcases/tag', {
|
|
86
|
+
uuids: unique,
|
|
87
|
+
tag
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (response.success && response.data) {
|
|
91
|
+
return {
|
|
92
|
+
tagged: response.data.tagged || 0,
|
|
93
|
+
failed: response.data.failed || 0,
|
|
94
|
+
errors: response.data.errors || []
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
throw new Error(response.error || 'Failed to tag test cases');
|
|
99
|
+
} catch (error) {
|
|
100
|
+
logger.error('Failed to tag test cases', {
|
|
101
|
+
error: error.message,
|
|
102
|
+
count: unique.length,
|
|
103
|
+
tag
|
|
104
|
+
});
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Auto-tag accepted results (main entry point)
|
|
111
|
+
*
|
|
112
|
+
* This is a fire-and-forget operation:
|
|
113
|
+
* 1. Check which UUIDs need tagging
|
|
114
|
+
* 2. Tag only those that don't have the tag yet
|
|
115
|
+
* 3. Process in batches for performance
|
|
116
|
+
* 4. Never throws - logs warnings on failure
|
|
117
|
+
*
|
|
118
|
+
* @param {string[]} acceptedUuids - UUIDs of accepted results
|
|
119
|
+
* @param {Object} options - Optional overrides
|
|
120
|
+
* @param {string} options.tagName - Custom tag name
|
|
121
|
+
* @returns {Promise<Object>} { tagged: 0, skipped: 0, failed: 0 }
|
|
122
|
+
*/
|
|
123
|
+
async autoTagAcceptedResults(acceptedUuids = [], options = {}) {
|
|
124
|
+
// Safety checks
|
|
125
|
+
if (!this.enabled) {
|
|
126
|
+
logger.debug('Auto-tagging disabled');
|
|
127
|
+
return { tagged: 0, skipped: 0, failed: 0 };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!acceptedUuids || acceptedUuids.length === 0) {
|
|
131
|
+
return { tagged: 0, skipped: 0, failed: 0 };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Deduplicate and filter
|
|
135
|
+
const unique = Array.from(new Set(acceptedUuids.filter(Boolean)));
|
|
136
|
+
if (unique.length === 0) {
|
|
137
|
+
return { tagged: 0, skipped: 0, failed: 0 };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const tag = options.tagName || this.tagName;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
logger.debug('Starting auto-tag process', { count: unique.length, tag });
|
|
144
|
+
|
|
145
|
+
// Step 1: Check which UUIDs need tagging
|
|
146
|
+
const statusCheck = await this.checkTagStatus(unique, tag);
|
|
147
|
+
const needsTagging = statusCheck.needsTagging || [];
|
|
148
|
+
const alreadyTagged = statusCheck.alreadyTagged || [];
|
|
149
|
+
logger.debug('Tag status check complete', {
|
|
150
|
+
needsTagging: needsTagging.length,
|
|
151
|
+
alreadyTagged: alreadyTagged.length,
|
|
152
|
+
errors: statusCheck.errors.length
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (needsTagging.length === 0) {
|
|
156
|
+
logger.debug('No test cases need tagging (all already tagged)');
|
|
157
|
+
return {
|
|
158
|
+
tagged: 0,
|
|
159
|
+
skipped: alreadyTagged.length,
|
|
160
|
+
failed: statusCheck.errors.length
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Step 2: Tag test cases (in batches if needed)
|
|
165
|
+
let totalTagged = 0;
|
|
166
|
+
let totalFailed = statusCheck.errors.length;
|
|
167
|
+
|
|
168
|
+
const batches = this.chunkArray(needsTagging, this.batchSize);
|
|
169
|
+
|
|
170
|
+
for (let i = 0; i < batches.length; i++) {
|
|
171
|
+
const batch = batches[i];
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const result = await this.tagTestCases(batch, tag);
|
|
175
|
+
totalTagged += result.tagged;
|
|
176
|
+
totalFailed += result.failed;
|
|
177
|
+
|
|
178
|
+
logger.debug(`Tagged batch ${i + 1}/${batches.length}`, {
|
|
179
|
+
tagged: result.tagged,
|
|
180
|
+
failed: result.failed
|
|
181
|
+
});
|
|
182
|
+
} catch (error) {
|
|
183
|
+
logger.warn(`Batch ${i + 1} tagging failed`, {
|
|
184
|
+
error: error.message,
|
|
185
|
+
batchSize: batch.length
|
|
186
|
+
});
|
|
187
|
+
totalFailed += batch.length;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const summary = {
|
|
192
|
+
tagged: totalTagged,
|
|
193
|
+
skipped: alreadyTagged.length,
|
|
194
|
+
failed: totalFailed
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
logger.info('Auto-tagging complete', summary);
|
|
198
|
+
|
|
199
|
+
return summary;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
// Non-blocking: Log warning and continue
|
|
202
|
+
logger.warn('Auto-tagging failed (non-blocking)', {
|
|
203
|
+
error: error.message,
|
|
204
|
+
count: unique.length,
|
|
205
|
+
tag
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
tagged: 0,
|
|
210
|
+
skipped: 0,
|
|
211
|
+
failed: unique.length
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Fire-and-forget tagging for accepted UUIDs.
|
|
218
|
+
* Does not throw; logs warnings on failure.
|
|
219
|
+
*
|
|
220
|
+
* @deprecated Use autoTagAcceptedResults() instead
|
|
221
|
+
* @param {string[]} uuids
|
|
222
|
+
* @param {string} tagName
|
|
223
|
+
*/
|
|
224
|
+
async tagAccepted(uuids = [], tagName) {
|
|
225
|
+
return this.autoTagAcceptedResults(uuids, { tagName });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Split array into chunks
|
|
230
|
+
* @private
|
|
231
|
+
*/
|
|
232
|
+
chunkArray(array, size) {
|
|
233
|
+
const chunks = [];
|
|
234
|
+
for (let i = 0; i < array.length; i += size) {
|
|
235
|
+
chunks.push(array.slice(i, i + size));
|
|
236
|
+
}
|
|
237
|
+
return chunks;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = TaggingService;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/utils/errors.js
CHANGED
|
File without changes
|
package/src/utils/index.js
CHANGED
|
File without changes
|
package/src/utils/logger.js
CHANGED
|
File without changes
|
package/src/utils/mapAppqUuid.js
CHANGED
|
File without changes
|
package/src/utils/validator.js
CHANGED
|
File without changes
|