@devicecloud.dev/dcd 4.1.5 → 4.1.7

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.
@@ -6,6 +6,7 @@ const cli_ux_1 = require("@oclif/core/lib/cli-ux");
6
6
  const api_gateway_1 = require("../gateways/api-gateway");
7
7
  const methods_1 = require("../methods");
8
8
  const connectivity_1 = require("../utils/connectivity");
9
+ const styling_1 = require("../utils/styling");
9
10
  /**
10
11
  * Custom error for run failures that includes the polling result
11
12
  */
@@ -32,72 +33,46 @@ class ResultsPollingService {
32
33
  */
33
34
  async pollUntilComplete(results, options) {
34
35
  const { apiUrl, apiKey, uploadId, consoleUrl, quiet = false, json = false, debug = false, logger } = options;
35
- if (!json) {
36
- core_1.ux.action.start('Waiting for results', 'Initializing', {
37
- stdout: true,
38
- });
39
- if (logger) {
40
- logger('\nYou can safely close this terminal and the tests will continue\n');
41
- }
42
- }
36
+ this.initializePollingDisplay(json, logger);
43
37
  let sequentialPollFailures = 0;
44
38
  let previousSummary = '';
45
39
  if (debug && logger) {
46
40
  logger(`DEBUG: Starting polling loop for results`);
47
41
  }
48
- return new Promise((resolve, reject) => {
49
- const intervalId = setInterval(async () => {
50
- try {
51
- if (debug && logger) {
52
- logger(`DEBUG: Polling for results: ${uploadId}`);
53
- }
54
- const { results: updatedResults } = await api_gateway_1.ApiGateway.getResultsForUpload(apiUrl, apiKey, uploadId);
55
- if (!updatedResults) {
56
- throw new Error('no results');
57
- }
58
- if (debug && logger) {
59
- logger(`DEBUG: Poll received ${updatedResults.length} results`);
60
- for (const result of updatedResults) {
61
- logger(`DEBUG: Result status: ${result.test_file_name} - ${result.status}`);
62
- }
63
- }
64
- const { summary } = this.calculateStatusSummary(updatedResults);
65
- previousSummary = this.updateDisplayStatus(updatedResults, quiet, json, summary, previousSummary);
66
- const allComplete = updatedResults.every((result) => !['PENDING', 'RUNNING'].includes(result.status));
67
- if (allComplete) {
68
- if (debug && logger) {
69
- logger(`DEBUG: All tests completed, stopping poll`);
70
- }
71
- this.displayFinalResults(updatedResults, consoleUrl, json, logger);
72
- clearInterval(intervalId);
73
- const output = this.buildPollingResult(updatedResults, uploadId, consoleUrl);
74
- if (output.status === 'FAILED') {
75
- if (debug && logger) {
76
- logger(`DEBUG: Some tests failed, returning failed status`);
77
- }
78
- reject(new RunFailedError(output));
79
- }
80
- else {
81
- if (debug && logger) {
82
- logger(`DEBUG: All tests passed, returning success status`);
83
- }
84
- sequentialPollFailures = 0;
85
- resolve(output);
86
- }
87
- }
42
+ // Poll in a loop until all tests complete
43
+ // eslint-disable-next-line no-constant-condition
44
+ while (true) {
45
+ try {
46
+ const updatedResults = await this.fetchAndLogResults(apiUrl, apiKey, uploadId, debug, logger);
47
+ const { summary } = this.calculateStatusSummary(updatedResults);
48
+ previousSummary = this.updateDisplayStatus(updatedResults, quiet, json, summary, previousSummary);
49
+ const allComplete = updatedResults.every((result) => !['PENDING', 'RUNNING'].includes(result.status));
50
+ if (allComplete) {
51
+ return await this.handleCompletedTests(updatedResults, {
52
+ consoleUrl,
53
+ debug,
54
+ json,
55
+ logger,
56
+ uploadId,
57
+ });
88
58
  }
89
- catch (error) {
90
- sequentialPollFailures++;
91
- try {
92
- await this.handlePollingError(error, sequentialPollFailures, debug, logger);
93
- }
94
- catch (error) {
95
- clearInterval(intervalId);
96
- reject(error);
97
- }
59
+ // Reset failure counter on successful poll
60
+ sequentialPollFailures = 0;
61
+ // Wait before next poll
62
+ await this.sleep(this.POLL_INTERVAL_MS);
63
+ }
64
+ catch (error) {
65
+ // Re-throw RunFailedError immediately (test failures, not polling errors)
66
+ if (error instanceof RunFailedError) {
67
+ throw error;
98
68
  }
99
- }, this.POLL_INTERVAL_MS);
100
- });
69
+ sequentialPollFailures++;
70
+ // Handle polling errors (network issues, etc.)
71
+ await this.handlePollingError(error, sequentialPollFailures, debug, logger);
72
+ // Wait before retrying after an error
73
+ await this.sleep(this.POLL_INTERVAL_MS);
74
+ }
75
+ }
101
76
  }
102
77
  buildPollingResult(results, uploadId, consoleUrl) {
103
78
  const resultsWithoutEarlierTries = this.filterLatestResults(results);
@@ -126,42 +101,105 @@ class ResultsPollingService {
126
101
  const running = statusCounts.RUNNING || 0;
127
102
  const total = results.length;
128
103
  const completed = passed + failed;
129
- const summary = `${completed}/${total} | ✓ ${passed} | ✗ ${failed} | ▶ ${running} | ⏸ ${pending}`;
104
+ const summary = (0, styling_1.formatTestSummary)({
105
+ completed,
106
+ failed,
107
+ passed,
108
+ pending,
109
+ running,
110
+ total,
111
+ });
130
112
  return { completed, failed, passed, pending, running, summary, total };
131
113
  }
132
114
  displayFinalResults(results, consoleUrl, json, logger) {
133
115
  if (json) {
134
116
  return;
135
117
  }
136
- core_1.ux.action.stop('completed');
118
+ core_1.ux.action.stop(styling_1.colors.success('completed'));
137
119
  if (logger) {
138
120
  logger('\n');
139
121
  }
140
122
  const hasFailedTests = results.some((result) => result.status === 'FAILED');
141
123
  (0, cli_ux_1.table)(results, {
142
124
  duration: {
143
- get: (row) => row.duration_seconds
144
- ? (0, methods_1.formatDurationSeconds)(Number(row.duration_seconds))
145
- : '-',
125
+ get(row) {
126
+ return row.duration_seconds
127
+ ? styling_1.colors.dim((0, methods_1.formatDurationSeconds)(Number(row.duration_seconds)))
128
+ : styling_1.colors.dim('-');
129
+ },
130
+ },
131
+ status: {
132
+ get(row) {
133
+ const statusUpper = row.status.toUpperCase();
134
+ switch (statusUpper) {
135
+ case 'PASSED': {
136
+ return styling_1.colors.success(row.status);
137
+ }
138
+ case 'FAILED': {
139
+ return styling_1.colors.error(row.status);
140
+ }
141
+ case 'RUNNING': {
142
+ return styling_1.colors.info(row.status);
143
+ }
144
+ case 'PENDING': {
145
+ return styling_1.colors.warning(row.status);
146
+ }
147
+ default: {
148
+ return styling_1.colors.dim(row.status);
149
+ }
150
+ }
151
+ },
146
152
  },
147
- status: { get: (row) => row.status },
148
153
  test: {
149
- get: (row) => `${row.test_file_name} ${row.retry_of ? '(retry)' : ''}`,
154
+ get(row) {
155
+ const testName = row.test_file_name;
156
+ const retry = row.retry_of ? styling_1.colors.dim(' (retry)') : '';
157
+ return `${testName}${retry}`;
158
+ },
150
159
  },
151
160
  ...(hasFailedTests && {
152
161
  // eslint-disable-next-line camelcase
153
162
  fail_reason: {
154
- get: (row) => row.status === 'FAILED' && row.fail_reason ? row.fail_reason : '',
163
+ get(row) {
164
+ return row.status === 'FAILED' && row.fail_reason
165
+ ? styling_1.colors.error(row.fail_reason)
166
+ : '';
167
+ },
155
168
  },
156
169
  }),
157
170
  }, { printLine: logger });
158
171
  if (logger) {
159
172
  logger('\n');
160
- logger('Run completed, you can access the results at:');
161
- logger(consoleUrl);
173
+ logger(styling_1.colors.bold('Run completed') + styling_1.colors.dim(', you can access the results at:'));
174
+ logger(styling_1.colors.url(consoleUrl));
162
175
  logger('\n');
163
176
  }
164
177
  }
178
+ /**
179
+ * Fetch results from API and log debug information
180
+ * @param apiUrl API base URL
181
+ * @param apiKey API authentication key
182
+ * @param uploadId Upload ID to fetch results for
183
+ * @param debug Whether debug logging is enabled
184
+ * @param logger Optional logger function
185
+ * @returns Promise resolving to test results
186
+ */
187
+ async fetchAndLogResults(apiUrl, apiKey, uploadId, debug, logger) {
188
+ if (debug && logger) {
189
+ logger(`DEBUG: Polling for results: ${uploadId}`);
190
+ }
191
+ const { results: updatedResults } = await api_gateway_1.ApiGateway.getResultsForUpload(apiUrl, apiKey, uploadId);
192
+ if (!updatedResults) {
193
+ throw new Error('no results');
194
+ }
195
+ if (debug && logger) {
196
+ logger(`DEBUG: Poll received ${updatedResults.length} results`);
197
+ for (const result of updatedResults) {
198
+ logger(`DEBUG: Result status: ${result.test_file_name} - ${result.status}`);
199
+ }
200
+ }
201
+ return updatedResults;
202
+ }
165
203
  filterLatestResults(results) {
166
204
  return results.filter((result) => {
167
205
  const originalTryId = result.retry_of || result.id;
@@ -169,6 +207,30 @@ class ResultsPollingService {
169
207
  return result.id === Math.max(...tries.map((t) => t.id));
170
208
  });
171
209
  }
210
+ /**
211
+ * Handle completed tests and return final result
212
+ * @param updatedResults Test results from API
213
+ * @param options Completion handling options
214
+ * @returns Promise resolving to final polling result
215
+ */
216
+ async handleCompletedTests(updatedResults, options) {
217
+ const { uploadId, consoleUrl, json, debug, logger } = options;
218
+ if (debug && logger) {
219
+ logger(`DEBUG: All tests completed, stopping poll`);
220
+ }
221
+ this.displayFinalResults(updatedResults, consoleUrl, json, logger);
222
+ const output = this.buildPollingResult(updatedResults, uploadId, consoleUrl);
223
+ if (output.status === 'FAILED') {
224
+ if (debug && logger) {
225
+ logger(`DEBUG: Some tests failed, returning failed status`);
226
+ }
227
+ throw new RunFailedError(output);
228
+ }
229
+ if (debug && logger) {
230
+ logger(`DEBUG: All tests passed, returning success status`);
231
+ }
232
+ return output;
233
+ }
172
234
  async handlePollingError(error, sequentialPollFailures, debug, logger) {
173
235
  if (debug && logger) {
174
236
  logger(`DEBUG: Error polling for results: ${error}`);
@@ -202,6 +264,32 @@ class ResultsPollingService {
202
264
  logger('unable to fetch results, trying again...');
203
265
  }
204
266
  }
267
+ /**
268
+ * Initialize the polling display UI
269
+ * @param json Whether to output in JSON format
270
+ * @param logger Optional logger function for output
271
+ * @returns void
272
+ */
273
+ initializePollingDisplay(json, logger) {
274
+ if (!json) {
275
+ core_1.ux.action.start(styling_1.colors.bold('Waiting for results'), styling_1.colors.dim('Initializing'), {
276
+ stdout: true,
277
+ });
278
+ if (logger) {
279
+ logger(styling_1.colors.dim('\nYou can safely close this terminal and the tests will continue\n'));
280
+ }
281
+ }
282
+ }
283
+ /**
284
+ * Sleep for the specified number of milliseconds
285
+ * @param ms Number of milliseconds to sleep
286
+ * @returns Promise that resolves after the delay
287
+ */
288
+ async sleep(ms) {
289
+ return new Promise((resolve) => {
290
+ setTimeout(resolve, ms);
291
+ });
292
+ }
205
293
  updateDisplayStatus(results, quiet, json, summary, previousSummary) {
206
294
  if (json) {
207
295
  return previousSummary;
@@ -213,9 +301,17 @@ class ResultsPollingService {
213
301
  }
214
302
  }
215
303
  else {
216
- core_1.ux.action.status = '\nStatus Test\n─────────── ───────────────';
304
+ core_1.ux.action.status = styling_1.colors.dim('\nStatus Test\n─────────── ───────────────');
217
305
  for (const { retry_of: isRetry, status, test_file_name: test, } of results) {
218
- core_1.ux.action.status += `\n${status.padEnd(10, ' ')} ${test} ${isRetry ? '(retry)' : ''}`;
306
+ const statusFormatted = status.toUpperCase() === 'PASSED'
307
+ ? styling_1.colors.success(status.padEnd(10, ' '))
308
+ : status.toUpperCase() === 'FAILED'
309
+ ? styling_1.colors.error(status.padEnd(10, ' '))
310
+ : status.toUpperCase() === 'RUNNING'
311
+ ? styling_1.colors.info(status.padEnd(10, ' '))
312
+ : styling_1.colors.warning(status.padEnd(10, ' '));
313
+ const retryText = isRetry ? styling_1.colors.dim(' (retry)') : '';
314
+ core_1.ux.action.status += `\n${statusFormatted} ${test}${retryText}`;
219
315
  }
220
316
  }
221
317
  return previousSummary;
@@ -28,6 +28,17 @@ class TestSubmissionService {
28
28
  if (Object.keys(metadataObject).length > 0) {
29
29
  this.logDebug(debug, logger, `DEBUG: User metadata: ${JSON.stringify(metadataObject)}`);
30
30
  }
31
+ // Log non-YAML file assets being uploaded
32
+ if (referencedFiles.length > 0) {
33
+ const nonYamlFiles = referencedFiles.filter((file) => !file.endsWith('.yaml') && !file.endsWith('.yml'));
34
+ if (nonYamlFiles.length > 0) {
35
+ this.logDebug(debug, logger, `DEBUG: Uploading ${nonYamlFiles.length} non-YAML file asset(s):`);
36
+ for (const file of nonYamlFiles) {
37
+ const normalizedPath = this.normalizeFilePath(file, commonRoot);
38
+ this.logDebug(debug, logger, `DEBUG: - ${normalizedPath}`);
39
+ }
40
+ }
41
+ }
31
42
  this.logDebug(debug, logger, `DEBUG: Compressing files from path: ${flowFile}`);
32
43
  const buffer = await (0, methods_1.compressFilesFromRelativePath)(flowFile?.endsWith('.yaml') || flowFile?.endsWith('.yml')
33
44
  ? path.dirname(flowFile)
@@ -443,6 +443,21 @@ export interface components {
443
443
  path: string;
444
444
  token: string;
445
445
  id: string;
446
+ b2?: {
447
+ strategy: 'simple' | 'large';
448
+ simple?: {
449
+ uploadUrl: string;
450
+ authorizationToken: string;
451
+ };
452
+ large?: {
453
+ fileId: string;
454
+ fileName: string;
455
+ uploadPartUrls: Array<{
456
+ uploadUrl: string;
457
+ authorizationToken: string;
458
+ }>;
459
+ };
460
+ };
446
461
  };
447
462
  ICheckForExistingUploadArgs: {
448
463
  sha: string;