Files
ttstd-infinite-canvas/web/test/creative-agent-state.test.ts

54 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, test } from "bun:test";
import { defaultConfig, type AiConfig } from "../src/stores/use-config-store";
import { defaultModelCapabilityConfig } from "../src/lib/model-capabilities";
import { assertCreativeBriefSpecifications, creativeProposalOps, creativeVideoSpecificationError, mergeCreativeBrief, normalizeCreativeProposal } from "../src/lib/creation/creative-agent-state";
import { applyCanvasAgentOps, type CanvasAgentSnapshot } from "../src/lib/canvas/canvas-agent-ops";
const image = defaultModelCapabilityConfig("openai-image", "image-model");
const video = defaultModelCapabilityConfig("volcengine-ark-video", "video-model");
const config: AiConfig = { ...defaultConfig, model: "managed::image-model", imageModel: "managed::image-model", videoModel: "managed::video-model", models: ["managed::image-model", "managed::video-model"], imageModels: ["managed::image-model"], videoModels: ["managed::video-model"], channels: [{ ...defaultConfig.channels[0]!, id: "managed", models: ["image-model", "video-model"], modelCosts: [{ model: "image-model", capability: "image", capabilityConfig: image }, { model: "video-model", capability: "video", capabilityConfig: video }] }] };
const empty: CanvasAgentSnapshot = { projectId: "canvas", title: "test", nodes: [], connections: [], selectedNodeIds: [], viewport: { x: 0, y: 0, k: 1 } };
function shortFilm() {
return { title: "悬疑短片", summary: "一个电梯故事", markdown: "完整故事大纲", deliverables: ["15 秒竖屏短片"], workflow: { nodes: [{ ref: "outline", kind: "text", title: "大纲", content: "叙事节奏" }, ...["character", "scene", "prop"].map((ref) => ({ ref, kind: "image", title: ref, prompt: ref })), { ref: "film", kind: "video", title: "短片", prompt: "电梯内发生异常", referenceRefs: ["character", "scene", "prop"] }], edges: ["character", "scene", "prop"].map((ref) => ({ from: "outline", to: ref })) }, generationItems: [...["character", "scene", "prop"].map((ref) => ({ ref, mode: "image", model: "managed::image-model" })), { ref: "film", mode: "video", model: "managed::video-model", seconds: 15, size: "9:16", referenceRefs: ["character", "scene", "prop"] }] };
}
describe("创作方案执行合同", () => {
test("实际视频元数据不符不能按方案宣称成功", () => {
const item = { ref: "film", mode: "video" as const, model: "video", seconds: 15, size: "9:16" };
expect(creativeVideoSpecificationError(item, { durationMs: 6000, width: 720, height: 1280 })).toContain("6.00 秒");
expect(creativeVideoSpecificationError(item, { durationMs: 15000, width: 1280, height: 720 })).toContain("比例不符");
expect(creativeVideoSpecificationError(item, { durationMs: 15033, width: 720, height: 1280 })).toBeUndefined();
});
test("单段短片为5节点6真实连线,参考图并排分层,ID跨恢复稳定", () => {
const proposal = normalizeCreativeProposal(shortFilm(), "p", 1, config);
const ops = creativeProposalOps("run", proposal, empty, config);
const next = applyCanvasAgentOps(empty, ops);
expect(next.nodes).toHaveLength(5); expect(next.connections).toHaveLength(6);
expect(ops.some((op) => op.type === "run_generation")).toBe(false);
expect(next.nodes.filter((node) => node.type === "video")).toHaveLength(1);
const refs = next.nodes.filter((node) => node.type === "image");
expect(new Set(refs.map((node) => node.position.x)).size).toBe(1);
expect(new Set(refs.map((node) => node.position.y)).size).toBe(3);
expect(creativeProposalOps("run", proposal, empty, config)).toEqual(ops);
});
test("不能静默缩短已确认时长,也不能忽略模型输入上限", () => {
const raw = shortFilm(); raw.generationItems[3]!.seconds = 10;
const proposal = normalizeCreativeProposal(raw, "p", 1, config);
expect(() => assertCreativeBriefSpecifications(proposal, { seconds: { value: 15, source: "user", status: "confirmed" } })).toThrow("15 秒");
const limited = structuredClone(config); limited.channels[0]!.modelCosts![1]!.capabilityConfig!.video!.references.maxImages = 1;
expect(() => normalizeCreativeProposal(shortFilm(), "p", 1, limited)).toThrow("参考图");
});
test("已有商品图进入正式引用节点但不进入收费生成项", () => {
const proposal = normalizeCreativeProposal({ title: "海报", summary: "产品海报", markdown: "两版卖点设计", deliverables: ["营销图"], workflow: { nodes: [{ ref: "product", kind: "image", title: "商品", assetId: "asset" }, { ref: "poster", kind: "image", title: "海报", prompt: "保持商品实物", referenceRefs: ["product"] }], edges: [] }, generationItems: [{ ref: "poster", mode: "image", model: "managed::image-model", referenceRefs: ["product"] }] }, "p", 1, config, [{ id: "asset", assetId: "asset", title: "商品实物", kind: "image", storageKey: "resource:product" }]);
expect(proposal.generationItems).toHaveLength(1);
const nodes = applyCanvasAgentOps(empty, creativeProposalOps("run", proposal, empty, config)).nodes;
expect(nodes[0]!.metadata?.assetId).toBe("asset"); expect(nodes[0]!.metadata?.status).toBe("success");
expect(nodes[1]!.metadata?.referenceNodeIds).toEqual([nodes[0]!.id]);
});
test("模型推断不能降低程序已确认字段的状态", () => {
const known = { seconds: { value: 15, source: "user" as const, status: "confirmed" as const } };
expect(mergeCreativeBrief(known, [{ field: "seconds", value: 15, source: "user", status: "inferred" }], "short-film")).toEqual(known);
expect(mergeCreativeBrief(known, [{ field: "seconds", value: 6, source: "user", status: "confirmed" }], "short-film", "请继续")).toEqual(known);
expect(mergeCreativeBrief(known, [{ field: "seconds", value: 30, source: "user", status: "confirmed", evidence: "改为30秒" }], "short-film", "时长改为30秒").seconds?.value).toBe(30);
});
});