@vizzly-testing/cli 0.35.2 → 0.35.3-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +8 -5
  2. package/dist/api/client.js +20 -5
  3. package/dist/api/core.js +17 -3
  4. package/dist/api/endpoints.js +24 -3
  5. package/dist/api/index.js +1 -1
  6. package/dist/cli.js +25 -65
  7. package/dist/commands/baselines.js +0 -7
  8. package/dist/commands/comparisons.js +52 -45
  9. package/dist/commands/config-cmd.js +0 -7
  10. package/dist/commands/context.js +444 -238
  11. package/dist/commands/doctor.js +0 -8
  12. package/dist/commands/init.js +7 -12
  13. package/dist/commands/login.js +0 -13
  14. package/dist/commands/logout.js +0 -12
  15. package/dist/commands/orgs.js +0 -9
  16. package/dist/commands/run.js +84 -29
  17. package/dist/commands/status.js +276 -59
  18. package/dist/commands/tdd.js +19 -3
  19. package/dist/commands/whoami.js +0 -12
  20. package/dist/context/local-workspace-provider.js +293 -37
  21. package/dist/plugin-loader.js +1 -1
  22. package/dist/reporter/reporter-bundle.css +1 -1
  23. package/dist/reporter/reporter-bundle.iife.js +17 -17
  24. package/dist/server/routers/events.js +4 -4
  25. package/dist/test-runner/operations.js +70 -28
  26. package/dist/types/index.d.ts +4 -2
  27. package/dist/uploader/core.js +6 -6
  28. package/dist/uploader/operations.js +4 -1
  29. package/dist/utils/visual-context-normalizers.js +455 -0
  30. package/package.json +13 -13
  31. package/skills/vizzly/SKILL.md +68 -101
  32. package/skills/vizzly/agents/openai.yaml +2 -2
  33. package/skills/vizzly/references/cli-context.md +79 -27
  34. package/skills/vizzly/references/dynamic-content.md +28 -33
  35. package/skills/vizzly/references/sdks.md +26 -39
  36. package/skills/vizzly/references/setup-ci.md +44 -20
  37. package/dist/config/index.js +0 -13
  38. package/dist/project/index.js +0 -13
  39. package/dist/tdd/index.js +0 -19
  40. package/dist/tdd/services/baseline-downloader.js +0 -151
  41. package/dist/tdd/services/hotspot-service.js +0 -58
  42. package/dist/utils/environment.js +0 -109
  43. package/skills/vizzly/references/public-screenshots.md +0 -44
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { existsSync as defaultExistsSync, readFileSync } from 'node:fs';
2
3
  import { basename, isAbsolute, join } from 'node:path';
3
4
  import { pathToFileURL } from 'node:url';
@@ -140,17 +141,28 @@ function buildComparisonLinks(snapshot, comparisonId) {
140
141
  comparison_url: `http://127.0.0.1:${snapshot.serverInfo.port}/comparison/${encodeURIComponent(comparisonId)}`
141
142
  };
142
143
  }
144
+
145
+ /**
146
+ * Describe the local evidence set without borrowing cloud run identity.
147
+ *
148
+ * A cloud `session.json` can coexist with older file-backed TDD evidence. Using
149
+ * that session here makes stale local comparisons look like the current cloud
150
+ * build, so local identity comes only from the active local server.
151
+ *
152
+ * @param {Object} snapshot - Persisted local workspace evidence.
153
+ * @returns {Object} Build-shaped identity for the local context payload.
154
+ */
143
155
  function buildBuildSnapshot(snapshot) {
144
- let buildId = snapshot.session?.buildId || snapshot.serverInfo?.buildId || 'local-workspace';
156
+ let buildId = snapshot.serverInfo?.buildId || 'local-workspace';
145
157
  return {
146
158
  id: buildId,
147
159
  name: buildId,
148
- branch: snapshot.session?.branch || 'local',
149
- commit_sha: snapshot.session?.commit || null,
160
+ branch: 'local',
161
+ commit_sha: null,
150
162
  commit_message: null,
151
163
  approval_status: snapshot.serverInfo ? 'pending' : 'approved',
152
164
  status: snapshot.serverInfo ? 'running' : 'completed',
153
- created_at: snapshot.session?.createdAt || null
165
+ created_at: null
154
166
  };
155
167
  }
156
168
  function buildBaselineSnapshot(snapshot) {
@@ -193,31 +205,6 @@ function buildReviewState(build, reviewSummary) {
193
205
  unresolved_comments: 0
194
206
  };
195
207
  }
196
- function mapLocalScreenshot(snapshot, comparison) {
197
- let mapped = mapLocalComparison(snapshot, comparison);
198
- let baselineBuildId = snapshot.baselineMetadata?.buildId || null;
199
- return {
200
- id: mapped.screenshot.id,
201
- name: mapped.screenshot.name,
202
- browser: mapped.screenshot.browser,
203
- viewport: {
204
- width: mapped.screenshot.viewport_width,
205
- height: mapped.screenshot.viewport_height
206
- },
207
- url: mapped.screenshot.original_url,
208
- baseline: mapped.baseline ? {
209
- id: mapped.baseline.id,
210
- build_id: baselineBuildId,
211
- name: mapped.baseline.name,
212
- browser: mapped.baseline.browser,
213
- viewport: {
214
- width: mapped.baseline.viewport_width,
215
- height: mapped.baseline.viewport_height
216
- },
217
- url: mapped.baseline.original_url
218
- } : null
219
- };
220
- }
221
208
  function mapLocalComparison(snapshot, comparison) {
222
209
  let details = snapshot.comparisonDetails[comparison.id] || {};
223
210
  let comparisonName = comparison.originalName || comparison.name;
@@ -295,6 +282,141 @@ function mapLocalComparison(snapshot, comparison) {
295
282
  }
296
283
  };
297
284
  }
285
+ function projectLocalScreenshot(screenshot, baseline) {
286
+ if (!screenshot) return null;
287
+ return {
288
+ id: screenshot.id,
289
+ name: screenshot.name,
290
+ browser: screenshot.browser,
291
+ viewport: {
292
+ width: screenshot.viewport_width,
293
+ height: screenshot.viewport_height
294
+ },
295
+ url: screenshot.original_url,
296
+ baseline: baseline ? {
297
+ id: baseline.id,
298
+ build_id: baseline.build_id,
299
+ name: baseline.name,
300
+ browser: baseline.browser,
301
+ viewport: {
302
+ width: baseline.viewport_width,
303
+ height: baseline.viewport_height
304
+ },
305
+ url: baseline.original_url
306
+ } : null
307
+ };
308
+ }
309
+ function projectLocalDiff(diff, includeDiffs) {
310
+ let projected = {
311
+ percentage: diff?.percentage ?? null,
312
+ changed_pixels: diff?.changed_pixels ?? null,
313
+ total_pixels: diff?.total_pixels ?? null,
314
+ threshold: diff?.threshold ?? null,
315
+ image_url: diff?.image_url ?? null,
316
+ fingerprint_hash: diff?.fingerprint_hash ?? null,
317
+ region_count: Array.isArray(diff?.regions) ? diff.regions.length : null
318
+ };
319
+ if (includeDiffs) {
320
+ projected.regions = diff?.regions || [];
321
+ projected.cluster_metadata = diff?.cluster_metadata ?? null;
322
+ projected.fingerprint_data = diff?.fingerprint_data ?? null;
323
+ projected.diff_lines = diff?.diff_lines ?? [];
324
+ }
325
+ return projected;
326
+ }
327
+ function projectLocalEvidence(comparison, includeDiffs) {
328
+ return {
329
+ type: 'comparison',
330
+ id: comparison.id,
331
+ screenshot_name: comparison.screenshot_name,
332
+ status: comparison.status,
333
+ result: comparison.result,
334
+ approval_status: comparison.approval_status,
335
+ needs_review: comparison.needs_review,
336
+ build_id: comparison.build_id,
337
+ build_name: comparison.build_name,
338
+ build_branch: comparison.build_branch,
339
+ build_commit_sha: comparison.build_commit_sha,
340
+ build_created_at: comparison.build_created_at,
341
+ screenshot: projectLocalScreenshot(comparison.screenshot, comparison.baseline),
342
+ diff: projectLocalDiff(comparison.diff, includeDiffs)
343
+ };
344
+ }
345
+ function projectLocalFocusedComparison(comparison, includeDiffs) {
346
+ if (includeDiffs) return comparison;
347
+ let analysis = comparison.analysis || {};
348
+ let {
349
+ diff_regions: _diffRegions,
350
+ cluster_metadata: _clusterMetadata,
351
+ fingerprint_data: _fingerprintData,
352
+ diff_lines: _diffLines,
353
+ ...summaryAnalysis
354
+ } = analysis;
355
+ return {
356
+ ...comparison,
357
+ diff: projectLocalDiff(comparison.diff, false),
358
+ analysis: summaryAnalysis
359
+ };
360
+ }
361
+ function projectLocalHistoryItem(comparison) {
362
+ return {
363
+ id: comparison.id,
364
+ screenshot_name: comparison.screenshot_name,
365
+ result: comparison.result,
366
+ status: comparison.status,
367
+ needs_review: comparison.needs_review,
368
+ build_id: comparison.build_id,
369
+ build_name: comparison.build_name,
370
+ build_branch: comparison.build_branch,
371
+ build_created_at: comparison.build_created_at,
372
+ screenshot: projectLocalScreenshot(comparison.screenshot, comparison.baseline),
373
+ diff: {
374
+ percentage: comparison.diff?.percentage ?? null,
375
+ fingerprint_hash: comparison.diff?.fingerprint_hash ?? null,
376
+ region_count: Array.isArray(comparison.diff?.regions) ? comparison.diff.regions.length : null
377
+ }
378
+ };
379
+ }
380
+ function compactLocalCollection(items) {
381
+ return {
382
+ total: Array.isArray(items) ? items.length : null,
383
+ included: false,
384
+ details_available: Array.isArray(items) && items.length > 0
385
+ };
386
+ }
387
+ function createLocalDynamicRegions(comparison = null) {
388
+ let confirmedRegions = comparison?.analysis?.confirmed_regions;
389
+ let hotspotAnalysis = comparison?.analysis?.hotspot_analysis;
390
+ return {
391
+ decision: null,
392
+ patterns: compactLocalCollection([]),
393
+ confirmed_regions: compactLocalCollection(confirmedRegions),
394
+ exclusions: {
395
+ total: null,
396
+ included: false,
397
+ details_available: false
398
+ },
399
+ hotspot_analysis: hotspotAnalysis ? {
400
+ total_builds_analyzed: hotspotAnalysis.total_builds_analyzed ?? null,
401
+ confidence: hotspotAnalysis.confidence ?? null,
402
+ confidence_score: hotspotAnalysis.confidence_score ?? null,
403
+ data_source: hotspotAnalysis.data_source ?? null,
404
+ coverage: hotspotAnalysis.coverage ?? null,
405
+ confirmed_region_coverage: hotspotAnalysis.confirmed_region_coverage ?? null
406
+ } : null
407
+ };
408
+ }
409
+ function createLocalSnapshotRevision(snapshot) {
410
+ let revisionInput = {
411
+ serverInfo: snapshot.serverInfo,
412
+ reportData: snapshot.reportData,
413
+ comparisonDetails: snapshot.comparisonDetails,
414
+ baselineMetadata: snapshot.baselineMetadata,
415
+ hotspotFile: snapshot.hotspotFile,
416
+ regionFile: snapshot.regionFile
417
+ };
418
+ return createHash('sha256').update(JSON.stringify(revisionInput)).digest('base64url');
419
+ }
298
420
  function buildReviewSummary(comparisons = []) {
299
421
  let approved = comparisons.filter(comparison => mapApprovalStatus(comparison.status) === 'approved').length;
300
422
  let rejected = comparisons.filter(comparison => mapApprovalStatus(comparison.status) === 'rejected').length;
@@ -333,7 +455,6 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
333
455
  vizzlyDir,
334
456
  existsSync,
335
457
  serverInfo: readJson(join(vizzlyDir, 'server.json')),
336
- session: readJson(join(vizzlyDir, 'session.json')),
337
458
  reportData: normalizeReportData(readJson(join(vizzlyDir, 'report-data.json')) || createEmptyReportData()),
338
459
  comparisonDetails: readJson(join(vizzlyDir, 'comparison-details.json')) || {},
339
460
  baselineMetadata: readJson(join(vizzlyDir, 'baselines', 'metadata.json')),
@@ -345,7 +466,7 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
345
466
  return snapshotCache;
346
467
  }
347
468
  function isAvailable(snapshot = loadSnapshot()) {
348
- return Boolean(snapshot.serverInfo || snapshot.session || snapshot.reportData.comparisons.length > 0 || snapshot.baselineMetadata);
469
+ return Boolean(snapshot.serverInfo || snapshot.reportData.comparisons.length > 0 || snapshot.baselineMetadata);
349
470
  }
350
471
  function findComparison(snapshot, target) {
351
472
  if (!target) {
@@ -395,17 +516,66 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
395
516
  } : {})
396
517
  };
397
518
  }
398
- function getBuildContext(buildId) {
519
+ function createLocalCursor(resource, target, query, stream, offset, revision) {
520
+ return Buffer.from(JSON.stringify({
521
+ version: 1,
522
+ resource,
523
+ target,
524
+ query,
525
+ stream,
526
+ offset,
527
+ revision
528
+ })).toString('base64url');
529
+ }
530
+ function readLocalCursor(cursor, resource, target, query, revision, streams) {
531
+ if (!cursor) return null;
532
+ try {
533
+ let parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString());
534
+ if (parsed.version !== 1 || parsed.resource !== resource || parsed.target !== target || parsed.query !== query || parsed.revision !== revision || !streams.includes(parsed.stream) || !Number.isInteger(parsed.offset) || parsed.offset < 0) {
535
+ throw new Error('mismatch');
536
+ }
537
+ return parsed;
538
+ } catch {
539
+ throw createLocalWorkspaceError('The local context cursor is invalid or the local results changed. Start again without --cursor.');
540
+ }
541
+ }
542
+ function createLocalPage(items, {
543
+ limit,
544
+ offset,
545
+ resource,
546
+ target,
547
+ query,
548
+ stream,
549
+ revision
550
+ }) {
551
+ if (offset > items.length) {
552
+ throw createLocalWorkspaceError('The local context cursor is invalid or the local results changed. Start again without --cursor.');
553
+ }
554
+ let pageItems = items.slice(offset, offset + limit);
555
+ let nextOffset = offset + pageItems.length;
556
+ let hasMore = nextOffset < items.length;
557
+ return {
558
+ items: pageItems,
559
+ page: {
560
+ limit,
561
+ returned: pageItems.length,
562
+ total: items.length,
563
+ has_more: hasMore,
564
+ next_cursor: hasMore ? createLocalCursor(resource, target, query, stream, nextOffset, revision) : null
565
+ }
566
+ };
567
+ }
568
+ function getBuildContext(buildId, query = {}) {
399
569
  let snapshot = loadSnapshot();
400
570
  let resolvedBuild = buildBuildSnapshot(snapshot);
401
571
  if (!(buildId === 'current' || buildId === 'local' || buildId === resolvedBuild.id)) {
402
- throw createLocalWorkspaceError(`Local workspace context is only available for the active session build (${resolvedBuild.id})`);
572
+ throw createLocalWorkspaceError(`Local workspace context is only available for the active local build (${resolvedBuild.id})`);
403
573
  }
404
574
  let mappedComparisons = snapshot.reportData.comparisons.map(comparison => mapLocalComparison(snapshot, comparison));
405
- let mappedScreenshots = snapshot.reportData.comparisons.map(comparison => mapLocalScreenshot(snapshot, comparison));
575
+ let mappedScreenshots = mappedComparisons.map(comparison => projectLocalScreenshot(comparison.screenshot, comparison.baseline));
406
576
  let reviewSummary = buildReviewSummary(snapshot.reportData.comparisons);
407
577
  let reviewState = buildReviewState(resolvedBuild, reviewSummary);
408
- return {
578
+ let context = {
409
579
  resource: 'build_context',
410
580
  source: LOCAL_CONTEXT_SOURCE,
411
581
  scope: createScope(),
@@ -436,8 +606,48 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
436
606
  },
437
607
  links: createBuildLinks(snapshot)
438
608
  };
609
+ if (!query.details) return context;
610
+ let revision = createLocalSnapshotRevision(snapshot);
611
+ let limit = query.limit || 10;
612
+ let cursorQuery = `${query.details}:${limit}`;
613
+ let cursor = readLocalCursor(query.cursor, 'build_context', resolvedBuild.id, cursorQuery, revision, ['evidence']);
614
+ let includeDiffs = query.details === 'diffs';
615
+ let evidence = mappedComparisons.map(comparison => projectLocalEvidence(comparison, includeDiffs));
616
+ return {
617
+ resource: 'build_context',
618
+ source: LOCAL_CONTEXT_SOURCE,
619
+ scope: context.scope,
620
+ build: context.build,
621
+ baseline: context.baseline,
622
+ status: context.status,
623
+ summary: context.summary,
624
+ preview: null,
625
+ signature_properties: [],
626
+ dynamic_regions: {
627
+ exclusions: {
628
+ total: null,
629
+ included: false,
630
+ details_available: false
631
+ },
632
+ item_details_included: false
633
+ },
634
+ evidence: createLocalPage(evidence, {
635
+ limit,
636
+ offset: cursor?.offset || 0,
637
+ resource: 'build_context',
638
+ target: resolvedBuild.id,
639
+ query: cursorQuery,
640
+ stream: 'evidence',
641
+ revision
642
+ }),
643
+ comments: {
644
+ build: compactLocalCollection(context.comments.build),
645
+ screenshot_count: context.comments.screenshot_count
646
+ },
647
+ links: context.links
648
+ };
439
649
  }
440
- function getComparisonContext(comparisonId) {
650
+ function getComparisonContext(comparisonId, query = {}) {
441
651
  let snapshot = loadSnapshot();
442
652
  let comparison = findComparison(snapshot, comparisonId);
443
653
  if (!comparison) {
@@ -446,7 +656,7 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
446
656
  let mappedComparison = mapLocalComparison(snapshot, comparison);
447
657
  let comparisonName = comparison.originalName || comparison.name;
448
658
  let history = snapshot.reportData.comparisons.filter(candidate => candidate.id !== comparison.id && (candidate.originalName || candidate.name) === comparisonName).map(candidate => mapLocalComparison(snapshot, candidate));
449
- return {
659
+ let context = {
450
660
  resource: 'comparison_context',
451
661
  source: LOCAL_CONTEXT_SOURCE,
452
662
  scope: createScope(),
@@ -475,6 +685,52 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
475
685
  },
476
686
  links: buildComparisonLinks(snapshot, comparison.id)
477
687
  };
688
+ if (!query.details) return context;
689
+ let revision = createLocalSnapshotRevision(snapshot);
690
+ let limit = query.limit || 10;
691
+ let cursorQuery = `${query.details}:${limit}`;
692
+ let cursor = readLocalCursor(query.cursor, 'comparison_context', comparison.id, cursorQuery, revision, ['similar_by_fingerprint', 'recent_by_name']);
693
+ let activeStream = cursor?.stream;
694
+ let similarOffset = activeStream === 'similar_by_fingerprint' ? cursor.offset : 0;
695
+ let recentOffset = activeStream === 'recent_by_name' ? cursor.offset : 0;
696
+ return {
697
+ resource: 'comparison_context',
698
+ source: LOCAL_CONTEXT_SOURCE,
699
+ scope: context.scope,
700
+ build: context.build,
701
+ signature_properties: [],
702
+ comparison: projectLocalFocusedComparison(context.comparison, query.details === 'diffs'),
703
+ dynamic_regions: createLocalDynamicRegions(context.comparison),
704
+ history: {
705
+ active_stream: activeStream || null,
706
+ similar_by_fingerprint: createLocalPage([], {
707
+ limit,
708
+ offset: similarOffset,
709
+ resource: 'comparison_context',
710
+ target: comparison.id,
711
+ query: cursorQuery,
712
+ stream: 'similar_by_fingerprint',
713
+ revision
714
+ }),
715
+ recent_by_name: createLocalPage(history.map(projectLocalHistoryItem), {
716
+ limit,
717
+ offset: recentOffset,
718
+ resource: 'comparison_context',
719
+ target: comparison.id,
720
+ query: cursorQuery,
721
+ stream: 'recent_by_name',
722
+ revision
723
+ })
724
+ },
725
+ review: {
726
+ review_summary: context.review.review_summary,
727
+ assignments: compactLocalCollection(context.review.assignments),
728
+ build_comments: compactLocalCollection(context.review.build_comments),
729
+ screenshot_comments: compactLocalCollection(context.review.screenshot_comments)
730
+ },
731
+ links: context.links,
732
+ details: query.details
733
+ };
478
734
  }
479
735
  function getScreenshotContext(screenshotName) {
480
736
  let snapshot = loadSnapshot();
@@ -55,7 +55,7 @@ export async function loadPlugins(configPath, config) {
55
55
  * Discover installed plugins from node_modules/@vizzly-testing/*
56
56
  * @returns {Promise<Array>} Array of plugin info objects
57
57
  */
58
- export async function discoverInstalledPlugins() {
58
+ async function discoverInstalledPlugins() {
59
59
  let plugins = [];
60
60
  try {
61
61
  // Find all @vizzly-testing packages