163 lines
7.4 KiB
TypeScript
163 lines
7.4 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
|
|
import { CanvasNode } from "@/components/canvas/canvas-node";
|
|
import { applyGeneratedMediaResultMetadata, videoMetadata } from "@/lib/canvas/canvas-generation-task-sync";
|
|
import { commitProducedModel, producedModelCandidateForGeneration, producedModelLabel, submittedProducedModel } from "@/lib/canvas/produced-model";
|
|
import { searchCanvasNodes } from "@/lib/canvas/canvas-node-search";
|
|
import { reconcileImageBatchRoot } from "@/lib/canvas/canvas-image-batch-retry";
|
|
import { effectiveConfigForCustomChannels, type AiConfig } from "@/stores/use-config-store";
|
|
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
|
|
|
|
const config = {
|
|
channels: [
|
|
{
|
|
id: "system-1",
|
|
name: "系统渠道",
|
|
scope: "system",
|
|
models: ["gpt-image-1"],
|
|
modelCosts: [{ model: "gpt-image-1", displayName: "图像 Pro", logicalModelId: "logical-image" }],
|
|
},
|
|
{
|
|
id: "user-1",
|
|
name: "我的渠道",
|
|
scope: "user",
|
|
models: ["flux-dev"],
|
|
modelCosts: [{ model: "flux-dev" }],
|
|
},
|
|
],
|
|
} as AiConfig;
|
|
|
|
function node(metadata: CanvasNodeData["metadata"]): CanvasNodeData {
|
|
return {
|
|
id: "node-1",
|
|
type: CanvasNodeType.Video,
|
|
title: "镜头",
|
|
position: { x: 0, y: 0 },
|
|
width: 320,
|
|
height: 180,
|
|
metadata,
|
|
};
|
|
}
|
|
|
|
describe("produced model", () => {
|
|
test("freezes the submitted model instead of the dropdown selection", () => {
|
|
const next = applyGeneratedMediaResultMetadata(
|
|
node({
|
|
model: "user-1::changed-later",
|
|
producedModel: "user-1::old",
|
|
producedModelCandidate: "system-1::gpt-image-1",
|
|
content: "https://example.test/old.mp4",
|
|
}),
|
|
videoMetadata({
|
|
url: "https://example.test/new.mp4",
|
|
storageKey: "video:new",
|
|
width: 1280,
|
|
height: 720,
|
|
bytes: 12,
|
|
mimeType: "video/mp4",
|
|
}),
|
|
{},
|
|
"logical-image",
|
|
);
|
|
|
|
expect(next.producedModel).toBe("system-1::gpt-image-1");
|
|
expect(next.producedModelCandidate).toBeUndefined();
|
|
expect(next.model).toBe("user-1::changed-later");
|
|
});
|
|
|
|
test("uses the task model when the node never recorded a candidate", () => {
|
|
const next = commitProducedModel({ model: "user-1::draft" }, "logical-image");
|
|
expect(next.producedModel).toBe("logical-image");
|
|
expect(next.model).toBe("user-1::draft");
|
|
});
|
|
|
|
test("workflow success records the submitted workflow name instead of the unused catalog model", () => {
|
|
expect(producedModelCandidateForGeneration({ model: "system-1::gpt-image-1", taskWorkflowProvider: "runninghub" })).toBeUndefined();
|
|
expect(submittedProducedModel("RunningHub · 分镜")).toBe("RunningHub · 分镜");
|
|
const next = commitProducedModel({ producedModel: "system-1::gpt-image-1", producedModelCandidate: undefined }, "RunningHub · 分镜");
|
|
expect(next.producedModel).toBe("RunningHub · 分镜");
|
|
expect(producedModelLabel(config, next.producedModel)).toBe("RunningHub · 分镜");
|
|
});
|
|
|
|
test("batch root projects the primary result model and clears it when no result remains", () => {
|
|
const root = { ...node({ isBatchRoot: true, batchChildIds: ["child"], producedModel: "old" }), type: CanvasNodeType.Image };
|
|
const child = { ...node({ batchRootId: root.id, status: "success", content: "https://example.test/image", producedModel: "new" }), id: "child", type: CanvasNodeType.Image };
|
|
expect(reconcileImageBatchRoot(root, [root, child]).metadata?.producedModel).toBe("new");
|
|
const failed = { ...child, metadata: { ...child.metadata, content: undefined, status: "error" as const } };
|
|
expect(reconcileImageBatchRoot(root, [root, failed]).metadata?.producedModel).toBeUndefined();
|
|
});
|
|
|
|
test("resolves a display name and otherwise shows the model id", () => {
|
|
const resolved = effectiveConfigForCustomChannels(config, true);
|
|
expect(producedModelLabel(resolved, "system-1::gpt-image-1")).toBe("图像 Pro");
|
|
expect(producedModelLabel(resolved, "user-1::flux-dev")).toBe("flux-dev");
|
|
expect(producedModelLabel(resolved, "logical-image")).toBe("图像 Pro");
|
|
expect(producedModelLabel(resolved, "deleted::gpt-image-1")).toBe("gpt-image-1");
|
|
expect(producedModelLabel(resolved, "missing-model")).toBe("missing-model");
|
|
expect(producedModelLabel(resolved, "system-1::gpt-image-1")).not.toBe("系统模型");
|
|
});
|
|
|
|
test("search matches the produced model label and the selected model", () => {
|
|
const image = node({ model: "wan-video", producedModel: "system-1::gpt-image-1" });
|
|
image.type = CanvasNodeType.Image;
|
|
expect(searchCanvasNodes([image], "图像 Pro", 80, config).map((item) => item.id)).toEqual(["node-1"]);
|
|
expect(searchCanvasNodes([image], "gpt-image-1", 80, config).map((item) => item.id)).toEqual(["node-1"]);
|
|
expect(searchCanvasNodes([image], "wan-video", 80, config).map((item) => item.id)).toEqual(["node-1"]);
|
|
});
|
|
});
|
|
|
|
describe("produced model badge", () => {
|
|
const noop = () => {};
|
|
function renderMedia(type: CanvasNodeType, showImageInfo: boolean, mediaActive = false, renderLOD: "full" | "compact" = "full") {
|
|
return renderToStaticMarkup(
|
|
<CanvasNode
|
|
data={{
|
|
id: "media",
|
|
type,
|
|
title: "结果",
|
|
position: { x: 0, y: 0 },
|
|
width: 320,
|
|
height: 180,
|
|
metadata: {
|
|
content: "https://example.test/media",
|
|
storageKey: "resource:1",
|
|
producedModel: "user-1::flux-dev",
|
|
naturalWidth: 1280,
|
|
naturalHeight: 720,
|
|
bytes: 2048,
|
|
},
|
|
}}
|
|
scale={1}
|
|
isSelected={false}
|
|
mediaActive={mediaActive}
|
|
isRelated={false}
|
|
isFocusRelated={false}
|
|
isConnectionTarget={false}
|
|
showImageInfo={showImageInfo}
|
|
renderLOD={renderLOD}
|
|
onMouseDown={noop}
|
|
onHoverStart={noop}
|
|
onHoverEnd={noop}
|
|
onConnectStart={noop}
|
|
onResize={noop}
|
|
onContentChange={noop}
|
|
onContextMenu={noop}
|
|
/>,
|
|
);
|
|
}
|
|
|
|
test("shows the model at the lower left only while media info is enabled", () => {
|
|
expect(renderMedia(CanvasNodeType.Image, false)).not.toContain("flux-dev");
|
|
const image = renderMedia(CanvasNodeType.Image, true);
|
|
expect(image).toContain("flux-dev");
|
|
expect(image).toContain("1280 x 720");
|
|
const video = renderMedia(CanvasNodeType.Video, true, true);
|
|
expect(video).toContain("bottom-16");
|
|
expect(video).toContain("flux-dev");
|
|
expect(video).not.toContain("1280 x 720");
|
|
expect(renderMedia(CanvasNodeType.Audio, true)).toContain("flux-dev");
|
|
expect(renderMedia(CanvasNodeType.Image, true, false, "compact")).not.toContain("flux-dev");
|
|
});
|
|
});
|