mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-08 02:36:10 +00:00
Post-release review of media-use ↔ figma coupling (spec §13.1): - figma asset imports now regenerate .media/index.md, the agent-readable inventory media-use maintains — format locked byte-identical via a cross-runner parity test against media-use's own index-gen.mjs - figma asset --description/--entity land in the manifest record, the index table, and <img alt>; component rasterize auto-describes with the node name. Named brand marks become visible to media-use's resolve --entity lookups. - spec §13.1 records the review verdict (loose coupling correct) and the follow-up queue (shared media-ledger module, global cache for figma assets, media-use version-keyed idempotency) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
282 lines
8.3 KiB
JavaScript
282 lines
8.3 KiB
JavaScript
import { strict as assert } from "node:assert";
|
|
import { mkdtempSync, rmSync, writeFileSync, readFileSync, mkdirSync, existsSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import { execFileSync } from "node:child_process";
|
|
import { appendRecord, readManifest } from "./lib/manifest.mjs";
|
|
import { regenerateIndex } from "./lib/index-gen.mjs";
|
|
import { getProvider } from "./lib/providers.mjs";
|
|
import { freezeLocalFile } from "./lib/freeze.mjs";
|
|
import { cachePut, cacheGet, importFromCache } from "./lib/cache.mjs";
|
|
|
|
const REPO_ROOT = join(import.meta.dirname, "..", "..", "..");
|
|
const RESOLVE_CLI = join(import.meta.dirname, "resolve.mjs");
|
|
let tmp;
|
|
|
|
function setup() {
|
|
tmp = mkdtempSync(join(tmpdir(), "mu-resolve-test-"));
|
|
}
|
|
|
|
function cleanup() {
|
|
if (tmp) rmSync(tmp, { recursive: true, force: true });
|
|
}
|
|
|
|
function makeRecord(overrides = {}) {
|
|
return {
|
|
id: "bgm_001",
|
|
type: "bgm",
|
|
path: ".media/audio/bgm/bgm_001.wav",
|
|
source: "search",
|
|
description: "soft minimal ambient",
|
|
duration: 11,
|
|
provenance: { provider: "test", prompt: "test prompt" },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
// Run resolve.mjs with argv passed as a literal array (no shell). Each token is
|
|
// a separate argv entry, so a value with spaces or shell metacharacters can't
|
|
// break out — never build a command string and hand it to a shell.
|
|
function runResolve(args, opts = {}) {
|
|
return execFileSync(process.execPath, [RESOLVE_CLI, ...args], {
|
|
cwd: REPO_ROOT,
|
|
encoding: "utf8",
|
|
...opts,
|
|
});
|
|
}
|
|
|
|
const tests = [];
|
|
function test(name, fn) {
|
|
tests.push({ name, fn });
|
|
}
|
|
|
|
// --- manifest cache hit ---
|
|
|
|
test("project manifest hit skips providers", () => {
|
|
setup();
|
|
const record = makeRecord({ provenance: { prompt: "cached query", provider: "test" } });
|
|
appendRecord(tmp, record);
|
|
const filePath = join(tmp, record.path);
|
|
mkdirSync(join(filePath, ".."), { recursive: true });
|
|
writeFileSync(filePath, "cached audio");
|
|
|
|
const out = runResolve(["--type", "bgm", "--intent", "cached query", "--project", tmp, "--json"]);
|
|
const parsed = JSON.parse(out.trim());
|
|
assert.equal(parsed.ok, true);
|
|
assert.equal(parsed.id, "bgm_001");
|
|
assert.equal(parsed._source, "cached");
|
|
cleanup();
|
|
});
|
|
|
|
test("entity hit matches across icon/image (figma-imported brand marks)", () => {
|
|
setup();
|
|
const record = makeRecord({
|
|
id: "image_001",
|
|
type: "image",
|
|
path: ".media/images/image_001.svg",
|
|
description: "Acme logo",
|
|
entity: "Acme logo",
|
|
provenance: { source: "figma", fileKey: "KEY", nodeId: "1:2", version: "1", format: "svg" },
|
|
});
|
|
delete record.duration;
|
|
appendRecord(tmp, record);
|
|
const filePath = join(tmp, record.path);
|
|
mkdirSync(join(filePath, ".."), { recursive: true });
|
|
writeFileSync(filePath, "<svg/>");
|
|
|
|
const out = runResolve([
|
|
"--type",
|
|
"icon",
|
|
"--intent",
|
|
"acme brand mark",
|
|
"--entity",
|
|
"Acme logo",
|
|
"--project",
|
|
tmp,
|
|
"--json",
|
|
]);
|
|
const parsed = JSON.parse(out.trim());
|
|
assert.equal(parsed.ok, true);
|
|
assert.equal(parsed.id, "image_001");
|
|
assert.equal(parsed._source, "cached");
|
|
cleanup();
|
|
});
|
|
|
|
// --- global cache hit ---
|
|
|
|
test("global cache hit copies to project and registers", () => {
|
|
setup();
|
|
const sourceFile = join(tmp, "source.wav");
|
|
writeFileSync(sourceFile, "cached globally for resolve");
|
|
const record = makeRecord({ provenance: { prompt: "global resolve test" } });
|
|
cachePut(sourceFile, record);
|
|
|
|
const cached = cacheGet("global resolve test", "bgm");
|
|
assert.ok(cached);
|
|
|
|
const projectDir = mkdtempSync(join(tmpdir(), "mu-resolve-proj-"));
|
|
const imported = importFromCache(cached, projectDir, "bgm_001", ".media/audio/bgm/bgm_001.wav");
|
|
assert.ok(imported);
|
|
assert.ok(existsSync(join(projectDir, ".media/audio/bgm/bgm_001.wav")));
|
|
|
|
appendRecord(projectDir, imported);
|
|
regenerateIndex(projectDir);
|
|
const manifest = readManifest(projectDir);
|
|
assert.equal(manifest.length, 1);
|
|
assert.equal(manifest[0].provenance.imported_from, cached.sha);
|
|
|
|
rmSync(projectDir, { recursive: true, force: true });
|
|
cleanup();
|
|
});
|
|
|
|
// --- provider interface ---
|
|
|
|
test("getProvider returns provider with type", () => {
|
|
const p = getProvider("bgm");
|
|
assert.equal(p.type, "bgm");
|
|
assert.ok(typeof p.search === "function");
|
|
});
|
|
|
|
test("getProvider throws for unknown type", () => {
|
|
assert.throws(() => getProvider("unknown_type"), /unknown media type/);
|
|
});
|
|
|
|
// --- freeze ---
|
|
|
|
test("freezeLocalFile creates parent dirs and copies", () => {
|
|
setup();
|
|
const src = join(tmp, "src.bin");
|
|
writeFileSync(src, "freeze test data");
|
|
const dest = join(tmp, "deep/nested/dir/file.bin");
|
|
freezeLocalFile(src, dest);
|
|
assert.ok(existsSync(dest));
|
|
assert.equal(readFileSync(dest, "utf8"), "freeze test data");
|
|
cleanup();
|
|
});
|
|
|
|
// --- adopt existing assets ---
|
|
|
|
test("--adopt registers existing assets/ files", () => {
|
|
setup();
|
|
mkdirSync(join(tmp, "assets/bgm"), { recursive: true });
|
|
mkdirSync(join(tmp, "assets/icons"), { recursive: true });
|
|
writeFileSync(join(tmp, "assets/bgm/track.mp3"), "fake mp3");
|
|
writeFileSync(join(tmp, "assets/icons/logo.svg"), "fake svg");
|
|
|
|
const out = runResolve(["--adopt", "--project", tmp, "--json"]);
|
|
const parsed = JSON.parse(out.trim());
|
|
assert.equal(parsed.ok, true);
|
|
assert.equal(parsed.adopted, 2);
|
|
assert.ok(parsed.assets.some((a) => a.path === "assets/bgm/track.mp3"));
|
|
assert.ok(parsed.assets.some((a) => a.path === "assets/icons/logo.svg"));
|
|
|
|
const manifest = readManifest(tmp);
|
|
assert.equal(manifest.length, 2);
|
|
cleanup();
|
|
});
|
|
|
|
test("--adopt skips already-registered assets", () => {
|
|
setup();
|
|
mkdirSync(join(tmp, "assets/bgm"), { recursive: true });
|
|
writeFileSync(join(tmp, "assets/bgm/track.mp3"), "fake mp3");
|
|
|
|
runResolve(["--adopt", "--project", tmp, "--json"]);
|
|
const out = runResolve(["--adopt", "--project", tmp, "--json"]);
|
|
const parsed = JSON.parse(out.trim());
|
|
assert.equal(parsed.adopted, 0);
|
|
|
|
const manifest = readManifest(tmp);
|
|
assert.equal(manifest.length, 1);
|
|
cleanup();
|
|
});
|
|
|
|
test("resolve finds existing unregistered asset before hitting providers", () => {
|
|
setup();
|
|
mkdirSync(join(tmp, "assets/bgm"), { recursive: true });
|
|
writeFileSync(join(tmp, "assets/bgm/ambient-track.mp3"), "existing bgm");
|
|
|
|
const out = runResolve([
|
|
"--type",
|
|
"bgm",
|
|
"--intent",
|
|
"ambient track",
|
|
"--project",
|
|
tmp,
|
|
"--json",
|
|
]);
|
|
const parsed = JSON.parse(out.trim());
|
|
assert.equal(parsed.ok, true);
|
|
assert.equal(parsed.path, "assets/bgm/ambient-track.mp3");
|
|
assert.equal(parsed._source, "existing");
|
|
cleanup();
|
|
});
|
|
|
|
// --- CLI interface ---
|
|
|
|
test("--help exits 0", () => {
|
|
const out = runResolve(["--help"]);
|
|
assert.ok(out.includes("media-use resolve"));
|
|
assert.ok(out.includes("--type"));
|
|
});
|
|
|
|
test("missing required args exits 2", () => {
|
|
try {
|
|
runResolve([], { stdio: "pipe" });
|
|
assert.fail("should have exited");
|
|
} catch (err) {
|
|
assert.equal(err.status, 2);
|
|
}
|
|
});
|
|
|
|
test("--json returns error JSON on stub provider failure", () => {
|
|
setup();
|
|
try {
|
|
runResolve(["--type", "bgm", "--intent", "stub fail", "--project", tmp, "--json"], {
|
|
stdio: "pipe",
|
|
});
|
|
assert.fail("should have exited");
|
|
} catch (err) {
|
|
const output = err.stdout || "";
|
|
const parsed = JSON.parse(output.trim());
|
|
assert.equal(parsed.ok, false);
|
|
assert.ok(parsed.error.includes("no provider"));
|
|
}
|
|
cleanup();
|
|
});
|
|
|
|
test("one-line output format matches contract", () => {
|
|
setup();
|
|
const record = makeRecord({ provenance: { prompt: "format test", provider: "test" } });
|
|
appendRecord(tmp, record);
|
|
const filePath = join(tmp, record.path);
|
|
mkdirSync(join(filePath, ".."), { recursive: true });
|
|
writeFileSync(filePath, "format check");
|
|
|
|
const out = runResolve(["--type", "bgm", "--intent", "format test", "--project", tmp]);
|
|
assert.match(out.trim(), /^resolved bgm_001 → .media\/audio\/bgm\/bgm_001\.wav \(bgm/);
|
|
cleanup();
|
|
});
|
|
|
|
// --- run ---
|
|
|
|
async function main() {
|
|
console.log("media-use · resolve engine tests\n");
|
|
let passed = 0;
|
|
let failed = 0;
|
|
for (const { name, fn } of tests) {
|
|
try {
|
|
await fn();
|
|
passed++;
|
|
console.log(` \x1b[32m✓\x1b[0m ${name}`);
|
|
} catch (err) {
|
|
failed++;
|
|
console.log(` \x1b[31m✗\x1b[0m ${name}`);
|
|
console.log(` ${err.message}`);
|
|
}
|
|
}
|
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
if (failed > 0) process.exit(1);
|
|
}
|
|
|
|
main();
|