Files
ttstd-infinite-canvas/web/test/canvas-batch-connection.test.ts
WindC0X 0e251c3c8f feat(canvas): 画布媒体工作流 - 支持批量连线、失败重试与模型能力增强
合并 PR #230。保留当前主线样式与组件结构,吸收批量连线、失败批次重试、媒体模型能力和相关后端增强,并修复集成检查发现的分辨率选择器启用条件与测试稳定性问题。
2026-08-15 15:48:16 +08:00

116 lines
5.8 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import { buildBatchConnectionCreateRequest, hasBatchConnectionCandidate, planBatchConnections } from "@/lib/canvas/canvas-batch-connection";
import { defaultConfig } from "@/stores/use-config-store";
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData } from "@/types/canvas";
const nodes: CanvasNodeData[] = [
{ id: "text-a", type: CanvasNodeType.Text, title: "文本 A", position: { x: 0, y: 0 }, width: 320, height: 180 },
{ id: "text-b", type: CanvasNodeType.Text, title: "文本 B", position: { x: 0, y: 220 }, width: 320, height: 180 },
{ id: "image-a", type: CanvasNodeType.Image, title: "图片 A", position: { x: 0, y: 440 }, width: 320, height: 180 },
{ id: "image-b", type: CanvasNodeType.Image, title: "图片 B", position: { x: 0, y: 660 }, width: 320, height: 180 },
{ id: "script", type: CanvasNodeType.Script, title: "分镜脚本", position: { x: 520, y: 0 }, width: 640, height: 520, metadata: { storyboard: { rows: [{ id: "row-1", shotNumber: 1, durationSeconds: 6, plotDescription: "", dialogue: "", characters: [], narrativeIntent: "", viewerPOV: "", performanceBlocking: "", shotSize: "", emotion: "", lightingAndAtmosphere: "", audioEffects: "", camera: "", motion: "", timeBeats: "", imageGenerationPrompt: "", videoMotionPrompt: "", mustHave: [], optionalDetails: [], continuityOut: "", negativePrompt: "", referenceNodeIds: [], status: "idle" }] } } },
{ id: "config", type: CanvasNodeType.Config, title: "图片配置", position: { x: 520, y: 560 }, width: 360, height: 420, metadata: { generationMode: "image" } },
{ id: "frame", type: CanvasNodeType.Frame, title: "背板", position: { x: 0, y: 680 }, width: 500, height: 500 },
];
const baseConfig = { ...defaultConfig };
describe("planBatchConnections", () => {
it("plans all legal source nodes and preserves the target handle", () => {
const result = planBatchConnections({
sourceNodeIds: ["text-a", "text-b"],
targetNodeId: "script",
targetHandleId: "row:row-1",
targetAnchorRatio: 0.4,
nodes,
connections: [],
config: baseConfig,
});
expect(result.connected).toEqual(["text-a", "text-b"]);
expect(result.connections).toHaveLength(2);
expect(result.connections.every((connection) => connection.toNodeId === "script" && connection.toHandleId === "row:row-1")).toBe(true);
expect(result.skipped).toEqual([]);
});
it("skips duplicate connections without creating another edge", () => {
const existing: CanvasConnection[] = [{ id: "existing", fromNodeId: "text-a", toNodeId: "config" }];
const result = planBatchConnections({
sourceNodeIds: ["text-a", "text-b"],
targetNodeId: "config",
nodes,
connections: existing,
config: baseConfig,
});
expect(result.connected).toEqual(["text-b"]);
expect(result.duplicates).toEqual(["text-a"]);
expect(result.connections).toHaveLength(1);
});
it("reports sources that cannot be used by the aggregate connector", () => {
const result = planBatchConnections({
sourceNodeIds: ["frame", "text-a"],
targetNodeId: "config",
nodes,
connections: [],
config: baseConfig,
});
expect(result.connected).toEqual(["text-a"]);
expect(result.skipped).toEqual([{ nodeId: "frame", reason: "背板不能作为连接源" }]);
});
it("keeps every legal graph edge when a newly created node exceeds provider reference capacity", () => {
const constrainedConfig = {
...baseConfig,
channels: [{
id: "grok",
name: "Grok",
baseUrl: "https://example.com",
apiKey: "test-key",
apiFormat: "openai" as const,
models: ["grok-imagine-image"],
modelCosts: [{ model: "grok-imagine-image", capability: "image" as const, billingMode: "fixed_request" as const, unitPriceMicrocredits: 1, protocol: "grok-image" as const }],
}],
models: ["grok::grok-imagine-image"],
imageModels: ["grok::grok-imagine-image"],
imageModel: "grok::grok-imagine-image",
model: "grok::grok-imagine-image",
};
const result = planBatchConnections({
sourceNodeIds: ["image-a", "image-b"],
targetNodeId: "new-image",
nodes: [...nodes, { id: "new-image", type: CanvasNodeType.Image, title: "新图片", position: { x: 1200, y: 0 }, width: 320, height: 180 }],
connections: [],
config: constrainedConfig,
allowCapacityOverflow: true,
});
expect(result.connected).toEqual(["image-a", "image-b"]);
expect(result.connections).toHaveLength(2);
});
});
describe("buildBatchConnectionCreateRequest", () => {
it("keeps unique eligible sources and uses the first one for the pending connection", () => {
const request = buildBatchConnectionCreateRequest(["frame", "text-a", "text-a", "text-b"], nodes, { x: 900, y: 240 });
expect(request).toEqual({
batchSourceNodeIds: ["text-a", "text-b"],
connection: { nodeId: "text-a", handleType: "source" },
position: { x: 900, y: 240 },
});
});
it("returns null when no selected node can be used as a batch source", () => {
expect(buildBatchConnectionCreateRequest(["frame", "script", "config"], nodes, { x: 0, y: 0 })).toBeNull();
});
});
describe("hasBatchConnectionCandidate", () => {
it("accepts a target when another selected source is legal even if the target itself is selected first", () => {
expect(hasBatchConnectionCandidate(["config", "text-a"], "config", nodes)).toBe(true);
});
});