Files
ttstd-infinite-canvas/web/test/agent-storyboard.test.ts

90 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 { expect, test } from "bun:test";
import { buildAgentStoryboardDraft, buildAgentStoryboardOperations, validateAgentStoryboardCreation } from "../src/lib/canvas/canvas-agent-storyboard";
import { inspectStoryboardReadiness } from "../src/lib/canvas/canvas-storyboard-context";
import { buildCanvasAgentContext } from "../src/lib/canvas/canvas-agent-context";
import { createShortDramaPipeline } from "../src/lib/canvas/canvas-short-drama";
import { createStoryboardRow } from "../src/lib/canvas/canvas-project-domain";
import { applyCanvasAgentOps, type CanvasAgentSnapshot } from "../src/lib/canvas/canvas-agent-ops";
import { prepareAnalysisNodeAction } from "../src/lib/plugins/analysis-node-action";
function fixture() {
const pipeline = createShortDramaPipeline({ x: 0, y: 0 });
const script = pipeline.nodes.find((node) => node.type === "script")!;
script.metadata!.storyboard!.rows = [createStoryboardRow(1, { id: "row-1", videoNodeId: "video-1", imageNodeId: "image-1", dialogue: "旧对白" }), createStoryboardRow(2, { id: "row-2" })];
const snapshot: CanvasAgentSnapshot = { projectId: "test", title: "test", nodes: pipeline.nodes, connections: pipeline.connections, selectedNodeIds: [], viewport: { x: 0, y: 0, k: 1 } };
return { snapshot, script };
}
test("待拆镜入口保存原始剧本但不伪造已完成的镜头", () => {
const { snapshot } = fixture();
const result = applyCanvasAgentOps(snapshot, buildAgentStoryboardDraft({ title: "旧机来电", prompt: "手机突然响起", x: 500, y: 100 }));
const node = result.nodes.find((item) => item.title === "旧机来电")!;
expect(node.type).toBe("script");
expect(node.metadata?.composerContent).toBe("手机突然响起");
expect(node.metadata?.storyboard?.rows).toEqual([]);
expect(node.metadata?.workflowDescription).toContain("尚未生成");
});
test("Agent 上下文提前指出未选择画风及分镜空行", () => {
const { snapshot, script } = fixture();
const readiness = buildCanvasAgentContext(snapshot).storyboardReadiness;
expect(readiness.canGenerateStoryboard).toBe(false);
expect(readiness.style.ready).toBe(false);
expect(readiness.blockingReason).toContain("画风");
expect(readiness.storyboards.find((item) => item.nodeId === script.id)?.incompleteRowIds).toEqual(["row-1", "row-2"]);
expect(readiness.nextActions.join(" ")).toContain("canvas_apply_style");
});
test("已应用画风与真实分镜写入后,返回实际总时长", () => {
const { snapshot, script } = fixture();
const style = snapshot.nodes.find((node) => node.metadata?.workflowKind === "styleboard")!;
style.metadata = { ...style.metadata, stylePresetId: "preset", content: "电影写实" };
script.metadata!.storyboard!.rows = [2, 2, 3, 2, 1].map((durationSeconds, i) => createStoryboardRow(i + 1, { durationSeconds, videoMotionPrompt: `镜头${i + 1}` }));
const readiness = inspectStoryboardReadiness(snapshot.nodes);
expect(readiness.canGenerateStoryboard).toBe(true);
expect(readiness.storyboards[0]).toMatchObject({ rowCount: 5, populated: true, totalDurationSeconds: 10 });
expect(readiness.nextActions).toEqual([]);
});
test("通用节点创建不能用正文代替真实分镜行", () => {
expect(() => validateAgentStoryboardCreation([{ type: "add_node", nodeType: "script", metadata: { content: "镜头1:0–2秒近景;镜头2:2–4秒特写" } }])).toThrow("真实镜头行");
const { script } = fixture();
expect(() => validateAgentStoryboardCreation([{ type: "add_node", nodeType: "script", metadata: script.metadata }])).toThrow();
});
test("按行修改保留其他镜头及已有生成素材关联", () => {
const { snapshot, script } = fixture();
const next = applyCanvasAgentOps(snapshot, buildAgentStoryboardOperations(snapshot, { nodeId: script.id, action: "update", rowId: "row-1", patch: { dialogue: "新对白", durationSeconds: 8 } }));
const rows = next.nodes.find((node) => node.id === script.id)!.metadata!.storyboard!.rows;
expect(rows[0]).toMatchObject({ id: "row-1", dialogue: "新对白", durationSeconds: 8, imageNodeId: "image-1", videoNodeId: "video-1" });
expect(rows[1]).toEqual(script.metadata!.storyboard!.rows[1]);
});
test("移除分镜只更新表格,保留行 ID 并重排序号", () => {
const { snapshot, script } = fixture();
const ops = buildAgentStoryboardOperations(snapshot, { nodeId: script.id, action: "remove", rowId: "row-1" });
expect(ops).toHaveLength(1);
expect(ops[0].type).toBe("update_node");
if (ops[0].type === "update_node") expect(ops[0].metadata?.storyboard?.rows[0]).toMatchObject({ id: "row-2", shotNumber: 1 });
});
test("分镜编辑拒绝伪造结果、未知行和非法时长", () => {
const { snapshot, script } = fixture();
for (const patch of [{ status: "success" }, { videoNodeId: "fake" }, { durationSeconds: -1 }]) {
expect(() => buildAgentStoryboardOperations(snapshot, { nodeId: script.id, action: "update", rowId: "row-1", patch })).toThrow();
}
expect(() => buildAgentStoryboardOperations(snapshot, { nodeId: script.id, action: "remove", rowId: "missing" })).toThrow();
});
test("分析节点准备复用相同输入且不启动模型", () => {
const { snapshot, script } = fixture();
snapshot.nodes.push({ ...script, id: "source", type: "image", metadata: { storageKey: "resource:image" } });
const action = prepareAnalysisNodeAction("test-analysis", "分析", () => ({}));
const ops = action.buildOperations({ sourceNodeId: "source" }, snapshot);
expect(ops.some((op) => op.type === "run_generation")).toBe(false);
expect(ops.some((op) => op.type === "connect_nodes")).toBe(true);
snapshot.nodes.push({ ...script, id: "analysis", type: "test-analysis" });
snapshot.connections.push({ id: "edge", fromNodeId: "source", toNodeId: "analysis" });
expect(action.buildOperations({ sourceNodeId: "source" }, snapshot)).toEqual([{ type: "select_nodes", ids: ["analysis"] }]);
});