import { describe, it, expect } from "vitest"; import { h, createHostRoot } from "@alkdev/ujsx"; import { Operation, Sequential, Parallel, Conditional, Map } from "../../src/component/index.js"; import { GraphologyHostConfig } from "../../src/host/graphology.js"; import type { GraphContext } from "../../src/host/graphology.js"; import { CycleError } from "../../src/error/index.js"; function renderTemplate(template: ReturnType): GraphContext { const root = createHostRoot(GraphologyHostConfig, null); root.render(template); return root.ctx; } describe("GraphologyHostConfig", () => { it("createRootContext creates fresh DirectedGraph with DAG constraints", () => { const ctx = GraphologyHostConfig.createRootContext(null); expect(ctx.graph).toBeDefined(); expect(ctx.graph.order).toBe(0); expect(ctx.parentStack).toEqual([]); }); it("createRootContext accepts options with registry", () => { const registry = { resolve: (name: string) => name }; const ctx = GraphologyHostConfig.createRootContext(null, { registry }); expect(ctx.operationRegistry).toBe(registry); }); }); describe("Sequential rendering", () => { it("creates sequential edges between consecutive siblings", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), h(Operation, { name: "C" }), ); const ctx = renderTemplate(template); expect(ctx.graph.nodes().sort()).toEqual(["A", "B", "C"]); expect(ctx.graph.hasDirectedEdge("A", "B")).toBe(true); expect(ctx.graph.hasDirectedEdge("B", "C")).toBe(true); expect(ctx.graph.hasDirectedEdge("A", "C")).toBe(false); }); it("creates sequential edges with correct edgeType attribute", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ); const ctx = renderTemplate(template); const edgeKey = ctx.graph.edge("A", "B"); const attrs = ctx.graph.getEdgeAttributes(edgeKey); expect(attrs.edgeType).toBe("sequential"); }); it("single child produces no edges", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), ); const ctx = renderTemplate(template); expect(ctx.graph.nodes()).toEqual(["A"]); expect(ctx.graph.size).toBe(0); }); it("empty Sequential produces no nodes or edges", () => { const template = h(Sequential, {}); const ctx = renderTemplate(template); expect(ctx.graph.order).toBe(0); expect(ctx.graph.size).toBe(0); }); }); describe("Parallel rendering", () => { it("creates no inter-child edges", () => { const template = h(Parallel, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), h(Operation, { name: "C" }), ); const ctx = renderTemplate(template); expect(ctx.graph.nodes().sort()).toEqual(["A", "B", "C"]); expect(ctx.graph.size).toBe(0); }); it("parallel inside sequential connects predecessor to all parallel children", () => { const template = h(Sequential, {}, h(Operation, { name: "pre" }), h(Parallel, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ), ); const ctx = renderTemplate(template); expect(ctx.graph.nodes().sort()).toEqual(["A", "B", "pre"]); expect(ctx.graph.hasDirectedEdge("pre", "A")).toBe(true); expect(ctx.graph.hasDirectedEdge("pre", "B")).toBe(true); expect(ctx.graph.hasDirectedEdge("A", "B")).toBe(false); }); it("parallel inside sequential: successors connect to all parallel children", () => { const template = h(Sequential, {}, h(Parallel, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ), h(Operation, { name: "post" }), ); const ctx = renderTemplate(template); expect(ctx.graph.nodes().sort()).toEqual(["A", "B", "post"]); expect(ctx.graph.hasDirectedEdge("A", "post")).toBe(true); expect(ctx.graph.hasDirectedEdge("B", "post")).toBe(true); }); it("full parallel sandwich in sequential", () => { const template = h(Sequential, {}, h(Operation, { name: "pre" }), h(Parallel, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ), h(Operation, { name: "post" }), ); const ctx = renderTemplate(template); expect(ctx.graph.nodes().sort()).toEqual(["A", "B", "post", "pre"]); expect(ctx.graph.hasDirectedEdge("pre", "A")).toBe(true); expect(ctx.graph.hasDirectedEdge("pre", "B")).toBe(true); expect(ctx.graph.hasDirectedEdge("A", "post")).toBe(true); expect(ctx.graph.hasDirectedEdge("B", "post")).toBe(true); }); }); describe("Conditional rendering", () => { it("creates conditional edges with dataFlow true", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Conditional, { test: "A" }, h(Operation, { name: "B" }), ), ); const ctx = renderTemplate(template); expect(ctx.graph.nodes().sort()).toEqual(["A", "B"]); expect(ctx.graph.hasDirectedEdge("A", "B")).toBe(true); const edgeKey = ctx.graph.edge("A", "B"); const attrs = ctx.graph.getEdgeAttributes(edgeKey); expect(attrs.edgeType).toBe("conditional"); expect(attrs.dataFlow).toBe(true); }); it("conditional edge carries condition attribute", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Conditional, { test: "A" }, h(Operation, { name: "B" }), ), ); const ctx = renderTemplate(template); const edgeKey = ctx.graph.edge("A", "B"); const attrs = ctx.graph.getEdgeAttributes(edgeKey); expect(attrs.condition).toBe("A"); }); it("conditional with function test carries condition attribute", () => { const testFn = (results: Record) => true; const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Conditional, { test: testFn }, h(Operation, { name: "B" }), ), ); const ctx = renderTemplate(template); const edgeKey = ctx.graph.edge("A", "B"); const attrs = ctx.graph.getEdgeAttributes(edgeKey); expect(attrs.condition).toBe(testFn); expect(attrs.edgeType).toBe("conditional"); expect(attrs.dataFlow).toBe(true); }); it("conditional between sequential operations", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Conditional, { test: "A" }, h(Operation, { name: "B" }), ), h(Operation, { name: "C" }), ); const ctx = renderTemplate(template); expect(ctx.graph.hasDirectedEdge("A", "B")).toBe(true); expect(ctx.graph.hasDirectedEdge("B", "C")).toBe(true); const abEdge = ctx.graph.edge("A", "B"); const abAttrs = ctx.graph.getEdgeAttributes(abEdge); expect(abAttrs.edgeType).toBe("conditional"); const bcEdge = ctx.graph.edge("B", "C"); const bcAttrs = ctx.graph.getEdgeAttributes(bcEdge); expect(bcAttrs.edgeType).toBe("sequential"); }); }); describe("Nested compositions", () => { it("parallel inside sequential inside parallel", () => { const template = h(Parallel, {}, h(Sequential, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ), h(Sequential, {}, h(Operation, { name: "C" }), h(Operation, { name: "D" }), ), ); const ctx = renderTemplate(template); expect(ctx.graph.nodes().sort()).toEqual(["A", "B", "C", "D"]); expect(ctx.graph.hasDirectedEdge("A", "B")).toBe(true); expect(ctx.graph.hasDirectedEdge("C", "D")).toBe(true); expect(ctx.graph.hasDirectedEdge("A", "C")).toBe(false); expect(ctx.graph.hasDirectedEdge("B", "D")).toBe(false); }); it("sequential inside parallel inside sequential", () => { const template = h(Sequential, {}, h(Operation, { name: "pre" }), h(Parallel, {}, h(Sequential, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ), h(Operation, { name: "C" }), ), h(Operation, { name: "post" }), ); const ctx = renderTemplate(template); expect(ctx.graph.nodes().sort()).toEqual(["A", "B", "C", "post", "pre"]); expect(ctx.graph.hasDirectedEdge("pre", "A")).toBe(true); expect(ctx.graph.hasDirectedEdge("pre", "C")).toBe(true); expect(ctx.graph.hasDirectedEdge("A", "B")).toBe(true); expect(ctx.graph.hasDirectedEdge("B", "post")).toBe(true); expect(ctx.graph.hasDirectedEdge("C", "post")).toBe(true); }); it("conditional inside sequential inside parallel", () => { const template = h(Sequential, {}, h(Operation, { name: "start" }), h(Parallel, {}, h(Sequential, {}, h(Conditional, { test: "start" }, h(Operation, { name: "condA" }), ), h(Operation, { name: "afterA" }), ), h(Operation, { name: "branchB" }), ), ); const ctx = renderTemplate(template); expect(ctx.graph.nodes().sort()).toEqual(["afterA", "branchB", "condA", "start"]); expect(ctx.graph.hasDirectedEdge("start", "condA")).toBe(true); expect(ctx.graph.hasDirectedEdge("start", "branchB")).toBe(true); expect(ctx.graph.hasDirectedEdge("condA", "afterA")).toBe(true); expect(ctx.graph.hasDirectedEdge("afterA", undefined as unknown as string)).toBe(false); const startCondAKey = ctx.graph.edge("start", "condA"); const startCondAAttrs = ctx.graph.getEdgeAttributes(startCondAKey); expect(startCondAAttrs.edgeType).toBe("conditional"); expect(startCondAAttrs.dataFlow).toBe(true); }); }); describe("dataFlow inference", () => { it("sequential edges default to dataFlow false", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Operation, { name: "B" }), ); const ctx = renderTemplate(template); const edgeKey = ctx.graph.edge("A", "B"); const attrs = ctx.graph.getEdgeAttributes(edgeKey); expect(attrs.edgeType).toBe("sequential"); expect(attrs.dataFlow).toBe(false); }); it("conditional edges always have dataFlow true", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), h(Conditional, { test: "A" }, h(Operation, { name: "B" }), ), ); const ctx = renderTemplate(template); const edgeKey = ctx.graph.edge("A", "B"); const attrs = ctx.graph.getEdgeAttributes(edgeKey); expect(attrs.dataFlow).toBe(true); }); }); describe("Cycle detection", () => { it("finalizeRoot throws CycleError when graph has a cycle", () => { const ctx = GraphologyHostConfig.createRootContext(null); ctx.graph.addNode("A", {}); ctx.graph.addNode("B", {}); ctx.graph.addNode("C", {}); ctx.graph.addEdgeWithKey("A->B", "A", "B", {}); ctx.graph.addEdgeWithKey("B->C", "B", "C", {}); ctx.graph.addEdgeWithKey("C->A", "C", "A", {}); expect(() => GraphologyHostConfig.finalizeRoot?.(ctx)).toThrow(CycleError); }); it("finalizeRoot does not throw for a valid DAG", () => { const ctx = GraphologyHostConfig.createRootContext(null); ctx.graph.addNode("A", {}); ctx.graph.addNode("B", {}); ctx.graph.addEdgeWithKey("A->B", "A", "B", {}); expect(() => GraphologyHostConfig.finalizeRoot?.(ctx)).not.toThrow(); }); }); describe("Operation node attributes", () => { it("creates graph node with OperationNodeAttrs", () => { const template = h(Sequential, {}, h(Operation, { name: "classify" }), ); const ctx = renderTemplate(template); const attrs = ctx.graph.getNodeAttributes("classify"); expect(attrs.name).toBe("classify"); expect(attrs.namespace).toBe(""); expect(attrs.version).toBe("0.0.1"); expect(attrs.type).toBe("mutation"); }); it("operation with custom attributes", () => { const template = h(Sequential, {}, h(Operation, { name: "enrich", namespace: "task", version: "1.0.0", type: "query", inputSchema: { type: "object" }, outputSchema: { type: "string" }, description: "Enriches data", tags: ["core", "data"], }), ); const ctx = renderTemplate(template); const attrs = ctx.graph.getNodeAttributes("enrich"); expect(attrs.name).toBe("enrich"); expect(attrs.namespace).toBe("task"); expect(attrs.version).toBe("1.0.0"); expect(attrs.type).toBe("query"); expect(attrs.inputSchema).toEqual({ type: "object" }); expect(attrs.outputSchema).toEqual({ type: "string" }); expect(attrs.description).toBe("Enriches data"); expect(attrs.tags).toEqual(["core", "data"]); }); }); describe("removeChild", () => { it("removes edge between operation nodes", () => { const ctx = GraphologyHostConfig.createRootContext(null); const parent: import("../../src/host/graphology.js").GraphNode = { key: "A", attributes: {} as any }; const child: import("../../src/host/graphology.js").GraphNode = { key: "B", attributes: {} as any }; ctx.graph.addNode("A", {}); ctx.graph.addNode("B", {}); ctx.graph.addEdgeWithKey("A->B", "A", "B", {}); GraphologyHostConfig.removeChild(parent, child, ctx); expect(ctx.graph.hasDirectedEdge("A", "B")).toBe(false); }); }); describe("createInstance structural containers", () => { it("structural containers get synthetic keys", () => { const ctx = GraphologyHostConfig.createRootContext(null); const seqNode = GraphologyHostConfig.createInstance("sequential", {}, ctx); expect(seqNode.key.startsWith("__sequential_")).toBe(true); const parNode = GraphologyHostConfig.createInstance("parallel", {}, ctx); expect(parNode.key.startsWith("__parallel_")).toBe(true); }); it("structural containers do not create graph nodes", () => { const template = h(Sequential, {}, h(Operation, { name: "A" }), ); const ctx = renderTemplate(template); const graphNodes = ctx.graph.nodes(); expect(graphNodes).toEqual(["A"]); expect(graphNodes.some((k) => k.startsWith("__"))).toBe(false); }); }); describe("render via createRoot", () => { it("renders a complete pipeline template", () => { const template = h(Sequential, {}, h(Operation, { name: "architect" }), h(Operation, { name: "reviewer" }), h(Parallel, {}, h(Operation, { name: "decomposer" }), h(Operation, { name: "specialist" }), ), h(Operation, { name: "synthesizer" }), ); const ctx = renderTemplate(template); expect(ctx.graph.nodes().sort()).toEqual(["architect", "decomposer", "reviewer", "specialist", "synthesizer"]); expect(ctx.graph.hasDirectedEdge("architect", "reviewer")).toBe(true); expect(ctx.graph.hasDirectedEdge("reviewer", "decomposer")).toBe(true); expect(ctx.graph.hasDirectedEdge("reviewer", "specialist")).toBe(true); expect(ctx.graph.hasDirectedEdge("decomposer", "synthesizer")).toBe(true); expect(ctx.graph.hasDirectedEdge("specialist", "synthesizer")).toBe(true); }); });