140 lines
6.5 KiB
TypeScript
140 lines
6.5 KiB
TypeScript
import { beforeEach, describe, expect, test } from "bun:test";
|
|
|
|
import { collectCanvasMediaAssetIds, rebindInconsistentCanvasAssets, repairMissingCanvasAssets } from "@/services/canvas-asset-repair";
|
|
import { useAssetStore, type Asset } from "@/stores/use-asset-store";
|
|
import { useCanvasStore, type CanvasProject } from "@/stores/canvas/use-canvas-store";
|
|
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
|
|
import type { TimelineClip } from "@/types/timeline";
|
|
|
|
function videoAsset(id: string, storageKey?: string, url = ""): Asset {
|
|
return {
|
|
id,
|
|
kind: "video",
|
|
title: `素材 ${id}`,
|
|
coverUrl: "",
|
|
tags: [],
|
|
createdAt: "2026-09-01T00:00:00.000Z",
|
|
updatedAt: "2026-09-01T00:00:00.000Z",
|
|
data: { url, ...(storageKey ? { storageKey } : {}), width: 1, height: 1, bytes: 1, mimeType: "video/mp4" },
|
|
} as Asset;
|
|
}
|
|
|
|
function videoNode(id: string, assetId: string | undefined, storageKey: string): CanvasNodeData {
|
|
return {
|
|
id,
|
|
type: CanvasNodeType.Video,
|
|
title: "镜头",
|
|
position: { x: 0, y: 0 },
|
|
width: 320,
|
|
height: 240,
|
|
metadata: { assetId, storageKey, content: "blob:http://localhost/preview" },
|
|
};
|
|
}
|
|
|
|
function project(id: string, nodes: CanvasNodeData[], clips: TimelineClip[] = []): CanvasProject {
|
|
return {
|
|
id,
|
|
title: `画布 ${id}`,
|
|
createdAt: "2026-09-01T00:00:00.000Z",
|
|
updatedAt: "2026-09-01T00:00:00.000Z",
|
|
nodes,
|
|
connections: [],
|
|
chatSessions: [],
|
|
activeChatId: null,
|
|
viewport: { x: 0, y: 0, k: 1 },
|
|
...(clips.length ? { timeline: { clips } } : {}),
|
|
};
|
|
}
|
|
|
|
function timelineClip(assetId: string | undefined, storageKey: string): TimelineClip {
|
|
return { id: "clip-1", kind: "video", directMedia: { id: "media-1", assetId, kind: "video", title: "片段", storageKey } } as TimelineClip;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
useCanvasStore.setState({ projects: [] });
|
|
useAssetStore.setState({ assets: [] });
|
|
});
|
|
|
|
describe("repairMissingCanvasAssets 增量会话", () => {
|
|
test("节点声明不能掩盖素材库的真实绑定,错误配对会被重绑", () => {
|
|
useAssetStore.setState({ assets: [videoAsset("asset-a", "resource:R1"), videoAsset("asset-c", "resource:R2")] });
|
|
useCanvasStore.setState({ projects: [project("canvas-1", [videoNode("node-1", "asset-a", "resource:R2")])] });
|
|
|
|
const result = repairMissingCanvasAssets(new Set(["canvas-1"]), true);
|
|
|
|
expect(result.createdAssets).toBe(0);
|
|
expect(useCanvasStore.getState().projects[0].nodes[0].metadata?.assetId).toBe("asset-c");
|
|
});
|
|
|
|
test("本地未加载的素材仍按节点声明接受,不会为远端素材分叉新建", () => {
|
|
useCanvasStore.setState({ projects: [project("canvas-1", [videoNode("node-1", "remote-only", "resource:R9")])] });
|
|
|
|
const result = repairMissingCanvasAssets(new Set(["canvas-1"]), true);
|
|
|
|
expect(result.createdAssets).toBe(0);
|
|
expect(useCanvasStore.getState().projects[0].nodes[0].metadata?.assetId).toBe("remote-only");
|
|
});
|
|
});
|
|
|
|
describe("rebindInconsistentCanvasAssets 强制对齐", () => {
|
|
test("声明的素材引用了其他资源时重绑到引用同一资源的素材", () => {
|
|
useCanvasStore.setState({ projects: [project("canvas-1", [videoNode("node-1", "asset-a", "resource:R2")])] });
|
|
|
|
const result = rebindInconsistentCanvasAssets([videoAsset("asset-a", "resource:R1"), videoAsset("asset-c", "resource:R2")]);
|
|
|
|
expect(result).toMatchObject({ reboundNodes: 1, createdAssets: 0, updatedProjects: 1 });
|
|
expect(useCanvasStore.getState().projects[0].nodes[0].metadata?.assetId).toBe("asset-c");
|
|
});
|
|
|
|
test("没有素材引用节点资源时按节点新建素材并重绑", () => {
|
|
useCanvasStore.setState({ projects: [project("canvas-1", [videoNode("node-1", "asset-a", "resource:R2")])] });
|
|
|
|
const result = rebindInconsistentCanvasAssets([videoAsset("asset-a", "resource:R1")]);
|
|
|
|
expect(result.reboundNodes).toBe(1);
|
|
expect(result.createdAssets).toBe(1);
|
|
const reboundId = useCanvasStore.getState().projects[0].nodes[0].metadata?.assetId;
|
|
const created = useAssetStore.getState().assets.find((asset) => asset.id === reboundId);
|
|
expect(created?.kind).toBe("video");
|
|
expect(created && "storageKey" in created.data ? created.data.storageKey : undefined).toBe("resource:R2");
|
|
});
|
|
|
|
test("时间线 directMedia 与节点走同一套绑定校验", () => {
|
|
useCanvasStore.setState({ projects: [project("canvas-1", [], [timelineClip("asset-a", "resource:R2")])] });
|
|
|
|
const result = rebindInconsistentCanvasAssets([videoAsset("asset-a", "resource:R1"), videoAsset("asset-c", "resource:R2")]);
|
|
|
|
expect(result.reboundNodes).toBe(1);
|
|
expect(useCanvasStore.getState().projects[0].timeline?.clips[0].directMedia?.assetId).toBe("asset-c");
|
|
});
|
|
|
|
test("素材以资源文件 URL 引用时与 resource: 前缀归一化匹配", () => {
|
|
useCanvasStore.setState({ projects: [project("canvas-1", [videoNode("node-1", "asset-a", "resource:R2")])] });
|
|
|
|
const result = rebindInconsistentCanvasAssets([videoAsset("asset-a", undefined, "/api/resources/R2/file")]);
|
|
|
|
expect(result.reboundNodes).toBe(0);
|
|
expect(useCanvasStore.getState().projects[0].nodes[0].metadata?.assetId).toBe("asset-a");
|
|
});
|
|
|
|
test("绑定的素材记录缺失(远端已删)时也按节点重建", () => {
|
|
useCanvasStore.setState({ projects: [project("canvas-1", [videoNode("node-1", "deleted-asset", "resource:R2")])] });
|
|
|
|
const result = rebindInconsistentCanvasAssets([]);
|
|
|
|
expect(result.reboundNodes).toBe(1);
|
|
expect(result.createdAssets).toBe(1);
|
|
expect(useCanvasStore.getState().projects[0].nodes[0].metadata?.assetId).not.toBe("deleted-asset");
|
|
});
|
|
});
|
|
|
|
describe("collectCanvasMediaAssetIds", () => {
|
|
test("只收集有资源身份的媒体节点与时间线片段声明", () => {
|
|
const textNode: CanvasNodeData = { id: "node-text", type: CanvasNodeType.Text, title: "文本", position: { x: 0, y: 0 }, width: 200, height: 100, metadata: { content: "正文", assetId: "text-asset" } };
|
|
const pendingNode = videoNode("node-pending", undefined, "resource:R3");
|
|
const projects = [project("canvas-1", [videoNode("node-1", "asset-a", "resource:R1"), textNode, pendingNode], [timelineClip("asset-b", "resource:R2")])];
|
|
|
|
expect(collectCanvasMediaAssetIds(projects)).toEqual(new Set(["asset-a", "asset-b"]));
|
|
});
|
|
});
|