Files
open-design/scripts/check-plugin-preview-manifest.test.ts
T
390fcf88f3 fix(plugin-previews): keep Community gallery previews in sync with shipped plugins (#5262)
* fix(plugin-previews): validate baked preview metadata against shipped plugins

The Home Community gallery renders whatever data/plugin-previews/manifest.json
names, and nothing kept that metadata in sync with reality: #4815 deleted 19
example plugins but left all 19 manifest entries behind, #4040 shipped a card
showing another plugin's imagery, a failed re-bake silently leaves the old
clip serving outdated content, and durationMs recorded the intended walk time
rather than the encoded file (23 entries carried holdMs > real duration, so
the card's idle loop pointed past the end of the clip).

- add scripts/check-plugin-preview-manifest.ts to pnpm guard: every manifest
  entry must name a shipped plugin, own its clip keys (<id>/<hash>/... or the
  legacy <id>.<hash>....), carry a coherent fingerprint, and never share a
  clip with another entry; prune the 19 orphaned entries it caught
- bake validation (BAKE_VERSION 5): reject blank clips (whole-clip luma range
  via ffprobe signalstats), record durationMs from the encoded file, clamp
  holdMs to it, and write a bake-report.json naming skipped/stale/blank
  plugins instead of swallowing the failure
- strict mode (--strict / PREVIEW_STRICT=1) fails the run when a bake left
  broken metadata behind; the pre-merge validation workflow now runs strict
  while the post-merge/nightly publishers stay lenient

* fix(plugins): make the hyperframes example self-contained

The example loaded cdn.tailwindcss.com (a render-blocking head script the
page never used — every style is inline) plus Google Fonts. Inside the
sandboxed gallery iframe or on a slow network the stalled script showed a
long blank instead of the composition, and the CI bake raced the webfonts.
Drop both: the page now renders from local system font stacks with zero
external requests, so the live preview, the detail iframe, and the baked
clip all show the same content.

* fix(plugin-previews): address review — fail fast on probe errors, widen strict trigger paths

- probeClipMs/clipLumaRange now throw instead of returning null: ffprobe is
  required validation infrastructure, and a swallowed probe error silently
  disabled exactly the checks this PR adds. A thrown error becomes an
  'error …' skip, is recorded in bake-report.json under 'errors', and fails
  strict mode — while the routine skips every sweep has (non-html plugins
  404ing the preview route) still never trip strict.
- bake-plugin-previews-pr.yml now also triggers on plugins/community/**,
  .github/actions/bake-previews/**, and itself, so community-plugin preview
  changes and bake-recipe changes cannot merge without the strict pre-merge
  validation.

* fix(plugin-previews): bake static pages as a held still instead of skipping forever

The strict pre-merge bake on this PR caught five manifest entries that could
NEVER refresh: example-dating-web, example-html-ppt-zhangzara-8-bit-orbit,
example-frame-logo-outro, od-new-generation, example-orbit-notion. All five
are (near-)static pages — the CDP screencast only delivers frames when
something repaints, so they produce 1-4 frames and the old 'frames < 5' guard
skipped them on every bake, nightly included, leaving their committed entries
permanently stale (their old clips are the 103-210ms degenerate files in the
manifest today).

Encode <5-frame captures as a still instead: hold the last frame for the
idle-loop span (half before the trailing repeated concat entry, half
inherited by it, summing to ~HOLD_MS). Only a zero-frame capture is a real
failure now. The blank-luma check still rejects stills of nothing.

* fix(plugin-previews): never persist a manifest entry without a fingerprint

When the preview fingerprint fetch failed, the loop still rendered and — on
an otherwise successful bake — persisted an entry with hash: null and
un-fingerprinted keys: metadata the content-hash reuse skip can never match
again and the new manifest guard rejects. Treat a missing fingerprint as an
infrastructure error instead: skip the plugin, record it under errors in
bake-report.json (strict mode exits non-zero), and leave any committed entry
untouched for the next sweep.

---------

Co-authored-by: Tuola Ge <gexingli@refly.ai>
2026-07-08 03:44:05 +00:00

99 lines
3.9 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import { validatePreviewManifest } from "./check-plugin-preview-manifest.ts";
const HASH_A = "a2e7f0664e7ceed4";
const HASH_B = "0123456789abcdef";
function manifestWith(previews: Record<string, unknown>): unknown {
return { generatedAt: null, previews };
}
function dirEntry(id: string, hash: string = HASH_A): Record<string, unknown> {
return {
video: `${id}/${hash}/preview.mp4`,
poster: `${id}/${hash}/poster.jpg`,
durationMs: 3667,
holdMs: 2500,
hash,
};
}
function flatEntry(id: string, hash: string = HASH_A): Record<string, unknown> {
return {
video: `${id}.${hash}.mp4`,
poster: `${id}.${hash}.poster.jpg`,
durationMs: 3667,
holdMs: 2500,
hash,
};
}
test("accepts directory-layout and legacy flat-layout entries", () => {
const manifest = manifestWith({
"example-video-hyperframes": dirEntry("example-video-hyperframes"),
"example-invoice": flatEntry("example-invoice", HASH_B),
});
const ids = new Set(["example-video-hyperframes", "example-invoice"]);
assert.deepEqual(validatePreviewManifest(manifest, ids), []);
});
test("flags an orphan entry whose plugin no longer ships", () => {
const manifest = manifestWith({ "example-luxury-botanical": flatEntry("example-luxury-botanical") });
const violations = validatePreviewManifest(manifest, new Set(["example-invoice"]));
assert.equal(violations.length, 1);
assert.match(violations[0]!, /example-luxury-botanical: no such plugin ships/);
});
test("flags clip keys that belong to a different plugin id", () => {
const manifest = manifestWith({
"example-video-hyperframes": {
...dirEntry("example-video-hyperframes"),
video: `example-figma/${HASH_A}/preview.mp4`,
},
});
const violations = validatePreviewManifest(manifest, new Set(["example-video-hyperframes"]));
assert.equal(violations.length, 1);
assert.match(violations[0]!, /video key "example-figma\/.*" does not belong to this plugin id/);
});
test("flags a key whose embedded fingerprint disagrees with the recorded hash", () => {
const manifest = manifestWith({
"example-invoice": { ...flatEntry("example-invoice", HASH_A), hash: HASH_B },
});
const violations = validatePreviewManifest(manifest, new Set(["example-invoice"]));
assert.equal(violations.length, 2); // video + poster both carry the old hash
assert.match(violations[0]!, /does not carry the recorded hash/);
});
test("flags two entries claiming the same clip", () => {
const shared = dirEntry("example-a");
const manifest = manifestWith({
"example-a": shared,
"example-b": { ...shared },
});
const violations = validatePreviewManifest(manifest, new Set(["example-a", "example-b"]));
// example-b's keys belong to example-a (2 ownership violations) and both
// keys are double-claimed (2 duplicate violations).
assert.equal(violations.filter((v) => /does not belong/.test(v)).length, 2);
assert.equal(violations.filter((v) => /claimed by 2 entries/.test(v)).length, 2);
});
test("flags malformed hash, missing keys, and non-positive timings", () => {
const manifest = manifestWith({
"example-a": { poster: "example-a/x/poster.jpg", hash: "NOT-HEX", durationMs: 0, holdMs: -5 },
});
const violations = validatePreviewManifest(manifest, new Set(["example-a"]));
assert.ok(violations.some((v) => /video must be a non-empty string/.test(v)));
assert.ok(violations.some((v) => /hash must be 16 lowercase hex chars/.test(v)));
assert.ok(violations.some((v) => /durationMs must be a positive finite number/.test(v)));
assert.ok(violations.some((v) => /holdMs must be a positive finite number/.test(v)));
});
test("rejects a manifest without a previews object", () => {
const violations = validatePreviewManifest({ previews: [] }, new Set());
assert.equal(violations.length, 1);
assert.match(violations[0]!, /expected a \{ previews: \{ <id>: … \} \} object/);
});