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>
199 lines
8.1 KiB
JavaScript
199 lines
8.1 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { existsSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { homedir, tmpdir } from "node:os";
|
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
import test from "node:test";
|
|
|
|
import { parsePrReference, resolvePrToVideoProjectDir } from "./project-dir.mjs";
|
|
import { buildFramePackets } from "./frame-packets.mjs";
|
|
import { hasCliCommand } from "./preflight.mjs";
|
|
|
|
function write(path, contents) {
|
|
mkdirSync(dirname(path), { recursive: true });
|
|
writeFileSync(path, contents);
|
|
}
|
|
|
|
test("default project directory is durable and outside the caller repository", () => {
|
|
const caller = mkdtempSync(join(tmpdir(), "p2v-caller-"));
|
|
const cache = mkdtempSync(join(tmpdir(), "p2v-cache-"));
|
|
const result = resolvePrToVideoProjectDir({
|
|
pr: "https://github.com/EveryInc/compound-engineering-plugin/pull/1092",
|
|
cwd: caller,
|
|
env: { XDG_CACHE_HOME: cache, HOME: homedir() },
|
|
});
|
|
|
|
assert.equal(
|
|
result,
|
|
join(
|
|
cache,
|
|
"hyperframes",
|
|
"pr-to-video",
|
|
"everyinc",
|
|
"compound-engineering-plugin",
|
|
"compound-engineering-plugin-pr-1092",
|
|
),
|
|
);
|
|
assert.ok(isAbsolute(result));
|
|
assert.ok(relative(caller, result).startsWith(".."));
|
|
});
|
|
|
|
test("explicit project directory is preserved exactly after absolute resolution", () => {
|
|
const caller = mkdtempSync(join(tmpdir(), "p2v-explicit-caller-"));
|
|
assert.equal(
|
|
resolvePrToVideoProjectDir({
|
|
pr: "EveryInc/compound-engineering-plugin#1092",
|
|
cwd: caller,
|
|
explicitDir: "../my-video",
|
|
env: {},
|
|
}),
|
|
resolve(caller, "../my-video"),
|
|
);
|
|
});
|
|
|
|
test("distinct owner and repository segments cannot collide in the durable cache", () => {
|
|
const cache = mkdtempSync(join(tmpdir(), "p2v-cache-collision-"));
|
|
const first = resolvePrToVideoProjectDir({
|
|
pr: "foo-bar/baz#1",
|
|
env: { XDG_CACHE_HOME: cache },
|
|
});
|
|
const second = resolvePrToVideoProjectDir({
|
|
pr: "foo/bar-baz#1",
|
|
env: { XDG_CACHE_HOME: cache },
|
|
});
|
|
|
|
assert.notEqual(first, second);
|
|
});
|
|
|
|
test("PR parsing sanitizes owner and repository path traversal", () => {
|
|
assert.deepEqual(
|
|
parsePrReference("https://github.com/EveryInc/compound-engineering-plugin/pull/1092"),
|
|
{
|
|
owner: "everyinc",
|
|
repo: "compound-engineering-plugin",
|
|
number: 1092,
|
|
},
|
|
);
|
|
assert.throws(() => parsePrReference("../../outside#1092"), /valid GitHub PR reference/i);
|
|
});
|
|
|
|
test("#1092 packets contain selected excerpts but never the full diff", () => {
|
|
const project = mkdtempSync(join(tmpdir(), "p2v-packets-"));
|
|
const largeDiff = `diff --git a/noise b/noise\n${"+unselected noise\n".repeat(10_000)}`;
|
|
write(join(project, "capture", "diff.patch"), largeDiff);
|
|
write(join(project, "frame.md"), "# compact frame tokens\n");
|
|
write(
|
|
join(project, "STORYBOARD.md"),
|
|
`---\nformat: 1920x1080\n---\n\n## Frame 1 — Diff\n\n- duration: 4s\n- src: compositions/frames/01-diff.html\n- focal: code-diff\n- blueprint: compose\n- rules: text-reveal\n\n### Source excerpt\n\n\`\`\`diff\n-oldCall()\n+newCall({ attested: true })\n\`\`\`\n\n## Frame 2 — Impact\n\n- duration: 3s\n- src: compositions/frames/02-impact.html\n- blueprint: dataviz-countup\n- rules: counting-dynamic-scale\n`,
|
|
);
|
|
|
|
const result = buildFramePackets({
|
|
projectDir: project,
|
|
storyboardPath: join(project, "STORYBOARD.md"),
|
|
outDir: join(project, ".hyperframes", "frame-packets"),
|
|
maxPacketBytes: 32_000,
|
|
});
|
|
|
|
assert.equal(result.length, 2);
|
|
const codePacket = readFileSync(result[0].path, "utf8");
|
|
assert.match(codePacket, /newCall\(\{ attested: true \}\)/);
|
|
assert.doesNotMatch(codePacket, /unselected noise/);
|
|
assert.doesNotMatch(codePacket, /code-scroll/);
|
|
assert.ok(Buffer.byteLength(codePacket) < 32_000);
|
|
assert.ok(result.every((packet) => packet.path.endsWith(".md")));
|
|
|
|
const role = readFileSync(join(project, ".hyperframes", "frame-packets", "_role.md"), "utf8");
|
|
assert.match(role, /# Frame worker — core contract/);
|
|
assert.match(role, /# Frame worker — PR-to-video delta/);
|
|
});
|
|
|
|
test("packet validation is atomic and leaves no partial output on overflow", () => {
|
|
const project = mkdtempSync(join(tmpdir(), "p2v-packets-atomic-"));
|
|
const outDir = join(project, ".hyperframes", "frame-packets");
|
|
write(join(project, "frame.md"), "# frame\n");
|
|
write(
|
|
join(project, "STORYBOARD.md"),
|
|
`---\nformat: 1920x1080\n---\n\n## Frame 1 — Intro\n\n- duration: 2s\n- src: compositions/frames/01-intro.html\n\n## Frame 2 — Diff\n\n- duration: 4s\n- src: compositions/frames/02-diff.html\n- focal: code-diff\n\n### Source excerpt\n\n\`\`\`diff\n${"+oversized line\n".repeat(300)}\`\`\`\n`,
|
|
);
|
|
|
|
assert.throws(
|
|
() => buildFramePackets({ projectDir: project, outDir, maxPacketBytes: 2_000 }),
|
|
/limit 2000/,
|
|
);
|
|
assert.equal(existsSync(outDir), false);
|
|
});
|
|
|
|
test("code frames without an upstream-selected excerpt fail before dispatch", () => {
|
|
const project = mkdtempSync(join(tmpdir(), "p2v-packets-missing-"));
|
|
write(join(project, "frame.md"), "# frame\n");
|
|
write(
|
|
join(project, "STORYBOARD.md"),
|
|
`---\nformat: 1920x1080\n---\n\n## Frame 1 — Diff\n\n- duration: 4s\n- src: compositions/frames/01-diff.html\n- focal: code-diff\n`,
|
|
);
|
|
|
|
assert.throws(
|
|
() =>
|
|
buildFramePackets({
|
|
projectDir: project,
|
|
storyboardPath: join(project, "STORYBOARD.md"),
|
|
outDir: join(project, ".hyperframes", "frame-packets"),
|
|
}),
|
|
/Source excerpt/i,
|
|
);
|
|
});
|
|
|
|
// The other half of this skill's frame-packets delta. The excerpt guard above is
|
|
// pinned; the code-vocabulary injection was not — deleting `codeVocabularySection`
|
|
// outright left all 455 skills tests green, so the section a code worker reads to
|
|
// pick its registry block could have been dropped silently.
|
|
test("a code frame carries the vocabulary excerpt for the block it names", () => {
|
|
const project = mkdtempSync(join(tmpdir(), "p2v-packets-vocab-"));
|
|
write(join(project, "frame.md"), "# frame\n");
|
|
write(
|
|
join(project, "STORYBOARD.md"),
|
|
`---\nformat: 1920x1080\n---\n\n## Frame 1 — Diff\n\n- duration: 4s\n- src: compositions/frames/01-diff.html\n- focal: code-diff\n\n### Source excerpt\n\n\`\`\`diff\n-oldCall()\n+newCall()\n\`\`\`\n`,
|
|
);
|
|
|
|
const [packet] = buildFramePackets({ projectDir: project });
|
|
const contents = readFileSync(packet.path, "utf8");
|
|
|
|
assert.match(contents, /## Code block excerpt \(code-diff\)/);
|
|
assert.match(contents, /`code-diff`/);
|
|
});
|
|
|
|
test("a code block the vocabulary does not describe still names itself for install", () => {
|
|
const project = mkdtempSync(join(tmpdir(), "p2v-packets-vocab-miss-"));
|
|
write(join(project, "frame.md"), "# frame\n");
|
|
write(
|
|
join(project, "STORYBOARD.md"),
|
|
`---\nformat: 1920x1080\n---\n\n## Frame 1 — Diff\n\n- duration: 4s\n- src: compositions/frames/01-diff.html\n- focal: code-not-in-the-vocabulary\n\n### Source excerpt\n\n\`\`\`diff\n-oldCall()\n+newCall()\n\`\`\`\n`,
|
|
);
|
|
|
|
const [packet] = buildFramePackets({ projectDir: project });
|
|
|
|
assert.match(
|
|
readFileSync(packet.path, "utf8"),
|
|
/## Code block\n\nUse registry block `code-not-in-the-vocabulary`\./,
|
|
);
|
|
});
|
|
|
|
test("a mechanism frame gets no code-block section at all", () => {
|
|
const project = mkdtempSync(join(tmpdir(), "p2v-packets-mechanism-"));
|
|
write(join(project, "frame.md"), "# frame\n");
|
|
write(
|
|
join(project, "STORYBOARD.md"),
|
|
`---\nformat: 1920x1080\n---\n\n## Frame 1 — Mechanism\n\n- duration: 4s\n- src: compositions/frames/01-mechanism.html\n- focal: the request-lifecycle flow\n`,
|
|
);
|
|
|
|
const [packet] = buildFramePackets({ projectDir: project });
|
|
|
|
assert.doesNotMatch(readFileSync(packet.path, "utf8"), /## Code block/);
|
|
});
|
|
|
|
test("CLI capability detection rejects skills newer than the available command surface", () => {
|
|
const stableHelp = `Project:\n lint Validate a composition\n snapshot Capture frames\n\nUnknown command check`;
|
|
const currentHelp = `Project:\n lint Validate a composition\n check Run the full project validation gate\n snapshot Capture frames`;
|
|
|
|
assert.equal(hasCliCommand(stableHelp, "check"), false);
|
|
assert.equal(hasCliCommand(currentHelp, "check"), true);
|
|
});
|