mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 00:56:23 +00:00
* fix(skills): resolve the blueprint id from a qualified `blueprint:` field visual-design.md documents `blueprint:` as the id plus a `(Reproduce)` / `(Adapt)` qualifier, and prints `dataviz-countup (Adapt)` as its worked example. The packet builder used that raw field as a filename, so a qualified blueprint looked for `<id> (Adapt).md`, found nothing, and inlined an empty string: `selectedFile()` returns "" for a missing path. Every packet shipped without the one document the frame was designed against, and the run still exited 0 with nothing on stderr. `compose (Adapt)` missed the `compose` check the same way. Parse the field into the id it names, once, so no caller resolves a raw field value against the blueprints directory. A blueprint that resolves to no file is now a named error rather than an empty section, matching how the builder already treats a missing `src` and an oversize packet. The existing tests only used bare ids, which is how the qualified form escaped; they now cover both, and the missing-file case. One owner: product-launch-video, faceless-explainer, pr-to-video and general-video all delegate to frame-packets-core.mjs. Co-Authored-By: anikam13 <22992075+anikam13@users.noreply.github.com> * fix(skills): degrade, not fail, when the blueprints library is absent Self-review catch on the previous commit. hyperframes-animation installs on demand, so its blueprints/ directory can legitimately be missing — that is a skill that isn't installed yet, not a frame naming a bad id. Throwing there turned a silent degrade into a hard failure for a valid setup. Distinguish the two: an absent blueprints/ warns and inlines nothing, exactly as an absent rules/ already does in knownRuleIds; a present library that has no file for this id still throws, because that is a typo or an unstripped qualifier. Co-Authored-By: anikam13 <22992075+anikam13@users.noreply.github.com> * fix(skills): point two dead blueprint references at real shapes CI surfaced these once an unresolvable blueprint stopped being silent. Both named ids that have never existed in hyperframes-animation/blueprints/: - faceless-explainer's frame template taught `messaging-multi-phase`, so an agent copying the template verbatim tagged a blueprint that resolves to nothing. dataviz-countup is what the same skill already uses in its own visual-design template and tests. - pr-to-video's diff-excerpt guardrail fixture used `number-lockup`. The test is about diff excerpting and the id was incidental; the frame's own `counting-dynamic-scale` rule makes dataviz-countup the natural real shape. A sweep of every `blueprint:` value across skills/ finds no others. Co-Authored-By: anikam13 <22992075+anikam13@users.noreply.github.com> --------- Co-authored-by: anikam13 <22992075+anikam13@users.noreply.github.com>
145 lines
6.9 KiB
JavaScript
145 lines
6.9 KiB
JavaScript
import assert from "node:assert/strict";
|
||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
||
import { tmpdir } from "node:os";
|
||
import { dirname, join } from "node:path";
|
||
import test from "node:test";
|
||
|
||
import { buildFramePackets } from "./frame-packets.mjs";
|
||
|
||
function write(path, contents) {
|
||
mkdirSync(dirname(path), { recursive: true });
|
||
writeFileSync(path, contents);
|
||
}
|
||
|
||
test("packets inline the blueprint body and the Scene-cited rule recipes", () => {
|
||
const project = mkdtempSync(join(tmpdir(), "plv-packets-"));
|
||
write(join(project, "frame.md"), "# tokens\n");
|
||
write(
|
||
join(project, "STORYBOARD.md"),
|
||
`---\nformat: 1920x1080\n---\n\n## Frame 1 — Hook\n\n- duration: 3s\n- src: compositions/frames/01-hook.html\n- blueprint: dataviz-countup\n- scene: hero stat punches in\n\nScene 1 (0.0–1.5s): the stat enters via spring-pop-entrance, then counting-dynamic-scale runs the tally.\n\n## Frame 2 — Freeform\n\n- duration: 4s\n- src: compositions/frames/02-freeform.html\n- blueprint: compose\n\nScene 1 (0.0–4.0s): a quiet hold, no named motion.\n`,
|
||
);
|
||
|
||
const result = buildFramePackets({ projectDir: project });
|
||
assert.equal(result.length, 2);
|
||
|
||
const hook = readFileSync(result[0].path, "utf8");
|
||
assert.match(hook, /## Selected blueprint: dataviz-countup/);
|
||
assert.match(hook, /## Selected motion rule: spring-pop-entrance/);
|
||
assert.match(hook, /## Selected motion rule: counting-dynamic-scale/);
|
||
assert.match(hook, /RULES_DIR: /);
|
||
|
||
const freeform = readFileSync(result[1].path, "utf8");
|
||
assert.doesNotMatch(freeform, /## Selected blueprint/);
|
||
assert.doesNotMatch(freeform, /## Selected motion rule/);
|
||
});
|
||
|
||
test("_role.md is the core contract + this workflow's delta, verbatim", () => {
|
||
const project = mkdtempSync(join(tmpdir(), "plv-role-"));
|
||
write(join(project, "frame.md"), "# tokens\n");
|
||
write(
|
||
join(project, "STORYBOARD.md"),
|
||
`---\nformat: 1920x1080\n---\n\n## Frame 1 — Hook\n\n- duration: 3s\n- src: compositions/frames/01-hook.html\n`,
|
||
);
|
||
|
||
buildFramePackets({ projectDir: project });
|
||
const rolePath = join(project, ".hyperframes", "frame-packets", "_role.md");
|
||
assert.ok(existsSync(rolePath));
|
||
const role = readFileSync(rolePath, "utf8");
|
||
assert.match(role, /# Frame worker — core contract/);
|
||
assert.match(role, /# Frame worker — product-launch delta/);
|
||
});
|
||
|
||
test("packet validation is atomic and leaves no partial output on overflow", () => {
|
||
const project = mkdtempSync(join(tmpdir(), "plv-atomic-"));
|
||
const outDir = join(project, ".hyperframes", "frame-packets");
|
||
write(join(project, "frame.md"), "# tokens\n");
|
||
write(
|
||
join(project, "STORYBOARD.md"),
|
||
`---\nformat: 1920x1080\n---\n\n## Frame 1 — Big\n\n- duration: 3s\n- src: compositions/frames/01-big.html\n\n${"padding line\n".repeat(300)}`,
|
||
);
|
||
|
||
assert.throws(
|
||
() => buildFramePackets({ projectDir: project, outDir, maxPacketBytes: 2_000 }),
|
||
/limit 2000/,
|
||
);
|
||
assert.equal(existsSync(outDir), false);
|
||
});
|
||
|
||
// ── the blueprint qualifier ──────────────────────────────────────────────────
|
||
// Regression: visual-design.md documents `blueprint:` as the id plus a
|
||
// `(Reproduce)` / `(Adapt)` qualifier, and prints `dataviz-countup (Adapt)` as
|
||
// its worked example. The resolver used the raw field as the filename, so every
|
||
// qualified blueprint looked for a file that cannot exist and inlined "" —
|
||
// packets shipped without the document the frame was designed against, and the
|
||
// run still reported success. The cases above only ever used bare ids.
|
||
|
||
test("a qualified blueprint resolves to the same body as the bare id", () => {
|
||
const project = mkdtempSync(join(tmpdir(), "plv-blueprint-qualified-"));
|
||
write(join(project, "frame.md"), "# tokens\n");
|
||
write(
|
||
join(project, "STORYBOARD.md"),
|
||
`---\nformat: 1920x1080\n---\n\n## Frame 1 — Adapted\n\n- duration: 3s\n- src: compositions/frames/01-adapted.html\n- blueprint: device-surface-showcase (Adapt)\n\n## Frame 2 — Reproduced\n\n- duration: 3s\n- src: compositions/frames/02-reproduced.html\n- blueprint: device-surface-showcase (Reproduce)\n\n## Frame 3 — Bare\n\n- duration: 3s\n- src: compositions/frames/03-bare.html\n- blueprint: device-surface-showcase\n`,
|
||
);
|
||
|
||
const packets = buildFramePackets({ projectDir: project });
|
||
const blueprintSections = packets.map((packet) => {
|
||
const body = readFileSync(packet.path, "utf8");
|
||
const start = body.indexOf("## Selected blueprint:");
|
||
assert.notEqual(start, -1, `${packet.frameId} inlined no blueprint`);
|
||
return body.slice(start);
|
||
});
|
||
|
||
assert.match(blueprintSections[0], /## Selected blueprint: device-surface-showcase\n/);
|
||
// The qualifier is direction for the worker, not a different document: all
|
||
// three frames must inline byte-identical blueprint bodies.
|
||
assert.equal(new Set(blueprintSections).size, 1);
|
||
});
|
||
|
||
test("a qualified `compose` still selects no blueprint", () => {
|
||
const project = mkdtempSync(join(tmpdir(), "plv-blueprint-compose-"));
|
||
write(join(project, "frame.md"), "# tokens\n");
|
||
write(
|
||
join(project, "STORYBOARD.md"),
|
||
`---\nformat: 1920x1080\n---\n\n## Frame 1 — Freeform\n\n- duration: 3s\n- src: compositions/frames/01-freeform.html\n- blueprint: compose (Adapt)\n`,
|
||
);
|
||
|
||
const [packet] = buildFramePackets({ projectDir: project });
|
||
|
||
assert.doesNotMatch(readFileSync(packet.path, "utf8"), /## Selected blueprint/);
|
||
});
|
||
|
||
test("a blueprint with no file fails the run instead of shipping an empty section", () => {
|
||
const project = mkdtempSync(join(tmpdir(), "plv-blueprint-missing-"));
|
||
const outDir = join(project, ".hyperframes", "frame-packets");
|
||
write(join(project, "frame.md"), "# tokens\n");
|
||
write(
|
||
join(project, "STORYBOARD.md"),
|
||
`---\nformat: 1920x1080\n---\n\n## Frame 1 — Typo\n\n- duration: 3s\n- src: compositions/frames/01-typo.html\n- blueprint: device-surface-showcses\n`,
|
||
);
|
||
|
||
assert.throws(
|
||
() => buildFramePackets({ projectDir: project, outDir }),
|
||
/01-typo: blueprint "device-surface-showcses" has no file/,
|
||
);
|
||
assert.equal(existsSync(outDir), false);
|
||
});
|
||
|
||
test("an uninstalled animation skill degrades with a warning, it does not fail the run", () => {
|
||
// hyperframes-animation installs on demand, so an absent blueprints/ means the
|
||
// library isn't there yet — not that the frame named a bad id. Matches how an
|
||
// absent rules/ already behaves.
|
||
const project = mkdtempSync(join(tmpdir(), "plv-blueprint-uninstalled-"));
|
||
write(join(project, "frame.md"), "# tokens\n");
|
||
write(
|
||
join(project, "STORYBOARD.md"),
|
||
`---\nformat: 1920x1080\n---\n\n## Frame 1 — Hook\n\n- duration: 3s\n- src: compositions/frames/01-hook.html\n- blueprint: dataviz-countup (Adapt)\n`,
|
||
);
|
||
|
||
const [packet] = buildFramePackets({
|
||
projectDir: project,
|
||
animationDir: join(project, "absent-animation-skill"),
|
||
});
|
||
|
||
assert.doesNotMatch(readFileSync(packet.path, "utf8"), /## Selected blueprint/);
|
||
});
|