Files
18998c4a42 Video pipeline fixes: visibility strip, rich briefs, spotlight timing + fps (#369)
* feat(video): pipeline visibility — strip, state-aware queue, render-error capture

Task 1 of the 2026-07-09 video-pipeline review. New CEO-gated GET
/video/pipeline lists every in-flight video item (authoring statuses,
rendering attempt n/max, terminal failures with the error — now stamped
onto the video_draft marker instead of dying as a log line).
source_task_id exposed on both video schemas. Panel: pipeline strip on
the Social page, state-aware queue empty copy, title/script on queue
rows, missing cuts disabled instead of a blank player, notifications
deep-link related_task_id. MAX_VIDEO_RENDER_ATTEMPTS moved to the
markers policy layer (single source of truth).

* feat(video): rich authoring briefs — changelog section, brand voice, kit pointer

Task 2 of the 2026-07-09 video-pipeline review. The release brief is
now a structured block (full CHANGELOG section capped at 4000 chars +
highlights) instead of one LLM-compressed sentence; brand_voice and a
motion/kit design-bar pointer are appended centrally in open_video_task
so release, spotlight, and on-demand paths all inherit them.
suggested_input_props seeded on the video_draft marker; third
acceptance criterion pins the design bar; propose_video docstring
points at the kit.

* fix(video): spotlight video drafts on CEO approval, renderer honors data-fps

Task 4 of the 2026-07-09 video-pipeline review. The companion-video
hook moves from propose_feature_spotlight (HoM authoring time) to
XPostService approve's posted-success branch for x_feature drafts,
mirroring the release-publish seam — a rejected spotlight no longer
burns a ux-dev cycle; wants_video/video_script ride the x_feature_ref
marker. Best-effort: a video-engine failure never breaks the post.
render.js reads data-fps from the composition HTML (clamped 24-60,
fallback 30) instead of hardcoding 30; parseFps covered by node --test.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-09 08:31:08 +02:00

47 lines
1.7 KiB
JavaScript

// node --test: node's built-in runner, no dependency needed. Only parseFps is
// exercised (a pure function) — renderComposition needs a real Chromium
// render and is covered by manual/e2e verification instead.
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { test } from "node:test";
import assert from "node:assert/strict";
import { parseFps } from "./render.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const compositionsRoot = path.join(__dirname, "..", "motion", "compositions");
test("parseFps reads data-fps off a real composition file", async () => {
const html = await readFile(
path.join(compositionsRoot, "release-announcement", "vertical.html"),
"utf8",
);
assert.equal(parseFps(html), 30);
});
test("parseFps reads a declared 24fps composition", () => {
const html = `<div id="stage" data-fps="24"></div>`;
assert.equal(parseFps(html), 24);
});
test("parseFps falls back to 30 when the attribute is missing", () => {
assert.equal(parseFps("<html><body>no fps here</body></html>"), 30);
});
test("parseFps clamps below the 24-60 bound to the 30 fallback", () => {
assert.equal(parseFps(`<div data-fps="15"></div>`), 30);
});
test("parseFps clamps above the 24-60 bound to the 30 fallback", () => {
assert.equal(parseFps(`<div data-fps="120"></div>`), 30);
});
test("parseFps falls back to 30 on an unparsable value", () => {
assert.equal(parseFps(`<div data-fps="abc"></div>`), 30);
});
test("parseFps accepts the boundary values 24 and 60", () => {
assert.equal(parseFps(`<div data-fps="24"></div>`), 24);
assert.equal(parseFps(`<div data-fps="60"></div>`), 60);
});