@copado/agentia-cli 0.112.0-alpha.0 → 0.113.0-alpha.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 +1965 -410
- package/dist/commands/testing/build/abort.d.ts +7 -1
- package/dist/commands/testing/build/abort.js +13 -3
- package/dist/commands/testing/build/abort.js.map +1 -1
- package/dist/commands/testing/build/get.d.ts +18 -0
- package/dist/commands/testing/build/get.js +38 -0
- package/dist/commands/testing/build/get.js.map +1 -0
- package/dist/commands/testing/build/latest.d.ts +12 -0
- package/dist/commands/testing/build/latest.js +40 -0
- package/dist/commands/testing/build/latest.js.map +1 -0
- package/dist/commands/testing/build/list.d.ts +6 -1
- package/dist/commands/testing/build/list.js +23 -1
- package/dist/commands/testing/build/list.js.map +1 -1
- package/dist/commands/testing/build/logs.d.ts +6 -0
- package/dist/commands/testing/build/logs.js +8 -1
- package/dist/commands/testing/build/logs.js.map +1 -1
- package/dist/commands/testing/build/run.d.ts +27 -4
- package/dist/commands/testing/build/run.js +4 -18
- package/dist/commands/testing/build/run.js.map +1 -1
- package/dist/commands/testing/build/search.d.ts +21 -0
- package/dist/commands/testing/build/search.js +57 -0
- package/dist/commands/testing/build/search.js.map +1 -0
- package/dist/commands/testing/create.d.ts +1 -15
- package/dist/commands/testing/create.js +1 -43
- package/dist/commands/testing/create.js.map +1 -1
- package/dist/commands/testing/data-table/create.d.ts +30 -0
- package/dist/commands/testing/data-table/create.js +52 -0
- package/dist/commands/testing/data-table/create.js.map +1 -0
- package/dist/commands/testing/data-table/delete.d.ts +25 -0
- package/dist/commands/testing/data-table/delete.js +40 -0
- package/dist/commands/testing/data-table/delete.js.map +1 -0
- package/dist/commands/testing/data-table/export.d.ts +33 -0
- package/dist/commands/testing/data-table/export.js +59 -0
- package/dist/commands/testing/data-table/export.js.map +1 -0
- package/dist/commands/testing/data-table/get.d.ts +21 -0
- package/dist/commands/testing/data-table/get.js +35 -0
- package/dist/commands/testing/data-table/get.js.map +1 -0
- package/dist/commands/testing/data-table/list.d.ts +16 -0
- package/dist/commands/testing/data-table/list.js +33 -0
- package/dist/commands/testing/data-table/list.js.map +1 -0
- package/dist/commands/testing/data-table/update.d.ts +34 -0
- package/dist/commands/testing/data-table/update.js +64 -0
- package/dist/commands/testing/data-table/update.js.map +1 -0
- package/dist/commands/testing/job/create.d.ts +18 -0
- package/dist/commands/testing/job/create.js +60 -4
- package/dist/commands/testing/job/create.js.map +1 -1
- package/dist/commands/testing/job/delete.d.ts +6 -1
- package/dist/commands/testing/job/delete.js +13 -3
- package/dist/commands/testing/job/delete.js.map +1 -1
- package/dist/commands/testing/job/refs.d.ts +17 -0
- package/dist/commands/testing/job/refs.js +30 -0
- package/dist/commands/testing/job/refs.js.map +1 -0
- package/dist/commands/testing/job/schedule/get.d.ts +23 -0
- package/dist/commands/testing/job/schedule/get.js +31 -0
- package/dist/commands/testing/job/schedule/get.js.map +1 -0
- package/dist/commands/testing/job/schedule/remove.d.ts +24 -0
- package/dist/commands/testing/job/schedule/remove.js +34 -0
- package/dist/commands/testing/job/schedule/remove.js.map +1 -0
- package/dist/commands/testing/job/schedule/set.d.ts +23 -0
- package/dist/commands/testing/job/schedule/set.js +39 -0
- package/dist/commands/testing/job/schedule/set.js.map +1 -0
- package/dist/commands/testing/job/update.d.ts +2 -0
- package/dist/commands/testing/job/update.js +7 -1
- package/dist/commands/testing/job/update.js.map +1 -1
- package/dist/commands/testing/job/upload.d.ts +6 -0
- package/dist/commands/testing/job/upload.js +22 -0
- package/dist/commands/testing/job/upload.js.map +1 -1
- package/dist/commands/testing/live/continue.d.ts +20 -0
- package/dist/commands/testing/live/continue.js +21 -0
- package/dist/commands/testing/live/continue.js.map +1 -0
- package/dist/commands/testing/live/execute.d.ts +30 -0
- package/dist/commands/testing/live/execute.js +43 -0
- package/dist/commands/testing/live/execute.js.map +1 -0
- package/dist/commands/testing/live/history.d.ts +23 -0
- package/dist/commands/testing/live/history.js +35 -0
- package/dist/commands/testing/live/history.js.map +1 -0
- package/dist/commands/testing/live/inspect.d.ts +18 -0
- package/dist/commands/testing/live/inspect.js +30 -0
- package/dist/commands/testing/live/inspect.js.map +1 -0
- package/dist/commands/testing/live/interrupt.d.ts +20 -0
- package/dist/commands/testing/live/interrupt.js +21 -0
- package/dist/commands/testing/live/interrupt.js.map +1 -0
- package/dist/commands/testing/live/recorder.d.ts +23 -0
- package/dist/commands/testing/live/recorder.js +134 -0
- package/dist/commands/testing/live/recorder.js.map +1 -0
- package/dist/commands/testing/live/reports.d.ts +20 -0
- package/dist/commands/testing/live/reports.js +31 -0
- package/dist/commands/testing/live/reports.js.map +1 -0
- package/dist/commands/testing/live/screenshot.d.ts +23 -0
- package/dist/commands/testing/live/screenshot.js +25 -0
- package/dist/commands/testing/live/screenshot.js.map +1 -0
- package/dist/commands/testing/live/set-options.d.ts +27 -0
- package/dist/commands/testing/live/set-options.js +28 -0
- package/dist/commands/testing/live/set-options.js.map +1 -0
- package/dist/commands/testing/live/start.d.ts +29 -0
- package/dist/commands/testing/live/start.js +53 -0
- package/dist/commands/testing/live/start.js.map +1 -0
- package/dist/commands/testing/live/status.d.ts +19 -0
- package/dist/commands/testing/live/status.js +27 -0
- package/dist/commands/testing/live/status.js.map +1 -0
- package/dist/commands/testing/live/stop.d.ts +24 -0
- package/dist/commands/testing/live/stop.js +38 -0
- package/dist/commands/testing/live/stop.js.map +1 -0
- package/dist/commands/testing/live/view.d.ts +16 -0
- package/dist/commands/testing/live/view.js +36 -0
- package/dist/commands/testing/live/view.js.map +1 -0
- package/dist/commands/testing/logs.d.ts +6 -0
- package/dist/commands/testing/logs.js +8 -1
- package/dist/commands/testing/logs.js.map +1 -1
- package/dist/commands/testing/project/create.d.ts +3 -0
- package/dist/commands/testing/project/create.js +17 -1
- package/dist/commands/testing/project/create.js.map +1 -1
- package/dist/commands/testing/project/delete.d.ts +7 -2
- package/dist/commands/testing/project/delete.js +15 -3
- package/dist/commands/testing/project/delete.js.map +1 -1
- package/dist/commands/testing/project/get.js +1 -1
- package/dist/commands/testing/project/get.js.map +1 -1
- package/dist/commands/testing/project/permission/get.d.ts +13 -0
- package/dist/commands/testing/project/permission/get.js +21 -0
- package/dist/commands/testing/project/permission/get.js.map +1 -0
- package/dist/commands/testing/project/permission/list.d.ts +13 -0
- package/dist/commands/testing/project/permission/list.js +21 -0
- package/dist/commands/testing/project/permission/list.js.map +1 -0
- package/dist/commands/testing/project/permission/set.d.ts +19 -0
- package/dist/commands/testing/project/permission/set.js +33 -0
- package/dist/commands/testing/project/permission/set.js.map +1 -0
- package/dist/commands/testing/project/permission/update.d.ts +19 -0
- package/dist/commands/testing/project/permission/update.js +33 -0
- package/dist/commands/testing/project/permission/update.js.map +1 -0
- package/dist/commands/testing/project/update.d.ts +14 -0
- package/dist/commands/testing/project/update.js +28 -0
- package/dist/commands/testing/project/update.js.map +1 -0
- package/dist/commands/testing/robot/create.d.ts +4 -1
- package/dist/commands/testing/robot/create.js +20 -5
- package/dist/commands/testing/robot/create.js.map +1 -1
- package/dist/commands/testing/robot/delete.d.ts +6 -1
- package/dist/commands/testing/robot/delete.js +13 -3
- package/dist/commands/testing/robot/delete.js.map +1 -1
- package/dist/commands/testing/robot/update.d.ts +19 -0
- package/dist/commands/testing/robot/update.js +49 -0
- package/dist/commands/testing/robot/update.js.map +1 -0
- package/dist/commands/testing/run.d.ts +27 -4
- package/dist/commands/testing/run.js +4 -11
- package/dist/commands/testing/run.js.map +1 -1
- package/dist/commands/testing/test/run.d.ts +27 -11
- package/dist/commands/testing/test/run.js +5 -37
- package/dist/commands/testing/test/run.js.map +1 -1
- package/dist/commands/testing/update.d.ts +6 -0
- package/dist/commands/testing/update.js +10 -0
- package/dist/commands/testing/update.js.map +1 -1
- package/dist/commands/testing/user-parameter/create.d.ts +19 -0
- package/dist/commands/testing/user-parameter/create.js +45 -0
- package/dist/commands/testing/user-parameter/create.js.map +1 -0
- package/dist/commands/testing/user-parameter/delete.d.ts +21 -0
- package/dist/commands/testing/user-parameter/delete.js +33 -0
- package/dist/commands/testing/user-parameter/delete.js.map +1 -0
- package/dist/commands/testing/user-parameter/get.d.ts +13 -0
- package/dist/commands/testing/user-parameter/get.js +21 -0
- package/dist/commands/testing/user-parameter/get.js.map +1 -0
- package/dist/commands/testing/user-parameter/list.d.ts +12 -0
- package/dist/commands/testing/user-parameter/list.js +23 -0
- package/dist/commands/testing/user-parameter/list.js.map +1 -0
- package/dist/commands/testing/user-parameter/update.d.ts +23 -0
- package/dist/commands/testing/user-parameter/update.js +45 -0
- package/dist/commands/testing/user-parameter/update.js.map +1 -0
- package/dist/commands/testing/variable/create.d.ts +6 -2
- package/dist/commands/testing/variable/create.js +14 -5
- package/dist/commands/testing/variable/create.js.map +1 -1
- package/dist/commands/testing/variable/delete.d.ts +6 -1
- package/dist/commands/testing/variable/delete.js +13 -3
- package/dist/commands/testing/variable/delete.js.map +1 -1
- package/dist/commands/testing/variable/get.d.ts +13 -0
- package/dist/commands/testing/variable/get.js +25 -0
- package/dist/commands/testing/variable/get.js.map +1 -0
- package/dist/commands/testing/variable/list.d.ts +4 -0
- package/dist/commands/testing/variable/list.js +15 -3
- package/dist/commands/testing/variable/list.js.map +1 -1
- package/dist/commands/testing/variable/update.d.ts +6 -2
- package/dist/commands/testing/variable/update.js +11 -5
- package/dist/commands/testing/variable/update.js.map +1 -1
- package/dist/lib/credential-store.d.ts +3 -0
- package/dist/lib/credential-store.js +16 -0
- package/dist/lib/credential-store.js.map +1 -1
- package/dist/lib/mcp/command-id.js +3 -0
- package/dist/lib/mcp/command-id.js.map +1 -1
- package/dist/lib/mcp/testing-live-tools.d.ts +126 -0
- package/dist/lib/mcp/testing-live-tools.js +215 -0
- package/dist/lib/mcp/testing-live-tools.js.map +1 -0
- package/dist/lib/mcp/testing-tools.d.ts +455 -9
- package/dist/lib/mcp/testing-tools.js +677 -37
- package/dist/lib/mcp/testing-tools.js.map +1 -1
- package/dist/lib/mcp/tools.d.ts +551 -14
- package/dist/lib/testing-config.d.ts +3 -0
- package/dist/lib/testing-config.js +9 -0
- package/dist/lib/testing-config.js.map +1 -1
- package/dist/lib/testing-confirmation.d.ts +14 -0
- package/dist/lib/testing-confirmation.js +17 -0
- package/dist/lib/testing-confirmation.js.map +1 -0
- package/dist/lib/testing-data-command.d.ts +21 -0
- package/dist/lib/testing-data-command.js +49 -0
- package/dist/lib/testing-data-command.js.map +1 -0
- package/dist/lib/testing-data-core.d.ts +22 -0
- package/dist/lib/testing-data-core.js +208 -0
- package/dist/lib/testing-data-core.js.map +1 -0
- package/dist/lib/testing-data-export.d.ts +1 -0
- package/dist/lib/testing-data-export.js +2 -0
- package/dist/lib/testing-data-export.js.map +1 -0
- package/dist/lib/testing-data-import.d.ts +1 -0
- package/dist/lib/testing-data-import.js +2 -0
- package/dist/lib/testing-data-import.js.map +1 -0
- package/dist/lib/testing-data-io.d.ts +21 -0
- package/dist/lib/testing-data-io.js +236 -0
- package/dist/lib/testing-data-io.js.map +1 -0
- package/dist/lib/testing-data-operations.d.ts +36 -0
- package/dist/lib/testing-data-operations.js +91 -0
- package/dist/lib/testing-data-operations.js.map +1 -0
- package/dist/lib/testing-data-types.d.ts +41 -0
- package/dist/lib/testing-data-types.js +3 -0
- package/dist/lib/testing-data-types.js.map +1 -0
- package/dist/lib/testing-flags.d.ts +62 -1
- package/dist/lib/testing-flags.js +177 -1
- package/dist/lib/testing-flags.js.map +1 -1
- package/dist/lib/testing-gateway.d.ts +17 -3
- package/dist/lib/testing-gateway.js +175 -45
- package/dist/lib/testing-gateway.js.map +1 -1
- package/dist/lib/testing-live-command.d.ts +24 -0
- package/dist/lib/testing-live-command.js +56 -0
- package/dist/lib/testing-live-command.js.map +1 -0
- package/dist/lib/testing-live-endpoints.d.ts +14 -0
- package/dist/lib/testing-live-endpoints.js +34 -0
- package/dist/lib/testing-live-endpoints.js.map +1 -0
- package/dist/lib/testing-live-history.d.ts +11 -0
- package/dist/lib/testing-live-history.js +96 -0
- package/dist/lib/testing-live-history.js.map +1 -0
- package/dist/lib/testing-live-operations.d.ts +118 -0
- package/dist/lib/testing-live-operations.js +676 -0
- package/dist/lib/testing-live-operations.js.map +1 -0
- package/dist/lib/testing-live-recorder.d.ts +14 -0
- package/dist/lib/testing-live-recorder.js +122 -0
- package/dist/lib/testing-live-recorder.js.map +1 -0
- package/dist/lib/testing-live-robot.d.ts +16 -0
- package/dist/lib/testing-live-robot.js +158 -0
- package/dist/lib/testing-live-robot.js.map +1 -0
- package/dist/lib/testing-live-state.d.ts +4 -0
- package/dist/lib/testing-live-state.js +90 -0
- package/dist/lib/testing-live-state.js.map +1 -0
- package/dist/lib/testing-live-types.d.ts +92 -0
- package/dist/lib/testing-live-types.js +2 -0
- package/dist/lib/testing-live-types.js.map +1 -0
- package/dist/lib/testing-operations.d.ts +157 -5
- package/dist/lib/testing-operations.js +829 -66
- package/dist/lib/testing-operations.js.map +1 -1
- package/dist/lib/testing-run-command.d.ts +25 -0
- package/dist/lib/testing-run-command.js +94 -0
- package/dist/lib/testing-run-command.js.map +1 -0
- package/dist/lib/testing-sanitization.d.ts +11 -0
- package/dist/lib/testing-sanitization.js +135 -0
- package/dist/lib/testing-sanitization.js.map +1 -0
- package/dist/lib/testing-schedule.d.ts +23 -0
- package/dist/lib/testing-schedule.js +48 -0
- package/dist/lib/testing-schedule.js.map +1 -0
- package/dist/lib/testing-secret-input.d.ts +12 -0
- package/dist/lib/testing-secret-input.js +56 -0
- package/dist/lib/testing-secret-input.js.map +1 -0
- package/dist/lib/testing-session.d.ts +8 -0
- package/dist/lib/testing-session.js +82 -0
- package/dist/lib/testing-session.js.map +1 -0
- package/dist/lib/testing-types.d.ts +143 -13
- package/dist/lib/testing-types.js +110 -5
- package/dist/lib/testing-types.js.map +1 -1
- package/dist/lib/testing-watch.d.ts +4 -0
- package/dist/lib/testing-watch.js +45 -0
- package/dist/lib/testing-watch.js.map +1 -0
- package/oclif.manifest.json +8354 -3760
- package/package.json +12 -1
- package/templates/skills/agentia-testing/SKILL.md +9 -6
- package/templates/skills/agentia-testing/references/tests.md +70 -36
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
/* eslint-disable no-await-in-loop, unicorn/text-encoding-identifier-case -- CRT polling waits between API checks and file operation payloads require the literal utf-8 enum. */
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import * as http from 'node:http';
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
4
3
|
import * as https from 'node:https';
|
|
5
4
|
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { inflateRawSync } from 'node:zlib';
|
|
6
6
|
import { createTransactionId, CRT_GATEWAY_EVENT_SERVICE, gatewayRefererHeaders, getGatewayTransactionId, scheduleGatewayEventLog, } from './cicd-gateway.js';
|
|
7
|
-
import { resolveTestingCredentials } from './testing-config.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { paceBaseUrl, resolveTestingCredentials } from './testing-config.js';
|
|
8
|
+
import { formatTestingDatasetKey, parseTestingDataTableRunSpecs, } from './testing-data-core.js';
|
|
9
|
+
import { createTestingDataSession, getTestingDataTable, } from './testing-data-operations.js';
|
|
10
|
+
import { TESTING_DEFAULT_TIMEOUT_MS, TESTING_MAX_RESPONSE_BYTES, TestingGateway, } from './testing-gateway.js';
|
|
11
|
+
import { sanitizeTestingOutput, sanitizeTestingText, sanitizeTestingUrl } from './testing-sanitization.js';
|
|
12
|
+
import { appendTestingQuery, createDefaultRobotSnapshotBase64, listSnapshotZipEntries, normalizeDownloadedFiles, normalizeTestingArray, normalizeTestingEnvelopeRecord, normalizeTestingId, normalizeTestingRecord, redactTestingVariables, testingBuildAbortEndpoint, testingBuildEndpoint, testingBuildLogsEndpoint, testingJobBuildsEndpoint, testingJobEndpoint, testingJobFilesDownloadEndpoint, testingJobFilesEndpoint, testingJobFilesUploadEndpoint, testingJobRefsEndpoint, testingJobsEndpoint, testingMetricsSearchEndpoint, testingPermissionEndpoint, testingPermissionsEndpoint, testingProjectBuildsEndpoint, testingProjectDeleteEndpoint, testingProjectEndpoint, testingProjectPermissionsEndpoint, testingProjectsEndpoint, testingRobotEndpoint, testingRobotJobsEndpoint, testingRobotsEndpoint, testingUserParameterEndpoint, testingUserParametersEndpoint, testingVariableEndpoint, testingVariablesEndpoint, } from './testing-types.js';
|
|
10
13
|
export const DEFAULT_TESTING_AUTHOR = {
|
|
11
14
|
email: 'agentia@copado.com',
|
|
12
15
|
name: 'Agentia CLI',
|
|
13
16
|
};
|
|
14
|
-
const TESTING_TERMINAL_BUILD_STATUSES = new Set([
|
|
17
|
+
const TESTING_TERMINAL_BUILD_STATUSES = new Set([
|
|
18
|
+
'aborted',
|
|
19
|
+
'error',
|
|
20
|
+
'failed',
|
|
21
|
+
'finished',
|
|
22
|
+
'invalidxml',
|
|
23
|
+
'passed',
|
|
24
|
+
'succeeded',
|
|
25
|
+
'timeout',
|
|
26
|
+
]);
|
|
15
27
|
const TESTING_WAIT_INITIAL_INTERVAL_MS = 2000;
|
|
16
28
|
const TESTING_WAIT_MAX_INTERVAL_MS = 10_000;
|
|
17
29
|
const TESTING_WAIT_TIMEOUT_MS = 30 * 60_000;
|
|
18
|
-
|
|
19
|
-
|
|
30
|
+
const TESTING_SECRET_VALUE_MAX_BYTES = 1024 * 1024;
|
|
31
|
+
const TESTING_UPLOAD_VALUE_MAX_BYTES = 10 * 1024 * 1024;
|
|
32
|
+
async function defaultTestingSnapshotFetch(url, headers, timeoutMs, maxResponseBytes) {
|
|
20
33
|
return new Promise((resolvePromise, reject) => {
|
|
21
|
-
const req =
|
|
34
|
+
const req = https.request({
|
|
22
35
|
headers,
|
|
23
36
|
hostname: url.hostname,
|
|
24
37
|
method: 'GET',
|
|
@@ -27,11 +40,19 @@ async function defaultTestingSnapshotFetch(url, headers, timeoutMs) {
|
|
|
27
40
|
timeout: timeoutMs,
|
|
28
41
|
}, (response) => {
|
|
29
42
|
const chunks = [];
|
|
30
|
-
|
|
43
|
+
let responseBytes = 0;
|
|
44
|
+
response.on('data', (chunk) => {
|
|
45
|
+
responseBytes += chunk.length;
|
|
46
|
+
if (responseBytes > maxResponseBytes) {
|
|
47
|
+
req.destroy(new Error(`CRT snapshot response exceeded the ${maxResponseBytes} byte safety limit.`));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
chunks.push(chunk);
|
|
51
|
+
});
|
|
31
52
|
response.on('end', () => {
|
|
32
53
|
const status = response.statusCode ?? 0;
|
|
33
54
|
if (status >= 400) {
|
|
34
|
-
reject(new Error(`CRT snapshot request failed (${status}). Request: GET ${url
|
|
55
|
+
reject(new Error(`CRT snapshot request failed (${status}). Request: GET ${sanitizeTestingUrl(url)}`));
|
|
35
56
|
return;
|
|
36
57
|
}
|
|
37
58
|
resolvePromise(Buffer.concat(chunks));
|
|
@@ -49,23 +70,58 @@ export async function listTestingProjects(options = {}) {
|
|
|
49
70
|
const credentials = resolveOptions(options);
|
|
50
71
|
const response = await TestingGateway.call({ endpoint: testingProjectsEndpoint(credentials.orgId), method: 'GET', service: 'platform' }, options);
|
|
51
72
|
const projects = normalizeTestingArray(response);
|
|
52
|
-
return filterByName(projects, options.name, false);
|
|
73
|
+
return sanitizeTestingOutput(filterByName(projects, options.name, false));
|
|
53
74
|
}
|
|
54
75
|
export async function createTestingProject(options) {
|
|
76
|
+
requireNonBlank(options.name, 'CRT project name');
|
|
55
77
|
const credentials = resolveOptions(options);
|
|
56
|
-
const body = {
|
|
57
|
-
|
|
78
|
+
const body = omitUndefined({
|
|
79
|
+
dashboardId: options.dashboardId,
|
|
80
|
+
description: options.description,
|
|
58
81
|
name: options.name,
|
|
59
|
-
|
|
60
|
-
|
|
82
|
+
projectExecutor: options.projectExecutor,
|
|
83
|
+
projectType: options.projectType ?? 'ta',
|
|
84
|
+
});
|
|
61
85
|
const response = await TestingGateway.call({ body, endpoint: testingProjectsEndpoint(credentials.orgId), method: 'POST', service: 'platform' }, options);
|
|
62
|
-
return normalizeTestingRecord(response);
|
|
86
|
+
return sanitizeTestingOutput(normalizeTestingRecord(response));
|
|
63
87
|
}
|
|
64
88
|
export async function getTestingProject(options) {
|
|
65
|
-
|
|
89
|
+
const response = await TestingGateway.call({ endpoint: testingProjectEndpoint(options.projectId), method: 'GET', service: 'platform' }, options);
|
|
90
|
+
return sanitizeTestingOutput(normalizeTestingRecord(response));
|
|
91
|
+
}
|
|
92
|
+
export async function updateTestingProject(options) {
|
|
93
|
+
const body = omitUndefined({ description: options.description, name: options.name });
|
|
94
|
+
requireBodyFields(body, 'Provide at least one project field to update.');
|
|
95
|
+
if (options.name !== undefined)
|
|
96
|
+
requireNonBlank(options.name, 'CRT project name');
|
|
97
|
+
const response = await TestingGateway.call({ body, endpoint: testingProjectEndpoint(options.projectId), method: 'PATCH', service: 'platform' }, options);
|
|
98
|
+
return sanitizeTestingOutput(normalizeTestingRecord(response));
|
|
99
|
+
}
|
|
100
|
+
export async function listTestingProjectPermissions(options) {
|
|
101
|
+
const response = await TestingGateway.call({ endpoint: testingProjectPermissionsEndpoint(options.projectId), method: 'GET', service: 'platform' }, options);
|
|
102
|
+
return sanitizeTestingOutput(normalizeTestingArray(response));
|
|
103
|
+
}
|
|
104
|
+
export async function getTestingProjectPermission(options) {
|
|
105
|
+
const response = await TestingGateway.call({ endpoint: testingPermissionsEndpoint(options.projectId), method: 'GET', service: 'platform' }, options);
|
|
106
|
+
return sanitizeTestingOutput(normalizeTestingRecord(response));
|
|
107
|
+
}
|
|
108
|
+
export async function setTestingProjectPermission(options) {
|
|
109
|
+
validateTestingPermissionRequest(options);
|
|
110
|
+
const body = [omitUndefined({
|
|
111
|
+
edit: options.edit,
|
|
112
|
+
groupId: options.groupId,
|
|
113
|
+
userId: options.userId,
|
|
114
|
+
view: options.view,
|
|
115
|
+
})];
|
|
116
|
+
const response = await TestingGateway.call({ body, endpoint: testingPermissionsEndpoint(options.projectId), method: 'POST', service: 'platform' }, options);
|
|
117
|
+
return sanitizeTestingOutput(normalizeTestingArray(response));
|
|
118
|
+
}
|
|
119
|
+
export async function deleteTestingProjectPermission(options) {
|
|
120
|
+
await TestingGateway.call({ endpoint: testingPermissionEndpoint(options.permissionId), method: 'DELETE', service: 'platform' }, options);
|
|
121
|
+
return { deleted: true, permissionId: options.permissionId };
|
|
66
122
|
}
|
|
67
123
|
export async function deleteTestingProject(options) {
|
|
68
|
-
await TestingGateway.call({ endpoint: testingProjectDeleteEndpoint(options.projectId), method: 'DELETE', service: '
|
|
124
|
+
await TestingGateway.call({ endpoint: testingProjectDeleteEndpoint(options.projectId), method: 'DELETE', service: 'platform' }, options);
|
|
69
125
|
return { deleted: true, projectId: options.projectId };
|
|
70
126
|
}
|
|
71
127
|
export async function listTestingRobots(options) {
|
|
@@ -73,19 +129,47 @@ export async function listTestingRobots(options) {
|
|
|
73
129
|
return normalizeTestingArray(response);
|
|
74
130
|
}
|
|
75
131
|
export async function createTestingRobot(options) {
|
|
76
|
-
|
|
132
|
+
requireNonBlank(options.name, 'CRT robot name');
|
|
133
|
+
const body = omitUndefined({
|
|
77
134
|
description: options.description ?? '',
|
|
78
|
-
fwVersion: options.fwVersion
|
|
135
|
+
fwVersion: options.fwVersion,
|
|
136
|
+
label: options.label,
|
|
79
137
|
name: options.name,
|
|
80
|
-
os: options.os
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
138
|
+
os: options.os,
|
|
139
|
+
reporting: options.reporting,
|
|
140
|
+
robotType: options.robotType,
|
|
141
|
+
runEnvironment: options.kind === 'QConnect' ? 'paceconnect' : (options.kind ?? 'cloud'),
|
|
142
|
+
});
|
|
84
143
|
const response = await TestingGateway.call({ body, endpoint: testingRobotsEndpoint(options.projectId), method: 'POST', service: 'pace' }, options);
|
|
85
|
-
return normalizeTestingRecord(response);
|
|
144
|
+
return sanitizeTestingOutput(normalizeTestingRecord(response));
|
|
86
145
|
}
|
|
87
146
|
export async function getTestingRobot(options) {
|
|
88
|
-
|
|
147
|
+
const response = await TestingGateway.call({ endpoint: testingRobotEndpoint(options.projectId, options.robotId), method: 'GET', service: 'pace' }, options);
|
|
148
|
+
return sanitizeTestingOutput(normalizeTestingRecord(response));
|
|
149
|
+
}
|
|
150
|
+
export async function updateTestingRobot(options) {
|
|
151
|
+
const updates = omitUndefined({
|
|
152
|
+
description: options.description,
|
|
153
|
+
fwVersion: options.fwVersion,
|
|
154
|
+
label: options.label,
|
|
155
|
+
name: options.name,
|
|
156
|
+
reporting: options.reporting,
|
|
157
|
+
robotType: options.robotType,
|
|
158
|
+
});
|
|
159
|
+
requireBodyFields(updates, 'Provide at least one robot field to update.');
|
|
160
|
+
if (options.name !== undefined)
|
|
161
|
+
requireNonBlank(options.name, 'CRT robot name');
|
|
162
|
+
const current = await getTestingRobot(options);
|
|
163
|
+
const body = omitUndefined({
|
|
164
|
+
description: options.description ?? stringField(current.description),
|
|
165
|
+
fwVersion: options.fwVersion ?? stringField(current.fwVersion),
|
|
166
|
+
label: options.label ?? stringField(current.label),
|
|
167
|
+
name: options.name ?? stringField(current.name),
|
|
168
|
+
reporting: options.reporting ?? stringField(current.reporting),
|
|
169
|
+
robotType: options.robotType ?? stringField(current.robotType),
|
|
170
|
+
});
|
|
171
|
+
const response = await TestingGateway.call({ body, endpoint: testingRobotEndpoint(options.projectId, options.robotId), method: 'PUT', service: 'pace' }, options);
|
|
172
|
+
return sanitizeTestingOutput(normalizeTestingRecord(response));
|
|
89
173
|
}
|
|
90
174
|
export async function deleteTestingRobot(options) {
|
|
91
175
|
await TestingGateway.call({ endpoint: testingRobotEndpoint(options.projectId, options.robotId), method: 'DELETE', service: 'pace' }, options);
|
|
@@ -137,12 +221,44 @@ export function buildTestingGitStorageUpdate(options) {
|
|
|
137
221
|
export async function createTestingJob(options) {
|
|
138
222
|
const storage = buildTestingCreateJobStorage(options);
|
|
139
223
|
const operations = storage.kind === 'git' || options.fileInput === undefined ? [] : buildFileAddOperations(options.fileInput);
|
|
140
|
-
const body = {
|
|
224
|
+
const body = omitUndefined({
|
|
225
|
+
analytics: options.analytics,
|
|
141
226
|
description: options.description ?? '',
|
|
142
227
|
domainCredentialId: storage.kind === 'git' ? options.gitDomainCredentialId : undefined,
|
|
143
228
|
name: options.name,
|
|
229
|
+
parallelExecution: options.parallelExecution,
|
|
230
|
+
screen: options.screen,
|
|
231
|
+
showVideoParams: options.showVideoParams,
|
|
144
232
|
storage,
|
|
145
|
-
|
|
233
|
+
suiteType: options.suiteType,
|
|
234
|
+
timeout: options.timeout,
|
|
235
|
+
validation: options.validation,
|
|
236
|
+
});
|
|
237
|
+
if (options.dryRun === true) {
|
|
238
|
+
return {
|
|
239
|
+
dryRun: true,
|
|
240
|
+
filesUploaded: operations.length,
|
|
241
|
+
job: sanitizeTestingOutput({
|
|
242
|
+
...body,
|
|
243
|
+
storage: storage.kind === 'git'
|
|
244
|
+
? omitUndefined({
|
|
245
|
+
branchOrTag: storage.branchOrTag,
|
|
246
|
+
gitUrl: storage.gitUrl,
|
|
247
|
+
gitUsername: storage.gitUsername,
|
|
248
|
+
kind: storage.kind,
|
|
249
|
+
useCallerCredentials: storage.useCallerCredentials,
|
|
250
|
+
})
|
|
251
|
+
: { kind: storage.kind },
|
|
252
|
+
upload: {
|
|
253
|
+
author: options.uploadAuthor ?? DEFAULT_TESTING_AUTHOR,
|
|
254
|
+
commitMessage: options.uploadCommitMessage,
|
|
255
|
+
operations: sanitizeTestingFilePlan(operations),
|
|
256
|
+
parentCommitHash: options.uploadParentCommitHash,
|
|
257
|
+
},
|
|
258
|
+
}),
|
|
259
|
+
projectId: options.projectId,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
146
262
|
const response = await TestingGateway.call({ body, endpoint: testingRobotJobsEndpoint(options.projectId, options.robotId), method: 'POST', service: 'pace' }, options);
|
|
147
263
|
const job = normalizeTestingRecord(response);
|
|
148
264
|
const jobId = normalizeTestingId(response);
|
|
@@ -155,8 +271,11 @@ export async function createTestingJob(options) {
|
|
|
155
271
|
if (jobId !== undefined && operations.length > 0) {
|
|
156
272
|
const upload = await uploadTestingJobFiles({
|
|
157
273
|
...options,
|
|
274
|
+
author: options.uploadAuthor,
|
|
275
|
+
commitMessage: options.uploadCommitMessage,
|
|
158
276
|
jobId: numericId(jobId, 'job'),
|
|
159
277
|
operations: [{ op: 'remove', path: 'tests.robot' }, ...operations],
|
|
278
|
+
parentCommitHash: options.uploadParentCommitHash,
|
|
160
279
|
projectId: options.projectId,
|
|
161
280
|
});
|
|
162
281
|
result.filesUploaded = upload.operations.length - 1;
|
|
@@ -194,17 +313,56 @@ export async function listTestingJobFiles(options) {
|
|
|
194
313
|
return listTestingSnapshotFiles(snapshotUrl, options);
|
|
195
314
|
}
|
|
196
315
|
export async function uploadTestingJobFiles(options) {
|
|
197
|
-
|
|
316
|
+
if (options.branch !== undefined)
|
|
317
|
+
validateTestingGitRef(options.branch, 'branch');
|
|
318
|
+
const operations = options.operations.map((operation) => ({
|
|
319
|
+
...operation,
|
|
320
|
+
path: normalizeRemotePath(operation.path),
|
|
321
|
+
}));
|
|
322
|
+
for (const operation of operations) {
|
|
323
|
+
if (operation.op === 'remove' && operation.value !== undefined) {
|
|
324
|
+
throw new Error('CRT remove file operations must not include content.');
|
|
325
|
+
}
|
|
326
|
+
if (operation.op !== 'remove' && operation.value === undefined) {
|
|
327
|
+
throw new Error(`CRT ${operation.op} file operations require content.`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const uploadBytes = operations.reduce((total, operation) => total + Buffer.byteLength(operation.value ?? ''), 0);
|
|
331
|
+
if (uploadBytes > TESTING_UPLOAD_VALUE_MAX_BYTES) {
|
|
332
|
+
throw new Error(`CRT file upload exceeds the ${TESTING_UPLOAD_VALUE_MAX_BYTES} byte safety limit.`);
|
|
333
|
+
}
|
|
334
|
+
const body = omitUndefined({
|
|
198
335
|
author: options.author ?? DEFAULT_TESTING_AUTHOR,
|
|
199
|
-
|
|
200
|
-
|
|
336
|
+
commitMessage: options.commitMessage,
|
|
337
|
+
operations,
|
|
338
|
+
parentCommitHash: options.parentCommitHash,
|
|
339
|
+
});
|
|
340
|
+
if (options.dryRun === true) {
|
|
341
|
+
return {
|
|
342
|
+
dryRun: true,
|
|
343
|
+
operations: sanitizeTestingFilePlan(operations),
|
|
344
|
+
response: sanitizeTestingOutput({
|
|
345
|
+
author: body.author,
|
|
346
|
+
commitMessage: body.commitMessage,
|
|
347
|
+
parentCommitHash: body.parentCommitHash,
|
|
348
|
+
}),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
201
351
|
const response = await TestingGateway.call({
|
|
202
352
|
body,
|
|
203
353
|
endpoint: testingJobFilesUploadEndpoint(options.projectId, options.jobId, options.branch),
|
|
204
354
|
method: 'POST',
|
|
205
355
|
service: 'pace',
|
|
206
356
|
}, options);
|
|
207
|
-
return { operations:
|
|
357
|
+
return { operations: sanitizeTestingFilePlan(operations), response: sanitizeTestingOutput(normalizeTestingRecord(response)) };
|
|
358
|
+
}
|
|
359
|
+
export async function getTestingJobRefs(options) {
|
|
360
|
+
const response = await TestingGateway.call({
|
|
361
|
+
endpoint: testingJobRefsEndpoint(options.projectId, options.jobId, options.sharedCredentials),
|
|
362
|
+
method: 'GET',
|
|
363
|
+
service: 'pace',
|
|
364
|
+
}, options);
|
|
365
|
+
return sanitizeTestingOutput(normalizeTestingRecord(response));
|
|
208
366
|
}
|
|
209
367
|
export async function downloadTestingJobFiles(options) {
|
|
210
368
|
const response = await TestingGateway.call({
|
|
@@ -227,34 +385,154 @@ export async function downloadTestingJobFiles(options) {
|
|
|
227
385
|
}
|
|
228
386
|
export async function runTestingBuild(options) {
|
|
229
387
|
const body = buildTestingRunRequest(options);
|
|
388
|
+
const dataTableSelections = parseTestingDataTableRunSpecs(options.dataTables);
|
|
389
|
+
if (dataTableSelections.length > 0) {
|
|
390
|
+
const session = await createTestingDataSession(options);
|
|
391
|
+
const tables = await Promise.all(dataTableSelections.map((selection) => getTestingDataTable({
|
|
392
|
+
...options,
|
|
393
|
+
jobId: undefined,
|
|
394
|
+
projectId: options.projectId,
|
|
395
|
+
session,
|
|
396
|
+
tableId: selection.tableId,
|
|
397
|
+
})));
|
|
398
|
+
const payloads = dataTableSelections[0]?.datasets.map((_, runIndex) => {
|
|
399
|
+
const inputParameters = [...(body.inputParameters ?? [])];
|
|
400
|
+
for (const [selectionIndex, selection] of dataTableSelections.entries()) {
|
|
401
|
+
const table = tables[selectionIndex];
|
|
402
|
+
const datasetIndex = selection.datasets[runIndex];
|
|
403
|
+
const headers = table?.data?.[0];
|
|
404
|
+
const dataset = datasetIndex === undefined ? undefined : table?.data?.[datasetIndex];
|
|
405
|
+
if (table === undefined || headers === undefined || dataset === undefined) {
|
|
406
|
+
throw new Error(`Data table ${selection.tableId} does not contain selected dataset ${datasetIndex ?? '(unknown)'}.`);
|
|
407
|
+
}
|
|
408
|
+
for (const [column, header] of headers.entries()) {
|
|
409
|
+
inputParameters.push({
|
|
410
|
+
key: formatTestingDatasetKey(header, table.name ?? ''),
|
|
411
|
+
type: 'dataset',
|
|
412
|
+
value: dataset[column] ?? '',
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
...body,
|
|
418
|
+
inputParameters: inputParameters.length > 0 ? inputParameters : undefined,
|
|
419
|
+
jobId: options.jobId,
|
|
420
|
+
};
|
|
421
|
+
}) ?? [];
|
|
422
|
+
const build = await TestingGateway.call({ body: payloads, endpoint: testingProjectBuildsEndpoint(options.projectId), method: 'POST', service: 'pace' }, options);
|
|
423
|
+
return {
|
|
424
|
+
body: sanitizeTestingOutput(payloads),
|
|
425
|
+
build: sanitizeTestingOutput(build),
|
|
426
|
+
dataDriven: true,
|
|
427
|
+
jobId: options.jobId,
|
|
428
|
+
projectId: options.projectId,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
230
431
|
const build = await TestingGateway.call({ body, endpoint: testingJobBuildsEndpoint(options.projectId, options.jobId), method: 'POST', service: 'pace' }, options);
|
|
231
|
-
return {
|
|
432
|
+
return {
|
|
433
|
+
body: sanitizeTestingOutput(body),
|
|
434
|
+
build: sanitizeTestingOutput(build),
|
|
435
|
+
jobId: options.jobId,
|
|
436
|
+
projectId: options.projectId,
|
|
437
|
+
};
|
|
232
438
|
}
|
|
233
439
|
export async function listTestingBuilds(options) {
|
|
234
440
|
const endpoint = options.jobId === undefined
|
|
235
|
-
? testingProjectBuildsEndpoint(options.projectId, {
|
|
236
|
-
|
|
441
|
+
? testingProjectBuildsEndpoint(options.projectId, {
|
|
442
|
+
page: options.page,
|
|
443
|
+
pageSize: options.pageSize,
|
|
444
|
+
resultSize: options.resultSize,
|
|
445
|
+
status: options.status,
|
|
446
|
+
})
|
|
447
|
+
: testingJobBuildsEndpoint(options.projectId, options.jobId, {
|
|
448
|
+
page: options.page,
|
|
449
|
+
pageSize: options.pageSize,
|
|
450
|
+
resultSize: options.resultSize,
|
|
451
|
+
status: options.status,
|
|
452
|
+
});
|
|
237
453
|
const response = await TestingGateway.call({ endpoint, method: 'GET', service: 'pace' }, options);
|
|
238
454
|
return normalizeTestingArray(response);
|
|
239
455
|
}
|
|
456
|
+
export async function countTestingBuilds(options) {
|
|
457
|
+
const result = await searchTestingBuilds({ ...options, count: true });
|
|
458
|
+
return result.count;
|
|
459
|
+
}
|
|
460
|
+
export async function getTestingBuild(options) {
|
|
461
|
+
return sanitizeTestingOutput(await getTestingBuildForArtifacts(options));
|
|
462
|
+
}
|
|
463
|
+
export async function getTestingBuildForArtifacts(options) {
|
|
464
|
+
const response = await TestingGateway.call({
|
|
465
|
+
endpoint: testingBuildEndpoint(options.projectId, options.jobId, options.buildId, { full: options.full }),
|
|
466
|
+
method: 'GET',
|
|
467
|
+
service: 'pace',
|
|
468
|
+
}, options);
|
|
469
|
+
return normalizeTestingEnvelopeRecord(response);
|
|
470
|
+
}
|
|
471
|
+
export async function latestTestingBuilds(options) {
|
|
472
|
+
const response = await TestingGateway.call({
|
|
473
|
+
endpoint: testingProjectBuildsEndpoint(options.projectId, {
|
|
474
|
+
resultSize: options.resultSize,
|
|
475
|
+
status: options.status,
|
|
476
|
+
}),
|
|
477
|
+
method: 'GET',
|
|
478
|
+
service: 'pace',
|
|
479
|
+
}, options);
|
|
480
|
+
return normalizeTestingArray(response);
|
|
481
|
+
}
|
|
482
|
+
export async function searchTestingBuilds(options) {
|
|
483
|
+
const credentials = resolveTestingCredentials(options);
|
|
484
|
+
const body = buildTestingMsearchBody(credentials.orgId, options);
|
|
485
|
+
const response = await TestingGateway.call({
|
|
486
|
+
body,
|
|
487
|
+
endpoint: testingMetricsSearchEndpoint(credentials.orgId),
|
|
488
|
+
headers: { 'Content-Type': 'application/x-ndjson' },
|
|
489
|
+
method: 'POST',
|
|
490
|
+
service: 'metrics',
|
|
491
|
+
}, options);
|
|
492
|
+
return normalizeTestingSearchResponse(response);
|
|
493
|
+
}
|
|
240
494
|
export async function waitForTestingBuildResult(options) {
|
|
241
495
|
const timeoutMs = options.timeoutMs ?? TESTING_WAIT_TIMEOUT_MS;
|
|
242
496
|
const deadline = Date.now() + timeoutMs;
|
|
243
497
|
let attempts = 0;
|
|
244
498
|
let intervalMs = options.intervalMs ?? TESTING_WAIT_INITIAL_INTERVAL_MS;
|
|
499
|
+
let log = '';
|
|
245
500
|
while (Date.now() <= deadline) {
|
|
246
501
|
attempts++;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
502
|
+
let build = options.streamLogs === true && options.buildId !== undefined
|
|
503
|
+
? await getTestingBuild({
|
|
504
|
+
...options,
|
|
505
|
+
buildId: options.buildId,
|
|
506
|
+
jobId: options.jobId,
|
|
507
|
+
projectId: options.projectId,
|
|
508
|
+
})
|
|
509
|
+
: findMatchingBuild(await listTestingBuilds({
|
|
510
|
+
...options,
|
|
511
|
+
jobId: options.jobId,
|
|
512
|
+
projectId: options.projectId,
|
|
513
|
+
resultSize: 'compact',
|
|
514
|
+
}), options.buildId);
|
|
254
515
|
if (build !== undefined) {
|
|
255
516
|
options.onPoll?.(build);
|
|
517
|
+
if (options.streamLogs === true && options.buildId !== undefined && isTestingLogPollable(build)) {
|
|
518
|
+
const logResult = await getTestingBuildLogs({
|
|
519
|
+
...options,
|
|
520
|
+
buildId: numericId(options.buildId, 'build'),
|
|
521
|
+
jobId: options.jobId,
|
|
522
|
+
knownSecrets: options.sensitiveValues,
|
|
523
|
+
projectId: options.projectId,
|
|
524
|
+
});
|
|
525
|
+
if (logResult.log.length > log.length) {
|
|
526
|
+
const chunk = logResult.log.slice(log.length);
|
|
527
|
+
options.onLog?.(chunk);
|
|
528
|
+
log = logResult.log;
|
|
529
|
+
}
|
|
530
|
+
if (logResult.status !== undefined && logResult.status.toLowerCase() !== 'executing') {
|
|
531
|
+
build = { ...build, status: logResult.status };
|
|
532
|
+
}
|
|
533
|
+
}
|
|
256
534
|
if (isTerminalBuild(build)) {
|
|
257
|
-
return { attempts, build, buildId: options.buildId, jobId: options.jobId, projectId: options.projectId };
|
|
535
|
+
return { attempts, build, buildId: options.buildId, jobId: options.jobId, log, projectId: options.projectId };
|
|
258
536
|
}
|
|
259
537
|
}
|
|
260
538
|
await delay(Math.min(intervalMs, Math.max(deadline - Date.now(), 0)));
|
|
@@ -263,15 +541,22 @@ export async function waitForTestingBuildResult(options) {
|
|
|
263
541
|
throw new Error(`Timed out waiting for CRT build result for job ${options.jobId}.`);
|
|
264
542
|
}
|
|
265
543
|
export async function getTestingBuildLogs(options) {
|
|
266
|
-
const
|
|
267
|
-
endpoint: testingBuildLogsEndpoint(options.projectId, options.jobId, options.buildId),
|
|
544
|
+
const response = await TestingGateway.call({
|
|
545
|
+
endpoint: testingBuildLogsEndpoint(options.projectId, options.jobId, options.buildId, options),
|
|
268
546
|
method: 'GET',
|
|
269
547
|
responseType: 'text',
|
|
270
548
|
service: 'pace',
|
|
271
549
|
}, options);
|
|
550
|
+
const normalized = normalizeTestingLogResponse(response);
|
|
551
|
+
const sanitizedLog = sanitizeTestingText(normalized.log, [
|
|
552
|
+
resolveTestingCredentials(options).pak,
|
|
553
|
+
...(options.knownSecrets ?? []),
|
|
554
|
+
]);
|
|
555
|
+
const log = options.tailLines === undefined ? sanitizedLog : tailTestingLogLines(sanitizedLog, options.tailLines);
|
|
272
556
|
if (options.output !== undefined) {
|
|
273
|
-
|
|
274
|
-
|
|
557
|
+
const target = validateTestingOutputPath(options.output);
|
|
558
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
559
|
+
writeFileSync(target, log, 'utf8');
|
|
275
560
|
}
|
|
276
561
|
return {
|
|
277
562
|
buildId: options.buildId,
|
|
@@ -279,8 +564,34 @@ export async function getTestingBuildLogs(options) {
|
|
|
279
564
|
log,
|
|
280
565
|
output: options.output,
|
|
281
566
|
projectId: options.projectId,
|
|
567
|
+
status: normalized.status,
|
|
282
568
|
};
|
|
283
569
|
}
|
|
570
|
+
export async function saveTestingBuildArtifacts(options) {
|
|
571
|
+
if (options.saveArtifacts === undefined && options.xunit === undefined)
|
|
572
|
+
return {};
|
|
573
|
+
const location = testingOutputArchiveLocation(options.build);
|
|
574
|
+
if (location === undefined)
|
|
575
|
+
throw new Error('No output archive found for this CRT build.');
|
|
576
|
+
const archive = await TestingGateway.downloadArtifact(location, options);
|
|
577
|
+
const result = {};
|
|
578
|
+
if (options.saveArtifacts !== undefined) {
|
|
579
|
+
const target = validateTestingOutputPath(options.saveArtifacts);
|
|
580
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
581
|
+
writeFileSync(target, archive);
|
|
582
|
+
result.archive = options.saveArtifacts;
|
|
583
|
+
}
|
|
584
|
+
if (options.xunit !== undefined) {
|
|
585
|
+
const target = validateTestingOutputPath(options.xunit);
|
|
586
|
+
const xunit = extractStoredOrDeflatedZipEntry(archive, 'xunit_report.xml');
|
|
587
|
+
if (xunit === undefined)
|
|
588
|
+
throw new Error('The CRT artifact archive does not contain xunit_report.xml.');
|
|
589
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
590
|
+
writeFileSync(target, xunit);
|
|
591
|
+
result.xunit = options.xunit;
|
|
592
|
+
}
|
|
593
|
+
return result;
|
|
594
|
+
}
|
|
284
595
|
export async function abortTestingBuild(options) {
|
|
285
596
|
const response = await TestingGateway.call({
|
|
286
597
|
endpoint: testingBuildAbortEndpoint(options.projectId, options.jobId, options.buildId),
|
|
@@ -295,13 +606,31 @@ export async function abortTestingBuild(options) {
|
|
|
295
606
|
};
|
|
296
607
|
}
|
|
297
608
|
export async function listTestingVariables(options) {
|
|
298
|
-
const
|
|
609
|
+
const endpoint = appendTestingQuery(testingVariablesEndpoint(options.projectId), {
|
|
610
|
+
jobId: options.jobId,
|
|
611
|
+
robotId: options.robotId,
|
|
612
|
+
sensitive: options.sensitive,
|
|
613
|
+
type: options.type,
|
|
614
|
+
userSpecific: options.userSpecific,
|
|
615
|
+
});
|
|
616
|
+
const response = await TestingGateway.call({ endpoint, method: 'GET', service: 'pace' }, options);
|
|
299
617
|
const variables = normalizeTestingArray(response)
|
|
300
618
|
.filter((variable) => options.jobId === undefined || variable.jobId?.toString() === options.jobId.toString())
|
|
301
|
-
.filter((variable) => options.robotId === undefined || variable.robotId?.toString() === options.robotId.toString())
|
|
619
|
+
.filter((variable) => options.robotId === undefined || variable.robotId?.toString() === options.robotId.toString())
|
|
620
|
+
.filter((variable) => options.key === undefined || variable.key === options.key)
|
|
621
|
+
.filter((variable) => options.sensitive === undefined || variable.sensitive === options.sensitive)
|
|
622
|
+
.filter((variable) => options.type === undefined || variable.type === options.type)
|
|
623
|
+
.filter((variable) => options.userSpecific === undefined || variable.userSpecific === options.userSpecific);
|
|
302
624
|
return redactTestingVariables(variables);
|
|
303
625
|
}
|
|
626
|
+
export async function getTestingVariable(options) {
|
|
627
|
+
const response = await TestingGateway.call({ endpoint: testingVariableEndpoint(options.projectId, options.variableId), method: 'GET', service: 'pace' }, options);
|
|
628
|
+
return redactTestingVariables(normalizeTestingRecord(response));
|
|
629
|
+
}
|
|
304
630
|
export async function createTestingVariable(options) {
|
|
631
|
+
requireNonBlank(options.key, 'CRT variable key');
|
|
632
|
+
requireBoundedSecretValue(options.value, 'CRT variable value');
|
|
633
|
+
await ensureUniqueTestingVariable(options);
|
|
305
634
|
const body = [omitUndefined({
|
|
306
635
|
description: options.description,
|
|
307
636
|
jobId: options.jobId,
|
|
@@ -324,6 +653,19 @@ export async function updateTestingVariable(options) {
|
|
|
324
653
|
value: options.value,
|
|
325
654
|
});
|
|
326
655
|
requireBodyFields(body, 'Provide at least one variable field to update.');
|
|
656
|
+
requireBoundedSecretValue(options.value, 'CRT variable value');
|
|
657
|
+
if (options.key !== undefined)
|
|
658
|
+
requireNonBlank(options.key, 'CRT variable key');
|
|
659
|
+
if (options.key !== undefined) {
|
|
660
|
+
const current = await getTestingVariable(options);
|
|
661
|
+
await ensureUniqueTestingVariable({
|
|
662
|
+
jobId: numericField(current.jobId),
|
|
663
|
+
key: options.key,
|
|
664
|
+
projectId: options.projectId,
|
|
665
|
+
robotId: numericField(current.robotId),
|
|
666
|
+
userSpecific: typeof current.userSpecific === 'boolean' ? current.userSpecific : undefined,
|
|
667
|
+
}, options.variableId);
|
|
668
|
+
}
|
|
327
669
|
const response = await TestingGateway.call({ body, endpoint: testingVariableEndpoint(options.projectId, options.variableId), method: 'PUT', service: 'pace' }, options);
|
|
328
670
|
return redactTestingVariables(response);
|
|
329
671
|
}
|
|
@@ -331,24 +673,77 @@ export async function deleteTestingVariable(options) {
|
|
|
331
673
|
await TestingGateway.call({ endpoint: testingVariableEndpoint(options.projectId, options.variableId), method: 'DELETE', service: 'pace' }, options);
|
|
332
674
|
return { projectId: options.projectId, variableId: options.variableId };
|
|
333
675
|
}
|
|
676
|
+
export async function listTestingUserParameters(options = {}) {
|
|
677
|
+
const response = await TestingGateway.call({ endpoint: testingUserParametersEndpoint(), method: 'GET', service: 'pace' }, options);
|
|
678
|
+
const parameters = normalizeTestingArray(response)
|
|
679
|
+
.filter((parameter) => options.key === undefined || parameter.key === options.key)
|
|
680
|
+
.filter((parameter) => options.projectId === undefined || parameter.projectId?.toString() === options.projectId.toString())
|
|
681
|
+
.filter((parameter) => options.jobId === undefined || parameter.jobId?.toString() === options.jobId.toString());
|
|
682
|
+
return redactTestingVariables(parameters);
|
|
683
|
+
}
|
|
684
|
+
export async function getTestingUserParameter(options) {
|
|
685
|
+
const response = await TestingGateway.call({ endpoint: testingUserParameterEndpoint(options.parameterId), method: 'GET', service: 'pace' }, options);
|
|
686
|
+
return redactTestingVariables(normalizeTestingRecord(response));
|
|
687
|
+
}
|
|
688
|
+
export async function createTestingUserParameter(options) {
|
|
689
|
+
requireNonBlank(options.key, 'CRT user-parameter key');
|
|
690
|
+
requireBoundedSecretValue(options.value, 'CRT user-parameter value');
|
|
691
|
+
if (options.unique === true)
|
|
692
|
+
await ensureUniqueTestingUserParameter(options.key, options);
|
|
693
|
+
const body = [omitUndefined({
|
|
694
|
+
description: options.description,
|
|
695
|
+
key: options.key,
|
|
696
|
+
keyIsUnique: options.unique === true ? true : options.keyIsUnique,
|
|
697
|
+
sensitive: options.sensitive ?? false,
|
|
698
|
+
type: options.type ?? 'parameter',
|
|
699
|
+
value: options.value,
|
|
700
|
+
})];
|
|
701
|
+
const response = await TestingGateway.call({ body, endpoint: testingUserParametersEndpoint(), method: 'POST', service: 'pace' }, options);
|
|
702
|
+
return redactTestingVariables(normalizeTestingArray(response)[0] ?? {});
|
|
703
|
+
}
|
|
704
|
+
export async function updateTestingUserParameter(options) {
|
|
705
|
+
const body = omitUndefined({ description: options.description, key: options.key, value: options.value });
|
|
706
|
+
requireBodyFields(body, 'Provide at least one user-parameter field to update.');
|
|
707
|
+
requireBoundedSecretValue(options.value, 'CRT user-parameter value');
|
|
708
|
+
if (options.key !== undefined)
|
|
709
|
+
requireNonBlank(options.key, 'CRT user-parameter key');
|
|
710
|
+
if (options.unique === true && options.key !== undefined) {
|
|
711
|
+
await ensureUniqueTestingUserParameter(options.key, options, options.parameterId);
|
|
712
|
+
}
|
|
713
|
+
const response = await TestingGateway.call({ body, endpoint: testingUserParameterEndpoint(options.parameterId), method: 'PUT', service: 'pace' }, options);
|
|
714
|
+
return redactTestingVariables(normalizeTestingRecord(response));
|
|
715
|
+
}
|
|
716
|
+
export async function deleteTestingUserParameter(options) {
|
|
717
|
+
await TestingGateway.call({ endpoint: testingUserParameterEndpoint(options.parameterId), method: 'DELETE', service: 'pace' }, options);
|
|
718
|
+
return { deleted: true, parameterId: options.parameterId };
|
|
719
|
+
}
|
|
334
720
|
export function buildFileAddOperations(input) {
|
|
335
721
|
const operations = [];
|
|
336
722
|
for (const file of input.files ?? []) {
|
|
337
|
-
|
|
723
|
+
const remote = remotePathForLocalFile(file, input.basePath);
|
|
724
|
+
operations.push(fileOperationFromLocalFile(file, joinRemotePrefix(input.remotePrefix, remote), 'add', input.basePath));
|
|
338
725
|
}
|
|
339
726
|
for (const dir of input.dirs ?? []) {
|
|
340
727
|
const root = resolve(dir);
|
|
728
|
+
if (input.basePath !== undefined)
|
|
729
|
+
assertLocalPathWithinBase(root, input.basePath);
|
|
341
730
|
for (const file of collectFiles(root)) {
|
|
342
|
-
|
|
731
|
+
const remote = input.basePath === undefined
|
|
732
|
+
? relative(root, file).split(sep).join('/')
|
|
733
|
+
: remotePathForLocalFile(file, input.basePath);
|
|
734
|
+
operations.push(fileOperationFromLocalFile(file, joinRemotePrefix(input.remotePrefix, remote), 'add', input.basePath));
|
|
343
735
|
}
|
|
344
736
|
}
|
|
345
737
|
return operations;
|
|
346
738
|
}
|
|
347
739
|
export function buildFileOperationsFromSpecs(specs) {
|
|
348
740
|
return [
|
|
349
|
-
...(specs.add ?? []).map((spec) => fileOperationFromSpec(spec, 'add')),
|
|
350
|
-
...(specs.replace ?? []).map((spec) => fileOperationFromSpec(spec, 'replace')),
|
|
351
|
-
...(specs.remove ?? []).map((path) => ({
|
|
741
|
+
...(specs.add ?? []).map((spec) => fileOperationFromSpec(spec, 'add', specs)),
|
|
742
|
+
...(specs.replace ?? []).map((spec) => fileOperationFromSpec(spec, 'replace', specs)),
|
|
743
|
+
...(specs.remove ?? []).map((path) => ({
|
|
744
|
+
op: 'remove',
|
|
745
|
+
path: normalizeRemotePath(joinRemotePrefix(specs.remotePrefix, path)),
|
|
746
|
+
})),
|
|
352
747
|
];
|
|
353
748
|
}
|
|
354
749
|
export function buildTestingRunRequest(options) {
|
|
@@ -356,6 +751,21 @@ export function buildTestingRunRequest(options) {
|
|
|
356
751
|
...(options.parameters ?? []),
|
|
357
752
|
...(options.include ?? []).map((value) => ({ key: 'include', type: 'clp', value })),
|
|
358
753
|
...(options.exclude ?? []).map((value) => ({ key: 'exclude', type: 'clp', value })),
|
|
754
|
+
...testingFlagParameters('--test', options.tests),
|
|
755
|
+
...testingFlagParameters('--suite', options.suites),
|
|
756
|
+
...testingFlagParameters('-v', options.variables),
|
|
757
|
+
...testingFlagParameters('--skip', options.skip),
|
|
758
|
+
...testingFlagParameters('--skiponfailure', options.skipOnFailure),
|
|
759
|
+
...testingFlagParameters('--settag', options.setTags),
|
|
760
|
+
...testingFlagParameters('--metadata', options.metadata),
|
|
761
|
+
...testingBooleanParameter('--dryrun', options.dryRun),
|
|
762
|
+
...testingBooleanParameter('--exitonerror', options.exitOnError),
|
|
763
|
+
...testingBooleanParameter('--exitonfailure', options.exitOnFailure),
|
|
764
|
+
...testingBooleanParameter('--runemptysuite', options.runEmptySuite),
|
|
765
|
+
...testingScalarParameter('--name', options.name),
|
|
766
|
+
...testingScalarParameter('--doc', options.doc),
|
|
767
|
+
...testingScalarParameter('--consolewidth', options.consoleWidth),
|
|
768
|
+
...testingScalarParameter('--consolecolors', normalizeConsoleColors(options.consoleColors)),
|
|
359
769
|
];
|
|
360
770
|
return omitUndefined({
|
|
361
771
|
branch: options.branch,
|
|
@@ -363,6 +773,7 @@ export function buildTestingRunRequest(options) {
|
|
|
363
773
|
record: options.record,
|
|
364
774
|
rerunFailedFromLatest: options.rerunFailedFromLatest,
|
|
365
775
|
rerunFailedId: options.rerunFailedId,
|
|
776
|
+
runType: options.runType === 'regression' ? 'normal' : options.runType,
|
|
366
777
|
tag: options.tag,
|
|
367
778
|
});
|
|
368
779
|
}
|
|
@@ -372,18 +783,22 @@ export function setTestingSnapshotFetchForTests(snapshotFetchFn) {
|
|
|
372
783
|
export function resetTestingSnapshotFetchForTests() {
|
|
373
784
|
testingSnapshotFetchFn = defaultTestingSnapshotFetch;
|
|
374
785
|
}
|
|
375
|
-
function fileOperationFromSpec(spec, op) {
|
|
786
|
+
function fileOperationFromSpec(spec, op, options) {
|
|
376
787
|
const separator = spec.indexOf(':');
|
|
377
788
|
const localPath = separator === -1 ? spec : spec.slice(0, separator);
|
|
378
|
-
const remotePath = separator === -1
|
|
379
|
-
|
|
789
|
+
const remotePath = separator === -1
|
|
790
|
+
? remotePathForLocalFile(localPath, options.basePath)
|
|
791
|
+
: spec.slice(separator + 1);
|
|
792
|
+
return fileOperationFromLocalFile(localPath, joinRemotePrefix(options.remotePrefix, remotePath), op, options.basePath);
|
|
380
793
|
}
|
|
381
|
-
function fileOperationFromLocalFile(localPath, remotePath, op) {
|
|
794
|
+
function fileOperationFromLocalFile(localPath, remotePath, op, basePath) {
|
|
382
795
|
const path = normalizeRemotePath(remotePath);
|
|
383
796
|
if (!existsSync(localPath))
|
|
384
797
|
throw new Error(`Local CRT file does not exist: ${localPath}`);
|
|
385
798
|
if (!statSync(localPath).isFile())
|
|
386
799
|
throw new Error(`Local CRT path is not a file: ${localPath}`);
|
|
800
|
+
if (basePath !== undefined)
|
|
801
|
+
assertLocalPathWithinBase(localPath, basePath);
|
|
387
802
|
return {
|
|
388
803
|
encoding: 'utf-8',
|
|
389
804
|
op,
|
|
@@ -406,13 +821,19 @@ function collectFiles(root) {
|
|
|
406
821
|
}
|
|
407
822
|
return files;
|
|
408
823
|
}
|
|
409
|
-
function
|
|
410
|
-
const
|
|
411
|
-
if (
|
|
824
|
+
export function normalizeTestingRemotePath(path) {
|
|
825
|
+
const raw = path.trim().replaceAll('\\', '/');
|
|
826
|
+
if (raw.startsWith('/') || /^[a-z]:\//iu.test(raw)) {
|
|
827
|
+
throw new Error(`Invalid CRT file path: ${path}`);
|
|
828
|
+
}
|
|
829
|
+
const parts = raw.split('/');
|
|
830
|
+
if (raw.length === 0 || parts.some((part) => part.length === 0 || part === '.' || part === '..')) {
|
|
412
831
|
throw new Error(`Invalid CRT file path: ${path}`);
|
|
413
832
|
}
|
|
833
|
+
const normalized = parts.join('/');
|
|
414
834
|
return normalized;
|
|
415
835
|
}
|
|
836
|
+
const normalizeRemotePath = normalizeTestingRemotePath;
|
|
416
837
|
function writeDownloadedFiles(outputDir, files) {
|
|
417
838
|
const root = resolve(outputDir);
|
|
418
839
|
mkdirSync(root, { recursive: true });
|
|
@@ -432,6 +853,41 @@ function basenameForPath(path) {
|
|
|
432
853
|
const segments = path.replaceAll('\\', '/').split('/').filter(Boolean);
|
|
433
854
|
return segments.at(-1) ?? path;
|
|
434
855
|
}
|
|
856
|
+
function remotePathForLocalFile(localPath, basePath) {
|
|
857
|
+
if (basePath === undefined)
|
|
858
|
+
return basenameForPath(localPath);
|
|
859
|
+
if (!existsSync(localPath))
|
|
860
|
+
throw new Error(`Local CRT file does not exist: ${localPath}`);
|
|
861
|
+
assertLocalPathWithinBase(localPath, basePath);
|
|
862
|
+
const root = realpathSync(resolve(basePath));
|
|
863
|
+
const target = realpathSync(resolve(localPath));
|
|
864
|
+
assertPathWithinRoot(target, root, 'Local CRT file escapes --base-path');
|
|
865
|
+
return relative(root, target).split(sep).join('/');
|
|
866
|
+
}
|
|
867
|
+
function joinRemotePrefix(prefix, path) {
|
|
868
|
+
if (prefix === undefined || prefix.trim().length === 0)
|
|
869
|
+
return path;
|
|
870
|
+
return `${normalizeRemotePath(prefix)}/${path}`;
|
|
871
|
+
}
|
|
872
|
+
function assertLocalPathWithinBase(localPath, basePath) {
|
|
873
|
+
const root = resolve(basePath);
|
|
874
|
+
if (!existsSync(root) || !statSync(root).isDirectory()) {
|
|
875
|
+
throw new Error(`CRT upload base path must be an existing directory: ${basePath}`);
|
|
876
|
+
}
|
|
877
|
+
assertPathWithinRoot(realpathSync(resolve(localPath)), realpathSync(root), 'Local CRT file escapes --base-path');
|
|
878
|
+
}
|
|
879
|
+
function assertPathWithinRoot(target, root, message) {
|
|
880
|
+
if (target !== root && !target.startsWith(`${root}${sep}`))
|
|
881
|
+
throw new Error(`${message}: ${target}`);
|
|
882
|
+
}
|
|
883
|
+
function sanitizeTestingFilePlan(operations) {
|
|
884
|
+
return operations.map((operation) => ({
|
|
885
|
+
encoding: operation.encoding,
|
|
886
|
+
op: operation.op,
|
|
887
|
+
path: operation.path,
|
|
888
|
+
...(operation.value === undefined ? {} : { value: `[${Buffer.byteLength(operation.value)} bytes]` }),
|
|
889
|
+
}));
|
|
890
|
+
}
|
|
435
891
|
function filterByName(records, name, insensitive) {
|
|
436
892
|
if (name === undefined || name.trim().length === 0)
|
|
437
893
|
return records;
|
|
@@ -447,10 +903,28 @@ function findMatchingBuild(builds, buildId) {
|
|
|
447
903
|
const expected = buildId.toString();
|
|
448
904
|
return builds.find((build) => build.id?.toString() === expected || build.buildId?.toString() === expected);
|
|
449
905
|
}
|
|
450
|
-
function
|
|
451
|
-
const
|
|
906
|
+
export function testingBuildStatus(build) {
|
|
907
|
+
const direct = build.status;
|
|
908
|
+
const nestedResult = isPlainRecord(build.result) ? build.result.status : undefined;
|
|
909
|
+
const value = direct === 'finished' && typeof nestedResult === 'string'
|
|
910
|
+
? nestedResult
|
|
911
|
+
: direct ?? build.buildStatus ?? build.state ?? nestedResult;
|
|
912
|
+
return typeof value === 'string' ? value.toLowerCase() : undefined;
|
|
913
|
+
}
|
|
914
|
+
export function isTerminalTestingBuild(build) {
|
|
915
|
+
const status = testingBuildStatus(build);
|
|
452
916
|
return status !== undefined && TESTING_TERMINAL_BUILD_STATUSES.has(status);
|
|
453
917
|
}
|
|
918
|
+
export function isSuccessfulTestingBuild(build) {
|
|
919
|
+
return testingBuildStatus(build) === 'succeeded';
|
|
920
|
+
}
|
|
921
|
+
function isTerminalBuild(build) {
|
|
922
|
+
return isTerminalTestingBuild(build);
|
|
923
|
+
}
|
|
924
|
+
function isTestingLogPollable(build) {
|
|
925
|
+
const status = testingBuildStatus(build);
|
|
926
|
+
return status === 'executing' || status === 'finished' || status === 'succeeded';
|
|
927
|
+
}
|
|
454
928
|
async function delay(ms) {
|
|
455
929
|
if (ms <= 0)
|
|
456
930
|
return;
|
|
@@ -462,20 +936,41 @@ async function listTestingSnapshotFiles(snapshotUrl, options) {
|
|
|
462
936
|
const credentials = resolveTestingCredentials(options);
|
|
463
937
|
const transactionId = getGatewayTransactionId() ?? createTransactionId();
|
|
464
938
|
const url = new URL(snapshotUrl);
|
|
939
|
+
validateTestingSnapshotUrl(url, credentials.domain);
|
|
465
940
|
scheduleGatewayEventLog({
|
|
466
941
|
eventId: transactionId,
|
|
467
942
|
method: 'GET',
|
|
468
943
|
orgId: credentials.orgId.toString(),
|
|
469
944
|
service: CRT_GATEWAY_EVENT_SERVICE,
|
|
470
|
-
url: url
|
|
945
|
+
url: sanitizeTestingUrl(url),
|
|
471
946
|
});
|
|
472
947
|
const snapshot = await testingSnapshotFetchFn(url, {
|
|
473
948
|
Accept: 'application/octet-stream',
|
|
474
949
|
...gatewayRefererHeaders(),
|
|
475
950
|
'X-Authorization': credentials.pak,
|
|
476
|
-
}, options.timeoutMs ?? TESTING_DEFAULT_TIMEOUT_MS);
|
|
951
|
+
}, options.timeoutMs ?? TESTING_DEFAULT_TIMEOUT_MS, TESTING_MAX_RESPONSE_BYTES);
|
|
952
|
+
if (snapshot.length > TESTING_MAX_RESPONSE_BYTES) {
|
|
953
|
+
throw new Error(`CRT snapshot response exceeded the ${TESTING_MAX_RESPONSE_BYTES} byte safety limit.`);
|
|
954
|
+
}
|
|
477
955
|
return listSnapshotZipEntries(snapshot);
|
|
478
956
|
}
|
|
957
|
+
function validateTestingSnapshotUrl(url, domain) {
|
|
958
|
+
const expected = new URL(paceBaseUrl(domain));
|
|
959
|
+
let decodedPath;
|
|
960
|
+
try {
|
|
961
|
+
decodedPath = decodeURIComponent(url.pathname);
|
|
962
|
+
}
|
|
963
|
+
catch {
|
|
964
|
+
throw new Error('CRT snapshot URL has an invalid encoded path.');
|
|
965
|
+
}
|
|
966
|
+
if (url.protocol !== 'https:' ||
|
|
967
|
+
url.origin !== expected.origin ||
|
|
968
|
+
url.username.length > 0 ||
|
|
969
|
+
url.password.length > 0 ||
|
|
970
|
+
decodedPath.split('/').includes('..')) {
|
|
971
|
+
throw new Error('CRT snapshot URL must use the configured HTTPS Pace API host.');
|
|
972
|
+
}
|
|
973
|
+
}
|
|
479
974
|
function snapshotUrlForJob(job) {
|
|
480
975
|
const { storage } = job;
|
|
481
976
|
if (storage === null || typeof storage !== 'object' || Array.isArray(storage))
|
|
@@ -503,6 +998,61 @@ function requireBodyFields(body, message) {
|
|
|
503
998
|
if (Object.keys(body).length === 0)
|
|
504
999
|
throw new Error(message);
|
|
505
1000
|
}
|
|
1001
|
+
function requireNonBlank(value, label) {
|
|
1002
|
+
if (value.trim().length === 0 || value.includes('\0'))
|
|
1003
|
+
throw new Error(`${label} must not be blank.`);
|
|
1004
|
+
}
|
|
1005
|
+
function requireBoundedSecretValue(value, label) {
|
|
1006
|
+
if (value !== undefined && Buffer.byteLength(value) > TESTING_SECRET_VALUE_MAX_BYTES) {
|
|
1007
|
+
throw new Error(`${label} exceeds the ${TESTING_SECRET_VALUE_MAX_BYTES} byte safety limit.`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
function validateTestingPermissionRequest(request) {
|
|
1011
|
+
if ((request.userId === undefined) === (request.groupId === undefined)) {
|
|
1012
|
+
throw new Error('Provide exactly one of userId or groupId for a project permission.');
|
|
1013
|
+
}
|
|
1014
|
+
if (request.view !== true && request.edit !== true) {
|
|
1015
|
+
throw new Error('Set view or edit to true for a project permission.');
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
async function ensureUniqueTestingVariable(options, excludedId) {
|
|
1019
|
+
const matches = await listTestingVariables({
|
|
1020
|
+
jobId: options.jobId,
|
|
1021
|
+
key: options.key,
|
|
1022
|
+
projectId: options.projectId,
|
|
1023
|
+
robotId: options.robotId,
|
|
1024
|
+
userSpecific: options.userSpecific,
|
|
1025
|
+
});
|
|
1026
|
+
const duplicate = matches.find((variable) => variable.id?.toString() !== excludedId &&
|
|
1027
|
+
sameOptionalId(variable.jobId, options.jobId) &&
|
|
1028
|
+
sameOptionalId(variable.robotId, options.robotId) &&
|
|
1029
|
+
Boolean(variable.userSpecific) === Boolean(options.userSpecific));
|
|
1030
|
+
if (duplicate !== undefined) {
|
|
1031
|
+
throw new Error(`A CRT variable with key "${options.key}" already exists in this scope.`);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
function sameOptionalId(left, right) {
|
|
1035
|
+
if (left === undefined || left === null)
|
|
1036
|
+
return right === undefined;
|
|
1037
|
+
return right !== undefined && left.toString() === right.toString();
|
|
1038
|
+
}
|
|
1039
|
+
async function ensureUniqueTestingUserParameter(key, options, excludedId) {
|
|
1040
|
+
const matches = await listTestingUserParameters({ ...options, key });
|
|
1041
|
+
const duplicate = matches.find((parameter) => parameter.id?.toString() !== excludedId?.toString());
|
|
1042
|
+
if (duplicate !== undefined) {
|
|
1043
|
+
throw new Error(`A CRT user parameter with key "${key}" already exists.`);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function numericField(value) {
|
|
1047
|
+
if (typeof value === 'number' && Number.isSafeInteger(value))
|
|
1048
|
+
return value;
|
|
1049
|
+
if (typeof value === 'string' && /^\d+$/u.test(value))
|
|
1050
|
+
return Number.parseInt(value, 10);
|
|
1051
|
+
return undefined;
|
|
1052
|
+
}
|
|
1053
|
+
function stringField(value) {
|
|
1054
|
+
return typeof value === 'string' ? value : undefined;
|
|
1055
|
+
}
|
|
506
1056
|
function branchOrTagFromOptions(options) {
|
|
507
1057
|
if (options.gitBranch !== undefined)
|
|
508
1058
|
return { branch: options.gitBranch };
|
|
@@ -539,8 +1089,221 @@ function validateTestingGitStorageOptions(options, requirements) {
|
|
|
539
1089
|
if ((options.gitUsername === undefined) !== (options.gitPassword === undefined)) {
|
|
540
1090
|
throw new Error('Provide --git-username and --git-password together.');
|
|
541
1091
|
}
|
|
1092
|
+
requireBoundedSecretValue(options.gitPassword, 'CRT Git password');
|
|
1093
|
+
if (options.gitBranch !== undefined)
|
|
1094
|
+
validateTestingGitRef(options.gitBranch, 'branch');
|
|
1095
|
+
if (options.gitTag !== undefined)
|
|
1096
|
+
validateTestingGitRef(options.gitTag, 'tag');
|
|
1097
|
+
}
|
|
1098
|
+
function validateTestingGitRef(value, label) {
|
|
1099
|
+
if (value.length > 255 ||
|
|
1100
|
+
value.trim() !== value ||
|
|
1101
|
+
value.length === 0 ||
|
|
1102
|
+
[...value].some((character) => {
|
|
1103
|
+
const code = character.codePointAt(0) ?? 0;
|
|
1104
|
+
return code < 32 || code === 127;
|
|
1105
|
+
}) ||
|
|
1106
|
+
/[ ~^:?*[\]\\]/u.test(value) ||
|
|
1107
|
+
value.includes('..') ||
|
|
1108
|
+
value.includes('@{') ||
|
|
1109
|
+
value.startsWith('-') ||
|
|
1110
|
+
value.endsWith('.') ||
|
|
1111
|
+
value.endsWith('.lock') ||
|
|
1112
|
+
value.split('/').some((part) => part.length === 0 || part.startsWith('.') || part.endsWith('.'))) {
|
|
1113
|
+
throw new Error(`Invalid CRT Git ${label}.`);
|
|
1114
|
+
}
|
|
542
1115
|
}
|
|
543
1116
|
function resolveOptions(options) {
|
|
544
1117
|
return { orgId: resolveTestingCredentials(options).orgId };
|
|
545
1118
|
}
|
|
1119
|
+
function testingFlagParameters(key, values) {
|
|
1120
|
+
return (values ?? []).map((value) => ({ key, type: 'clp', value }));
|
|
1121
|
+
}
|
|
1122
|
+
function testingBooleanParameter(key, enabled) {
|
|
1123
|
+
return enabled === true ? [{ key, type: 'clp', value: '' }] : [];
|
|
1124
|
+
}
|
|
1125
|
+
function testingScalarParameter(key, value) {
|
|
1126
|
+
return value === undefined ? [] : [{ key, type: 'clp', value: value.toString() }];
|
|
1127
|
+
}
|
|
1128
|
+
function normalizeConsoleColors(value) {
|
|
1129
|
+
return value === 'ansi' ? 'auto' : value;
|
|
1130
|
+
}
|
|
1131
|
+
function isPlainRecord(value) {
|
|
1132
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1133
|
+
}
|
|
1134
|
+
function normalizeTestingLogResponse(response) {
|
|
1135
|
+
try {
|
|
1136
|
+
const parsed = JSON.parse(response);
|
|
1137
|
+
const record = normalizeTestingRecord(parsed);
|
|
1138
|
+
const { log } = record;
|
|
1139
|
+
const { status } = record;
|
|
1140
|
+
if (typeof log === 'string') {
|
|
1141
|
+
return { log, status: typeof status === 'string' ? status : undefined };
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
catch {
|
|
1145
|
+
// Plain text is the backward-compatible CRT logs response.
|
|
1146
|
+
}
|
|
1147
|
+
return { log: response };
|
|
1148
|
+
}
|
|
1149
|
+
export function tailTestingLogLines(log, count) {
|
|
1150
|
+
if (count === 0)
|
|
1151
|
+
return '';
|
|
1152
|
+
const lines = log.split('\n');
|
|
1153
|
+
const hasTrailingNewline = lines.at(-1) === '';
|
|
1154
|
+
if (hasTrailingNewline)
|
|
1155
|
+
lines.pop();
|
|
1156
|
+
if (lines.length <= count)
|
|
1157
|
+
return log;
|
|
1158
|
+
const result = lines.slice(-count).join('\n');
|
|
1159
|
+
return hasTrailingNewline ? `${result}\n` : result;
|
|
1160
|
+
}
|
|
1161
|
+
export function validateTestingOutputPath(path) {
|
|
1162
|
+
if (path.trim().length === 0 || path.includes('\0'))
|
|
1163
|
+
throw new Error('CRT output path must not be empty.');
|
|
1164
|
+
const target = resolve(path);
|
|
1165
|
+
if (existsSync(target) && statSync(target).isDirectory()) {
|
|
1166
|
+
throw new Error(`CRT output path must be a file: ${path}`);
|
|
1167
|
+
}
|
|
1168
|
+
return target;
|
|
1169
|
+
}
|
|
1170
|
+
function testingOutputArchiveLocation(build) {
|
|
1171
|
+
const candidates = [
|
|
1172
|
+
build.outputArchive,
|
|
1173
|
+
build.output_archive,
|
|
1174
|
+
build.artifactUrl,
|
|
1175
|
+
build.artifact_url,
|
|
1176
|
+
];
|
|
1177
|
+
for (const candidate of candidates) {
|
|
1178
|
+
if (typeof candidate === 'string' && candidate.trim().length > 0)
|
|
1179
|
+
return candidate;
|
|
1180
|
+
if (isPlainRecord(candidate)) {
|
|
1181
|
+
for (const value of [candidate.location, candidate.url, candidate.href]) {
|
|
1182
|
+
if (typeof value === 'string' && value.trim().length > 0)
|
|
1183
|
+
return value;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
return undefined;
|
|
1188
|
+
}
|
|
1189
|
+
function extractStoredOrDeflatedZipEntry(archive, expectedName) {
|
|
1190
|
+
const centralOffset = centralDirectoryOffsetForExtraction(archive);
|
|
1191
|
+
if (centralOffset === undefined)
|
|
1192
|
+
throw new Error('CRT artifact is not a valid ZIP archive.');
|
|
1193
|
+
let offset = centralOffset;
|
|
1194
|
+
while (offset + 46 <= archive.length && archive.readUInt32LE(offset) === 0x02_01_4b_50) {
|
|
1195
|
+
const method = archive.readUInt16LE(offset + 10);
|
|
1196
|
+
const compressedSize = archive.readUInt32LE(offset + 20);
|
|
1197
|
+
const nameLength = archive.readUInt16LE(offset + 28);
|
|
1198
|
+
const extraLength = archive.readUInt16LE(offset + 30);
|
|
1199
|
+
const commentLength = archive.readUInt16LE(offset + 32);
|
|
1200
|
+
const localOffset = archive.readUInt32LE(offset + 42);
|
|
1201
|
+
const name = archive.subarray(offset + 46, offset + 46 + nameLength).toString('utf8');
|
|
1202
|
+
if (name === expectedName || name.endsWith(`/${expectedName}`)) {
|
|
1203
|
+
if (localOffset + 30 > archive.length || archive.readUInt32LE(localOffset) !== 0x04_03_4b_50) {
|
|
1204
|
+
throw new Error('CRT artifact contains an invalid ZIP entry.');
|
|
1205
|
+
}
|
|
1206
|
+
const localNameLength = archive.readUInt16LE(localOffset + 26);
|
|
1207
|
+
const localExtraLength = archive.readUInt16LE(localOffset + 28);
|
|
1208
|
+
const dataOffset = localOffset + 30 + localNameLength + localExtraLength;
|
|
1209
|
+
const compressed = archive.subarray(dataOffset, dataOffset + compressedSize);
|
|
1210
|
+
if (compressed.length !== compressedSize)
|
|
1211
|
+
throw new Error('CRT artifact contains a truncated ZIP entry.');
|
|
1212
|
+
if (method === 0)
|
|
1213
|
+
return Buffer.from(compressed);
|
|
1214
|
+
if (method === 8)
|
|
1215
|
+
return inflateRawSync(compressed);
|
|
1216
|
+
throw new Error(`CRT xUnit extraction does not support ZIP compression method ${method}.`);
|
|
1217
|
+
}
|
|
1218
|
+
offset += 46 + nameLength + extraLength + commentLength;
|
|
1219
|
+
}
|
|
1220
|
+
return undefined;
|
|
1221
|
+
}
|
|
1222
|
+
function centralDirectoryOffsetForExtraction(archive) {
|
|
1223
|
+
for (let offset = archive.length - 22; offset >= 0; offset--) {
|
|
1224
|
+
if (archive.readUInt32LE(offset) === 0x06_05_4b_50)
|
|
1225
|
+
return archive.readUInt32LE(offset + 16);
|
|
1226
|
+
}
|
|
1227
|
+
return undefined;
|
|
1228
|
+
}
|
|
1229
|
+
export function buildTestingMsearchBody(orgId, options) {
|
|
1230
|
+
const filters = [
|
|
1231
|
+
{ term: { projectId: options.projectId } },
|
|
1232
|
+
{ term: { jobId: options.jobId } },
|
|
1233
|
+
];
|
|
1234
|
+
if ((options.statuses?.length ?? 0) > 0)
|
|
1235
|
+
filters.push({ terms: { 'categories.state.keyword': options.statuses } });
|
|
1236
|
+
if ((options.runTypes?.length ?? 0) > 0) {
|
|
1237
|
+
filters.push({
|
|
1238
|
+
terms: {
|
|
1239
|
+
'runType.keyword': options.runTypes?.map((runType) => (runType === 'regression' ? 'normal' : 'development')),
|
|
1240
|
+
},
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
if ((options.robots?.length ?? 0) > 0)
|
|
1244
|
+
filters.push({ terms: { 'categories.robotId.keyword': options.robots } });
|
|
1245
|
+
if (options.from !== undefined || options.to !== undefined) {
|
|
1246
|
+
filters.push({ range: { date: omitUndefined({ gte: options.from, lte: options.to }) } });
|
|
1247
|
+
}
|
|
1248
|
+
const sortFields = {
|
|
1249
|
+
date: 'date',
|
|
1250
|
+
duration: 'testExecutionTime',
|
|
1251
|
+
id: 'buildNumber',
|
|
1252
|
+
status: 'categories.state.keyword',
|
|
1253
|
+
};
|
|
1254
|
+
const page = Math.max(1, options.page ?? 1);
|
|
1255
|
+
const pageSize = options.pageSize ?? 50;
|
|
1256
|
+
const query = {
|
|
1257
|
+
_source: [
|
|
1258
|
+
'buildNumber',
|
|
1259
|
+
'categories.state',
|
|
1260
|
+
'categories.jobName',
|
|
1261
|
+
'categories.robotId',
|
|
1262
|
+
'runType',
|
|
1263
|
+
'date',
|
|
1264
|
+
'testExecutionTime',
|
|
1265
|
+
],
|
|
1266
|
+
from: (page - 1) * pageSize,
|
|
1267
|
+
query: { bool: { filter: filters } },
|
|
1268
|
+
size: options.count === true ? 0 : pageSize,
|
|
1269
|
+
sort: [{ [sortFields[options.sort ?? 'date']]: { order: options.order ?? 'desc' } }],
|
|
1270
|
+
// Metrics API follows the Elasticsearch wire name.
|
|
1271
|
+
// eslint-disable-next-line camelcase
|
|
1272
|
+
track_total_hits: true,
|
|
1273
|
+
};
|
|
1274
|
+
return `${JSON.stringify({ index: `org_${orgId}_type_testresults` })}\n${JSON.stringify(query)}\n`;
|
|
1275
|
+
}
|
|
1276
|
+
function normalizeTestingSearchResponse(response) {
|
|
1277
|
+
const root = normalizeTestingRecord(response);
|
|
1278
|
+
const responses = Array.isArray(root.responses) ? root.responses : [];
|
|
1279
|
+
const first = isPlainRecord(responses[0]) ? responses[0] : {};
|
|
1280
|
+
const hits = isPlainRecord(first.hits) ? first.hits : {};
|
|
1281
|
+
const totalRecord = isPlainRecord(hits.total) ? hits.total : {};
|
|
1282
|
+
const totalValue = totalRecord.value ?? hits.total;
|
|
1283
|
+
const count = typeof totalValue === 'number'
|
|
1284
|
+
? totalValue
|
|
1285
|
+
: typeof totalValue === 'string' && /^\d+$/u.test(totalValue)
|
|
1286
|
+
? Number.parseInt(totalValue, 10)
|
|
1287
|
+
: 0;
|
|
1288
|
+
const documents = Array.isArray(hits.hits) ? hits.hits : [];
|
|
1289
|
+
const runs = documents
|
|
1290
|
+
.map((document) => (isPlainRecord(document) && isPlainRecord(document._source) ? document._source : document))
|
|
1291
|
+
.filter((document) => isPlainRecord(document))
|
|
1292
|
+
.map((source) => normalizeTestingSearchBuild(source));
|
|
1293
|
+
return { count, runs };
|
|
1294
|
+
}
|
|
1295
|
+
function normalizeTestingSearchBuild(source) {
|
|
1296
|
+
const categories = isPlainRecord(source.categories) ? source.categories : {};
|
|
1297
|
+
const id = source.buildNumber ?? source.id;
|
|
1298
|
+
const jobId = source.jobId ?? categories.jobId;
|
|
1299
|
+
const status = categories.state ?? source.status;
|
|
1300
|
+
const runType = source.runType === 'normal' ? 'regression' : source.runType;
|
|
1301
|
+
return {
|
|
1302
|
+
...source,
|
|
1303
|
+
id: typeof id === 'number' || typeof id === 'string' ? id : undefined,
|
|
1304
|
+
jobId: typeof jobId === 'number' || typeof jobId === 'string' ? jobId : undefined,
|
|
1305
|
+
runType: typeof runType === 'string' ? runType : undefined,
|
|
1306
|
+
status: typeof status === 'string' ? status : undefined,
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
546
1309
|
//# sourceMappingURL=testing-operations.js.map
|