@appliqation/automation-sdk 2.7.0 → 2.8.1
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/README.md +299 -1273
- package/package.json +5 -34
- package/src/AppliqationClient.js +125 -25
- package/src/cli/auth-setup.js +71 -8
- package/src/constants.js +21 -1
- package/src/core/AuthManager.js +5 -0
- package/src/index.d.ts +102 -6
- package/src/login/index.d.ts +2 -1
- package/src/login/index.js +2 -1
- package/src/playwright/fixture.js +4 -1
- package/src/reporters/playwright/AppliqationReporter.js +191 -13
- package/src/services/OrphanTestService.js +21 -2
- package/src/services/ResultService.js +76 -45
- package/src/services/RunMatrixService.js +21 -1
- package/src/services/ScopeValidator.js +314 -0
- package/src/services/TaggingService.js +5 -5
- package/src/utils/PayloadBuilder.js +112 -21
- package/src/utils/logger.js +59 -5
- package/src/reporters/cypress/CypressReporter.js +0 -434
- package/src/reporters/cypress/UuidExtractor.js +0 -139
- package/src/reporters/cypress/index.js +0 -30
- package/src/reporters/jest/JestReporter.js +0 -408
- package/src/reporters/jest/UuidExtractor.js +0 -174
- package/src/reporters/jest/index.js +0 -28
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const AppliqationClient = require('../../AppliqationClient');
|
|
3
|
+
const ScopeValidator = require('../../services/ScopeValidator');
|
|
3
4
|
const UuidExtractor = require('./helpers/UuidExtractor');
|
|
4
5
|
const DeviceOsDetector = require('./helpers/DeviceOsDetector');
|
|
5
6
|
const PayloadBuilder = require('../../utils/PayloadBuilder');
|
|
@@ -176,6 +177,21 @@ class AppliqationReporter {
|
|
|
176
177
|
this.executionStartTime = null;
|
|
177
178
|
this.executionEndTime = null;
|
|
178
179
|
this.playwrightOutputDir = null;
|
|
180
|
+
|
|
181
|
+
// Playwright does not reliably await a reporter's onTestEnd() return
|
|
182
|
+
// value before calling onEnd() — only onEnd()'s own promise is
|
|
183
|
+
// guaranteed to be awaited. onTestEnd() can itself wait (via
|
|
184
|
+
// waitForRunMatrix) for a still-in-flight run creation, so without
|
|
185
|
+
// this tracking, onEnd() can start reading resultsByRun/orphansByRun
|
|
186
|
+
// before onTestEnd has finished populating them, silently submitting
|
|
187
|
+
// nothing. Every onTestEnd() invocation pushes its own promise here;
|
|
188
|
+
// onEnd() awaits them all first.
|
|
189
|
+
this.pendingTestEndPromises = [];
|
|
190
|
+
|
|
191
|
+
// C5 — scope validation state
|
|
192
|
+
this.scopeFilterEnabled = false; // true when strategy === 'filter'
|
|
193
|
+
this.inScopeUuids = null; // Set<string>, null = no filter
|
|
194
|
+
this.droppedOutOfScopeCount = 0;
|
|
179
195
|
}
|
|
180
196
|
|
|
181
197
|
/**
|
|
@@ -211,6 +227,37 @@ class AppliqationReporter {
|
|
|
211
227
|
}
|
|
212
228
|
}
|
|
213
229
|
|
|
230
|
+
// Extract every Appliqation-mapped UUID from the suite once, then
|
|
231
|
+
// share with scope validation (C5) and run pre-population (C4/E3).
|
|
232
|
+
// Single walk over the test tree, single source of truth.
|
|
233
|
+
const allSuiteUuids = ScopeValidator.prototype.extractUuidsFromSuite.call(
|
|
234
|
+
{ /* no scope needed for extraction */ }, suite
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// C5 — pre-execution scope validation.
|
|
238
|
+
// If the user set scenarioId or testSetId, verify every test about to
|
|
239
|
+
// run actually belongs to that scope. Mismatch → apply the configured
|
|
240
|
+
// resolution strategy (cancel / adhoc / filter). This is the SDK's
|
|
241
|
+
// primary defence against silent data corruption: without it, a
|
|
242
|
+
// tag-based selection that spans scenarios writes to the wrong run.
|
|
243
|
+
const scopeOutcome = await this._runScopeValidation(suite, allSuiteUuids);
|
|
244
|
+
if (scopeOutcome.cancelled) {
|
|
245
|
+
// User chose cancel (or default in CI on mismatch). Skip run
|
|
246
|
+
// creation and result submission entirely; tests still execute
|
|
247
|
+
// locally so the user keeps their own Playwright output.
|
|
248
|
+
this.appqEnabled = false;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// C4 / E3 — UUIDs to pre-populate on the run document. If the filter
|
|
253
|
+
// strategy applied, only the in-scope subset is sent; otherwise the
|
|
254
|
+
// full extracted set. Backend stores these in data[] so the UI grid
|
|
255
|
+
// and orphan detector know which TCs belong to the run before any
|
|
256
|
+
// result lands.
|
|
257
|
+
this.preExecutionUuids = this.scopeFilterEnabled && this.inScopeUuids
|
|
258
|
+
? Array.from(this.inScopeUuids)
|
|
259
|
+
: allSuiteUuids;
|
|
260
|
+
|
|
214
261
|
if (!this.config.autoCreateRun) {
|
|
215
262
|
logger.info('Auto-create run disabled. Skipping run matrix creation.');
|
|
216
263
|
return;
|
|
@@ -276,7 +323,11 @@ class AppliqationReporter {
|
|
|
276
323
|
browsers: matrixConfig.browsers,
|
|
277
324
|
device: matrixConfig.device,
|
|
278
325
|
os: matrixConfig.os,
|
|
279
|
-
title: this.config.title
|
|
326
|
+
title: this.config.title,
|
|
327
|
+
// C4 / E3 — pre-populate the run's data[] array with the
|
|
328
|
+
// UUIDs about to execute. Same set across all matrix configs
|
|
329
|
+
// (Playwright reruns the same tests per browser project).
|
|
330
|
+
uuids: this.preExecutionUuids
|
|
280
331
|
};
|
|
281
332
|
|
|
282
333
|
const run = await this.client.createRun(runOptions);
|
|
@@ -364,14 +415,36 @@ class AppliqationReporter {
|
|
|
364
415
|
}
|
|
365
416
|
|
|
366
417
|
/**
|
|
367
|
-
* Called after a test completes
|
|
418
|
+
* Called after a test completes.
|
|
419
|
+
*
|
|
420
|
+
* Playwright does not reliably await this method's returned promise
|
|
421
|
+
* before calling onEnd() — so the actual work (which can wait on a
|
|
422
|
+
* still-in-flight run creation via waitForRunMatrix) runs in
|
|
423
|
+
* _processTestEnd(), and this wrapper only tracks that promise in
|
|
424
|
+
* pendingTestEndPromises so onEnd() can await it explicitly.
|
|
368
425
|
*/
|
|
369
|
-
|
|
426
|
+
onTestEnd(test, result) {
|
|
370
427
|
if (!this.appqEnabled) return;
|
|
428
|
+
const promise = this._processTestEnd(test, result);
|
|
429
|
+
this.pendingTestEndPromises.push(promise);
|
|
430
|
+
return promise;
|
|
431
|
+
}
|
|
371
432
|
|
|
433
|
+
/** @private */
|
|
434
|
+
async _processTestEnd(test, result) {
|
|
372
435
|
try {
|
|
373
436
|
const uuid = UuidExtractor.extractFromAnnotations(result.annotations || []) || UuidExtractor.extractFromTest(test);
|
|
374
437
|
|
|
438
|
+
// C5 filter strategy: scope validation in onBegin flagged this test
|
|
439
|
+
// as out-of-scope for the configured scenarioId/testSetId. The test
|
|
440
|
+
// still executed (the customer has their local report), but its
|
|
441
|
+
// verdict must not land in the wrong run.
|
|
442
|
+
if (this.scopeFilterEnabled && uuid && this.inScopeUuids && !this.inScopeUuids.has(uuid)) {
|
|
443
|
+
this.droppedOutOfScopeCount++;
|
|
444
|
+
logger.debug('Dropping out-of-scope result (C5 filter strategy)', { uuid });
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
375
448
|
const project = test.parent?.project?.() || test.parent?.project;
|
|
376
449
|
const deviceInfo = DeviceOsDetector.getDeviceInfo(project, null);
|
|
377
450
|
const projectKey = `${deviceInfo.device}-${deviceInfo.os}`;
|
|
@@ -405,7 +478,15 @@ class AppliqationReporter {
|
|
|
405
478
|
return;
|
|
406
479
|
}
|
|
407
480
|
|
|
408
|
-
// Duplicate detection
|
|
481
|
+
// Duplicate detection — user-facing warning only, NOT a data-integrity
|
|
482
|
+
// guard. The backend's upsert key is (run_id, uuid, parent_uuid,
|
|
483
|
+
// browser), so re-submitting the same UUID for the same browser is
|
|
484
|
+
// idempotent at the DB level (see appq PR #532's automation.attempt
|
|
485
|
+
// $inc semantics). This tracking exists purely so the end-of-run
|
|
486
|
+
// summary can point out authoring mistakes (two tests annotated with
|
|
487
|
+
// the same UUID), and it's per-reporter-instance — cross-shard
|
|
488
|
+
// duplicates from `--shard` are not caught here, and don't need to
|
|
489
|
+
// be, because the backend's upsert still handles them correctly.
|
|
409
490
|
const trackingKey = `${runInfo.runId}:${deviceInfo.browser}`;
|
|
410
491
|
const submittedUuids = this.submittedUuidsByRun.get(trackingKey) || new Map();
|
|
411
492
|
|
|
@@ -462,9 +543,9 @@ class AppliqationReporter {
|
|
|
462
543
|
|
|
463
544
|
this.trackResult(runInfo.runId, testResult);
|
|
464
545
|
|
|
465
|
-
if (testResult.status === '
|
|
466
|
-
else if (testResult.status === '
|
|
467
|
-
else if (testResult.status === '
|
|
546
|
+
if (testResult.status === 'passed') this.passedTests++;
|
|
547
|
+
else if (testResult.status === 'failed') this.failedTests++;
|
|
548
|
+
else if (testResult.status === 'skipped') this.skippedTests++;
|
|
468
549
|
|
|
469
550
|
if (!this.config.batchSubmit) {
|
|
470
551
|
await this.client.submitResult(runInfo.runId, testResult);
|
|
@@ -565,6 +646,14 @@ class AppliqationReporter {
|
|
|
565
646
|
logger.info('Test run complete.');
|
|
566
647
|
|
|
567
648
|
try {
|
|
649
|
+
// See onTestEnd()'s doc comment — Playwright doesn't reliably await
|
|
650
|
+
// per-test reporter hooks, so results/orphans tracked by still-
|
|
651
|
+
// pending onTestEnd() calls (e.g. waiting on a slow run creation)
|
|
652
|
+
// would otherwise be silently missing from what we're about to read.
|
|
653
|
+
if (this.pendingTestEndPromises.length > 0) {
|
|
654
|
+
await Promise.allSettled(this.pendingTestEndPromises);
|
|
655
|
+
}
|
|
656
|
+
|
|
568
657
|
const orphanDeletedRuns = await this.handleOrphanOnlyRuns();
|
|
569
658
|
|
|
570
659
|
if (this.appqEnabled) {
|
|
@@ -660,20 +749,33 @@ class AppliqationReporter {
|
|
|
660
749
|
|
|
661
750
|
/** @private */
|
|
662
751
|
mapStatus(status) {
|
|
752
|
+
// Playwright's `interrupted` means the entire test run was aborted
|
|
753
|
+
// (Ctrl+C, CI timeout, OOM). Mapping it to 'skipped' would mask
|
|
754
|
+
// failures and inflate the skipped counter — surface as failed instead
|
|
755
|
+
// so the run summary honestly reflects an aborted execution.
|
|
756
|
+
//
|
|
757
|
+
// Values MUST match PayloadBuilder.validateAutomationResultPayload's
|
|
758
|
+
// ALLOWED_STATUSES ({passed, failed, skipped}, lowercase past-tense —
|
|
759
|
+
// mirrors the backend's InputValidator). This reporter's own mapped
|
|
760
|
+
// value is what ultimately reaches that validator (via trackResult →
|
|
761
|
+
// submitBatch → PayloadBuilder.buildResultPayload → normalizeStatus,
|
|
762
|
+
// which passes an already-recognized lowercase value straight through),
|
|
763
|
+
// so drifting from that vocabulary here silently fails every submission.
|
|
663
764
|
const statusMap = {
|
|
664
|
-
'passed': '
|
|
665
|
-
'failed': '
|
|
666
|
-
'timedOut': '
|
|
667
|
-
'skipped': '
|
|
668
|
-
'interrupted': '
|
|
765
|
+
'passed': 'passed',
|
|
766
|
+
'failed': 'failed',
|
|
767
|
+
'timedOut': 'failed',
|
|
768
|
+
'skipped': 'skipped',
|
|
769
|
+
'interrupted': 'failed'
|
|
669
770
|
};
|
|
670
|
-
return statusMap[status] || '
|
|
771
|
+
return statusMap[status] || 'failed';
|
|
671
772
|
}
|
|
672
773
|
|
|
673
774
|
/** @private */
|
|
674
775
|
buildComment(test, result) {
|
|
675
776
|
const parts = [];
|
|
676
777
|
if (result.duration) parts.push(`Duration: ${(result.duration / 1000).toFixed(2)}s`);
|
|
778
|
+
if (result.status === 'interrupted') parts.push('Test run interrupted');
|
|
677
779
|
if (result.error) {
|
|
678
780
|
const errorMsg = result.error.message || result.error.toString();
|
|
679
781
|
parts.push(`Error: ${errorMsg.substring(0, 500)}`);
|
|
@@ -868,6 +970,82 @@ class AppliqationReporter {
|
|
|
868
970
|
return Array.from(projectNames);
|
|
869
971
|
}
|
|
870
972
|
|
|
973
|
+
/**
|
|
974
|
+
* Run C5 scope validation against the Playwright suite. Mutates
|
|
975
|
+
* `this.config` if the user chooses the 'adhoc' strategy (clears
|
|
976
|
+
* scenarioId/testSetId so subsequent run creation is ad-hoc).
|
|
977
|
+
* Sets `this.scopeFilterEnabled` and `this.inScopeUuids` for the
|
|
978
|
+
* 'filter' strategy so `onTestEnd` can drop out-of-scope results.
|
|
979
|
+
*
|
|
980
|
+
* @private
|
|
981
|
+
* @param {Object} suite - Playwright Suite from onBegin
|
|
982
|
+
* @returns {Promise<{cancelled: boolean}>}
|
|
983
|
+
*/
|
|
984
|
+
async _runScopeValidation(suite, preExtractedUuids) {
|
|
985
|
+
const validator = new ScopeValidator({
|
|
986
|
+
scenarioId: this.config.scenarioId,
|
|
987
|
+
testSetId: this.config.testSetId,
|
|
988
|
+
strategy: this.config.onScopeMismatch,
|
|
989
|
+
fetchTestSetScope: this._fetchTestSetScope.bind(this)
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
if (!validator.isScoped()) {
|
|
993
|
+
return { cancelled: false };
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const uuids = preExtractedUuids || validator.extractUuidsFromSuite(suite);
|
|
997
|
+
const validation = await validator.validate(uuids);
|
|
998
|
+
if (!validation.hasMismatch) {
|
|
999
|
+
return { cancelled: false };
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const outcome = await validator.resolve(validation);
|
|
1003
|
+
// Surface the resolution message at WARN so it's visible at default
|
|
1004
|
+
// log level — this is operational signal, not noise.
|
|
1005
|
+
logger.warn(outcome.message);
|
|
1006
|
+
|
|
1007
|
+
if (outcome.action === ScopeValidator.STRATEGY.CANCEL) {
|
|
1008
|
+
// Set non-zero exit code so CI fails. Don't throw — we still want
|
|
1009
|
+
// Playwright to finish executing tests locally for the user's logs.
|
|
1010
|
+
process.exitCode = 1;
|
|
1011
|
+
return { cancelled: true };
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
if (outcome.overrideToAdhoc) {
|
|
1015
|
+
// Drop the configured scope so createRun produces an ad-hoc run.
|
|
1016
|
+
delete this.config.scenarioId;
|
|
1017
|
+
delete this.config.testSetId;
|
|
1018
|
+
return { cancelled: false };
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (outcome.action === ScopeValidator.STRATEGY.FILTER) {
|
|
1022
|
+
this.scopeFilterEnabled = true;
|
|
1023
|
+
this.inScopeUuids = new Set(outcome.submitUuids);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
return { cancelled: false };
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Resolve a testSet's UUID membership. Requires a backend endpoint
|
|
1031
|
+
* (GET /api/automation/testset/{id}/scope) that returns the UUID list.
|
|
1032
|
+
* Until that endpoint ships, this throws — ScopeValidator catches and
|
|
1033
|
+
* gracefully degrades to skip testset validation with a warning.
|
|
1034
|
+
*
|
|
1035
|
+
* @private
|
|
1036
|
+
* @param {number} testSetId
|
|
1037
|
+
* @returns {Promise<Set<string>>}
|
|
1038
|
+
*/
|
|
1039
|
+
async _fetchTestSetScope(testSetId) {
|
|
1040
|
+
if (!this.client || typeof this.client.fetchTestSetScope !== 'function') {
|
|
1041
|
+
throw new Error(
|
|
1042
|
+
'TestSet scope endpoint not yet available in AppliqationClient — '
|
|
1043
|
+
+ 'testset scope validation will be skipped'
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
return this.client.fetchTestSetScope(testSetId);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
871
1049
|
/**
|
|
872
1050
|
* Auto-detect browser versions
|
|
873
1051
|
* @private
|
|
@@ -44,10 +44,29 @@ class OrphanTestService {
|
|
|
44
44
|
|
|
45
45
|
return response;
|
|
46
46
|
} catch (error) {
|
|
47
|
-
|
|
47
|
+
// S5 — as of appq_automation_api.routing.yml, /api/automation/
|
|
48
|
+
// orphan-tests is not a registered route. Backend implementation
|
|
49
|
+
// is pending. Until it lands, a 404 here is expected — demote to
|
|
50
|
+
// debug so it doesn't spam customer CI logs. Orphans are still
|
|
51
|
+
// tracked locally and shown in the reporter's end-of-run summary.
|
|
52
|
+
const status = error.status
|
|
53
|
+
|| error.response?.status
|
|
54
|
+
|| error.details?.status
|
|
55
|
+
|| null;
|
|
56
|
+
|
|
57
|
+
if (status === 404) {
|
|
58
|
+
logger.debug('Orphan tests endpoint not available on backend; skipping network log', {
|
|
59
|
+
runId,
|
|
60
|
+
count: orphanTests.length
|
|
61
|
+
});
|
|
62
|
+
return { success: false, notImplemented: true, count: orphanTests.length };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
logger.warn('Failed to log orphan tests', {
|
|
48
66
|
error: error.message,
|
|
49
67
|
runId,
|
|
50
|
-
count: orphanTests.length
|
|
68
|
+
count: orphanTests.length,
|
|
69
|
+
status
|
|
51
70
|
});
|
|
52
71
|
throw new Error(`Failed to log orphan tests: ${error.message}`);
|
|
53
72
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const logger = require('../utils/logger');
|
|
2
2
|
const { normalizeBrowser } = require('../utils/RunDataNormalizer');
|
|
3
|
+
const PayloadBuilder = require('../utils/PayloadBuilder');
|
|
3
4
|
|
|
4
5
|
class ResultService {
|
|
5
6
|
constructor(httpClient, taggingService = null, config = { options: {} }) {
|
|
@@ -27,6 +28,16 @@ class ResultService {
|
|
|
27
28
|
|
|
28
29
|
const payload = this.buildAutomationResultPayload(runId, result, runMetadata);
|
|
29
30
|
|
|
31
|
+
// S8 — final shape check before the wire. Catches schema drift
|
|
32
|
+
// between SDK assembly and backend expectations at the client
|
|
33
|
+
// instead of failing at the server with a generic 400.
|
|
34
|
+
const validation = PayloadBuilder.validateAutomationResultPayload(payload);
|
|
35
|
+
if (!validation.valid) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Result payload failed pre-submission validation: ${validation.errors.join('; ')}`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
logger.debug('Submitting single result', {
|
|
31
42
|
runId,
|
|
32
43
|
uuid: result.uuid,
|
|
@@ -36,16 +47,12 @@ class ResultService {
|
|
|
36
47
|
// Use automation endpoint to enable project validation
|
|
37
48
|
const response = await this.http.post('/api/automation/result/submit', payload);
|
|
38
49
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
console.log('Status:', response.status);
|
|
47
|
-
console.log('Success:', response.success);
|
|
48
|
-
console.log('===========================================\n');
|
|
50
|
+
logger.debug('Result submitted', {
|
|
51
|
+
uuid: result.uuid,
|
|
52
|
+
status: response.status,
|
|
53
|
+
success: response.success,
|
|
54
|
+
projectId: response.data?.debug_context?.project_id
|
|
55
|
+
});
|
|
49
56
|
|
|
50
57
|
// Check if request was successful
|
|
51
58
|
if (!response.success) {
|
|
@@ -132,30 +139,51 @@ class ResultService {
|
|
|
132
139
|
results: batch.map(r => this.buildAutomationResultPayload(runId, r, runMetadata))
|
|
133
140
|
};
|
|
134
141
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
//
|
|
138
|
-
|
|
142
|
+
// S8 — validate every result in the batch before the wire.
|
|
143
|
+
// A malformed entry is filtered out and moved to failures rather
|
|
144
|
+
// than tainting the whole batch (backend would 400 the whole
|
|
145
|
+
// batch otherwise). Track per-result so downstream reporting
|
|
146
|
+
// still knows which UUIDs never made it.
|
|
147
|
+
const validResults = [];
|
|
139
148
|
payload.results.forEach((r, idx) => {
|
|
140
|
-
|
|
149
|
+
const v = PayloadBuilder.validateAutomationResultPayload(r);
|
|
150
|
+
if (v.valid) {
|
|
151
|
+
validResults.push(r);
|
|
152
|
+
} else {
|
|
153
|
+
logger.warn('Skipping malformed result at client-side validation', {
|
|
154
|
+
uuid: r.test_case_uuid,
|
|
155
|
+
errors: v.errors
|
|
156
|
+
});
|
|
157
|
+
failures.push(batch[idx]);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
if (validResults.length === 0) {
|
|
161
|
+
logger.warn('Every result in batch failed pre-submission validation; skipping HTTP call', {
|
|
162
|
+
runId, batchIndex: i + 1
|
|
163
|
+
});
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
payload.results = validResults;
|
|
167
|
+
|
|
168
|
+
logger.debug('Submitting batch', {
|
|
169
|
+
runId,
|
|
170
|
+
batchIndex: i + 1,
|
|
171
|
+
totalBatches: batches.length,
|
|
172
|
+
uuids: payload.results.map(r => r.test_case_uuid)
|
|
141
173
|
});
|
|
142
174
|
|
|
143
175
|
try {
|
|
144
176
|
const response = await this.http.post('/api/automation/result/batch', payload);
|
|
145
177
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
console.log('⚠️ No debug_context in response');
|
|
156
|
-
}
|
|
157
|
-
console.log('Status:', response.status);
|
|
158
|
-
console.log('Success:', response.success);
|
|
178
|
+
logger.debug('Batch submitted', {
|
|
179
|
+
runId,
|
|
180
|
+
batchIndex: i + 1,
|
|
181
|
+
totalBatches: batches.length,
|
|
182
|
+
resultsInBatch: batch.length,
|
|
183
|
+
status: response.status,
|
|
184
|
+
success: response.success,
|
|
185
|
+
projectId: response.data?.debug_context?.project_id
|
|
186
|
+
});
|
|
159
187
|
|
|
160
188
|
// Check for validation failures - response has nested data object
|
|
161
189
|
const failedResults = response.data?.data?.results?.failed || response.data?.results?.failed || response.data?.failed || [];
|
|
@@ -163,24 +191,20 @@ class ResultService {
|
|
|
163
191
|
const submittedCount = typeof submittedResults === 'number' ? submittedResults : (Array.isArray(submittedResults) ? submittedResults.length : 0);
|
|
164
192
|
|
|
165
193
|
if (failedResults.length > 0) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
console.log('');
|
|
194
|
+
// Validation failures are operational signal — surface at WARN, not DEBUG.
|
|
195
|
+
// Logger sanitization redacts any sensitive fields in the payload.
|
|
196
|
+
logger.warn(`Backend rejected ${failedResults.length} of ${batch.length} results`, {
|
|
197
|
+
runId,
|
|
198
|
+
batchIndex: i + 1,
|
|
199
|
+
accepted: submittedCount,
|
|
200
|
+
rejected: failedResults.length,
|
|
201
|
+
rejections: failedResults.map(f => ({
|
|
202
|
+
uuid: f.test_case_uuid || f.uuid,
|
|
203
|
+
reason: f.error || f.message,
|
|
204
|
+
code: f.code
|
|
205
|
+
}))
|
|
180
206
|
});
|
|
181
|
-
console.log('===========================================');
|
|
182
207
|
}
|
|
183
|
-
console.log('============================================\n');
|
|
184
208
|
|
|
185
209
|
if (!response.success) {
|
|
186
210
|
throw new Error(response.error || 'Batch submission failed');
|
|
@@ -348,12 +372,19 @@ class ResultService {
|
|
|
348
372
|
failed: 'failed',
|
|
349
373
|
skipped: 'skipped',
|
|
350
374
|
timedOut: 'failed',
|
|
351
|
-
|
|
375
|
+
// Aborted run, not a skip — see AppliqationReporter.mapStatus.
|
|
376
|
+
interrupted: 'failed'
|
|
352
377
|
};
|
|
353
378
|
|
|
354
379
|
return {
|
|
355
380
|
run_id: runId || result.runId,
|
|
356
381
|
test_case_uuid: result.uuid,
|
|
382
|
+
// Routes the backend to submitAutomationResult() — writes to the
|
|
383
|
+
// automation.* subdocument only, never overwrites a human verdict on
|
|
384
|
+
// top-level status/comment/verdict_source. Post PR #532 the backend
|
|
385
|
+
// routes API-key traffic to this path unconditionally; sending the
|
|
386
|
+
// field explicitly makes intent unambiguous and is forward-safe.
|
|
387
|
+
source: 'automation',
|
|
357
388
|
status: statusMap[result.status] || result.status,
|
|
358
389
|
duration: result.duration || 0,
|
|
359
390
|
error_message: result.error || result.comment || '',
|
|
@@ -53,9 +53,17 @@ class RunMatrixService {
|
|
|
53
53
|
const browsers = options.browsers || ['Chrome'];
|
|
54
54
|
const normalizedBrowsers = browsers.map(browser => normalizeBrowser(browser));
|
|
55
55
|
|
|
56
|
+
// S2 — backend (AutomationApiController::createRun) uppercases env
|
|
57
|
+
// names on storage and matches case-insensitively on validation.
|
|
58
|
+
// Uppercase locally so the run-creation log and the stored doc
|
|
59
|
+
// agree, and so any downstream filter that matches on the case the
|
|
60
|
+
// SDK sent doesn't quietly mismatch.
|
|
61
|
+
const envRaw = options.environment || 'Local';
|
|
62
|
+
const environment = typeof envRaw === 'string' ? envRaw.toUpperCase() : envRaw;
|
|
63
|
+
|
|
56
64
|
const payload = {
|
|
57
65
|
project_key: projectKeyValue,
|
|
58
|
-
environment
|
|
66
|
+
environment,
|
|
59
67
|
browsers: normalizedBrowsers,
|
|
60
68
|
device: normalizeDevice(options.device || this.detectDevice()),
|
|
61
69
|
os: normalizeOS(options.os || this.detectOS()),
|
|
@@ -75,6 +83,18 @@ class RunMatrixService {
|
|
|
75
83
|
payload.type = options.type || 'automation';
|
|
76
84
|
}
|
|
77
85
|
|
|
86
|
+
// C4 / E3 — pre-populate data[] with the UUIDs about to be executed.
|
|
87
|
+
// Backend stores this on the run document so the UI grid knows which
|
|
88
|
+
// TCs belong to the run before any result lands. Without it, ad-hoc
|
|
89
|
+
// runs show 0 expected tests and the orphan detector flags every
|
|
90
|
+
// valid test as orphan.
|
|
91
|
+
//
|
|
92
|
+
// PR #532 (appq) changed data[] from objects ({uuid: '...'}) to
|
|
93
|
+
// plain strings — match that format.
|
|
94
|
+
if (Array.isArray(options.uuids) && options.uuids.length > 0) {
|
|
95
|
+
payload.data = options.uuids.map((u) => String(u));
|
|
96
|
+
}
|
|
97
|
+
|
|
78
98
|
logger.info('Creating run matrix...', {
|
|
79
99
|
type: payload.type,
|
|
80
100
|
id: payload.scenario_id || payload.testset_id,
|