feat(canvas): finalize mixer reconnect swap and related updates

This commit is contained in:
2026-04-11 07:42:42 +02:00
parent f3dcaf89f2
commit 028fce35c2
52 changed files with 3859 additions and 272 deletions

View File

@@ -136,6 +136,10 @@ describe("agent prompting helpers", () => {
const user = messages[1]?.content ?? "";
expect(system).toContain("execution rules");
expect(system).toContain("deliverable-first rules");
expect(system).toContain("Prioritize publishable, user-facing deliverables");
expect(system).toContain("Do not produce reasoning-dominant output");
expect(system).toContain("For Campaign Distributor steps, output channel-ready publishable copy first");
expect(system).toContain("channel-notes");
expect(system).toContain("German (de-DE)");
expect(user).toContain("Execution plan summary: Ship launch content");

View File

@@ -47,6 +47,10 @@ describe("normalizeAgentStructuredOutput", () => {
language: "en",
tags: ["launch", "saas"],
},
metadataLabels: {
language: "language",
tags: "tags",
},
qualityChecks: ["concise", "channel-fit"],
body: "Legacy flat content",
});
@@ -145,4 +149,38 @@ describe("normalizeAgentStructuredOutput", () => {
"Hook:\nLead with a bold claim.\n\nCTA:\nInvite replies with a concrete question.",
);
});
it("slugifies non-ascii metadata keys and preserves original labels", () => {
const normalized = normalizeAgentStructuredOutput(
{
sections: [
{
id: "caption",
label: "Caption",
content: "Publish-ready caption.",
},
],
metadataEntries: [
{ key: "tonalität", values: ["freundlich"] },
{ key: "hashtags", values: ["dogs", "berner-sennenhund"] },
{ key: "empty", values: [] },
{ key: " ", values: ["ignored"] },
],
},
{
title: "Fallback Title",
channel: "fallback-channel",
artifactType: "fallback-artifact",
},
);
expect(normalized.metadata).toEqual({
tonalitaet: "freundlich",
hashtags: ["dogs", "berner-sennenhund"],
});
expect(normalized.metadataLabels).toEqual({
tonalitaet: "tonalität",
hashtags: "hashtags",
});
});
});

View File

@@ -2,7 +2,12 @@ import { describe, expect, it } from "vitest";
import { nodeTypes } from "@/components/canvas/node-types";
import { CANVAS_NODE_TEMPLATES } from "@/lib/canvas-node-templates";
import { NODE_CATALOG, isNodePaletteEnabled } from "@/lib/canvas-node-catalog";
import {
NODE_CATALOG,
NODE_CATEGORY_META,
catalogEntriesByCategory,
isNodePaletteEnabled,
} from "@/lib/canvas-node-catalog";
import { NODE_DEFAULTS, NODE_HANDLE_MAP } from "@/lib/canvas-utils";
describe("canvas agent config", () => {
@@ -22,6 +27,27 @@ describe("canvas agent config", () => {
expect(entry && isNodePaletteEnabled(entry)).toBe(true);
});
it("moves agent nodes into an Agents category", () => {
expect(NODE_CATEGORY_META.agents.label).toBe("Agents");
const byCategory = catalogEntriesByCategory();
const agentsEntries = byCategory.get("agents") ?? [];
const aiOutputEntries = byCategory.get("ai-output") ?? [];
expect(agentsEntries.map((entry) => entry.type)).toEqual(["agent", "agent-output"]);
expect(agentsEntries[0]).toMatchObject({
label: "Campaign Orchestrator",
category: "agents",
});
expect(aiOutputEntries.map((entry) => entry.type)).toEqual([
"prompt",
"video-prompt",
"ai-text",
]);
expect(NODE_CATALOG.find((entry) => entry.type === "ai-video")?.category).toBe("source");
});
it("keeps the agent input-only in MVP", () => {
expect(NODE_HANDLE_MAP.agent?.target).toBe("agent-in");
expect(NODE_HANDLE_MAP.agent?.source).toBeUndefined();

View File

@@ -0,0 +1,198 @@
import { describe, expect, it } from "vitest";
import { buildGraphSnapshot } from "@/lib/canvas-render-preview";
import { resolveMixerPreviewFromGraph } from "@/lib/canvas-mixer-preview";
describe("resolveMixerPreviewFromGraph", () => {
it("resolves base and overlay URLs by target handle", () => {
const graph = buildGraphSnapshot(
[
{
id: "image-base",
type: "image",
data: { url: "https://cdn.example.com/base.png" },
},
{
id: "asset-source",
type: "asset",
data: { url: "https://cdn.example.com/overlay.png" },
},
{
id: "render-overlay",
type: "render",
data: {},
},
{
id: "mixer-1",
type: "mixer",
data: { blendMode: "screen", opacity: 70, offsetX: 12, offsetY: -8 },
},
],
[
{ source: "asset-source", target: "render-overlay" },
{ source: "image-base", target: "mixer-1", targetHandle: "base" },
{ source: "render-overlay", target: "mixer-1", targetHandle: "overlay" },
],
);
expect(resolveMixerPreviewFromGraph({ nodeId: "mixer-1", graph })).toEqual({
status: "ready",
baseUrl: "https://cdn.example.com/base.png",
overlayUrl: "https://cdn.example.com/overlay.png",
blendMode: "screen",
opacity: 70,
offsetX: 12,
offsetY: -8,
});
});
it("prefers render output URL over upstream preview source when available", () => {
const graph = buildGraphSnapshot(
[
{
id: "image-base",
type: "image",
data: { url: "https://cdn.example.com/base.png" },
},
{
id: "image-upstream",
type: "image",
data: { url: "https://cdn.example.com/upstream.png" },
},
{
id: "render-overlay",
type: "render",
data: {
lastUploadUrl: "https://cdn.example.com/render-output.png",
},
},
{
id: "mixer-1",
type: "mixer",
data: {},
},
],
[
{ source: "image-upstream", target: "render-overlay" },
{ source: "image-base", target: "mixer-1", targetHandle: "base" },
{ source: "render-overlay", target: "mixer-1", targetHandle: "overlay" },
],
);
expect(resolveMixerPreviewFromGraph({ nodeId: "mixer-1", graph })).toEqual({
status: "ready",
baseUrl: "https://cdn.example.com/base.png",
overlayUrl: "https://cdn.example.com/render-output.png",
blendMode: "normal",
opacity: 100,
offsetX: 0,
offsetY: 0,
});
});
it("returns partial when one input is missing", () => {
const graph = buildGraphSnapshot(
[
{
id: "image-base",
type: "image",
data: { url: "https://cdn.example.com/base.png" },
},
{
id: "mixer-1",
type: "mixer",
data: {},
},
],
[{ source: "image-base", target: "mixer-1", targetHandle: "base" }],
);
expect(resolveMixerPreviewFromGraph({ nodeId: "mixer-1", graph })).toEqual({
status: "partial",
baseUrl: "https://cdn.example.com/base.png",
overlayUrl: undefined,
blendMode: "normal",
opacity: 100,
offsetX: 0,
offsetY: 0,
});
});
it("normalizes blend mode and clamps numeric values", () => {
const graph = buildGraphSnapshot(
[
{
id: "base-ai",
type: "ai-image",
data: { url: "https://cdn.example.com/base-ai.png" },
},
{
id: "overlay-asset",
type: "asset",
data: { url: "https://cdn.example.com/overlay-asset.png" },
},
{
id: "mixer-1",
type: "mixer",
data: {
blendMode: "unknown",
opacity: 180,
offsetX: 9999,
offsetY: "-9999",
},
},
],
[
{ source: "base-ai", target: "mixer-1", targetHandle: "base" },
{ source: "overlay-asset", target: "mixer-1", targetHandle: "overlay" },
],
);
expect(resolveMixerPreviewFromGraph({ nodeId: "mixer-1", graph })).toEqual({
status: "ready",
baseUrl: "https://cdn.example.com/base-ai.png",
overlayUrl: "https://cdn.example.com/overlay-asset.png",
blendMode: "normal",
opacity: 100,
offsetX: 2048,
offsetY: -2048,
});
});
it("returns error when multiple edges target the same mixer handle", () => {
const graph = buildGraphSnapshot(
[
{
id: "image-a",
type: "image",
data: { url: "https://cdn.example.com/a.png" },
},
{
id: "image-b",
type: "image",
data: { url: "https://cdn.example.com/b.png" },
},
{
id: "mixer-1",
type: "mixer",
data: {},
},
],
[
{ source: "image-a", target: "mixer-1", targetHandle: "base" },
{ source: "image-b", target: "mixer-1", targetHandle: "base" },
],
);
expect(resolveMixerPreviewFromGraph({ nodeId: "mixer-1", graph })).toEqual({
status: "error",
baseUrl: undefined,
overlayUrl: undefined,
blendMode: "normal",
opacity: 100,
offsetX: 0,
offsetY: 0,
error: "duplicate-handle-edge",
});
});
});