@devicecloud.dev/dcd 5.5.0 → 5.6.0-beta.4
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 +4 -2
- package/dist/commands/artifacts.d.ts +1 -1
- package/dist/commands/artifacts.js +1 -1
- package/dist/commands/cloud.d.ts +9 -4
- package/dist/commands/cloud.js +25 -8
- package/dist/commands/switch-org.js +5 -1
- package/dist/commands/upload.js +5 -1
- package/dist/commands/whoami.d.ts +7 -1
- package/dist/commands/whoami.js +26 -2
- package/dist/config/flags/device.flags.d.ts +3 -3
- package/dist/config/flags/device.flags.js +5 -3
- package/dist/config/flags/execution.flags.d.ts +5 -0
- package/dist/config/flags/execution.flags.js +5 -0
- package/dist/config/flags/output.flags.d.ts +1 -1
- package/dist/config/flags/output.flags.js +1 -1
- package/dist/constants.d.ts +9 -4
- package/dist/gateways/api-gateway.js +2 -1
- package/dist/gateways/realtime-gateway.d.ts +7 -0
- package/dist/gateways/realtime-gateway.js +12 -1
- package/dist/mcp/context.d.ts +14 -3
- package/dist/mcp/context.js +15 -3
- package/dist/mcp/tools/download-artifacts.js +9 -6
- package/dist/mcp/tools/list-devices.js +1 -1
- package/dist/mcp/tools/run-cloud-test.js +20 -4
- package/dist/services/device-validation.service.d.ts +17 -2
- package/dist/services/device-validation.service.js +93 -12
- package/dist/services/execution-plan.service.d.ts +6 -0
- package/dist/services/execution-plan.service.js +98 -2
- package/dist/services/execution-plan.utils.js +69 -6
- package/dist/services/flow-paths.d.ts +9 -1
- package/dist/services/flow-paths.js +14 -2
- package/dist/services/notices.service.js +3 -0
- package/dist/services/report-download.service.js +9 -3
- package/dist/services/results-polling.service.d.ts +42 -1
- package/dist/services/results-polling.service.js +133 -7
- package/dist/services/test-submission.service.d.ts +18 -0
- package/dist/services/test-submission.service.js +30 -6
- package/dist/services/workspace-config.schema.d.ts +1 -0
- package/dist/services/workspace-config.schema.js +6 -0
- package/dist/types/domain/auth.types.d.ts +6 -0
- package/dist/types/domain/device.types.d.ts +18 -4
- package/dist/types/domain/device.types.js +17 -3
- package/dist/types/generated/schema.types.d.ts +127 -167
- package/dist/utils/auth.d.ts +33 -0
- package/dist/utils/auth.js +61 -2
- package/package.json +52 -12
|
@@ -6,6 +6,14 @@ export declare class RunFailedError extends Error {
|
|
|
6
6
|
result: PollingResult;
|
|
7
7
|
constructor(result: PollingResult);
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* The `dcd login` session behind a poll expired and only a new login can fix
|
|
11
|
+
* it, so the poll stops at once instead of spending its retry budget — meant
|
|
12
|
+
* for network blips — on a failure that can't clear.
|
|
13
|
+
*/
|
|
14
|
+
export declare class PollingAuthError extends Error {
|
|
15
|
+
constructor(message: string);
|
|
16
|
+
}
|
|
9
17
|
export interface PollingOptions {
|
|
10
18
|
auth: AuthContext;
|
|
11
19
|
apiUrl: string;
|
|
@@ -45,9 +53,29 @@ export declare function deviceFromResultRow(r: {
|
|
|
45
53
|
config?: unknown;
|
|
46
54
|
simulator_name?: string | null;
|
|
47
55
|
}): TestDevice | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Was this row cancelled because a newer run from the same CI context
|
|
58
|
+
* replaced it (`dcd cloud --cancel-previous`)?
|
|
59
|
+
*
|
|
60
|
+
* Read through a structural cast for the same reason deviceFromResultRow
|
|
61
|
+
* does: the committed generated types are regenerated wholesale from dev's
|
|
62
|
+
* swagger and lag the API, and this must work against an API that already
|
|
63
|
+
* sends the field.
|
|
64
|
+
*/
|
|
65
|
+
export declare function isSupersededRow(r: unknown): boolean;
|
|
66
|
+
/** The upload that superseded this run, for the console link. */
|
|
67
|
+
export declare function supersedingUploadId(results: unknown[]): string | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* The superseding run's console link, derived from this run's.
|
|
70
|
+
*
|
|
71
|
+
* The `result` parameter deep-links a single test, and that id belongs to
|
|
72
|
+
* THIS upload — carrying it over would point at a result the newer upload
|
|
73
|
+
* does not contain. Swap the upload id and drop it.
|
|
74
|
+
*/
|
|
75
|
+
export declare function supersedingConsoleUrl(consoleUrl: string, uploadId: string, supersededBy: string): string;
|
|
48
76
|
export interface PollingResult {
|
|
49
77
|
consoleUrl: string;
|
|
50
|
-
status: 'FAILED' | 'PASSED';
|
|
78
|
+
status: 'FAILED' | 'PASSED' | 'SUPERSEDED';
|
|
51
79
|
tests: Array<{
|
|
52
80
|
/** Device this result ran on (present when the API reports it). */
|
|
53
81
|
device?: TestDevice;
|
|
@@ -95,6 +123,19 @@ export declare class ResultsPollingService {
|
|
|
95
123
|
* @returns Promise resolving to test results
|
|
96
124
|
*/
|
|
97
125
|
private fetchAndLogResults;
|
|
126
|
+
/**
|
|
127
|
+
* Swap in a fresh `dcd login` token when the current one is about to
|
|
128
|
+
* expire, and hand it to the realtime socket too; API-key auth comes back
|
|
129
|
+
* as-is. A transient refresh failure is thrown as-is, to be retried like
|
|
130
|
+
* any failed poll; one only `dcd login` can fix becomes a PollingAuthError.
|
|
131
|
+
* @param auth The credential the previous poll used
|
|
132
|
+
* @param uploadId Upload being polled, for the reconnect hint
|
|
133
|
+
* @param subscription Realtime subscription to pass a new token to
|
|
134
|
+
* @param debug Whether debug logging is enabled
|
|
135
|
+
* @param logger Optional logger function
|
|
136
|
+
* @returns The credential for the next poll
|
|
137
|
+
*/
|
|
138
|
+
private refreshPollingAuth;
|
|
98
139
|
private filterLatestResults;
|
|
99
140
|
/**
|
|
100
141
|
* Handle completed tests and return final result
|
|
@@ -2,6 +2,7 @@ import * as path from 'node:path';
|
|
|
2
2
|
import { ApiGateway } from '../gateways/api-gateway.js';
|
|
3
3
|
import { RealtimeResultsGateway, } from '../gateways/realtime-gateway.js';
|
|
4
4
|
import { formatDurationSeconds } from '../methods.js';
|
|
5
|
+
import { isDefinitiveAuthFailure, refreshAuth } from '../utils/auth.js';
|
|
5
6
|
import { checkInternetConnectivity } from '../utils/connectivity.js';
|
|
6
7
|
import { isCI } from '../utils/ci.js';
|
|
7
8
|
import { ux } from '../utils/progress.js';
|
|
@@ -18,6 +19,17 @@ export class RunFailedError extends Error {
|
|
|
18
19
|
this.name = 'RunFailedError';
|
|
19
20
|
}
|
|
20
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* The `dcd login` session behind a poll expired and only a new login can fix
|
|
24
|
+
* it, so the poll stops at once instead of spending its retry budget — meant
|
|
25
|
+
* for network blips — on a failure that can't clear.
|
|
26
|
+
*/
|
|
27
|
+
export class PollingAuthError extends Error {
|
|
28
|
+
constructor(message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = 'PollingAuthError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
21
33
|
/**
|
|
22
34
|
* Structured device for a result row. Prefers the friendly deviceName/osVersion
|
|
23
35
|
* the per-flow targeting / matrix fan-out stamps onto each result's config
|
|
@@ -38,6 +50,45 @@ export function deviceFromResultRow(r) {
|
|
|
38
50
|
googlePlay: sim ? /(_PLAY|-play)$/.test(sim) : undefined,
|
|
39
51
|
};
|
|
40
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Was this row cancelled because a newer run from the same CI context
|
|
55
|
+
* replaced it (`dcd cloud --cancel-previous`)?
|
|
56
|
+
*
|
|
57
|
+
* Read through a structural cast for the same reason deviceFromResultRow
|
|
58
|
+
* does: the committed generated types are regenerated wholesale from dev's
|
|
59
|
+
* swagger and lag the API, and this must work against an API that already
|
|
60
|
+
* sends the field.
|
|
61
|
+
*/
|
|
62
|
+
export function isSupersededRow(r) {
|
|
63
|
+
const reason = r
|
|
64
|
+
?.cancellation_reason;
|
|
65
|
+
return typeof reason === 'string' && reason.startsWith('superseded_by:');
|
|
66
|
+
}
|
|
67
|
+
/** The upload that superseded this run, for the console link. */
|
|
68
|
+
export function supersedingUploadId(results) {
|
|
69
|
+
for (const r of results) {
|
|
70
|
+
const reason = r
|
|
71
|
+
?.cancellation_reason;
|
|
72
|
+
if (typeof reason === 'string' && reason.startsWith('superseded_by:')) {
|
|
73
|
+
return reason.slice('superseded_by:'.length) || undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The superseding run's console link, derived from this run's.
|
|
80
|
+
*
|
|
81
|
+
* The `result` parameter deep-links a single test, and that id belongs to
|
|
82
|
+
* THIS upload — carrying it over would point at a result the newer upload
|
|
83
|
+
* does not contain. Swap the upload id and drop it.
|
|
84
|
+
*/
|
|
85
|
+
export function supersedingConsoleUrl(consoleUrl, uploadId, supersededBy) {
|
|
86
|
+
return consoleUrl
|
|
87
|
+
.replace(uploadId, supersededBy)
|
|
88
|
+
.replace(/&result=[^&]*/, '')
|
|
89
|
+
.replace(/\?result=[^&]*&/, '?')
|
|
90
|
+
.replace(/\?result=[^&]*$/, '');
|
|
91
|
+
}
|
|
41
92
|
/**
|
|
42
93
|
* Service for polling test results from the API
|
|
43
94
|
*/
|
|
@@ -76,7 +127,10 @@ export class ResultsPollingService {
|
|
|
76
127
|
}
|
|
77
128
|
}
|
|
78
129
|
async pollLoop(options, testMetadata) {
|
|
79
|
-
const { apiUrl,
|
|
130
|
+
const { apiUrl, uploadId, consoleUrl, quiet = false, json = false, debug = false, logger } = options;
|
|
131
|
+
// Re-checked before every poll: a run can outlast a `dcd login` access
|
|
132
|
+
// token (about an hour). API-key auth passes through unchanged.
|
|
133
|
+
let { auth } = options;
|
|
80
134
|
this.initializePollingDisplay(json, logger);
|
|
81
135
|
let sequentialPollFailures = 0;
|
|
82
136
|
const pollIntervalMs = auth.mode === 'bearer'
|
|
@@ -174,6 +228,7 @@ export class ResultsPollingService {
|
|
|
174
228
|
try {
|
|
175
229
|
nextPollAt = null;
|
|
176
230
|
renderStatus();
|
|
231
|
+
auth = await this.refreshPollingAuth(auth, uploadId, subscription, debug, logger);
|
|
177
232
|
const updatedResults = await this.fetchAndLogResults(apiUrl, auth, uploadId, debug, logger);
|
|
178
233
|
const { summary } = this.calculateStatusSummary(updatedResults);
|
|
179
234
|
if (!json) {
|
|
@@ -199,8 +254,9 @@ export class ResultsPollingService {
|
|
|
199
254
|
await waitForNextPoll(pollIntervalMs);
|
|
200
255
|
}
|
|
201
256
|
catch (error) {
|
|
202
|
-
// Re-throw RunFailedError immediately (test failures, not polling
|
|
203
|
-
|
|
257
|
+
// Re-throw RunFailedError immediately (test failures, not polling
|
|
258
|
+
// errors), and an expired login no retry can fix.
|
|
259
|
+
if (error instanceof RunFailedError || error instanceof PollingAuthError) {
|
|
204
260
|
throw error;
|
|
205
261
|
}
|
|
206
262
|
sequentialPollFailures++;
|
|
@@ -226,13 +282,23 @@ export class ResultsPollingService {
|
|
|
226
282
|
}
|
|
227
283
|
buildPollingResult(results, uploadId, consoleUrl, testMetadata) {
|
|
228
284
|
const resultsWithoutEarlierTries = this.filterLatestResults(results);
|
|
285
|
+
// ANY superseded row settles the whole verdict, even alongside a test
|
|
286
|
+
// that had genuinely failed before the newer run replaced this one: this
|
|
287
|
+
// run no longer speaks for the commit, so failing the build on its behalf
|
|
288
|
+
// is wrong. Actions reaches the same conclusion — a cancelled run's
|
|
289
|
+
// conclusion is `cancelled`, whatever had already failed inside it. The
|
|
290
|
+
// failure is still printed and still in `tests[]`; only the exit code
|
|
291
|
+
// changes.
|
|
292
|
+
const superseded = resultsWithoutEarlierTries.some(isSupersededRow);
|
|
229
293
|
return {
|
|
230
294
|
consoleUrl,
|
|
231
295
|
// Anything other than an explicit pass (CANCELLED, ERROR, a status we
|
|
232
296
|
// don't know about yet) must fail the run — this gates CI exit codes.
|
|
233
|
-
status:
|
|
234
|
-
? '
|
|
235
|
-
: '
|
|
297
|
+
status: superseded
|
|
298
|
+
? 'SUPERSEDED'
|
|
299
|
+
: resultsWithoutEarlierTries.every((result) => result.status === 'PASSED')
|
|
300
|
+
? 'PASSED'
|
|
301
|
+
: 'FAILED',
|
|
236
302
|
tests: resultsWithoutEarlierTries.map((r) => ({
|
|
237
303
|
// r carries config/simulator_name at runtime; the committed generated
|
|
238
304
|
// types lag the API (regenerated wholesale from dev's swagger), so read
|
|
@@ -259,8 +325,12 @@ export class ResultsPollingService {
|
|
|
259
325
|
const pending = statusCounts.PENDING || 0;
|
|
260
326
|
const queued = statusCounts.QUEUED || 0;
|
|
261
327
|
const running = statusCounts.RUNNING || 0;
|
|
328
|
+
// CANCELLED is terminal, so it counts as completed. Without it a
|
|
329
|
+
// cancelled or superseded run's footer sticks at "8/12 completed"
|
|
330
|
+
// forever, having already stopped polling.
|
|
331
|
+
const cancelled = statusCounts.CANCELLED || 0;
|
|
262
332
|
const total = results.length;
|
|
263
|
-
const completed = passed + failed;
|
|
333
|
+
const completed = passed + failed + cancelled;
|
|
264
334
|
const summary = formatTestSummary({
|
|
265
335
|
completed,
|
|
266
336
|
failed,
|
|
@@ -346,6 +416,41 @@ export class ResultsPollingService {
|
|
|
346
416
|
}
|
|
347
417
|
return updatedResults;
|
|
348
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* Swap in a fresh `dcd login` token when the current one is about to
|
|
421
|
+
* expire, and hand it to the realtime socket too; API-key auth comes back
|
|
422
|
+
* as-is. A transient refresh failure is thrown as-is, to be retried like
|
|
423
|
+
* any failed poll; one only `dcd login` can fix becomes a PollingAuthError.
|
|
424
|
+
* @param auth The credential the previous poll used
|
|
425
|
+
* @param uploadId Upload being polled, for the reconnect hint
|
|
426
|
+
* @param subscription Realtime subscription to pass a new token to
|
|
427
|
+
* @param debug Whether debug logging is enabled
|
|
428
|
+
* @param logger Optional logger function
|
|
429
|
+
* @returns The credential for the next poll
|
|
430
|
+
*/
|
|
431
|
+
async refreshPollingAuth(auth, uploadId, subscription, debug, logger) {
|
|
432
|
+
let next;
|
|
433
|
+
try {
|
|
434
|
+
next = await refreshAuth(auth);
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
if (isDefinitiveAuthFailure(error)) {
|
|
438
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
439
|
+
throw new PollingAuthError(`Your dcd login session expired and could not be refreshed: ${reason}\n\n` +
|
|
440
|
+
`The test is still running in the cloud. Run \`dcd login\`, then reconnect with:\n dcd status --upload-id ${uploadId}`);
|
|
441
|
+
}
|
|
442
|
+
throw error;
|
|
443
|
+
}
|
|
444
|
+
if (next !== auth) {
|
|
445
|
+
if (debug && logger) {
|
|
446
|
+
logger('[DEBUG] Refreshed the dcd login session for polling');
|
|
447
|
+
}
|
|
448
|
+
if (next.accessToken && next.accessToken !== auth.accessToken) {
|
|
449
|
+
subscription?.updateAccessToken(next.accessToken);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return next;
|
|
453
|
+
}
|
|
349
454
|
filterLatestResults(results) {
|
|
350
455
|
// Resolve each row to the root of its retry chain (a retry's `retry_of`
|
|
351
456
|
// may point at the previous retry rather than the original attempt), then
|
|
@@ -386,6 +491,27 @@ export class ResultsPollingService {
|
|
|
386
491
|
}
|
|
387
492
|
this.displayFinalResults(updatedResults, consoleUrl, json, logger);
|
|
388
493
|
const output = this.buildPollingResult(updatedResults, uploadId, consoleUrl, testMetadata);
|
|
494
|
+
if (output.status === 'SUPERSEDED') {
|
|
495
|
+
// Exit 0: a newer run of the same CI context replaced this one, so
|
|
496
|
+
// failing the build here would fail it for work nobody is waiting on.
|
|
497
|
+
// Falls through to the success return below — RunFailedError, and with
|
|
498
|
+
// it the exit code 2 in `dcd cloud`, is never reached.
|
|
499
|
+
if (logger && !json) {
|
|
500
|
+
const newer = supersedingUploadId(updatedResults);
|
|
501
|
+
logger('\n');
|
|
502
|
+
logger(ui.warn('Run superseded by a newer run from the same CI context — exiting 0'));
|
|
503
|
+
if (newer) {
|
|
504
|
+
logger(ui.branch(ui.fields([
|
|
505
|
+
[
|
|
506
|
+
'superseded by',
|
|
507
|
+
colors.url(supersedingConsoleUrl(consoleUrl, uploadId, newer)),
|
|
508
|
+
],
|
|
509
|
+
])));
|
|
510
|
+
}
|
|
511
|
+
logger('\n');
|
|
512
|
+
}
|
|
513
|
+
return output;
|
|
514
|
+
}
|
|
389
515
|
if (output.status === 'FAILED') {
|
|
390
516
|
if (debug && logger) {
|
|
391
517
|
logger(`[DEBUG] Some tests failed, returning failed status`);
|
|
@@ -6,12 +6,30 @@ export interface TestSubmissionConfig {
|
|
|
6
6
|
androidNoSnapshot?: boolean;
|
|
7
7
|
apiUrl?: string;
|
|
8
8
|
appBinaryId: string;
|
|
9
|
+
/**
|
|
10
|
+
* Platform of the app binary, when the CLI can tell from a local file
|
|
11
|
+
* (.apk → android; .app / .zip / Expo .tar.gz → ios). Unknown for
|
|
12
|
+
* --app-binary-id, which falls back to the device flags.
|
|
13
|
+
*/
|
|
14
|
+
appPlatform?: 'android' | 'ios';
|
|
15
|
+
/**
|
|
16
|
+
* Ask the API to cancel the still-queued tests of the previous run from
|
|
17
|
+
* the same CI context. Sent as its own field rather than inside `config`,
|
|
18
|
+
* which is stamped onto every result row and shipped to the runner — this
|
|
19
|
+
* is a one-off submission directive, not run configuration.
|
|
20
|
+
*/
|
|
21
|
+
cancelPrevious?: boolean;
|
|
9
22
|
cliVersion: string;
|
|
10
23
|
commonRoot: string;
|
|
11
24
|
continueOnFailure?: boolean;
|
|
12
25
|
debug?: boolean;
|
|
13
26
|
deviceLocale?: string;
|
|
14
27
|
deviceMatrix?: DeviceMatrixConfig[];
|
|
28
|
+
/**
|
|
29
|
+
* --disable-animations as given on the command line: true / false when the
|
|
30
|
+
* flag (or --no-disable-animations) was passed, undefined when it wasn't —
|
|
31
|
+
* only then does the workspace config's per-platform value apply.
|
|
32
|
+
*/
|
|
15
33
|
disableAnimations?: boolean;
|
|
16
34
|
/**
|
|
17
35
|
* Encrypt the flow zip and env vars before upload (#1151/#1152), each with its
|
|
@@ -20,8 +20,8 @@ export class TestSubmissionService {
|
|
|
20
20
|
* @returns The flow zip buffer, its SHA-256, and the string-encoded fields
|
|
21
21
|
*/
|
|
22
22
|
async buildTestPayload(config) {
|
|
23
|
-
const { apiUrl, appBinaryId, encrypt = false, flowFile, executionPlan, commonRoot, cliVersion, env = [], metadata = [], googlePlay = false, androidApiLevel, androidDevice, androidNoSnapshot, iOSVersion, iOSDevice, name, runnerType, maestroVersion, deviceLocale, orientation, retry, continueOnFailure = true, renderEngine, report, showCrosshairs, maestroChromeOnboarding, raw, disableAnimations, deviceMatrix, debug = false, logger, } = config;
|
|
24
|
-
const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, workspaceConfig, } = executionPlan;
|
|
23
|
+
const { apiUrl, appBinaryId, appPlatform, encrypt = false, flowFile, executionPlan, commonRoot, cliVersion, env = [], metadata = [], cancelPrevious = false, googlePlay = false, androidApiLevel, androidDevice, androidNoSnapshot, iOSVersion, iOSDevice, name, runnerType, maestroVersion, deviceLocale, orientation, retry, continueOnFailure = true, renderEngine, report, showCrosshairs, maestroChromeOnboarding, raw, disableAnimations, deviceMatrix, debug = false, logger, } = config;
|
|
24
|
+
const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, includedFiles = [], referencedFiles, sequence, workspaceConfig, } = executionPlan;
|
|
25
25
|
const { flows: sequentialFlows = [] } = sequence ?? {};
|
|
26
26
|
const envObject = this.parseKeyValuePairs(env);
|
|
27
27
|
const metadataObject = this.parseKeyValuePairs(metadata);
|
|
@@ -42,6 +42,17 @@ export class TestSubmissionService {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
// Logged separately from referencedFiles: these come from config.yaml's
|
|
46
|
+
// `includedPaths` rather than from a flow command, and they can raise the
|
|
47
|
+
// common root (see computeCommonRoot) — which shows up here as every flow
|
|
48
|
+
// key gaining a leading directory segment.
|
|
49
|
+
if (includedFiles.length > 0) {
|
|
50
|
+
this.logDebug(debug, logger, `[DEBUG] Uploading ${includedFiles.length} file(s) from \`includedPaths\`:`);
|
|
51
|
+
for (const file of includedFiles) {
|
|
52
|
+
const normalizedPath = this.normalizeFilePath(file, commonRoot);
|
|
53
|
+
this.logDebug(debug, logger, `[DEBUG] - ${normalizedPath}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
45
56
|
this.logDebug(debug, logger, `[DEBUG] Compressing files from path: ${flowFile}`);
|
|
46
57
|
const plaintextZip = await compressFilesFromRelativePath(flowFile?.endsWith('.yaml') || flowFile?.endsWith('.yml')
|
|
47
58
|
? path.dirname(flowFile)
|
|
@@ -50,6 +61,7 @@ export class TestSubmissionService {
|
|
|
50
61
|
...referencedFiles,
|
|
51
62
|
...testFileNames,
|
|
52
63
|
...sequentialFlows,
|
|
64
|
+
...includedFiles,
|
|
53
65
|
]),
|
|
54
66
|
], commonRoot);
|
|
55
67
|
this.logDebug(debug, logger, `[DEBUG] Compressed file size: ${plaintextZip.length} bytes`);
|
|
@@ -108,18 +120,25 @@ export class TestSubmissionService {
|
|
|
108
120
|
fields.deviceMatrix = JSON.stringify(deviceMatrix);
|
|
109
121
|
}
|
|
110
122
|
// Platform used only to pick which workspace-config disableAnimations flag
|
|
111
|
-
// applies.
|
|
112
|
-
//
|
|
123
|
+
// applies. The binary decides when the CLI knows it — otherwise an iOS run
|
|
124
|
+
// without --ios-device/--ios-version read the android block. For an
|
|
125
|
+
// --app-binary-id, a device matrix (single-platform; its first cell
|
|
126
|
+
// decides) or the scalar iOS flags are the best evidence left.
|
|
113
127
|
const matrixPlatform = deviceMatrix && deviceMatrix.length > 0
|
|
114
128
|
? 'iOSDevice' in deviceMatrix[0]
|
|
115
129
|
? 'ios'
|
|
116
130
|
: 'android'
|
|
117
131
|
: undefined;
|
|
118
|
-
const targetPlatform =
|
|
132
|
+
const targetPlatform = appPlatform ??
|
|
133
|
+
matrixPlatform ??
|
|
134
|
+
(iOSDevice || iOSVersion ? 'ios' : 'android');
|
|
119
135
|
const configYamlDisableAnimations = targetPlatform === 'ios'
|
|
120
136
|
? Boolean(workspaceConfig?.platform?.ios?.disableAnimations)
|
|
121
137
|
: Boolean(workspaceConfig?.platform?.android?.disableAnimations);
|
|
122
|
-
|
|
138
|
+
// An explicit --disable-animations / --no-disable-animations wins over
|
|
139
|
+
// config.yaml; previously `flag || config` meant the flag could only ever
|
|
140
|
+
// turn animations off, never keep them on against the config.
|
|
141
|
+
const effectiveDisableAnimations = disableAnimations ?? configYamlDisableAnimations;
|
|
123
142
|
const configPayload = {
|
|
124
143
|
allExcludeTags,
|
|
125
144
|
allIncludeTags,
|
|
@@ -144,6 +163,11 @@ export class TestSubmissionService {
|
|
|
144
163
|
fields.metadata = JSON.stringify(metadataPayload);
|
|
145
164
|
this.logDebug(debug, logger, `[DEBUG] Sending metadata to API: ${JSON.stringify(metadataPayload)}`);
|
|
146
165
|
}
|
|
166
|
+
// Only sent when asked for, so every other submission's wire shape is
|
|
167
|
+
// unchanged and the flag is simply ignored by an older API.
|
|
168
|
+
if (cancelPrevious) {
|
|
169
|
+
fields.cancelPrevious = 'true';
|
|
170
|
+
}
|
|
147
171
|
this.setOptionalFields(fields, {
|
|
148
172
|
androidApiLevel,
|
|
149
173
|
androidDevice,
|
|
@@ -12,6 +12,7 @@ export declare const WorkspaceConfigSchema: z.ZodObject<{
|
|
|
12
12
|
flowsOrder: z.ZodArray<z.ZodString>;
|
|
13
13
|
}, z.core.$strip>>>;
|
|
14
14
|
flows: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
15
|
+
includedPaths: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
15
16
|
includeTags: z.ZodOptional<z.ZodNullable<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>>;
|
|
16
17
|
local: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
17
18
|
deterministicOrder: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
@@ -42,6 +42,7 @@ export const WorkspaceConfigSchema = z.looseObject({
|
|
|
42
42
|
excludeTags: tagList.nullish(),
|
|
43
43
|
executionOrder: ExecutionOrderSchema.nullish(),
|
|
44
44
|
flows: z.array(z.string()).nullish(),
|
|
45
|
+
includedPaths: z.array(z.string()).nullish(),
|
|
45
46
|
includeTags: tagList.nullish(),
|
|
46
47
|
local: z
|
|
47
48
|
.looseObject({ deterministicOrder: z.boolean().nullish() })
|
|
@@ -80,10 +81,15 @@ export const WORKSPACE_CONFIG_KEYS = new Set(Object.keys(WorkspaceConfigSchema.s
|
|
|
80
81
|
* Near-misses that aren't just a casing slip on a real key. Keyed lowercase.
|
|
81
82
|
*/
|
|
82
83
|
const KEY_ALIASES = {
|
|
84
|
+
assets: 'includedPaths',
|
|
83
85
|
continueonfailure: 'executionOrder.continueOnFailure',
|
|
84
86
|
excludetag: 'excludeTags',
|
|
87
|
+
files: 'includedPaths',
|
|
85
88
|
floworder: 'executionOrder.flowsOrder',
|
|
86
89
|
flowsorder: 'executionOrder.flowsOrder',
|
|
90
|
+
includedpath: 'includedPaths',
|
|
91
|
+
includefiles: 'includedPaths',
|
|
92
|
+
includepaths: 'includedPaths',
|
|
87
93
|
includetag: 'includeTags',
|
|
88
94
|
tags: 'includeTags / excludeTags',
|
|
89
95
|
};
|
|
@@ -13,6 +13,12 @@ export interface AuthContext {
|
|
|
13
13
|
accessToken?: string;
|
|
14
14
|
/** Environment the session belongs to — present when mode === 'bearer'. */
|
|
15
15
|
env?: DcdEnvName;
|
|
16
|
+
/**
|
|
17
|
+
* Unix epoch seconds at which `accessToken` expires — present when
|
|
18
|
+
* mode === 'bearer'. Long-lived callers check it (utils/auth.ts
|
|
19
|
+
* isAuthExpiring / refreshAuth) because the headers below stop working then.
|
|
20
|
+
*/
|
|
21
|
+
expiresAt?: number;
|
|
16
22
|
/** Present when mode === 'bearer'. */
|
|
17
23
|
orgId?: string;
|
|
18
24
|
/** Present when mode === 'bearer'. */
|
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Device type definitions - should be kept in sync with API
|
|
3
|
-
* @see
|
|
3
|
+
* @see dcd/api/src/common/types/device.types.ts
|
|
4
|
+
*
|
|
5
|
+
* These drive the `--ios-device` / `--ios-version` / `--android-device` /
|
|
6
|
+
* `--android-api-level` help text and nothing else: validation runs against
|
|
7
|
+
* the live compatibility payload (device-validation.service.ts). A value the
|
|
8
|
+
* API has dropped gets a "not supported" error listing what it offers now, and
|
|
9
|
+
* one it has added is accepted even if it is missing here — so keeping these
|
|
10
|
+
* lists current only improves the help text; it never gates a run.
|
|
11
|
+
*
|
|
12
|
+
* `iphone-14-pro` and `iphone-15-pro` were listed here but have never existed
|
|
13
|
+
* in the API enum; removed 2026-09-21 rather than carried forward.
|
|
4
14
|
*/
|
|
5
15
|
export declare enum EiOSDevices {
|
|
6
16
|
'ipad-pro-6th-gen' = "ipad-pro-6th-gen",
|
|
17
|
+
'ipad-pro-m5-11' = "ipad-pro-m5-11",
|
|
18
|
+
'ipad-pro-m5-13' = "ipad-pro-m5-13",
|
|
7
19
|
'iphone-14' = "iphone-14",
|
|
8
|
-
'iphone-14-pro' = "iphone-14-pro",
|
|
9
20
|
'iphone-15' = "iphone-15",
|
|
10
|
-
'iphone-15-pro' = "iphone-15-pro",
|
|
11
21
|
'iphone-16' = "iphone-16",
|
|
12
22
|
'iphone-16-plus' = "iphone-16-plus",
|
|
13
23
|
'iphone-16-pro' = "iphone-16-pro",
|
|
14
|
-
'iphone-16-pro-max' = "iphone-16-pro-max"
|
|
24
|
+
'iphone-16-pro-max' = "iphone-16-pro-max",
|
|
25
|
+
'iphone-17' = "iphone-17",
|
|
26
|
+
'iphone-18-pro' = "iphone-18-pro",
|
|
27
|
+
'iphone-18-pro-max' = "iphone-18-pro-max",
|
|
28
|
+
'iphone-air' = "iphone-air"
|
|
15
29
|
}
|
|
16
30
|
export declare enum EAndroidDevices {
|
|
17
31
|
'generic-tablet' = "generic-tablet",
|
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Device type definitions - should be kept in sync with API
|
|
3
|
-
* @see
|
|
3
|
+
* @see dcd/api/src/common/types/device.types.ts
|
|
4
|
+
*
|
|
5
|
+
* These drive the `--ios-device` / `--ios-version` / `--android-device` /
|
|
6
|
+
* `--android-api-level` help text and nothing else: validation runs against
|
|
7
|
+
* the live compatibility payload (device-validation.service.ts). A value the
|
|
8
|
+
* API has dropped gets a "not supported" error listing what it offers now, and
|
|
9
|
+
* one it has added is accepted even if it is missing here — so keeping these
|
|
10
|
+
* lists current only improves the help text; it never gates a run.
|
|
11
|
+
*
|
|
12
|
+
* `iphone-14-pro` and `iphone-15-pro` were listed here but have never existed
|
|
13
|
+
* in the API enum; removed 2026-09-21 rather than carried forward.
|
|
4
14
|
*/
|
|
5
15
|
export var EiOSDevices;
|
|
6
16
|
(function (EiOSDevices) {
|
|
7
17
|
EiOSDevices["ipad-pro-6th-gen"] = "ipad-pro-6th-gen";
|
|
18
|
+
EiOSDevices["ipad-pro-m5-11"] = "ipad-pro-m5-11";
|
|
19
|
+
EiOSDevices["ipad-pro-m5-13"] = "ipad-pro-m5-13";
|
|
8
20
|
EiOSDevices["iphone-14"] = "iphone-14";
|
|
9
|
-
EiOSDevices["iphone-14-pro"] = "iphone-14-pro";
|
|
10
21
|
EiOSDevices["iphone-15"] = "iphone-15";
|
|
11
|
-
EiOSDevices["iphone-15-pro"] = "iphone-15-pro";
|
|
12
22
|
EiOSDevices["iphone-16"] = "iphone-16";
|
|
13
23
|
EiOSDevices["iphone-16-plus"] = "iphone-16-plus";
|
|
14
24
|
EiOSDevices["iphone-16-pro"] = "iphone-16-pro";
|
|
15
25
|
EiOSDevices["iphone-16-pro-max"] = "iphone-16-pro-max";
|
|
26
|
+
EiOSDevices["iphone-17"] = "iphone-17";
|
|
27
|
+
EiOSDevices["iphone-18-pro"] = "iphone-18-pro";
|
|
28
|
+
EiOSDevices["iphone-18-pro-max"] = "iphone-18-pro-max";
|
|
29
|
+
EiOSDevices["iphone-air"] = "iphone-air";
|
|
16
30
|
})(EiOSDevices || (EiOSDevices = {}));
|
|
17
31
|
export var EAndroidDevices;
|
|
18
32
|
(function (EAndroidDevices) {
|