feat(agent): add execution-plan skeleton workflow
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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" }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user