90 lines
5.9 KiB
TypeScript
90 lines
5.9 KiB
TypeScript
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"] }]);
|
||
});
|