@vizzly-testing/cli 0.35.1 → 0.35.3-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/api/endpoints.js +24 -3
- package/dist/api/index.js +1 -1
- package/dist/cli.js +16 -16
- package/dist/commands/comparisons.js +52 -45
- package/dist/commands/context.js +399 -135
- package/dist/commands/run.js +84 -29
- package/dist/commands/status.js +276 -59
- package/dist/commands/tdd.js +19 -3
- package/dist/context/local-workspace-provider.js +17 -7
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +5 -5
- package/dist/test-runner/operations.js +70 -28
- package/dist/types/index.d.ts +4 -2
- package/dist/uploader/core.js +6 -6
- package/dist/uploader/operations.js +4 -1
- package/dist/utils/visual-context-normalizers.js +446 -0
- package/package.json +4 -5
- package/skills/vizzly/SKILL.md +68 -101
- package/skills/vizzly/agents/openai.yaml +2 -2
- package/skills/vizzly/references/cli-context.md +75 -27
- package/skills/vizzly/references/dynamic-content.md +28 -33
- package/skills/vizzly/references/sdks.md +26 -39
- package/skills/vizzly/references/setup-ci.md +44 -20
- package/skills/vizzly/references/public-screenshots.md +0 -44
package/dist/commands/context.js
CHANGED
|
@@ -8,6 +8,7 @@ import { resolveContextSource as defaultResolveContextSource } from '../context/
|
|
|
8
8
|
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
9
9
|
import * as defaultOutput from '../utils/output.js';
|
|
10
10
|
import { readSession as defaultReadSession } from '../utils/session.js';
|
|
11
|
+
import { normalizeBuildContext } from '../utils/visual-context-normalizers.js';
|
|
11
12
|
function buildAuthErrorMessage() {
|
|
12
13
|
return 'Authentication required. Use --token, set VIZZLY_TOKEN, run "vizzly login", or link a project.';
|
|
13
14
|
}
|
|
@@ -76,6 +77,24 @@ function createClient(config, createApiClient) {
|
|
|
76
77
|
command: 'context'
|
|
77
78
|
});
|
|
78
79
|
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Attach the provider that produced a context payload.
|
|
83
|
+
*
|
|
84
|
+
* Context endpoints return API-owned evidence, while source selection belongs
|
|
85
|
+
* to the CLI. Keeping both in one payload prevents follow-up agent commands
|
|
86
|
+
* from silently crossing between cloud and local evidence.
|
|
87
|
+
*
|
|
88
|
+
* @param {Object} context - Context payload returned by a provider.
|
|
89
|
+
* @param {string} source - Provider source selected by the CLI.
|
|
90
|
+
* @returns {Object} Context payload with explicit source provenance.
|
|
91
|
+
*/
|
|
92
|
+
function attachContextSource(context, source) {
|
|
93
|
+
return {
|
|
94
|
+
...context,
|
|
95
|
+
source: context?.source || source
|
|
96
|
+
};
|
|
97
|
+
}
|
|
79
98
|
async function loadContextConfig(globalOptions, options, deps) {
|
|
80
99
|
let {
|
|
81
100
|
loadConfig = defaultLoadConfig,
|
|
@@ -108,20 +127,20 @@ function createCloudContextProvider(config, deps = {}) {
|
|
|
108
127
|
let client = createClient(config, createApiClient);
|
|
109
128
|
return {
|
|
110
129
|
source: 'cloud',
|
|
111
|
-
async getBuildContext(buildId) {
|
|
112
|
-
return await getBuildContext(client, buildId);
|
|
130
|
+
async getBuildContext(buildId, query) {
|
|
131
|
+
return attachContextSource(await getBuildContext(client, buildId, query), 'cloud');
|
|
113
132
|
},
|
|
114
133
|
async getComparisonContext(comparisonId, query) {
|
|
115
|
-
return await getComparisonContext(client, comparisonId, query);
|
|
134
|
+
return attachContextSource(await getComparisonContext(client, comparisonId, query), 'cloud');
|
|
116
135
|
},
|
|
117
136
|
async getScreenshotContext(screenshotName, query) {
|
|
118
|
-
return await getScreenshotContext(client, screenshotName, query);
|
|
137
|
+
return attachContextSource(await getScreenshotContext(client, screenshotName, query), 'cloud');
|
|
119
138
|
},
|
|
120
139
|
async getSimilarFingerprintContext(fingerprintHash, query) {
|
|
121
|
-
return await getSimilarFingerprintContext(client, fingerprintHash, query);
|
|
140
|
+
return attachContextSource(await getSimilarFingerprintContext(client, fingerprintHash, query), 'cloud');
|
|
122
141
|
},
|
|
123
142
|
async getReviewQueueContext(query) {
|
|
124
|
-
return await getReviewQueueContext(client, query);
|
|
143
|
+
return attachContextSource(await getReviewQueueContext(client, query), 'cloud');
|
|
125
144
|
}
|
|
126
145
|
};
|
|
127
146
|
}
|
|
@@ -235,139 +254,286 @@ function getComparisonDiffPercentage(comparison = {}) {
|
|
|
235
254
|
function getComparisonFingerprint(comparison = {}) {
|
|
236
255
|
return comparison.diff?.fingerprint_hash || comparison.analysis?.fingerprint_hash || null;
|
|
237
256
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
let diff = comparison.diff || comparison.analysis || {};
|
|
253
|
-
let compact = {
|
|
254
|
-
percentage: getComparisonDiffPercentage(comparison),
|
|
255
|
-
changed_pixels: diff.changed_pixels ?? comparison.changed_pixels ?? null,
|
|
256
|
-
total_pixels: diff.total_pixels ?? comparison.total_pixels ?? null,
|
|
257
|
-
threshold: diff.threshold ?? comparison.threshold ?? null,
|
|
258
|
-
fingerprint_hash: getComparisonFingerprint(comparison),
|
|
259
|
-
region_count: getComparisonRegionCount(comparison),
|
|
260
|
-
image_url: getComparisonDiffImageUrl(comparison)
|
|
261
|
-
};
|
|
262
|
-
if (includeDiffs) {
|
|
263
|
-
compact.regions = diff.regions || diff.diff_regions || [];
|
|
264
|
-
compact.cluster_metadata = diff.cluster_metadata || null;
|
|
265
|
-
compact.ssim_score = diff.ssim_score ?? null;
|
|
266
|
-
compact.gmsd_score = diff.gmsd_score ?? null;
|
|
267
|
-
compact.diff_lines = diff.diff_lines || [];
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Decide whether a comparison belongs in the agent handoff.
|
|
260
|
+
*
|
|
261
|
+
* Explicit API review state wins because an already-reviewed visual change is
|
|
262
|
+
* not actionable. Legacy result fallback keeps older flat responses useful
|
|
263
|
+
* only when the server did not supply that review fact.
|
|
264
|
+
*
|
|
265
|
+
* @param {Object} comparison - Normalized comparison record.
|
|
266
|
+
* @returns {boolean} Whether the record is actionable evidence.
|
|
267
|
+
*/
|
|
268
|
+
function isEvidenceCandidate(comparison = {}) {
|
|
269
|
+
if (comparison.needs_review != null) {
|
|
270
|
+
return comparison.needs_review === true;
|
|
268
271
|
}
|
|
269
|
-
return
|
|
272
|
+
return ['changed', 'new', 'failed', 'error'].includes(comparison.result);
|
|
270
273
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Keep the group facts needed to understand one comparison in isolation.
|
|
277
|
+
*
|
|
278
|
+
* Repeating this small server-owned summary on each record avoids returning
|
|
279
|
+
* the full, potentially unbounded group tree in compact agent output.
|
|
280
|
+
*
|
|
281
|
+
* @param {Object} group - Normalized screenshot group.
|
|
282
|
+
* @returns {Object} Compact API-backed aggregate facts for the group.
|
|
283
|
+
*/
|
|
284
|
+
function buildEvidenceGroup(group = {}) {
|
|
285
|
+
let aggregate = group.aggregate_status || {};
|
|
276
286
|
return {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
is_flaky: Boolean(comparison.is_flaky),
|
|
283
|
-
screenshot: {
|
|
284
|
-
id: screenshot.id || null,
|
|
285
|
-
url: screenshot.url || screenshot.original_url || null
|
|
286
|
-
},
|
|
287
|
-
baseline: {
|
|
288
|
-
id: baseline.id || null,
|
|
289
|
-
build_id: baseline.build_id || null,
|
|
290
|
-
url: baseline.url || baseline.original_url || null
|
|
291
|
-
},
|
|
292
|
-
diff: buildCompactDiff(comparison, includeDiffs)
|
|
287
|
+
name: group.name || null,
|
|
288
|
+
variant_count: group.variant_count ?? null,
|
|
289
|
+
needs_review_count: aggregate.needs_review_count ?? null,
|
|
290
|
+
failed_count: aggregate.failed_count ?? null,
|
|
291
|
+
max_diff_percentage: aggregate.max_diff_percentage ?? null
|
|
293
292
|
};
|
|
294
293
|
}
|
|
295
|
-
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Shape one normalized comparison for the bounded evidence queue.
|
|
297
|
+
*
|
|
298
|
+
* The projection keeps visual result, review state, render assets, and
|
|
299
|
+
* Honeydiff facts together so an agent can reason about a diff without
|
|
300
|
+
* joining separate collections client-side.
|
|
301
|
+
*
|
|
302
|
+
* @param {Object} comparison - Normalized comparison record.
|
|
303
|
+
* @param {Object} group - Normalized group containing the comparison.
|
|
304
|
+
* @returns {Object} One self-contained comparison evidence record.
|
|
305
|
+
*/
|
|
306
|
+
function buildComparisonEvidence(comparison = {}, group = {}) {
|
|
296
307
|
return {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
308
|
+
kind: 'comparison',
|
|
309
|
+
id: comparison.id,
|
|
310
|
+
name: comparison.name,
|
|
311
|
+
result: comparison.result,
|
|
312
|
+
status: comparison.status,
|
|
313
|
+
review_state: comparison.review_state,
|
|
314
|
+
visual_review: comparison.visual_review,
|
|
315
|
+
approval_status: comparison.approval_status,
|
|
316
|
+
needs_review: comparison.needs_review,
|
|
317
|
+
is_flaky: comparison.is_flaky,
|
|
318
|
+
group: buildEvidenceGroup(group),
|
|
319
|
+
screenshot: comparison.screenshot,
|
|
320
|
+
baseline: comparison.baseline,
|
|
321
|
+
diff: comparison.diff
|
|
301
322
|
};
|
|
302
323
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Represent a capture failure without pretending it is a comparison.
|
|
327
|
+
*
|
|
328
|
+
* Failed screenshots have useful render evidence but no comparison ID. The
|
|
329
|
+
* explicit kind and null ID prevent suggested commands from sending a
|
|
330
|
+
* screenshot identifier to the comparison endpoint.
|
|
331
|
+
*
|
|
332
|
+
* @param {Object} capture - Normalized failed screenshot capture.
|
|
333
|
+
* @returns {Object} One failed-capture evidence record.
|
|
334
|
+
*/
|
|
335
|
+
function buildFailedCaptureEvidence(capture = {}) {
|
|
313
336
|
return {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
337
|
+
...buildComparisonEvidence(capture, {
|
|
338
|
+
name: capture.name
|
|
339
|
+
}),
|
|
340
|
+
kind: 'failed_capture',
|
|
341
|
+
id: null,
|
|
342
|
+
error_message: capture.error_message
|
|
318
343
|
};
|
|
319
344
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Read actionable variants without overriding a server-reviewed group.
|
|
348
|
+
*
|
|
349
|
+
* A false aggregate is authoritative even when a partial variant payload
|
|
350
|
+
* appears pending, which prevents the client from reopening completed work.
|
|
351
|
+
*
|
|
352
|
+
* @param {Object} group - Normalized screenshot group.
|
|
353
|
+
* @returns {Object[]} Actionable variants in API order.
|
|
354
|
+
*/
|
|
355
|
+
function getGroupEvidence(group = {}) {
|
|
356
|
+
if (group.aggregate_status?.needs_review === false) {
|
|
357
|
+
return [];
|
|
358
|
+
}
|
|
359
|
+
return (group.variants || []).filter(isEvidenceCandidate);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Interleave actionable variants across groups in their original API order.
|
|
364
|
+
*
|
|
365
|
+
* Taking one variant per group before taking second variants preserves useful
|
|
366
|
+
* breadth when the final handoff is capped and one screenshot has many device
|
|
367
|
+
* or browser variants.
|
|
368
|
+
*
|
|
369
|
+
* @param {Object[]} groups - Normalized screenshot groups.
|
|
370
|
+
* @returns {Object[]} Self-contained evidence records in breadth-first order.
|
|
371
|
+
*/
|
|
372
|
+
function selectBreadthFirstEvidence(groups = []) {
|
|
373
|
+
let candidatesByGroup = groups.map(getGroupEvidence);
|
|
374
|
+
let evidence = [];
|
|
375
|
+
let variantIndex = 0;
|
|
376
|
+
let remaining = candidatesByGroup.some(candidates => candidates.length > 0);
|
|
377
|
+
while (remaining) {
|
|
378
|
+
remaining = false;
|
|
379
|
+
for (let groupIndex = 0; groupIndex < groups.length; groupIndex += 1) {
|
|
380
|
+
let comparison = candidatesByGroup[groupIndex][variantIndex];
|
|
381
|
+
if (comparison) {
|
|
382
|
+
evidence.push(buildComparisonEvidence(comparison, groups[groupIndex]));
|
|
383
|
+
remaining = true;
|
|
384
|
+
}
|
|
327
385
|
}
|
|
328
|
-
|
|
386
|
+
variantIndex += 1;
|
|
387
|
+
}
|
|
388
|
+
return evidence;
|
|
329
389
|
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Quote a value only when a suggested command needs shell protection.
|
|
393
|
+
*
|
|
394
|
+
* Suggested commands are meant to be executable, so names with whitespace or
|
|
395
|
+
* apostrophes must survive copy and paste without changing their value.
|
|
396
|
+
*
|
|
397
|
+
* @param {unknown} value - CLI argument value.
|
|
398
|
+
* @returns {string} A shell-safe argument for the displayed command.
|
|
399
|
+
*/
|
|
400
|
+
function quoteCommandArgument(value) {
|
|
401
|
+
let stringValue = String(value);
|
|
402
|
+
if (/^[A-Za-z0-9._:/-]+$/.test(stringValue)) {
|
|
403
|
+
return stringValue;
|
|
404
|
+
}
|
|
405
|
+
return `'${stringValue.replaceAll("'", `'\\''`)}'`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Recognize both local source labels emitted across supported context shapes.
|
|
410
|
+
*
|
|
411
|
+
* @param {Object} context - Normalized context response.
|
|
412
|
+
* @returns {boolean} Whether follow-up commands must stay in local mode.
|
|
413
|
+
*/
|
|
414
|
+
function isLocalContext(context = {}) {
|
|
415
|
+
return ['local', 'local_workspace'].includes(context.source);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Keep executable suggestions on the source that produced their evidence.
|
|
420
|
+
*
|
|
421
|
+
* Mixed workspaces can contain both a cloud run session and persisted local
|
|
422
|
+
* TDD results. Pinning either source prevents a follow-up command from
|
|
423
|
+
* silently crossing that boundary and describing a different build.
|
|
424
|
+
*
|
|
425
|
+
* @param {string} command - Base CLI command.
|
|
426
|
+
* @param {Object} context - Context that produced the command.
|
|
427
|
+
* @returns {string} Command pinned to its originating source.
|
|
428
|
+
*/
|
|
429
|
+
function appendContextSource(command, context = {}) {
|
|
430
|
+
let source = isLocalContext(context) ? 'local' : 'cloud';
|
|
431
|
+
return `${command} --source ${source}`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Build concrete drill-down paths from the evidence actually returned.
|
|
436
|
+
*
|
|
437
|
+
* Executable commands replace generic client-authored advice. They let an
|
|
438
|
+
* agent ask the API for deeper comparison, history, or raw diff context, and
|
|
439
|
+
* only suggest the full build when the bounded queue omitted records.
|
|
440
|
+
*
|
|
441
|
+
* @param {Object} context - Normalized build context.
|
|
442
|
+
* @param {Object[]} evidence - Evidence included in the compact handoff.
|
|
443
|
+
* @param {boolean} truncated - Whether additional evidence was omitted.
|
|
444
|
+
* @returns {{label: string, command: string}[]} Suggested CLI commands.
|
|
445
|
+
*/
|
|
446
|
+
function buildSuggestedCommands(context = {}, evidence = [], truncated = false) {
|
|
447
|
+
let commands = [];
|
|
448
|
+
let firstComparison = evidence.find(item => item.kind === 'comparison' && item.id);
|
|
449
|
+
let firstNamedEvidence = evidence.find(item => item.name);
|
|
450
|
+
let buildTarget = isLocalContext(context) ? 'current' : context.build?.id || null;
|
|
451
|
+
if (firstComparison?.id) {
|
|
452
|
+
commands.push({
|
|
453
|
+
label: 'Inspect comparison context',
|
|
454
|
+
command: appendContextSource(`vizzly --json context comparison ${quoteCommandArgument(firstComparison.id)}`, context)
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
if (firstNamedEvidence?.name) {
|
|
458
|
+
commands.push({
|
|
459
|
+
label: 'Inspect screenshot history',
|
|
460
|
+
command: appendContextSource(`vizzly --json context screenshot ${quoteCommandArgument(firstNamedEvidence.name)}`, context)
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
if (buildTarget && evidence.length > 0) {
|
|
464
|
+
commands.push({
|
|
465
|
+
label: 'Load raw diff diagnostics',
|
|
466
|
+
command: appendContextSource(`vizzly --json context build ${quoteCommandArgument(buildTarget)} --agent --include diffs`, context)
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
if (buildTarget && truncated) {
|
|
470
|
+
commands.push({
|
|
471
|
+
label: 'Load full build context',
|
|
472
|
+
command: appendContextSource(`vizzly --json context build ${quoteCommandArgument(buildTarget)} --agent --full`, context)
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
return commands;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Create the compact agent presentation without rewriting API truth.
|
|
480
|
+
*
|
|
481
|
+
* Status, summaries, review facts, assets, and Honeydiff values pass through
|
|
482
|
+
* normalization from the server. The client owns only the bounded selection,
|
|
483
|
+
* truthful truncation facts, explicit includes, and follow-up commands.
|
|
484
|
+
*
|
|
485
|
+
* @param {Object} context - Raw build context returned by the provider.
|
|
486
|
+
* @param {Object} options - Compact presentation options.
|
|
487
|
+
* @param {string|null} [options.source] - Resolved source fallback.
|
|
488
|
+
* @param {string[]} [options.include] - Explicit detail collections.
|
|
489
|
+
* @param {number} [options.evidenceLimit] - Maximum evidence record count.
|
|
490
|
+
* @returns {Object} Bounded agent build context.
|
|
491
|
+
*/
|
|
330
492
|
function buildAgentBuildPayload(context, {
|
|
331
493
|
source = null,
|
|
332
494
|
include = [],
|
|
333
495
|
evidenceLimit = 10
|
|
334
496
|
} = {}) {
|
|
335
|
-
let comparisons = context.comparisons || [];
|
|
336
497
|
let includeSet = new Set(include);
|
|
337
498
|
let includeDiffs = includeSet.has('diffs');
|
|
338
|
-
let
|
|
339
|
-
let fresh = comparisons.filter(isNewComparison);
|
|
340
|
-
let evidence = [...changed, ...fresh].slice(0, evidenceLimit).map(comparison => buildCompactComparison(comparison, {
|
|
499
|
+
let normalized = normalizeBuildContext(context, {
|
|
341
500
|
includeDiffs
|
|
342
|
-
})
|
|
343
|
-
let
|
|
501
|
+
});
|
|
502
|
+
let candidates = [...normalized.failed_captures.map(buildFailedCaptureEvidence), ...selectBreadthFirstEvidence(normalized.groups)];
|
|
503
|
+
let evidence = candidates.slice(0, evidenceLimit);
|
|
504
|
+
let evidenceTruncated = candidates.length > evidence.length;
|
|
344
505
|
let payload = {
|
|
345
506
|
resource: 'build_agent_context',
|
|
346
|
-
source:
|
|
347
|
-
|
|
507
|
+
source: normalized.source || source,
|
|
508
|
+
review_flow: normalized.review_flow,
|
|
509
|
+
scope: normalized.scope || null,
|
|
348
510
|
project: {
|
|
349
|
-
organization:
|
|
350
|
-
slug:
|
|
351
|
-
name:
|
|
352
|
-
visibility:
|
|
511
|
+
organization: normalized.scope?.organization?.slug || null,
|
|
512
|
+
slug: normalized.scope?.project?.slug || null,
|
|
513
|
+
name: normalized.scope?.project?.name || null,
|
|
514
|
+
visibility: normalized.scope?.project?.visibility || null
|
|
353
515
|
},
|
|
354
|
-
build:
|
|
516
|
+
build: normalized.build || null,
|
|
355
517
|
baseline: {
|
|
356
|
-
selected:
|
|
357
|
-
selection_reason:
|
|
518
|
+
selected: normalized.baseline?.selected || null,
|
|
519
|
+
selection_reason: normalized.baseline?.selection_reason || null
|
|
358
520
|
},
|
|
359
|
-
status:
|
|
360
|
-
summary:
|
|
521
|
+
status: normalized.status || null,
|
|
522
|
+
summary: normalized.summary || null,
|
|
523
|
+
signature_properties: normalized.signature_properties ?? null,
|
|
524
|
+
evidence_limit: evidenceLimit,
|
|
525
|
+
evidence_returned: evidence.length,
|
|
526
|
+
evidence_truncated: evidenceTruncated,
|
|
361
527
|
evidence,
|
|
362
|
-
links:
|
|
363
|
-
preview:
|
|
364
|
-
|
|
528
|
+
links: normalized.links || {},
|
|
529
|
+
preview: normalized.preview || null,
|
|
530
|
+
suggested_commands: buildSuggestedCommands(normalized, evidence, evidenceTruncated)
|
|
365
531
|
};
|
|
366
532
|
if (includeSet.has('screenshots')) {
|
|
367
|
-
payload.screenshots =
|
|
533
|
+
payload.screenshots = normalized.screenshots || [];
|
|
368
534
|
}
|
|
369
535
|
if (includeSet.has('comments')) {
|
|
370
|
-
payload.comments =
|
|
536
|
+
payload.comments = normalized.comments || {};
|
|
371
537
|
}
|
|
372
538
|
return payload;
|
|
373
539
|
}
|
|
@@ -396,12 +562,17 @@ function formatNeedsReview(status = {}) {
|
|
|
396
562
|
if (!status || status.needs_review == null) {
|
|
397
563
|
return null;
|
|
398
564
|
}
|
|
399
|
-
let pending = status.pending_comparisons || 0;
|
|
400
|
-
let unresolved = status.unresolved_comments || 0;
|
|
401
565
|
if (!status.needs_review) {
|
|
402
566
|
return 'no';
|
|
403
567
|
}
|
|
404
|
-
|
|
568
|
+
let details = [];
|
|
569
|
+
if (status.pending_comparisons != null) {
|
|
570
|
+
details.push(`${status.pending_comparisons} comparisons`);
|
|
571
|
+
}
|
|
572
|
+
if (status.unresolved_comments != null) {
|
|
573
|
+
details.push(`${status.unresolved_comments} unresolved comments`);
|
|
574
|
+
}
|
|
575
|
+
return details.length > 0 ? `yes · ${details.join(' · ')}` : 'yes';
|
|
405
576
|
}
|
|
406
577
|
function formatConfirmedRegionLabels(regions = []) {
|
|
407
578
|
return regions.map(region => region.label).filter(Boolean).slice(0, 3).join(' · ');
|
|
@@ -436,42 +607,131 @@ function printComparisonList(output, comparisons = [], {
|
|
|
436
607
|
}
|
|
437
608
|
}
|
|
438
609
|
}
|
|
610
|
+
function getGroupDisplayState(group = {}) {
|
|
611
|
+
let aggregate = group.aggregate_status || {};
|
|
612
|
+
if ((aggregate.failed_count ?? 0) > 0) return 'failed';
|
|
613
|
+
if (aggregate.has_rejected === true) return 'rejected';
|
|
614
|
+
if (aggregate.has_changes === true) return 'changed';
|
|
615
|
+
if (aggregate.has_new === true) return 'new';
|
|
616
|
+
if (aggregate.needs_review === true) return 'needs review';
|
|
617
|
+
if (aggregate.all_approved === true) return 'approved';
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
function formatViewport(viewport) {
|
|
621
|
+
if (!viewport) {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
return `${viewport.width ?? '?'}×${viewport.height ?? '?'}`;
|
|
625
|
+
}
|
|
626
|
+
function printScreenshotGroups(output, groups = [], {
|
|
627
|
+
limit = 8
|
|
628
|
+
} = {}) {
|
|
629
|
+
let colors = output.getColors();
|
|
630
|
+
let visibleGroups = groups.slice(0, limit);
|
|
631
|
+
output.print(' Screenshot groups');
|
|
632
|
+
for (let group of visibleGroups) {
|
|
633
|
+
let displayState = getGroupDisplayState(group);
|
|
634
|
+
let tone = getStatusTone(colors, displayState);
|
|
635
|
+
let aggregate = group.aggregate_status || {};
|
|
636
|
+
let details = [];
|
|
637
|
+
if (group.variant_count != null) {
|
|
638
|
+
details.push(`${group.variant_count} variants`);
|
|
639
|
+
}
|
|
640
|
+
if (aggregate.needs_review_count != null) {
|
|
641
|
+
details.push(`${aggregate.needs_review_count} needs review`);
|
|
642
|
+
}
|
|
643
|
+
if (aggregate.failed_count != null && aggregate.failed_count > 0) {
|
|
644
|
+
details.push(`${aggregate.failed_count} failed`);
|
|
645
|
+
}
|
|
646
|
+
if (aggregate.max_diff_percentage != null) {
|
|
647
|
+
details.push(`${aggregate.max_diff_percentage}% max diff`);
|
|
648
|
+
}
|
|
649
|
+
output.print(` ${colors.bold(group.name)}${displayState ? ` ${tone(displayState.toUpperCase())}` : ''}`);
|
|
650
|
+
if (details.length > 0) {
|
|
651
|
+
output.print(` ${colors.dim(details.join(' · '))}`);
|
|
652
|
+
}
|
|
653
|
+
for (let variant of group.variants.slice(0, 3)) {
|
|
654
|
+
let variantDetails = [variant.review_state, variant.browser, formatViewport(variant.viewport)].filter(Boolean);
|
|
655
|
+
output.print(` ${getStatusTone(colors, variant.result)((variant.result || 'unknown').toUpperCase())}${variantDetails.length > 0 ? ` · ${colors.dim(variantDetails.join(' · '))}` : ''}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (groups.length > visibleGroups.length) {
|
|
659
|
+
output.print(` ${colors.dim(`...${groups.length - visibleGroups.length} more groups`)}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function printFailedCaptures(output, captures = []) {
|
|
663
|
+
let colors = output.getColors();
|
|
664
|
+
output.print(' Failed captures');
|
|
665
|
+
for (let capture of captures.slice(0, 8)) {
|
|
666
|
+
let details = [capture.error_message, capture.browser, formatViewport(capture.viewport), capture.screenshot.url].filter(Boolean);
|
|
667
|
+
output.print(` ${colors.bold(capture.name)} ${colors.brand.error('FAILED')}`);
|
|
668
|
+
if (details.length > 0) {
|
|
669
|
+
output.print(` ${colors.dim(details.join(' · '))}`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
function formatReviewSummary(review = {}) {
|
|
674
|
+
let details = [review.pending != null ? `${review.pending} pending` : null, review.approved != null ? `${review.approved} approved` : null, review.rejected != null ? `${review.rejected} rejected` : null].filter(Boolean);
|
|
675
|
+
return details.length > 0 ? details.join(' · ') : null;
|
|
676
|
+
}
|
|
439
677
|
function displayBuildContext(output, context) {
|
|
440
678
|
output.header('context', 'build');
|
|
679
|
+
let normalizedContext = normalizeBuildContext(context);
|
|
441
680
|
let colors = output.getColors();
|
|
442
|
-
let
|
|
443
|
-
let
|
|
444
|
-
let
|
|
445
|
-
let
|
|
446
|
-
let
|
|
447
|
-
let
|
|
448
|
-
let
|
|
449
|
-
|
|
450
|
-
|
|
681
|
+
let build = normalizedContext.build || {};
|
|
682
|
+
let buildTone = getStatusTone(colors, build.status);
|
|
683
|
+
let comparisons = normalizedContext.comparisons;
|
|
684
|
+
let groups = normalizedContext.groups;
|
|
685
|
+
let screenshots = normalizedContext.screenshots || [];
|
|
686
|
+
let failedCaptures = normalizedContext.failed_captures;
|
|
687
|
+
let reviewSummary = formatReviewSummary(normalizedContext.summary?.review);
|
|
688
|
+
let commentsSummary = normalizedContext.summary?.comments || {};
|
|
689
|
+
let needsReview = formatNeedsReview(normalizedContext.status);
|
|
690
|
+
let baseline = normalizedContext.baseline?.selected || null;
|
|
691
|
+
let comparisonCount = normalizedContext.summary?.comparisons?.total ?? normalizedContext.total_comparisons ?? (Array.isArray(context.comparisons) ? comparisons.length : null);
|
|
692
|
+
let screenshotCount = normalizedContext.summary?.screenshots?.total ?? normalizedContext.screenshot_count ?? (Array.isArray(context.screenshots) ? screenshots.length : null);
|
|
693
|
+
output.print(` ${colors.bold(build.name || build.id || 'unknown build')} ${buildTone((build.status || 'unknown').toUpperCase())}`);
|
|
694
|
+
output.print(` ${colors.dim(`@${normalizedContext.scope?.organization?.slug || 'unknown'}/${normalizedContext.scope?.project?.slug || 'unknown'}`)}`);
|
|
451
695
|
output.blank();
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
696
|
+
if (comparisonCount != null) {
|
|
697
|
+
output.labelValue('Comparisons', String(comparisonCount));
|
|
698
|
+
}
|
|
699
|
+
if (groups.length > 0) {
|
|
700
|
+
output.labelValue('Screenshot Groups', String(groups.length));
|
|
701
|
+
}
|
|
702
|
+
if (screenshotCount != null) {
|
|
703
|
+
output.labelValue('Screenshots', String(screenshotCount));
|
|
455
704
|
}
|
|
456
705
|
if (baseline) {
|
|
457
|
-
output.labelValue('Baseline', `${baseline.name || baseline.id || 'selected'}${
|
|
706
|
+
output.labelValue('Baseline', `${baseline.name || baseline.id || 'selected'}${normalizedContext.baseline.selection_reason ? ` · ${normalizedContext.baseline.selection_reason}` : ''}`);
|
|
458
707
|
}
|
|
459
708
|
if (needsReview) {
|
|
460
709
|
output.labelValue('Needs Review', needsReview);
|
|
461
710
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
if (context.preview) {
|
|
465
|
-
let previewUrl = context.preview.preview_url || context.preview.url;
|
|
466
|
-
output.labelValue('Preview', `${context.preview.status || 'unknown'}${previewUrl ? ' · available' : ''}`);
|
|
711
|
+
if (reviewSummary) {
|
|
712
|
+
output.labelValue('Review', reviewSummary);
|
|
467
713
|
}
|
|
468
|
-
|
|
469
|
-
|
|
714
|
+
let buildComments = commentsSummary.build ?? (Array.isArray(normalizedContext.comments?.build) ? getBuildCommentsCount(normalizedContext) : null);
|
|
715
|
+
let screenshotComments = commentsSummary.screenshot ?? (Number.isInteger(normalizedContext.comments?.screenshot_count) ? getScreenshotCommentsCount(normalizedContext) : null);
|
|
716
|
+
let assignments = Array.isArray(normalizedContext.review?.assignments) ? getReviewAssignmentsCount(normalizedContext) : null;
|
|
717
|
+
let memory = [buildComments != null ? `${buildComments} build comments` : null, screenshotComments != null ? `${screenshotComments} screenshot comments` : null, assignments != null ? `${assignments} assignments` : null].filter(Boolean);
|
|
718
|
+
if (memory.length > 0) {
|
|
719
|
+
output.labelValue('Memory', memory.join(' · '));
|
|
470
720
|
}
|
|
471
|
-
if (
|
|
721
|
+
if (normalizedContext.preview) {
|
|
722
|
+
let previewUrl = normalizedContext.preview.preview_url || normalizedContext.preview.url;
|
|
723
|
+
output.labelValue('Preview', `${normalizedContext.preview.status || 'unknown'}${previewUrl ? ' · available' : ''}`);
|
|
724
|
+
}
|
|
725
|
+
if (normalizedContext.links?.build_url) {
|
|
726
|
+
output.labelValue('Build URL', normalizedContext.links.build_url);
|
|
727
|
+
}
|
|
728
|
+
if (groups.length > 0) {
|
|
729
|
+
output.blank();
|
|
730
|
+
printScreenshotGroups(output, groups);
|
|
731
|
+
}
|
|
732
|
+
if (failedCaptures.length > 0) {
|
|
472
733
|
output.blank();
|
|
473
|
-
output
|
|
474
|
-
printComparisonList(output, comparisons);
|
|
734
|
+
printFailedCaptures(output, failedCaptures);
|
|
475
735
|
}
|
|
476
736
|
}
|
|
477
737
|
function formatAgentBuildContext(context) {
|
|
@@ -631,13 +891,17 @@ export async function contextBuildCommand(buildId, options = {}, globalOptions =
|
|
|
631
891
|
return;
|
|
632
892
|
}
|
|
633
893
|
let resolvedBuildId = resolveBuildContextId(buildId, runtime, deps);
|
|
894
|
+
let include = parseIncludeOption(options.include);
|
|
895
|
+
let query = globalOptions.json && options.agent && !options.full ? {
|
|
896
|
+
details: include.includes('diffs') ? 'diffs' : 'summary'
|
|
897
|
+
} : undefined;
|
|
634
898
|
output.startSpinner('Fetching build context...');
|
|
635
|
-
let context = await runtime.provider.getBuildContext(resolvedBuildId);
|
|
899
|
+
let context = await runtime.provider.getBuildContext(resolvedBuildId, query);
|
|
636
900
|
output.stopSpinner();
|
|
637
901
|
if (globalOptions.json && options.agent && !options.full) {
|
|
638
902
|
output.data(buildAgentBuildPayload(context, {
|
|
639
903
|
source: runtime.source,
|
|
640
|
-
include
|
|
904
|
+
include
|
|
641
905
|
}));
|
|
642
906
|
output.cleanup();
|
|
643
907
|
return;
|