@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.
Files changed (277) hide show
  1. package/README.md +1965 -410
  2. package/dist/commands/testing/build/abort.d.ts +7 -1
  3. package/dist/commands/testing/build/abort.js +13 -3
  4. package/dist/commands/testing/build/abort.js.map +1 -1
  5. package/dist/commands/testing/build/get.d.ts +18 -0
  6. package/dist/commands/testing/build/get.js +38 -0
  7. package/dist/commands/testing/build/get.js.map +1 -0
  8. package/dist/commands/testing/build/latest.d.ts +12 -0
  9. package/dist/commands/testing/build/latest.js +40 -0
  10. package/dist/commands/testing/build/latest.js.map +1 -0
  11. package/dist/commands/testing/build/list.d.ts +6 -1
  12. package/dist/commands/testing/build/list.js +23 -1
  13. package/dist/commands/testing/build/list.js.map +1 -1
  14. package/dist/commands/testing/build/logs.d.ts +6 -0
  15. package/dist/commands/testing/build/logs.js +8 -1
  16. package/dist/commands/testing/build/logs.js.map +1 -1
  17. package/dist/commands/testing/build/run.d.ts +27 -4
  18. package/dist/commands/testing/build/run.js +4 -18
  19. package/dist/commands/testing/build/run.js.map +1 -1
  20. package/dist/commands/testing/build/search.d.ts +21 -0
  21. package/dist/commands/testing/build/search.js +57 -0
  22. package/dist/commands/testing/build/search.js.map +1 -0
  23. package/dist/commands/testing/create.d.ts +1 -15
  24. package/dist/commands/testing/create.js +1 -43
  25. package/dist/commands/testing/create.js.map +1 -1
  26. package/dist/commands/testing/data-table/create.d.ts +30 -0
  27. package/dist/commands/testing/data-table/create.js +52 -0
  28. package/dist/commands/testing/data-table/create.js.map +1 -0
  29. package/dist/commands/testing/data-table/delete.d.ts +25 -0
  30. package/dist/commands/testing/data-table/delete.js +40 -0
  31. package/dist/commands/testing/data-table/delete.js.map +1 -0
  32. package/dist/commands/testing/data-table/export.d.ts +33 -0
  33. package/dist/commands/testing/data-table/export.js +59 -0
  34. package/dist/commands/testing/data-table/export.js.map +1 -0
  35. package/dist/commands/testing/data-table/get.d.ts +21 -0
  36. package/dist/commands/testing/data-table/get.js +35 -0
  37. package/dist/commands/testing/data-table/get.js.map +1 -0
  38. package/dist/commands/testing/data-table/list.d.ts +16 -0
  39. package/dist/commands/testing/data-table/list.js +33 -0
  40. package/dist/commands/testing/data-table/list.js.map +1 -0
  41. package/dist/commands/testing/data-table/update.d.ts +34 -0
  42. package/dist/commands/testing/data-table/update.js +64 -0
  43. package/dist/commands/testing/data-table/update.js.map +1 -0
  44. package/dist/commands/testing/job/create.d.ts +18 -0
  45. package/dist/commands/testing/job/create.js +60 -4
  46. package/dist/commands/testing/job/create.js.map +1 -1
  47. package/dist/commands/testing/job/delete.d.ts +6 -1
  48. package/dist/commands/testing/job/delete.js +13 -3
  49. package/dist/commands/testing/job/delete.js.map +1 -1
  50. package/dist/commands/testing/job/refs.d.ts +17 -0
  51. package/dist/commands/testing/job/refs.js +30 -0
  52. package/dist/commands/testing/job/refs.js.map +1 -0
  53. package/dist/commands/testing/job/schedule/get.d.ts +23 -0
  54. package/dist/commands/testing/job/schedule/get.js +31 -0
  55. package/dist/commands/testing/job/schedule/get.js.map +1 -0
  56. package/dist/commands/testing/job/schedule/remove.d.ts +24 -0
  57. package/dist/commands/testing/job/schedule/remove.js +34 -0
  58. package/dist/commands/testing/job/schedule/remove.js.map +1 -0
  59. package/dist/commands/testing/job/schedule/set.d.ts +23 -0
  60. package/dist/commands/testing/job/schedule/set.js +39 -0
  61. package/dist/commands/testing/job/schedule/set.js.map +1 -0
  62. package/dist/commands/testing/job/update.d.ts +2 -0
  63. package/dist/commands/testing/job/update.js +7 -1
  64. package/dist/commands/testing/job/update.js.map +1 -1
  65. package/dist/commands/testing/job/upload.d.ts +6 -0
  66. package/dist/commands/testing/job/upload.js +22 -0
  67. package/dist/commands/testing/job/upload.js.map +1 -1
  68. package/dist/commands/testing/live/continue.d.ts +20 -0
  69. package/dist/commands/testing/live/continue.js +21 -0
  70. package/dist/commands/testing/live/continue.js.map +1 -0
  71. package/dist/commands/testing/live/execute.d.ts +30 -0
  72. package/dist/commands/testing/live/execute.js +43 -0
  73. package/dist/commands/testing/live/execute.js.map +1 -0
  74. package/dist/commands/testing/live/history.d.ts +23 -0
  75. package/dist/commands/testing/live/history.js +35 -0
  76. package/dist/commands/testing/live/history.js.map +1 -0
  77. package/dist/commands/testing/live/inspect.d.ts +18 -0
  78. package/dist/commands/testing/live/inspect.js +30 -0
  79. package/dist/commands/testing/live/inspect.js.map +1 -0
  80. package/dist/commands/testing/live/interrupt.d.ts +20 -0
  81. package/dist/commands/testing/live/interrupt.js +21 -0
  82. package/dist/commands/testing/live/interrupt.js.map +1 -0
  83. package/dist/commands/testing/live/recorder.d.ts +23 -0
  84. package/dist/commands/testing/live/recorder.js +134 -0
  85. package/dist/commands/testing/live/recorder.js.map +1 -0
  86. package/dist/commands/testing/live/reports.d.ts +20 -0
  87. package/dist/commands/testing/live/reports.js +31 -0
  88. package/dist/commands/testing/live/reports.js.map +1 -0
  89. package/dist/commands/testing/live/screenshot.d.ts +23 -0
  90. package/dist/commands/testing/live/screenshot.js +25 -0
  91. package/dist/commands/testing/live/screenshot.js.map +1 -0
  92. package/dist/commands/testing/live/set-options.d.ts +27 -0
  93. package/dist/commands/testing/live/set-options.js +28 -0
  94. package/dist/commands/testing/live/set-options.js.map +1 -0
  95. package/dist/commands/testing/live/start.d.ts +29 -0
  96. package/dist/commands/testing/live/start.js +53 -0
  97. package/dist/commands/testing/live/start.js.map +1 -0
  98. package/dist/commands/testing/live/status.d.ts +19 -0
  99. package/dist/commands/testing/live/status.js +27 -0
  100. package/dist/commands/testing/live/status.js.map +1 -0
  101. package/dist/commands/testing/live/stop.d.ts +24 -0
  102. package/dist/commands/testing/live/stop.js +38 -0
  103. package/dist/commands/testing/live/stop.js.map +1 -0
  104. package/dist/commands/testing/live/view.d.ts +16 -0
  105. package/dist/commands/testing/live/view.js +36 -0
  106. package/dist/commands/testing/live/view.js.map +1 -0
  107. package/dist/commands/testing/logs.d.ts +6 -0
  108. package/dist/commands/testing/logs.js +8 -1
  109. package/dist/commands/testing/logs.js.map +1 -1
  110. package/dist/commands/testing/project/create.d.ts +3 -0
  111. package/dist/commands/testing/project/create.js +17 -1
  112. package/dist/commands/testing/project/create.js.map +1 -1
  113. package/dist/commands/testing/project/delete.d.ts +7 -2
  114. package/dist/commands/testing/project/delete.js +15 -3
  115. package/dist/commands/testing/project/delete.js.map +1 -1
  116. package/dist/commands/testing/project/get.js +1 -1
  117. package/dist/commands/testing/project/get.js.map +1 -1
  118. package/dist/commands/testing/project/permission/get.d.ts +13 -0
  119. package/dist/commands/testing/project/permission/get.js +21 -0
  120. package/dist/commands/testing/project/permission/get.js.map +1 -0
  121. package/dist/commands/testing/project/permission/list.d.ts +13 -0
  122. package/dist/commands/testing/project/permission/list.js +21 -0
  123. package/dist/commands/testing/project/permission/list.js.map +1 -0
  124. package/dist/commands/testing/project/permission/set.d.ts +19 -0
  125. package/dist/commands/testing/project/permission/set.js +33 -0
  126. package/dist/commands/testing/project/permission/set.js.map +1 -0
  127. package/dist/commands/testing/project/permission/update.d.ts +19 -0
  128. package/dist/commands/testing/project/permission/update.js +33 -0
  129. package/dist/commands/testing/project/permission/update.js.map +1 -0
  130. package/dist/commands/testing/project/update.d.ts +14 -0
  131. package/dist/commands/testing/project/update.js +28 -0
  132. package/dist/commands/testing/project/update.js.map +1 -0
  133. package/dist/commands/testing/robot/create.d.ts +4 -1
  134. package/dist/commands/testing/robot/create.js +20 -5
  135. package/dist/commands/testing/robot/create.js.map +1 -1
  136. package/dist/commands/testing/robot/delete.d.ts +6 -1
  137. package/dist/commands/testing/robot/delete.js +13 -3
  138. package/dist/commands/testing/robot/delete.js.map +1 -1
  139. package/dist/commands/testing/robot/update.d.ts +19 -0
  140. package/dist/commands/testing/robot/update.js +49 -0
  141. package/dist/commands/testing/robot/update.js.map +1 -0
  142. package/dist/commands/testing/run.d.ts +27 -4
  143. package/dist/commands/testing/run.js +4 -11
  144. package/dist/commands/testing/run.js.map +1 -1
  145. package/dist/commands/testing/test/run.d.ts +27 -11
  146. package/dist/commands/testing/test/run.js +5 -37
  147. package/dist/commands/testing/test/run.js.map +1 -1
  148. package/dist/commands/testing/update.d.ts +6 -0
  149. package/dist/commands/testing/update.js +10 -0
  150. package/dist/commands/testing/update.js.map +1 -1
  151. package/dist/commands/testing/user-parameter/create.d.ts +19 -0
  152. package/dist/commands/testing/user-parameter/create.js +45 -0
  153. package/dist/commands/testing/user-parameter/create.js.map +1 -0
  154. package/dist/commands/testing/user-parameter/delete.d.ts +21 -0
  155. package/dist/commands/testing/user-parameter/delete.js +33 -0
  156. package/dist/commands/testing/user-parameter/delete.js.map +1 -0
  157. package/dist/commands/testing/user-parameter/get.d.ts +13 -0
  158. package/dist/commands/testing/user-parameter/get.js +21 -0
  159. package/dist/commands/testing/user-parameter/get.js.map +1 -0
  160. package/dist/commands/testing/user-parameter/list.d.ts +12 -0
  161. package/dist/commands/testing/user-parameter/list.js +23 -0
  162. package/dist/commands/testing/user-parameter/list.js.map +1 -0
  163. package/dist/commands/testing/user-parameter/update.d.ts +23 -0
  164. package/dist/commands/testing/user-parameter/update.js +45 -0
  165. package/dist/commands/testing/user-parameter/update.js.map +1 -0
  166. package/dist/commands/testing/variable/create.d.ts +6 -2
  167. package/dist/commands/testing/variable/create.js +14 -5
  168. package/dist/commands/testing/variable/create.js.map +1 -1
  169. package/dist/commands/testing/variable/delete.d.ts +6 -1
  170. package/dist/commands/testing/variable/delete.js +13 -3
  171. package/dist/commands/testing/variable/delete.js.map +1 -1
  172. package/dist/commands/testing/variable/get.d.ts +13 -0
  173. package/dist/commands/testing/variable/get.js +25 -0
  174. package/dist/commands/testing/variable/get.js.map +1 -0
  175. package/dist/commands/testing/variable/list.d.ts +4 -0
  176. package/dist/commands/testing/variable/list.js +15 -3
  177. package/dist/commands/testing/variable/list.js.map +1 -1
  178. package/dist/commands/testing/variable/update.d.ts +6 -2
  179. package/dist/commands/testing/variable/update.js +11 -5
  180. package/dist/commands/testing/variable/update.js.map +1 -1
  181. package/dist/lib/credential-store.d.ts +3 -0
  182. package/dist/lib/credential-store.js +16 -0
  183. package/dist/lib/credential-store.js.map +1 -1
  184. package/dist/lib/mcp/command-id.js +3 -0
  185. package/dist/lib/mcp/command-id.js.map +1 -1
  186. package/dist/lib/mcp/testing-live-tools.d.ts +126 -0
  187. package/dist/lib/mcp/testing-live-tools.js +215 -0
  188. package/dist/lib/mcp/testing-live-tools.js.map +1 -0
  189. package/dist/lib/mcp/testing-tools.d.ts +455 -9
  190. package/dist/lib/mcp/testing-tools.js +677 -37
  191. package/dist/lib/mcp/testing-tools.js.map +1 -1
  192. package/dist/lib/mcp/tools.d.ts +551 -14
  193. package/dist/lib/testing-config.d.ts +3 -0
  194. package/dist/lib/testing-config.js +9 -0
  195. package/dist/lib/testing-config.js.map +1 -1
  196. package/dist/lib/testing-confirmation.d.ts +14 -0
  197. package/dist/lib/testing-confirmation.js +17 -0
  198. package/dist/lib/testing-confirmation.js.map +1 -0
  199. package/dist/lib/testing-data-command.d.ts +21 -0
  200. package/dist/lib/testing-data-command.js +49 -0
  201. package/dist/lib/testing-data-command.js.map +1 -0
  202. package/dist/lib/testing-data-core.d.ts +22 -0
  203. package/dist/lib/testing-data-core.js +208 -0
  204. package/dist/lib/testing-data-core.js.map +1 -0
  205. package/dist/lib/testing-data-export.d.ts +1 -0
  206. package/dist/lib/testing-data-export.js +2 -0
  207. package/dist/lib/testing-data-export.js.map +1 -0
  208. package/dist/lib/testing-data-import.d.ts +1 -0
  209. package/dist/lib/testing-data-import.js +2 -0
  210. package/dist/lib/testing-data-import.js.map +1 -0
  211. package/dist/lib/testing-data-io.d.ts +21 -0
  212. package/dist/lib/testing-data-io.js +236 -0
  213. package/dist/lib/testing-data-io.js.map +1 -0
  214. package/dist/lib/testing-data-operations.d.ts +36 -0
  215. package/dist/lib/testing-data-operations.js +91 -0
  216. package/dist/lib/testing-data-operations.js.map +1 -0
  217. package/dist/lib/testing-data-types.d.ts +41 -0
  218. package/dist/lib/testing-data-types.js +3 -0
  219. package/dist/lib/testing-data-types.js.map +1 -0
  220. package/dist/lib/testing-flags.d.ts +62 -1
  221. package/dist/lib/testing-flags.js +177 -1
  222. package/dist/lib/testing-flags.js.map +1 -1
  223. package/dist/lib/testing-gateway.d.ts +17 -3
  224. package/dist/lib/testing-gateway.js +175 -45
  225. package/dist/lib/testing-gateway.js.map +1 -1
  226. package/dist/lib/testing-live-command.d.ts +24 -0
  227. package/dist/lib/testing-live-command.js +56 -0
  228. package/dist/lib/testing-live-command.js.map +1 -0
  229. package/dist/lib/testing-live-endpoints.d.ts +14 -0
  230. package/dist/lib/testing-live-endpoints.js +34 -0
  231. package/dist/lib/testing-live-endpoints.js.map +1 -0
  232. package/dist/lib/testing-live-history.d.ts +11 -0
  233. package/dist/lib/testing-live-history.js +96 -0
  234. package/dist/lib/testing-live-history.js.map +1 -0
  235. package/dist/lib/testing-live-operations.d.ts +118 -0
  236. package/dist/lib/testing-live-operations.js +676 -0
  237. package/dist/lib/testing-live-operations.js.map +1 -0
  238. package/dist/lib/testing-live-recorder.d.ts +14 -0
  239. package/dist/lib/testing-live-recorder.js +122 -0
  240. package/dist/lib/testing-live-recorder.js.map +1 -0
  241. package/dist/lib/testing-live-robot.d.ts +16 -0
  242. package/dist/lib/testing-live-robot.js +158 -0
  243. package/dist/lib/testing-live-robot.js.map +1 -0
  244. package/dist/lib/testing-live-state.d.ts +4 -0
  245. package/dist/lib/testing-live-state.js +90 -0
  246. package/dist/lib/testing-live-state.js.map +1 -0
  247. package/dist/lib/testing-live-types.d.ts +92 -0
  248. package/dist/lib/testing-live-types.js +2 -0
  249. package/dist/lib/testing-live-types.js.map +1 -0
  250. package/dist/lib/testing-operations.d.ts +157 -5
  251. package/dist/lib/testing-operations.js +829 -66
  252. package/dist/lib/testing-operations.js.map +1 -1
  253. package/dist/lib/testing-run-command.d.ts +25 -0
  254. package/dist/lib/testing-run-command.js +94 -0
  255. package/dist/lib/testing-run-command.js.map +1 -0
  256. package/dist/lib/testing-sanitization.d.ts +11 -0
  257. package/dist/lib/testing-sanitization.js +135 -0
  258. package/dist/lib/testing-sanitization.js.map +1 -0
  259. package/dist/lib/testing-schedule.d.ts +23 -0
  260. package/dist/lib/testing-schedule.js +48 -0
  261. package/dist/lib/testing-schedule.js.map +1 -0
  262. package/dist/lib/testing-secret-input.d.ts +12 -0
  263. package/dist/lib/testing-secret-input.js +56 -0
  264. package/dist/lib/testing-secret-input.js.map +1 -0
  265. package/dist/lib/testing-session.d.ts +8 -0
  266. package/dist/lib/testing-session.js +82 -0
  267. package/dist/lib/testing-session.js.map +1 -0
  268. package/dist/lib/testing-types.d.ts +143 -13
  269. package/dist/lib/testing-types.js +110 -5
  270. package/dist/lib/testing-types.js.map +1 -1
  271. package/dist/lib/testing-watch.d.ts +4 -0
  272. package/dist/lib/testing-watch.js +45 -0
  273. package/dist/lib/testing-watch.js.map +1 -0
  274. package/oclif.manifest.json +8354 -3760
  275. package/package.json +12 -1
  276. package/templates/skills/agentia-testing/SKILL.md +9 -6
  277. 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 { TESTING_DEFAULT_TIMEOUT_MS, TestingGateway } from './testing-gateway.js';
9
- import { createDefaultRobotSnapshotBase64, listSnapshotZipEntries, normalizeDownloadedFiles, normalizeTestingArray, normalizeTestingId, normalizeTestingRecord, redactTestingVariables, testingBuildAbortEndpoint, testingBuildLogsEndpoint, testingJobBuildsEndpoint, testingJobEndpoint, testingJobFilesDownloadEndpoint, testingJobFilesEndpoint, testingJobFilesUploadEndpoint, testingJobsEndpoint, testingProjectBuildsEndpoint, testingProjectDeleteEndpoint, testingProjectsEndpoint, testingRobotEndpoint, testingRobotJobsEndpoint, testingRobotsEndpoint, testingVariableEndpoint, testingVariablesEndpoint, } from './testing-types.js';
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(['aborted', 'failed', 'finished', 'succeeded']);
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
- async function defaultTestingSnapshotFetch(url, headers, timeoutMs) {
19
- const transport = url.protocol === 'http:' ? http : https;
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 = transport.request({
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
- response.on('data', (chunk) => chunks.push(chunk));
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.toString()}`));
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
- description: options.description ?? '',
78
+ const body = omitUndefined({
79
+ dashboardId: options.dashboardId,
80
+ description: options.description,
58
81
  name: options.name,
59
- projectType: 'ta',
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
- return findTestingRecord(await listTestingProjects(options), options.projectId, 'project');
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: 'pace' }, options);
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
- const body = {
132
+ requireNonBlank(options.name, 'CRT robot name');
133
+ const body = omitUndefined({
77
134
  description: options.description ?? '',
78
- fwVersion: options.fwVersion ?? 'stable',
135
+ fwVersion: options.fwVersion,
136
+ label: options.label,
79
137
  name: options.name,
80
- os: options.os ?? 'linux',
81
- robotType: options.robotType ?? 'default',
82
- runEnvironment: options.kind === 'QConnect' ? 'paceconnect' : 'cloud',
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
- return findTestingRecord(await listTestingRobots(options), options.robotId, 'robot');
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
- const body = {
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
- operations: options.operations,
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: options.operations, response: normalizeTestingRecord(response) };
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 { body, build, jobId: options.jobId, projectId: options.projectId };
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, { resultSize: options.resultSize, status: options.status })
236
- : testingJobBuildsEndpoint(options.projectId, options.jobId, { resultSize: options.resultSize, status: options.status });
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
- const builds = await listTestingBuilds({
248
- ...options,
249
- jobId: options.jobId,
250
- projectId: options.projectId,
251
- resultSize: 'compact',
252
- });
253
- const build = findMatchingBuild(builds, options.buildId);
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 log = await TestingGateway.call({
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
- mkdirSync(dirname(resolve(options.output)), { recursive: true });
274
- writeFileSync(options.output, log, 'utf8');
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 response = await TestingGateway.call({ endpoint: testingVariablesEndpoint(options.projectId), method: 'GET', service: 'pace' }, options);
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
- operations.push(fileOperationFromLocalFile(file, basenameForPath(file), 'add'));
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
- operations.push(fileOperationFromLocalFile(file, relative(root, file).split(sep).join('/'), 'add'));
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) => ({ op: 'remove', path: normalizeRemotePath(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 ? basenameForPath(localPath) : spec.slice(separator + 1);
379
- return fileOperationFromLocalFile(localPath, remotePath, op);
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 normalizeRemotePath(path) {
410
- const normalized = path.trim().replaceAll('\\', '/').replace(/^\/+/u, '');
411
- if (normalized.length === 0 || normalized.includes('..')) {
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 isTerminalBuild(build) {
451
- const status = typeof build.status === 'string' ? build.status.toLowerCase() : undefined;
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.toString(),
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