@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.
Files changed (38) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +509 -7
  3. package/package.json +2 -2
  4. package/src/AppliqationClient.js +47 -2
  5. package/src/constants.js +0 -0
  6. package/src/core/AuthManager.js +0 -0
  7. package/src/core/HttpClient.js +52 -0
  8. package/src/index.d.ts +0 -0
  9. package/src/index.js +0 -0
  10. package/src/playwright/JwtBrowserAuth.js +0 -0
  11. package/src/playwright/fixture.js +0 -0
  12. package/src/playwright/global-setup.js +43 -0
  13. package/src/playwright/global-teardown.js +0 -0
  14. package/src/playwright/helpers/jwt-browser-auth.js +0 -0
  15. package/src/playwright/index.js +0 -0
  16. package/src/reporters/cypress/CypressReporter.js +49 -2
  17. package/src/reporters/cypress/UuidExtractor.js +0 -0
  18. package/src/reporters/cypress/index.js +0 -0
  19. package/src/reporters/jest/JestReporter.js +49 -2
  20. package/src/reporters/jest/UuidExtractor.js +0 -0
  21. package/src/reporters/jest/index.js +0 -0
  22. package/src/reporters/playwright/AppliqationReporter.js +734 -26
  23. package/src/reporters/playwright/helpers/DeviceOsDetector.js +0 -0
  24. package/src/reporters/playwright/helpers/UuidExtractor.js +8 -3
  25. package/src/reporters/playwright/index.d.ts +0 -0
  26. package/src/reporters/playwright/index.js +0 -0
  27. package/src/services/OrphanTestService.js +0 -0
  28. package/src/services/ResultService.js +193 -24
  29. package/src/services/RunMatrixService.js +44 -0
  30. package/src/services/TaggingService.js +241 -0
  31. package/src/utils/PayloadBuilder.js +0 -0
  32. package/src/utils/RunDataNormalizer.js +0 -0
  33. package/src/utils/UuidValidator.js +0 -0
  34. package/src/utils/errors.js +0 -0
  35. package/src/utils/index.js +0 -0
  36. package/src/utils/logger.js +0 -0
  37. package/src/utils/mapAppqUuid.js +0 -0
  38. package/src/utils/validator.js +0 -0
@@ -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
- const payload = [this.buildLegacyResultPayload(result, runMetadata)];
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 existing /api/insert/scenario/result endpoint (expects array)
27
- const response = await this.http.post('/api/insert/scenario/result', payload);
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
- return response;
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 browser for efficient metadata updates
75
- const grouped = this.groupByBrowser(results);
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 [browser, browserResults] of Object.entries(grouped)) {
81
- const batches = this.chunkArray(browserResults, batchSize);
123
+ for (const [runId, runResults] of Object.entries(groupedByRun)) {
124
+ const batches = this.chunkArray(runResults, batchSize);
82
125
 
83
- logger.info(`Submitting ${browserResults.length} results for ${browser} in ${batches.length} batches`);
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
- // Transform results to legacy format for existing endpoint
88
- const payloads = batch.map(r => this.buildLegacyResultPayload(r, runMetadata));
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(payloads, null, 2) });
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
- // Use existing /api/insert/scenario/result endpoint
94
- const response = await this.http.post('/api/insert/scenario/result', payloads);
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
- // Track successful submissions
102
- allResults.push(...batch);
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
- browser,
211
+ runId,
108
212
  batchIndex: i + 1,
109
213
  totalBatches: batches.length,
110
- completed: (i + 1) * batchSize,
111
- total: browserResults.length
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 ${browser}`, {
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 ${browser}`, {
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
File without changes
File without changes
File without changes
File without changes
File without changes