@devicecloud.dev/dcd 5.5.0-beta.5 → 5.6.0-beta.3
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/dist/commands/cloud.js +9 -2
- package/dist/mcp/tools/run-cloud-test.js +1 -1
- 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/results-polling.service.d.ts +8 -0
- package/dist/services/results-polling.service.js +15 -1
- package/dist/services/test-submission.service.js +13 -1
- package/dist/services/workspace-config.schema.d.ts +1 -0
- package/dist/services/workspace-config.schema.js +6 -0
- package/package.json +51 -11
package/dist/commands/cloud.js
CHANGED
|
@@ -402,15 +402,22 @@ export const cloudCommand = defineCommand({
|
|
|
402
402
|
}
|
|
403
403
|
throw error;
|
|
404
404
|
}
|
|
405
|
-
const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, } = executionPlan;
|
|
405
|
+
const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, includedFiles, referencedFiles, sequence, } = executionPlan;
|
|
406
406
|
if (debug) {
|
|
407
407
|
out(`[DEBUG] All include tags: ${allIncludeTags?.join(', ') || 'none'}`);
|
|
408
408
|
out(`[DEBUG] All exclude tags: ${allExcludeTags?.join(', ') || 'none'}`);
|
|
409
409
|
out(`[DEBUG] Test file names: ${testFileNames.join(', ')}`);
|
|
410
410
|
}
|
|
411
|
-
const commonRoot = computeCommonRoot(testFileNames, referencedFiles);
|
|
411
|
+
const commonRoot = computeCommonRoot(testFileNames, referencedFiles, includedFiles);
|
|
412
412
|
if (debug) {
|
|
413
413
|
out(`[DEBUG] Common root directory: ${commonRoot}`);
|
|
414
|
+
// `includedPaths` files sitting beside the flows tree rather than
|
|
415
|
+
// inside it raise the common root, so every server-side flow key gains
|
|
416
|
+
// a leading segment. Harmless but visible in the console, so say it.
|
|
417
|
+
const rootWithoutIncludes = computeCommonRoot(testFileNames, referencedFiles);
|
|
418
|
+
if (includedFiles.length > 0 && rootWithoutIncludes !== commonRoot) {
|
|
419
|
+
out(`[DEBUG] \`includedPaths\` raised the common root from ${rootWithoutIncludes} to ${commonRoot} — flow paths gain a leading segment`);
|
|
420
|
+
}
|
|
414
421
|
}
|
|
415
422
|
const testMetadataMap = buildTestMetadataMap(flowMetadata, commonRoot);
|
|
416
423
|
if (debug) {
|
|
@@ -120,7 +120,7 @@ export function registerRunCloudTest(server) {
|
|
|
120
120
|
// stdout is the JSON-RPC channel — config warnings must go to stderr.
|
|
121
121
|
warn: logStderr,
|
|
122
122
|
});
|
|
123
|
-
const commonRoot = computeCommonRoot(executionPlan.flowsToRun, executionPlan.referencedFiles);
|
|
123
|
+
const commonRoot = computeCommonRoot(executionPlan.flowsToRun, executionPlan.referencedFiles, executionPlan.includedFiles);
|
|
124
124
|
const testMetadataMap = buildTestMetadataMap(executionPlan.flowMetadata, commonRoot);
|
|
125
125
|
if (args.dryRun) {
|
|
126
126
|
return jsonResult({
|
|
@@ -21,6 +21,12 @@ export interface IExecutionPlan {
|
|
|
21
21
|
flowMetadata: Record<string, Record<string, unknown>>;
|
|
22
22
|
flowOverrides: Record<string, Record<string, unknown>>;
|
|
23
23
|
flowsToRun: string[];
|
|
24
|
+
/**
|
|
25
|
+
* Extra files pulled in by `config.yaml`'s `includedPaths`. Kept separate
|
|
26
|
+
* from `referencedFiles` (which is derived from flow commands) so the zip
|
|
27
|
+
* manifest and the common-root calculation can tell the two apart.
|
|
28
|
+
*/
|
|
29
|
+
includedFiles: string[];
|
|
24
30
|
referencedFiles: string[];
|
|
25
31
|
sequence?: IFlowSequence | null;
|
|
26
32
|
totalFlowFiles: number;
|
|
@@ -90,7 +90,7 @@ function extractDeviceCloudOverrides(config) {
|
|
|
90
90
|
* @param resolvedConfigFile - Optional absolute path to a custom config file
|
|
91
91
|
* @returns Execution plan for the single file with dependencies
|
|
92
92
|
*/
|
|
93
|
-
async function planSingleFile(normalizedInput, warn, resolvedConfigFile) {
|
|
93
|
+
async function planSingleFile(normalizedInput, warn, resolvedConfigFile, debug = false) {
|
|
94
94
|
const inputBasename = path.basename(normalizedInput);
|
|
95
95
|
if (inputBasename === 'config.yaml' ||
|
|
96
96
|
inputBasename === 'config.yml' ||
|
|
@@ -118,11 +118,18 @@ async function planSingleFile(normalizedInput, warn, resolvedConfigFile) {
|
|
|
118
118
|
`Pass the workspace folder instead so the named flows can be discovered and sequenced.`);
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
|
+
// A single-file input has no workspace directory, so `includedPaths` (which
|
|
122
|
+
// only reaches here via --config) anchors on the flow file's own directory —
|
|
123
|
+
// the same place Maestro resolves an assertScreenshot baseline from.
|
|
124
|
+
const includedFiles = workspaceConfig
|
|
125
|
+
? resolveIncludedPaths(workspaceConfig, path.dirname(normalizedInput), warn, debug)
|
|
126
|
+
: [];
|
|
121
127
|
const checkedDependancies = await checkDependencies(normalizedInput);
|
|
122
128
|
return {
|
|
123
129
|
flowMetadata,
|
|
124
130
|
flowOverrides,
|
|
125
131
|
flowsToRun: [normalizedInput],
|
|
132
|
+
includedFiles,
|
|
126
133
|
referencedFiles: [...new Set(checkedDependancies)],
|
|
127
134
|
totalFlowFiles: 1,
|
|
128
135
|
workspaceConfig,
|
|
@@ -176,6 +183,94 @@ async function applyFlowGlobs(workspaceConfig, normalizedInput, unfilteredFlowFi
|
|
|
176
183
|
}
|
|
177
184
|
return unfilteredFlowFiles.filter((file) => !isExcludedConfig(file));
|
|
178
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* The whole archive is buffered in memory by `compressFilesFromRelativePath`,
|
|
188
|
+
* so an unbounded `**` glob is a real footgun. These are warn thresholds, not
|
|
189
|
+
* hard limits — a legitimately large baseline set should still upload.
|
|
190
|
+
*/
|
|
191
|
+
const INCLUDED_PATHS_FILE_WARN_THRESHOLD = 200;
|
|
192
|
+
const INCLUDED_PATHS_BYTES_WARN_THRESHOLD = 50 * 1024 * 1024;
|
|
193
|
+
/**
|
|
194
|
+
* Resolve `config.yaml`'s `includedPaths` globs into absolute file paths.
|
|
195
|
+
*
|
|
196
|
+
* This is the general-purpose escape hatch for shipping files the flow
|
|
197
|
+
* commands don't reference: `assertScreenshot` baselines above all, but also
|
|
198
|
+
* fixtures, test data and certificates. Only `addMedia` / `runFlow` /
|
|
199
|
+
* `runScript` arguments are discovered by walking the flows, so without this
|
|
200
|
+
* key such files are silently absent from the uploaded zip.
|
|
201
|
+
*
|
|
202
|
+
* Glob semantics deliberately mirror `flows:` (`applyFlowGlobs`): patterns
|
|
203
|
+
* resolve against the workspace root, never the config file's directory, so
|
|
204
|
+
* `--config ci/workspace.yaml` behaves identically to an auto-detected config.
|
|
205
|
+
*
|
|
206
|
+
* @param workspaceConfig - Validated workspace config
|
|
207
|
+
* @param normalizedInput - Normalized path to the workspace directory
|
|
208
|
+
* @param warn - Sink for non-fatal problems
|
|
209
|
+
* @param debug - Whether to emit debug logging
|
|
210
|
+
* @returns Absolute paths of every matched file, deduped and sorted
|
|
211
|
+
* @throws Error if a pattern escapes the workspace root
|
|
212
|
+
*/
|
|
213
|
+
function resolveIncludedPaths(workspaceConfig, normalizedInput, warn, debug = false) {
|
|
214
|
+
const patterns = workspaceConfig.includedPaths;
|
|
215
|
+
if (!patterns || patterns.length === 0)
|
|
216
|
+
return [];
|
|
217
|
+
const workspaceRoot = path.resolve(normalizedInput);
|
|
218
|
+
const resolved = new Set();
|
|
219
|
+
const unmatched = [];
|
|
220
|
+
for (const pattern of patterns) {
|
|
221
|
+
// fs.globSync lands in Node 22; the CLI's `engines.node` already requires
|
|
222
|
+
// it. No `nodir` option — directories are stripped by the stat check below.
|
|
223
|
+
const matches = fs.globSync(pattern, { cwd: normalizedInput });
|
|
224
|
+
let matchedFile = false;
|
|
225
|
+
for (const match of matches) {
|
|
226
|
+
const absolute = path.resolve(normalizedInput, match);
|
|
227
|
+
// Containment guard: `flows:` has none because its matches are only ever
|
|
228
|
+
// parsed as YAML, but this key ships arbitrary bytes to a remote runner,
|
|
229
|
+
// so a `../../../` climb must not silently leave the workspace.
|
|
230
|
+
const relative = path.relative(workspaceRoot, absolute);
|
|
231
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
232
|
+
throw new Error(`\`includedPaths\` pattern "${pattern}" resolves outside the workspace: ${absolute}\n\n` +
|
|
233
|
+
`Included paths must stay within ${workspaceRoot}.`);
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
if (!fs.statSync(absolute).isFile())
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
matchedFile = true;
|
|
243
|
+
resolved.add(absolute);
|
|
244
|
+
}
|
|
245
|
+
if (!matchedFile)
|
|
246
|
+
unmatched.push(pattern);
|
|
247
|
+
}
|
|
248
|
+
if (unmatched.length > 0) {
|
|
249
|
+
warn(`Warning: \`includedPaths\` pattern(s) in config matched no files:\n` +
|
|
250
|
+
`${unmatched.map((pattern) => ` ${pattern}`).join('\n')}\n\n` +
|
|
251
|
+
`Patterns are resolved relative to ${workspaceRoot}.`);
|
|
252
|
+
}
|
|
253
|
+
const files = [...resolved].sort((a, b) => a.localeCompare(b));
|
|
254
|
+
let totalBytes = 0;
|
|
255
|
+
for (const file of files) {
|
|
256
|
+
try {
|
|
257
|
+
totalBytes += fs.statSync(file).size;
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// Raced away between glob and stat; the zip step reports it properly.
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (files.length > INCLUDED_PATHS_FILE_WARN_THRESHOLD ||
|
|
264
|
+
totalBytes > INCLUDED_PATHS_BYTES_WARN_THRESHOLD) {
|
|
265
|
+
warn(`Warning: \`includedPaths\` matched ${files.length} file(s) totalling ` +
|
|
266
|
+
`${Math.round(totalBytes / (1024 * 1024))} MB. The flow archive is built in ` +
|
|
267
|
+
`memory, so consider narrowing the patterns.`);
|
|
268
|
+
}
|
|
269
|
+
if (debug) {
|
|
270
|
+
console.log(`[DEBUG] includedPaths matched ${files.length} file(s):\n${files.join('\n')}`);
|
|
271
|
+
}
|
|
272
|
+
return files;
|
|
273
|
+
}
|
|
179
274
|
/**
|
|
180
275
|
* Resolve sequential execution order from workspace config
|
|
181
276
|
* @param workspaceConfig - Workspace configuration with executionOrder
|
|
@@ -241,7 +336,7 @@ export async function plan(options) {
|
|
|
241
336
|
throw new Error(`Flow path does not exist: ${path.resolve(normalizedInput)}`);
|
|
242
337
|
}
|
|
243
338
|
if (fs.lstatSync(normalizedInput).isFile()) {
|
|
244
|
-
return planSingleFile(normalizedInput, warn, resolvedConfigFile);
|
|
339
|
+
return planSingleFile(normalizedInput, warn, resolvedConfigFile, debug);
|
|
245
340
|
}
|
|
246
341
|
let unfilteredFlowFiles = await readDirectory(normalizedInput, isFlowFile);
|
|
247
342
|
if (unfilteredFlowFiles.length === 0) {
|
|
@@ -325,6 +420,7 @@ export async function plan(options) {
|
|
|
325
420
|
flowMetadata,
|
|
326
421
|
flowOverrides,
|
|
327
422
|
flowsToRun: normalFlows,
|
|
423
|
+
includedFiles: resolveIncludedPaths(workspaceConfig, normalizedInput, warn, debug),
|
|
328
424
|
referencedFiles: [...new Set(allFiles)],
|
|
329
425
|
sequence: {
|
|
330
426
|
continueOnFailure: workspaceConfig.executionOrder?.continueOnFailure,
|
|
@@ -2,7 +2,50 @@ import * as yaml from 'js-yaml';
|
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { parseWorkspaceConfig, WORKSPACE_CONFIG_KEYS, } from './workspace-config.schema.js';
|
|
5
|
-
const commandsThatRequireFiles = new Set([
|
|
5
|
+
const commandsThatRequireFiles = new Set([
|
|
6
|
+
'addMedia',
|
|
7
|
+
'assertScreenshot',
|
|
8
|
+
'runFlow',
|
|
9
|
+
'runScript',
|
|
10
|
+
]);
|
|
11
|
+
/**
|
|
12
|
+
* Commands whose file references are best-effort rather than mandatory.
|
|
13
|
+
*
|
|
14
|
+
* `assertScreenshot` baselines are legitimately absent on a first run, and
|
|
15
|
+
* Maestro's own "Screenshot file not found — searched in: …" error is more
|
|
16
|
+
* useful than ours, so a missing baseline must not abort the upload the way a
|
|
17
|
+
* missing `addMedia` file does.
|
|
18
|
+
*/
|
|
19
|
+
const commandsWithOptionalFiles = new Set(['assertScreenshot']);
|
|
20
|
+
/**
|
|
21
|
+
* Extensions Maestro's `normalizeScreenshotPath` recognises; anything else
|
|
22
|
+
* gets `.png` appended, so `assertScreenshot: home` means `home.png`.
|
|
23
|
+
*/
|
|
24
|
+
const SCREENSHOT_EXTENSIONS = new Set([
|
|
25
|
+
'.bmp',
|
|
26
|
+
'.gif',
|
|
27
|
+
'.heic',
|
|
28
|
+
'.heif',
|
|
29
|
+
'.jpeg',
|
|
30
|
+
'.jpg',
|
|
31
|
+
'.png',
|
|
32
|
+
'.tiff',
|
|
33
|
+
'.wbmp',
|
|
34
|
+
]);
|
|
35
|
+
/**
|
|
36
|
+
* Mirror Maestro's `Orchestra.normalizeScreenshotPath`: a screenshot path with
|
|
37
|
+
* no image extension gets `.png`. Without this, `assertScreenshot: home` looks
|
|
38
|
+
* like a missing file here while resolving fine on the device.
|
|
39
|
+
*
|
|
40
|
+
* @param relativePath - The path as written in the flow
|
|
41
|
+
* @returns The path with an image extension guaranteed
|
|
42
|
+
*/
|
|
43
|
+
function normalizeScreenshotPath(relativePath) {
|
|
44
|
+
const extension = path.extname(relativePath).toLowerCase();
|
|
45
|
+
return SCREENSHOT_EXTENSIONS.has(extension)
|
|
46
|
+
? relativePath
|
|
47
|
+
: `${relativePath}.png`;
|
|
48
|
+
}
|
|
6
49
|
export function getFlowsToRunInSequence(paths, flowOrder, debug = false) {
|
|
7
50
|
if (flowOrder.length === 0) {
|
|
8
51
|
if (debug) {
|
|
@@ -145,12 +188,27 @@ export const checkIfFilesExistInWorkspace = (commandName, command, absoluteFileP
|
|
|
145
188
|
const errors = [];
|
|
146
189
|
const files = [];
|
|
147
190
|
const directory = path.dirname(absoluteFilePath);
|
|
191
|
+
const isScreenshot = commandName === 'assertScreenshot';
|
|
192
|
+
const isOptional = commandsWithOptionalFiles.has(commandName);
|
|
148
193
|
const buildError = (error) => `Flow file "${absoluteFilePath}" has a command "${commandName}" that references a ${error} ${JSON.stringify(command)}`;
|
|
149
194
|
const processFilePath = (relativePath) => {
|
|
150
|
-
|
|
195
|
+
// A JS/variable-interpolated path (`screenshots/${DCD_DEVICE}/home`) can't
|
|
196
|
+
// be resolved without running the flow. Skip it rather than guessing — the
|
|
197
|
+
// config.yaml `includedPaths` key is how those files get bundled.
|
|
198
|
+
if (relativePath.includes('${'))
|
|
199
|
+
return;
|
|
200
|
+
const resolvedRelativePath = isScreenshot
|
|
201
|
+
? normalizeScreenshotPath(relativePath)
|
|
202
|
+
: relativePath;
|
|
203
|
+
const absoluteFilePath = path.normalize(path.resolve(directory, resolvedRelativePath));
|
|
151
204
|
const error = checkFile(absoluteFilePath);
|
|
152
|
-
if (error)
|
|
205
|
+
if (error) {
|
|
206
|
+
// Optional references drop out entirely when missing: pushing them onto
|
|
207
|
+
// `files` would put a non-existent path into the zip manifest.
|
|
208
|
+
if (isOptional)
|
|
209
|
+
return;
|
|
153
210
|
errors.push(buildError(error));
|
|
211
|
+
}
|
|
154
212
|
files.push(absoluteFilePath);
|
|
155
213
|
};
|
|
156
214
|
// simple command — processFilePath already resolves against `directory`
|
|
@@ -163,10 +221,15 @@ export const checkIfFilesExistInWorkspace = (commandName, command, absoluteFileP
|
|
|
163
221
|
processFilePath(file);
|
|
164
222
|
}
|
|
165
223
|
}
|
|
166
|
-
// object command
|
|
224
|
+
// object command. `file` is addMedia/runFlow/runScript; `path` is
|
|
225
|
+
// assertScreenshot's own key for the same thing.
|
|
167
226
|
const x = command; // prevent annoying ts error
|
|
168
|
-
if (typeof command === 'object' &&
|
|
169
|
-
|
|
227
|
+
if (typeof command === 'object' && !Array.isArray(command)) {
|
|
228
|
+
if (x?.file)
|
|
229
|
+
processFilePath(x.file);
|
|
230
|
+
if (isScreenshot && typeof x?.path === 'string')
|
|
231
|
+
processFilePath(x.path);
|
|
232
|
+
}
|
|
170
233
|
return { errors, files };
|
|
171
234
|
};
|
|
172
235
|
const checkFile = (filePath) => {
|
|
@@ -3,8 +3,16 @@
|
|
|
3
3
|
* file path. Segment comparison (not `startsWith`) so sibling dirs like
|
|
4
4
|
* `flows`/`flows-extra` can't merge, and the file segment itself is never
|
|
5
5
|
* consumed. Returns '' when the paths share no root at all (or none are given).
|
|
6
|
+
*
|
|
7
|
+
* `includedFiles` (config.yaml `includedPaths`) must be folded in for the same
|
|
8
|
+
* reason referenced files are: the zip strips this root as an anchored prefix,
|
|
9
|
+
* so a file outside it would get a non-relative entry name. Folding them in
|
|
10
|
+
* can raise the root — flows in `flows/` beside baselines in `screenshots/`
|
|
11
|
+
* shifts it from `<root>/flows` to `<root>`, so flow keys gain a `flows/`
|
|
12
|
+
* segment. That shift is what preserves the flow→baseline relative offset
|
|
13
|
+
* Maestro resolves against, and is already how `addMedia` behaves.
|
|
6
14
|
*/
|
|
7
|
-
export declare function computeCommonRoot(testFileNames: string[], referencedFiles: string[]): string;
|
|
15
|
+
export declare function computeCommonRoot(testFileNames: string[], referencedFiles: string[], includedFiles?: string[]): string;
|
|
8
16
|
export interface FlowMetadataEntry {
|
|
9
17
|
flowName: string;
|
|
10
18
|
tags: string[];
|
|
@@ -12,9 +12,21 @@ import { toPortableRelativePath } from '../utils/paths.js';
|
|
|
12
12
|
* file path. Segment comparison (not `startsWith`) so sibling dirs like
|
|
13
13
|
* `flows`/`flows-extra` can't merge, and the file segment itself is never
|
|
14
14
|
* consumed. Returns '' when the paths share no root at all (or none are given).
|
|
15
|
+
*
|
|
16
|
+
* `includedFiles` (config.yaml `includedPaths`) must be folded in for the same
|
|
17
|
+
* reason referenced files are: the zip strips this root as an anchored prefix,
|
|
18
|
+
* so a file outside it would get a non-relative entry name. Folding them in
|
|
19
|
+
* can raise the root — flows in `flows/` beside baselines in `screenshots/`
|
|
20
|
+
* shifts it from `<root>/flows` to `<root>`, so flow keys gain a `flows/`
|
|
21
|
+
* segment. That shift is what preserves the flow→baseline relative offset
|
|
22
|
+
* Maestro resolves against, and is already how `addMedia` behaves.
|
|
15
23
|
*/
|
|
16
|
-
export function computeCommonRoot(testFileNames, referencedFiles) {
|
|
17
|
-
const pathsShortestToLongest = [
|
|
24
|
+
export function computeCommonRoot(testFileNames, referencedFiles, includedFiles = []) {
|
|
25
|
+
const pathsShortestToLongest = [
|
|
26
|
+
...testFileNames,
|
|
27
|
+
...referencedFiles,
|
|
28
|
+
...includedFiles,
|
|
29
|
+
].sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
|
|
18
30
|
if (pathsShortestToLongest.length === 0)
|
|
19
31
|
return '';
|
|
20
32
|
const splitPaths = pathsShortestToLongest.map((p) => p.split(path.sep));
|
|
@@ -57,6 +57,14 @@ export declare function deviceFromResultRow(r: {
|
|
|
57
57
|
export declare function isSupersededRow(r: unknown): boolean;
|
|
58
58
|
/** The upload that superseded this run, for the console link. */
|
|
59
59
|
export declare function supersedingUploadId(results: unknown[]): string | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* The superseding run's console link, derived from this run's.
|
|
62
|
+
*
|
|
63
|
+
* The `result` parameter deep-links a single test, and that id belongs to
|
|
64
|
+
* THIS upload — carrying it over would point at a result the newer upload
|
|
65
|
+
* does not contain. Swap the upload id and drop it.
|
|
66
|
+
*/
|
|
67
|
+
export declare function supersedingConsoleUrl(consoleUrl: string, uploadId: string, supersededBy: string): string;
|
|
60
68
|
export interface PollingResult {
|
|
61
69
|
consoleUrl: string;
|
|
62
70
|
status: 'FAILED' | 'PASSED' | 'SUPERSEDED';
|
|
@@ -63,6 +63,20 @@ export function supersedingUploadId(results) {
|
|
|
63
63
|
}
|
|
64
64
|
return undefined;
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* The superseding run's console link, derived from this run's.
|
|
68
|
+
*
|
|
69
|
+
* The `result` parameter deep-links a single test, and that id belongs to
|
|
70
|
+
* THIS upload — carrying it over would point at a result the newer upload
|
|
71
|
+
* does not contain. Swap the upload id and drop it.
|
|
72
|
+
*/
|
|
73
|
+
export function supersedingConsoleUrl(consoleUrl, uploadId, supersededBy) {
|
|
74
|
+
return consoleUrl
|
|
75
|
+
.replace(uploadId, supersededBy)
|
|
76
|
+
.replace(/&result=[^&]*/, '')
|
|
77
|
+
.replace(/\?result=[^&]*&/, '?')
|
|
78
|
+
.replace(/\?result=[^&]*$/, '');
|
|
79
|
+
}
|
|
66
80
|
/**
|
|
67
81
|
* Service for polling test results from the API
|
|
68
82
|
*/
|
|
@@ -438,7 +452,7 @@ export class ResultsPollingService {
|
|
|
438
452
|
logger(ui.branch(ui.fields([
|
|
439
453
|
[
|
|
440
454
|
'superseded by',
|
|
441
|
-
colors.url(consoleUrl
|
|
455
|
+
colors.url(supersedingConsoleUrl(consoleUrl, uploadId, newer)),
|
|
442
456
|
],
|
|
443
457
|
])));
|
|
444
458
|
}
|
|
@@ -21,7 +21,7 @@ export class TestSubmissionService {
|
|
|
21
21
|
*/
|
|
22
22
|
async buildTestPayload(config) {
|
|
23
23
|
const { apiUrl, appBinaryId, 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, referencedFiles, sequence, workspaceConfig, } = executionPlan;
|
|
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`);
|
|
@@ -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
|
};
|
package/package.json
CHANGED
|
@@ -57,9 +57,19 @@
|
|
|
57
57
|
},
|
|
58
58
|
"repository": {
|
|
59
59
|
"type": "git",
|
|
60
|
-
"url": "https://devicecloud.
|
|
60
|
+
"url": "git+https://github.com/devicecloud-dev/dcd-cli.git"
|
|
61
61
|
},
|
|
62
|
-
"
|
|
62
|
+
"scripts": {
|
|
63
|
+
"dcd": "tsx src/index.ts",
|
|
64
|
+
"build": "shx rm -rf dist && tsc -b && shx chmod +x dist/index.js dist/mcp/index.js",
|
|
65
|
+
"build:binaries": "node scripts/build-binaries.mjs",
|
|
66
|
+
"lint": "eslint src test --ext .ts",
|
|
67
|
+
"prepare": "pnpm build && husky",
|
|
68
|
+
"test": "node scripts/test-runner.mjs",
|
|
69
|
+
"test:unit": "node scripts/test-runner.mjs --unit",
|
|
70
|
+
"typecheck": "tsc --noEmit -p tsconfig.test.json"
|
|
71
|
+
},
|
|
72
|
+
"version": "5.6.0-beta.3",
|
|
63
73
|
"bugs": {
|
|
64
74
|
"url": "https://discord.gg/gm3mJwcNw8"
|
|
65
75
|
},
|
|
@@ -72,13 +82,43 @@
|
|
|
72
82
|
"testing"
|
|
73
83
|
],
|
|
74
84
|
"types": "dist/index.d.ts",
|
|
75
|
-
"
|
|
76
|
-
|
|
77
|
-
"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
"packageManager": "pnpm@10.17.1",
|
|
86
|
+
"pnpm": {
|
|
87
|
+
"overrides": {
|
|
88
|
+
"js-yaml@<3.14.2": ">=3.14.2",
|
|
89
|
+
"js-yaml@>=4.0.0 <4.2.0": ">=4.2.0",
|
|
90
|
+
"js-yaml@>=5.0.0 <5.2.2": ">=5.2.2",
|
|
91
|
+
"tar@<7.5.16": ">=7.5.16",
|
|
92
|
+
"@isaacs/brace-expansion": ">=5.0.1",
|
|
93
|
+
"fast-xml-parser": ">=5.5.7",
|
|
94
|
+
"flatted": ">=3.4.2",
|
|
95
|
+
"lodash": ">=4.18.0",
|
|
96
|
+
"@xmldom/xmldom": ">=0.9.12",
|
|
97
|
+
"minimatch@<3.1.4": "3.1.4",
|
|
98
|
+
"minimatch@>=5.0.0 <5.1.8": "5.1.8",
|
|
99
|
+
"minimatch@>=9.0.0 <9.0.7": "9.0.7",
|
|
100
|
+
"minimatch@>=10.0.0 <10.2.3": "10.2.3",
|
|
101
|
+
"picomatch@<2.3.2": "2.3.2",
|
|
102
|
+
"picomatch@>=4.0.0 <4.0.4": "4.0.4",
|
|
103
|
+
"serialize-javascript": ">=7.0.5",
|
|
104
|
+
"ajv@<7.0.0": "6.14.0",
|
|
105
|
+
"diff@>=4.0.0 <6.0.0": "4.0.4",
|
|
106
|
+
"diff@>=6.0.0": "8.0.3",
|
|
107
|
+
"brace-expansion@<1.1.16": "1.1.16",
|
|
108
|
+
"brace-expansion@>=2.0.0 <2.0.3": "2.0.3",
|
|
109
|
+
"brace-expansion@>=3.0.0 <5.0.9": "5.0.9",
|
|
110
|
+
"ws@>=8.0.0 <8.21.0": "8.21.0",
|
|
111
|
+
"esbuild@<0.28.1": ">=0.28.1",
|
|
112
|
+
"qs@<6.16.0": "6.16.0",
|
|
113
|
+
"fast-uri@>=3.0.0 <3.1.6": "3.1.6",
|
|
114
|
+
"hono@>=4.0.0 <4.13.5": "4.13.5",
|
|
115
|
+
"micromatch>picomatch": "^2.3.2",
|
|
116
|
+
"tinyglobby>picomatch": "^4.0.4"
|
|
117
|
+
},
|
|
118
|
+
"auditConfig": {
|
|
119
|
+
"ignoreGhsas": [
|
|
120
|
+
"GHSA-frvp-7c67-39w9"
|
|
121
|
+
]
|
|
122
|
+
}
|
|
83
123
|
}
|
|
84
|
-
}
|
|
124
|
+
}
|