166 lines
4.8 KiB
TypeScript
166 lines
4.8 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { buildWorkflowGraph, type WorkflowGraphNodeData } from "./graph-model.js";
|
|
|
|
const samplePlan = {
|
|
nodes: [
|
|
{
|
|
id: "open",
|
|
type: "node",
|
|
node: "local.browser_click.open_click_page",
|
|
input: [],
|
|
output: [],
|
|
},
|
|
{
|
|
id: "wait",
|
|
type: "node",
|
|
node: "local.browser_click.wait_for_click",
|
|
input: [],
|
|
output: [],
|
|
},
|
|
{
|
|
id: "check",
|
|
type: "condition",
|
|
check: { op: "exists", path: "state.clicked" },
|
|
},
|
|
{
|
|
id: "ask",
|
|
type: "interrupt",
|
|
kind: "approval",
|
|
request: [],
|
|
resume: [],
|
|
outcomes: ["approved", "rejected"],
|
|
},
|
|
{
|
|
id: "__end__",
|
|
type: "end",
|
|
outcome: "ok",
|
|
},
|
|
],
|
|
edges: [
|
|
{ from: "open", outcome: "ok", to: "wait" },
|
|
{ from: "wait", outcome: "ok", to: "check" },
|
|
{ from: "check", outcome: "true", to: "ask" },
|
|
{ from: "check", outcome: "false", to: "__end__" },
|
|
{ from: "ask", outcome: "approved", to: "__end__" },
|
|
],
|
|
};
|
|
|
|
describe("buildWorkflowGraph", () => {
|
|
it("produces stable node ids from plan", () => {
|
|
const model = buildWorkflowGraph(samplePlan);
|
|
const nodeIds = model.nodes.map((n) => n.id);
|
|
expect(nodeIds).toEqual(["__end__", "ask", "check", "open", "wait"]);
|
|
});
|
|
|
|
it("maps node types correctly", () => {
|
|
const model = buildWorkflowGraph(samplePlan);
|
|
const kinds = model.nodes.map((n) => n.data.kind);
|
|
expect(kinds).toEqual(["end", "interrupt", "condition", "use", "use"]);
|
|
});
|
|
|
|
it("preserves node references", () => {
|
|
const model = buildWorkflowGraph(samplePlan);
|
|
const openNode = model.nodes.find((n) => n.id === "open");
|
|
expect(openNode?.data.nodeRef).toBe("local.browser_click.open_click_page");
|
|
});
|
|
|
|
it("labels subgraph nodes from the workflow name", () => {
|
|
const model = buildWorkflowGraph({
|
|
nodes: [
|
|
{
|
|
id: "nested",
|
|
type: "subgraph",
|
|
workflow: "workflows.report.review",
|
|
input: [],
|
|
output: [],
|
|
},
|
|
],
|
|
edges: [],
|
|
});
|
|
|
|
expect(model.nodes[0]?.data.kind).toBe("subgraph");
|
|
expect(model.nodes[0]?.data.label).toBe("review");
|
|
});
|
|
|
|
it("creates edges from plan edges", () => {
|
|
const model = buildWorkflowGraph(samplePlan);
|
|
expect(model.edges.length).toBe(5);
|
|
});
|
|
|
|
it("labels edges with outcome names", () => {
|
|
const model = buildWorkflowGraph(samplePlan);
|
|
const okEdge = model.edges.find(
|
|
(e) => e.source === "open" && e.target === "wait",
|
|
);
|
|
expect(okEdge?.label).toBe("ok");
|
|
});
|
|
|
|
it("accepts a presentation label override without changing node refs", () => {
|
|
const model = buildWorkflowGraph(samplePlan, {
|
|
label: (node) => node.id === "open" ? "Open page" : undefined,
|
|
});
|
|
const openNode = model.nodes.find((node) => node.id === "open");
|
|
|
|
expect(openNode?.data.label).toBe("Open page");
|
|
expect(openNode?.data.nodeRef).toBe("local.browser_click.open_click_page");
|
|
expect(model.nodes.find((node) => node.id === "wait")?.data.label).toBe(
|
|
"wait_for_click",
|
|
);
|
|
});
|
|
|
|
it("keeps the default layout top-to-bottom", () => {
|
|
const model = buildWorkflowGraph(samplePlan);
|
|
const open = model.nodes.find((node) => node.id === "open");
|
|
const end = model.nodes.find((node) => node.id === "__end__");
|
|
|
|
expect(open?.position.y).toBeLessThan(end?.position.y ?? 0);
|
|
});
|
|
|
|
it("supports horizontal layout options without changing edge labels", () => {
|
|
const model = buildWorkflowGraph(samplePlan, {
|
|
direction: "LR",
|
|
nodeWidth: 190,
|
|
nodeHeight: 72,
|
|
nodesep: 55,
|
|
ranksep: 100,
|
|
});
|
|
const open = model.nodes.find((node) => node.id === "open");
|
|
const end = model.nodes.find((node) => node.id === "__end__");
|
|
|
|
expect(open?.position.x).toBeLessThan(end?.position.x ?? 0);
|
|
expect(model.edges.map((edge) => edge.label)).toEqual([
|
|
"ok",
|
|
"ok",
|
|
"true",
|
|
"false",
|
|
"approved",
|
|
]);
|
|
});
|
|
|
|
it("assigns deterministic coordinates", () => {
|
|
const model1 = buildWorkflowGraph(samplePlan);
|
|
const model2 = buildWorkflowGraph(structuredClone(samplePlan));
|
|
expect(model1.nodes.map((n) => n.position)).toEqual(
|
|
model2.nodes.map((n) => n.position),
|
|
);
|
|
});
|
|
|
|
it("does not mutate the input plan", () => {
|
|
const original = structuredClone(samplePlan);
|
|
buildWorkflowGraph(samplePlan);
|
|
expect(samplePlan).toEqual(original);
|
|
});
|
|
|
|
it("handles empty plan", () => {
|
|
const model = buildWorkflowGraph({ nodes: [], edges: [] });
|
|
expect(model.nodes).toEqual([]);
|
|
expect(model.edges).toEqual([]);
|
|
});
|
|
|
|
it("includes raw node data", () => {
|
|
const model = buildWorkflowGraph(samplePlan);
|
|
const openNode = model.nodes.find((n) => n.id === "open");
|
|
expect(openNode?.data.raw).toBeDefined();
|
|
});
|
|
});
|