@appliqation/automation-sdk 2.5.1 ā 2.8.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/README.md +330 -1169
- package/package.json +13 -34
- package/src/AppliqationClient.js +125 -25
- package/src/cli/auth-setup.js +308 -0
- package/src/constants.js +21 -1
- package/src/core/AuthManager.js +5 -0
- package/src/index.d.ts +159 -6
- package/src/login/index.d.ts +77 -0
- package/src/login/index.js +49 -0
- package/src/playwright/fixture.js +4 -1
- package/src/reporters/playwright/AppliqationReporter.js +159 -11
- 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/index.js +12 -0
- package/src/utils/logger.js +59 -5
- package/src/utils/setupAuth.js +99 -0
- 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 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,
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
const UuidValidator = require('../utils/UuidValidator');
|
|
3
|
+
const UuidExtractor = require('../reporters/playwright/helpers/UuidExtractor');
|
|
4
|
+
const logger = require('../utils/logger');
|
|
5
|
+
|
|
6
|
+
const STRATEGY = {
|
|
7
|
+
CANCEL: 'cancel',
|
|
8
|
+
ADHOC: 'adhoc',
|
|
9
|
+
FILTER: 'filter'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const VALID_STRATEGIES = Object.values(STRATEGY);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validates that the UUIDs about to be executed match the user's configured
|
|
16
|
+
* scenarioId or testSetId scope, and applies a resolution strategy when they
|
|
17
|
+
* don't.
|
|
18
|
+
*
|
|
19
|
+
* Closes finding C5 in the SDK audit ā without this, the SDK silently
|
|
20
|
+
* submits cross-scenario results to a single-scenario run, corrupting the
|
|
21
|
+
* Appliqation data store.
|
|
22
|
+
*
|
|
23
|
+
* Three resolution strategies, selected via `config.onScopeMismatch`:
|
|
24
|
+
* 'cancel' (default in CI) ā fail fast, exit 1, submit nothing
|
|
25
|
+
* 'adhoc' ā override config, create an ad-hoc run
|
|
26
|
+
* 'filter' ā drop out-of-scope results, submit the rest
|
|
27
|
+
*
|
|
28
|
+
* In a TTY (no `CI=true` and stdin is a TTY), the user is prompted
|
|
29
|
+
* interactively before any test runs. In CI, the configured strategy is
|
|
30
|
+
* used; if unset, 'cancel' is the fail-safe default.
|
|
31
|
+
*/
|
|
32
|
+
class ScopeValidator {
|
|
33
|
+
/**
|
|
34
|
+
* @param {Object} options
|
|
35
|
+
* @param {number} [options.scenarioId]
|
|
36
|
+
* @param {number} [options.testSetId]
|
|
37
|
+
* @param {'cancel'|'adhoc'|'filter'} [options.strategy]
|
|
38
|
+
* @param {Function} [options.fetchTestSetScope] async (testSetId) -> Set<uuid>
|
|
39
|
+
*/
|
|
40
|
+
constructor(options = {}) {
|
|
41
|
+
this.scenarioId = options.scenarioId || null;
|
|
42
|
+
this.testSetId = options.testSetId || null;
|
|
43
|
+
this.strategy = this._resolveStrategy(options.strategy);
|
|
44
|
+
this.fetchTestSetScope = options.fetchTestSetScope || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_resolveStrategy(explicit) {
|
|
48
|
+
const envValue = process.env.APPLIQATION_ON_SCOPE_MISMATCH;
|
|
49
|
+
const raw = (explicit || envValue || STRATEGY.CANCEL).toLowerCase();
|
|
50
|
+
if (!VALID_STRATEGIES.includes(raw)) {
|
|
51
|
+
logger.warn(`Unknown onScopeMismatch strategy "${raw}", falling back to "cancel"`);
|
|
52
|
+
return STRATEGY.CANCEL;
|
|
53
|
+
}
|
|
54
|
+
return raw;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @returns {boolean} True if scenarioId or testSetId is set ā i.e., the
|
|
59
|
+
* user has declared a scope and we need to validate against it.
|
|
60
|
+
*/
|
|
61
|
+
isScoped() {
|
|
62
|
+
return Boolean(this.scenarioId || this.testSetId);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Extract every Appliqation UUID from a Playwright suite tree.
|
|
67
|
+
*
|
|
68
|
+
* @param {Object} suite - Playwright Suite from onBegin
|
|
69
|
+
* @returns {string[]} Unique UUIDs, in test-discovery order
|
|
70
|
+
*/
|
|
71
|
+
extractUuidsFromSuite(suite) {
|
|
72
|
+
if (!suite || typeof suite.allTests !== 'function') return [];
|
|
73
|
+
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
const ordered = [];
|
|
76
|
+
for (const test of suite.allTests()) {
|
|
77
|
+
const uuid = UuidExtractor.extractFromAnnotations(test.annotations || [])
|
|
78
|
+
|| UuidExtractor.extractFromTest(test);
|
|
79
|
+
if (uuid && !seen.has(uuid)) {
|
|
80
|
+
seen.add(uuid);
|
|
81
|
+
ordered.push(uuid);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return ordered;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Validate the set of UUIDs against the configured scope.
|
|
89
|
+
*
|
|
90
|
+
* @param {string[]} uuids
|
|
91
|
+
* @returns {Promise<{
|
|
92
|
+
* hasMismatch: boolean,
|
|
93
|
+
* inScope: string[],
|
|
94
|
+
* outOfScope: Array<{uuid: string, actualScenarioId?: number}>,
|
|
95
|
+
* reason: 'scenario' | 'testset' | null
|
|
96
|
+
* }>}
|
|
97
|
+
*/
|
|
98
|
+
async validate(uuids) {
|
|
99
|
+
if (!this.isScoped()) {
|
|
100
|
+
return { hasMismatch: false, inScope: uuids, outOfScope: [], reason: null };
|
|
101
|
+
}
|
|
102
|
+
if (!Array.isArray(uuids) || uuids.length === 0) {
|
|
103
|
+
return { hasMismatch: false, inScope: [], outOfScope: [], reason: null };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (this.scenarioId) {
|
|
107
|
+
return this._validateScenario(uuids);
|
|
108
|
+
}
|
|
109
|
+
return this._validateTestSet(uuids);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
_validateScenario(uuids) {
|
|
113
|
+
const inScope = [];
|
|
114
|
+
const outOfScope = [];
|
|
115
|
+
|
|
116
|
+
for (const uuid of uuids) {
|
|
117
|
+
const nid = UuidValidator.extractNid(uuid);
|
|
118
|
+
if (nid === null) {
|
|
119
|
+
// Malformed UUID ā let the regular submission path reject it.
|
|
120
|
+
// Don't fail the scope check on bad data we didn't author.
|
|
121
|
+
inScope.push(uuid);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (nid === Number(this.scenarioId)) {
|
|
125
|
+
inScope.push(uuid);
|
|
126
|
+
} else {
|
|
127
|
+
outOfScope.push({ uuid, actualScenarioId: nid });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
hasMismatch: outOfScope.length > 0,
|
|
133
|
+
inScope,
|
|
134
|
+
outOfScope,
|
|
135
|
+
reason: 'scenario'
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async _validateTestSet(uuids) {
|
|
140
|
+
if (!this.fetchTestSetScope) {
|
|
141
|
+
// No way to fetch testset membership ā fail open with a warning
|
|
142
|
+
// rather than blocking the run on a missing dependency.
|
|
143
|
+
logger.warn(
|
|
144
|
+
'testSetId is set but no testset-scope fetcher was provided; '
|
|
145
|
+
+ 'skipping scope validation. Pass `fetchTestSetScope` to enable.'
|
|
146
|
+
);
|
|
147
|
+
return { hasMismatch: false, inScope: uuids, outOfScope: [], reason: null };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let scope;
|
|
151
|
+
try {
|
|
152
|
+
scope = await this.fetchTestSetScope(this.testSetId);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
logger.warn('Failed to fetch testset scope; skipping validation', {
|
|
155
|
+
testSetId: this.testSetId,
|
|
156
|
+
error: error.message
|
|
157
|
+
});
|
|
158
|
+
return { hasMismatch: false, inScope: uuids, outOfScope: [], reason: null };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const scopeSet = scope instanceof Set ? scope : new Set(scope || []);
|
|
162
|
+
const inScope = [];
|
|
163
|
+
const outOfScope = [];
|
|
164
|
+
|
|
165
|
+
for (const uuid of uuids) {
|
|
166
|
+
if (scopeSet.has(uuid)) {
|
|
167
|
+
inScope.push(uuid);
|
|
168
|
+
} else {
|
|
169
|
+
outOfScope.push({ uuid });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
hasMismatch: outOfScope.length > 0,
|
|
175
|
+
inScope,
|
|
176
|
+
outOfScope,
|
|
177
|
+
reason: 'testset'
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Resolve a scope mismatch by applying the chosen strategy. In a TTY,
|
|
183
|
+
* the user is prompted interactively before any strategy is applied.
|
|
184
|
+
*
|
|
185
|
+
* @param {Object} validation - result of `validate()`
|
|
186
|
+
* @returns {Promise<{
|
|
187
|
+
* action: 'cancel' | 'adhoc' | 'filter',
|
|
188
|
+
* submitUuids: string[],
|
|
189
|
+
* droppedUuids: string[],
|
|
190
|
+
* overrideToAdhoc: boolean,
|
|
191
|
+
* message: string
|
|
192
|
+
* }>}
|
|
193
|
+
*/
|
|
194
|
+
async resolve(validation) {
|
|
195
|
+
if (!validation.hasMismatch) {
|
|
196
|
+
return {
|
|
197
|
+
action: 'pass',
|
|
198
|
+
submitUuids: validation.inScope,
|
|
199
|
+
droppedUuids: [],
|
|
200
|
+
overrideToAdhoc: false,
|
|
201
|
+
message: 'Scope check passed.'
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const strategy = this._isInteractive()
|
|
206
|
+
? await this._promptInteractive(validation)
|
|
207
|
+
: this.strategy;
|
|
208
|
+
|
|
209
|
+
return this._applyStrategy(strategy, validation);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
_isInteractive() {
|
|
213
|
+
if (process.env.CI === 'true' || process.env.CI === '1') return false;
|
|
214
|
+
return Boolean(process.stdin && process.stdin.isTTY);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async _promptInteractive(validation) {
|
|
218
|
+
const summary = this._formatMismatchSummary(validation);
|
|
219
|
+
// Use process.stdout (not logger) because this is a synchronous user
|
|
220
|
+
// prompt that must appear regardless of log level.
|
|
221
|
+
process.stdout.write('\n');
|
|
222
|
+
process.stdout.write(summary);
|
|
223
|
+
process.stdout.write('\n');
|
|
224
|
+
process.stdout.write('Choose how to proceed:\n');
|
|
225
|
+
process.stdout.write(' [1] cancel ā fail fast, submit nothing (safest)\n');
|
|
226
|
+
process.stdout.write(' [2] adhoc ā create an ad-hoc run and submit everything\n');
|
|
227
|
+
process.stdout.write(' [3] filter ā submit only in-scope results, drop the rest\n');
|
|
228
|
+
|
|
229
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
230
|
+
const ask = (q) => new Promise((res) => rl.question(q, (a) => res(a)));
|
|
231
|
+
try {
|
|
232
|
+
const answer = (await ask('Selection [1/2/3]: ')).trim();
|
|
233
|
+
const map = { '1': STRATEGY.CANCEL, '2': STRATEGY.ADHOC, '3': STRATEGY.FILTER };
|
|
234
|
+
return map[answer] || STRATEGY.CANCEL;
|
|
235
|
+
} finally {
|
|
236
|
+
rl.close();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
_applyStrategy(strategy, validation) {
|
|
241
|
+
const droppedUuids = validation.outOfScope.map((o) => o.uuid);
|
|
242
|
+
const summary = this._formatMismatchSummary(validation);
|
|
243
|
+
|
|
244
|
+
if (strategy === STRATEGY.CANCEL) {
|
|
245
|
+
return {
|
|
246
|
+
action: STRATEGY.CANCEL,
|
|
247
|
+
submitUuids: [],
|
|
248
|
+
droppedUuids: [...validation.inScope, ...droppedUuids],
|
|
249
|
+
overrideToAdhoc: false,
|
|
250
|
+
message:
|
|
251
|
+
`${summary}\n`
|
|
252
|
+
+ 'Strategy: cancel. No results will be submitted to Appliqation.\n'
|
|
253
|
+
+ 'Set onScopeMismatch (or APPLIQATION_ON_SCOPE_MISMATCH) to '
|
|
254
|
+
+ '"adhoc" or "filter" to change this behaviour.'
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (strategy === STRATEGY.ADHOC) {
|
|
259
|
+
return {
|
|
260
|
+
action: STRATEGY.ADHOC,
|
|
261
|
+
submitUuids: [...validation.inScope, ...droppedUuids],
|
|
262
|
+
droppedUuids: [],
|
|
263
|
+
overrideToAdhoc: true,
|
|
264
|
+
message:
|
|
265
|
+
`${summary}\n`
|
|
266
|
+
+ 'Strategy: adhoc. The configured scenarioId/testSetId is being '
|
|
267
|
+
+ 'overridden ā an ad-hoc run will be created instead and all '
|
|
268
|
+
+ 'results will be submitted to it.'
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// FILTER
|
|
273
|
+
return {
|
|
274
|
+
action: STRATEGY.FILTER,
|
|
275
|
+
submitUuids: validation.inScope,
|
|
276
|
+
droppedUuids,
|
|
277
|
+
overrideToAdhoc: false,
|
|
278
|
+
message:
|
|
279
|
+
`${summary}\n`
|
|
280
|
+
+ `Strategy: filter. ${validation.inScope.length} in-scope result(s) `
|
|
281
|
+
+ `will be submitted; ${droppedUuids.length} out-of-scope result(s) `
|
|
282
|
+
+ 'will execute locally but their verdicts will NOT be uploaded.'
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
_formatMismatchSummary(validation) {
|
|
287
|
+
const target = validation.reason === 'scenario'
|
|
288
|
+
? `scenarioId ${this.scenarioId}`
|
|
289
|
+
: `testSetId ${this.testSetId}`;
|
|
290
|
+
|
|
291
|
+
const examples = validation.outOfScope.slice(0, 5).map((o) => {
|
|
292
|
+
if (o.actualScenarioId !== undefined) {
|
|
293
|
+
return ` - ${o.uuid} (belongs to scenario ${o.actualScenarioId})`;
|
|
294
|
+
}
|
|
295
|
+
return ` - ${o.uuid} (not in testset)`;
|
|
296
|
+
}).join('\n');
|
|
297
|
+
|
|
298
|
+
const more = validation.outOfScope.length > 5
|
|
299
|
+
? `\n ⦠and ${validation.outOfScope.length - 5} more`
|
|
300
|
+
: '';
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
`[Appliqation] Scope mismatch detected.\n`
|
|
304
|
+
+ `Configured: ${target}\n`
|
|
305
|
+
+ `Out-of-scope test(s): ${validation.outOfScope.length} of ${validation.inScope.length + validation.outOfScope.length}\n`
|
|
306
|
+
+ examples
|
|
307
|
+
+ more
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
ScopeValidator.STRATEGY = STRATEGY;
|
|
313
|
+
|
|
314
|
+
module.exports = ScopeValidator;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const logger = require('../utils/logger');
|
|
2
|
+
const { AUTO_TAG_NAME } = require('../constants');
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Service for auto-tagging test cases after successful runs
|
|
@@ -8,12 +9,11 @@ class TaggingService {
|
|
|
8
9
|
this.http = httpClient;
|
|
9
10
|
this.config = config;
|
|
10
11
|
|
|
11
|
-
//
|
|
12
|
+
// Whether to auto-tag is configurable; the tag name itself is not ā
|
|
13
|
+
// see AUTO_TAG_NAME's doc comment.
|
|
12
14
|
this.enabled = this.config?.options?.autoTag !== false &&
|
|
13
15
|
process.env.APPLIQATION_AUTO_TAG_ENABLED !== 'false';
|
|
14
|
-
this.tagName =
|
|
15
|
-
process.env.APPLIQATION_AUTO_TAG_NAME ||
|
|
16
|
-
'Appq_automated';
|
|
16
|
+
this.tagName = AUTO_TAG_NAME;
|
|
17
17
|
this.batchSize = this.config?.options?.autoTagBatchSize || 50;
|
|
18
18
|
this.retries = this.config?.options?.autoTagRetries || 2;
|
|
19
19
|
}
|
|
@@ -42,7 +42,7 @@ class TaggingService {
|
|
|
42
42
|
const unique = Array.from(new Set(uuids.filter(Boolean)));
|
|
43
43
|
|
|
44
44
|
try {
|
|
45
|
-
// Build query string: uuids=123-xxx,124-yyy&tag=
|
|
45
|
+
// Build query string: uuids=123-xxx,124-yyy&tag=Appq_Auto
|
|
46
46
|
const uuidsParam = unique.join(',');
|
|
47
47
|
const url = `/api/automation/testcases/tags/check?uuids=${encodeURIComponent(uuidsParam)}&tag=${encodeURIComponent(tag)}`;
|
|
48
48
|
const response = await this.http.get(url);
|