feat(agent): add execution-plan skeleton workflow

This commit is contained in:
2026-04-09 21:11:21 +02:00
parent 29c93eeb35
commit 26d008705f
8 changed files with 708 additions and 98 deletions

View File

@@ -293,4 +293,51 @@ describe("AgentNode runtime", () => {
expect(mocks.runAgent).not.toHaveBeenCalled();
expect(mocks.resumeAgent).not.toHaveBeenCalled();
});
it("disables run button and shows progress while executing", async () => {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
await act(async () => {
root?.render(
React.createElement(AgentNode, {
id: "agent-3",
selected: false,
dragging: false,
draggable: true,
selectable: true,
deletable: true,
zIndex: 1,
isConnectable: true,
type: "agent",
data: {
canvasId: "canvas-1",
templateId: "campaign-distributor",
modelId: "openai/gpt-5.4-mini",
_status: "executing",
_statusMessage: "Executing step 2/4",
} as Record<string, unknown>,
positionAbsoluteX: 0,
positionAbsoluteY: 0,
}),
);
});
const runButton = Array.from(container.querySelectorAll("button")).find((element) =>
element.textContent?.includes("Run agent"),
);
if (!(runButton instanceof HTMLButtonElement)) {
throw new Error("Run button not found");
}
expect(runButton.disabled).toBe(true);
expect(container.textContent).toContain("Executing step 2/4");
await act(async () => {
runButton.click();
});
expect(mocks.runAgent).not.toHaveBeenCalled();
});
});

View File

@@ -45,7 +45,7 @@ describe("AgentNode", () => {
root = null;
});
it("renders campaign distributor metadata and input-only handle", async () => {
it("renders campaign distributor metadata and source/target handles", async () => {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
@@ -76,7 +76,7 @@ describe("AgentNode", () => {
expect(container.textContent).toContain("Instagram Feed");
expect(container.textContent).toContain("Caption-Pakete");
expect(handleCalls.filter((call) => call.type === "target")).toHaveLength(1);
expect(handleCalls.filter((call) => call.type === "source")).toHaveLength(0);
expect(handleCalls.filter((call) => call.type === "source")).toHaveLength(1);
});
it("falls back to the default template when templateId is missing", async () => {

View File

@@ -112,4 +112,41 @@ describe("AgentOutputNode", () => {
expect(handleCalls).toEqual([{ type: "target", id: "agent-output-in" }]);
});
it("renders skeleton mode with counter and placeholder", async () => {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
await act(async () => {
root?.render(
React.createElement(AgentOutputNode, {
id: "agent-output-3",
selected: false,
dragging: false,
draggable: true,
selectable: true,
deletable: true,
zIndex: 1,
isConnectable: true,
type: "agent-output",
data: {
title: "Planned headline",
channel: "linkedin",
outputType: "post",
isSkeleton: true,
stepIndex: 1,
stepTotal: 4,
} as Record<string, unknown>,
positionAbsoluteX: 0,
positionAbsoluteY: 0,
}),
);
});
expect(container.textContent).toContain("Skeleton");
expect(container.textContent).toContain("2/4");
expect(container.querySelector('[data-testid="agent-output-skeleton-body"]')).not.toBeNull();
expect(handleCalls).toEqual([{ type: "target", id: "agent-output-in" }]);
});
});

View File

@@ -2,9 +2,11 @@ import { describe, expect, it } from "vitest";
import {
areClarificationAnswersComplete,
normalizeAgentExecutionPlan,
normalizeAgentOutputDraft,
type AgentClarificationAnswerMap,
type AgentClarificationQuestion,
type AgentExecutionPlan,
} from "@/lib/agent-run-contract";
describe("agent run contract helpers", () => {
@@ -93,4 +95,87 @@ describe("agent run contract helpers", () => {
expect(normalized.body).toBe("");
});
});
describe("normalizeAgentExecutionPlan", () => {
it("trims summary and step metadata while preserving valid values", () => {
const normalized = normalizeAgentExecutionPlan({
summary: " Ship a launch kit ",
steps: [
{
id: " STEP-1 ",
title: " Instagram captions ",
channel: " Instagram ",
outputType: " caption-pack ",
},
],
});
expect(normalized).toEqual<AgentExecutionPlan>({
summary: "Ship a launch kit",
steps: [
{
id: "step-1",
title: "Instagram captions",
channel: "Instagram",
outputType: "caption-pack",
},
],
});
});
it("falls back to safe defaults for invalid payloads", () => {
const normalized = normalizeAgentExecutionPlan({
summary: null,
steps: [
{
id: "",
title: "",
channel: " ",
outputType: undefined,
},
null,
],
});
expect(normalized).toEqual<AgentExecutionPlan>({
summary: "",
steps: [
{
id: "step-1",
title: "Untitled",
channel: "general",
outputType: "text",
},
],
});
});
it("deduplicates step ids and creates deterministic fallback ids", () => {
const normalized = normalizeAgentExecutionPlan({
summary: "ready",
steps: [
{
id: "step",
title: "One",
channel: "email",
outputType: "copy",
},
{
id: "step",
title: "Two",
channel: "x",
outputType: "thread",
},
{
id: "",
title: "Three",
channel: "linkedin",
outputType: "post",
},
],
});
expect(normalized.steps.map((step) => step.id)).toEqual(["step", "step-2", "step-3"]);
});
});
});