import { describe, it, expect } from "vitest"; import { Type } from "@alkdev/typebox"; import { h, createHostRoot } from "@alkdev/ujsx"; import { Operation, Sequential, Parallel, Conditional } from "../../src/component/index.js"; import { validatePreconditions, validateTemplate } from "../../src/analysis/workflow.js"; import { FlowGraph } from "../../src/graph/construction.js"; import type { OperationNodeAttrs, OperationEdgeAttrs } from "../../src/schema/index.js"; type OpGraph = FlowGraph; function createOperationGraph( specs: Array<{ name: string; namespace?: string; inputSchema?: Record; outputSchema?: Record; }>, ): OpGraph { const graph = new FlowGraph() as OpGraph; for (const spec of specs) { const ns = spec.namespace ?? "test"; const key = `${ns}.${spec.name}`; graph.addNode(key, { name: spec.name, namespace: ns, version: "1.0.0", type: "query", inputSchema: spec.inputSchema ?? Type.Object({}), outputSchema: spec.outputSchema ?? Type.Object({}), } as OperationNodeAttrs); } return graph; } function createOperationGraphWithEdges( specs: Array<{ name: string; namespace?: string; inputSchema?: Record; outputSchema?: Record; }>, edges?: Array<{ source: string; target: string; compatible: boolean; mismatches?: Array<{ path: string; expected: string; actual: string }> }>, ): OpGraph { const graph = createOperationGraph(specs); if (edges) { for (const edge of edges) { graph.addTypedEdge(edge.source, edge.target, { compatible: edge.compatible, mismatches: edge.mismatches, }); } } return graph; } describe("validatePreconditions", () => { it("returns empty for valid graph with no required fields", () => { const graph = createOperationGraph([ { name: "a", outputSchema: Type.Object({ x: Type.Number() }) }, { name: "b", inputSchema: Type.Object({}) }, ]); graph.addEdge("test.a", "test.b"); const errors = validatePreconditions(graph); expect(errors).toEqual([]); }); it("returns empty when all required input fields are provided by predecessors", () => { const graph = createOperationGraph([ { name: "a", outputSchema: Type.Object({ x: Type.Number(), y: Type.String() }) }, { name: "b", inputSchema: Type.Object({ x: Type.Number() }) }, ]); graph.addEdge("test.a", "test.b"); const errors = validatePreconditions(graph); expect(errors).toEqual([]); }); it("returns errors when required input field is not provided by any predecessor", () => { const graph = createOperationGraph([ { name: "a", outputSchema: Type.Object({ x: Type.Number() }) }, { name: "b", inputSchema: Type.Object({ x: Type.Number(), y: Type.String() }) }, ]); graph.addEdge("test.a", "test.b"); const errors = validatePreconditions(graph); expect(errors.length).toBeGreaterThan(0); const fieldNames = errors.map((e) => e.field); expect(fieldNames).toContain("y"); }); it("returns errors when node with required fields has no predecessors", () => { const graph = createOperationGraph([ { name: "a", inputSchema: Type.Object({ x: Type.Number() }), outputSchema: Type.Object({}) }, ]); const errors = validatePreconditions(graph); expect(errors.length).toBeGreaterThan(0); expect(errors[0]!.message).toContain("no predecessor"); }); it("collects provided fields from multiple predecessors", () => { const graph = createOperationGraph([ { name: "a", outputSchema: Type.Object({ x: Type.Number() }) }, { name: "c", outputSchema: Type.Object({ y: Type.String() }) }, { name: "b", inputSchema: Type.Object({ x: Type.Number(), y: Type.String() }) }, ]); graph.addEdge("test.a", "test.b"); graph.addEdge("test.c", "test.b"); const errors = validatePreconditions(graph); expect(errors).toEqual([]); }); it("returns empty for graph with no nodes", () => { const graph = new FlowGraph() as OpGraph; const errors = validatePreconditions(graph); expect(errors).toEqual([]); }); }); describe("validateTemplate", () => { it("returns empty for valid template with all operations in graph", () => { const graph = createOperationGraphWithEdges([ { name: "a", outputSchema: Type.Object({ x: Type.Number() }) }, { name: "b", inputSchema: Type.Object({ x: Type.Number() }) }, ], [ { source: "test.a", target: "test.b", compatible: true }, ]); const template = h(Sequential, {}, h(Operation, { name: "test.a" }), h(Operation, { name: "test.b" }), ); const errors = validateTemplate(template, graph); expect(errors).toEqual([]); }); it("returns error when operation name is not in operation graph", () => { const graph = createOperationGraph([ { name: "a" }, ]); const template = h(Sequential, {}, h(Operation, { name: "test.a" }), h(Operation, { name: "test.missing" }), ); const errors = validateTemplate(template, graph); expect(errors.length).toBeGreaterThan(0); const missingErrors = errors.filter( (e) => e.type === "graph" && (e as { type: string; category: string; details: unknown }).category === "orphan-node" && JSON.stringify((e as { details: unknown }).details).includes("missing"), ); expect(missingErrors.length).toBeGreaterThan(0); }); it("returns error for type incompatibility between sequential operations", () => { const graph = createOperationGraphWithEdges([ { name: "a", outputSchema: Type.Object({ x: Type.String() }) }, { name: "b", inputSchema: Type.Object({ x: Type.Number() }) }, ], [ { source: "test.a", target: "test.b", compatible: false, mismatches: [{ path: "/x", expected: "number", actual: "string" }] }, ]); const template = h(Sequential, {}, h(Operation, { name: "test.a" }), h(Operation, { name: "test.b" }), ); const errors = validateTemplate(template, graph); const typeErrors = errors.filter((e) => e.type === "type-compat"); expect(typeErrors.length).toBeGreaterThan(0); }); it("returns empty for single-operation template", () => { const graph = createOperationGraph([ { name: "a" }, ]); const template = h(Operation, { name: "test.a" }); const errors = validateTemplate(template, graph); expect(errors).toEqual([]); }); it("detects unreachable nodes", () => { const graph = createOperationGraphWithEdges([ { name: "a" }, { name: "b" }, { name: "c" }, ]); const template = h(Sequential, {}, h(Operation, { name: "test.a" }), h(Operation, { name: "test.b" }), ); const errors = validateTemplate(template, graph); const reachableErrors = errors.filter( (e) => e.type === "graph" && JSON.stringify((e as { details: unknown }).details).includes("not reachable"), ); expect(reachableErrors.length).toBe(0); }); it("returns empty for valid parallel template", () => { const graph = createOperationGraphWithEdges([ { name: "a" }, { name: "b" }, { name: "c" }, ]); const template = h(Sequential, {}, h(Operation, { name: "test.a" }), h(Parallel, {}, h(Operation, { name: "test.b" }), h(Operation, { name: "test.c" }), ), ); const errors = validateTemplate(template, graph); expect(errors).toEqual([]); }); it("handles template with conditional", () => { const graph = createOperationGraphWithEdges([ { name: "a", outputSchema: Type.Object({ result: Type.Boolean() }) }, { name: "b" }, { name: "c" }, ]); const template = h(Sequential, {}, h(Operation, { name: "test.a" }), h(Conditional, { test: "test.a" }, h(Operation, { name: "test.b" }), ), h(Operation, { name: "test.c" }), ); const errors = validateTemplate(template, graph); expect(errors).toEqual([]); }); it("template validation is advisory - never throws", () => { const graph = new FlowGraph() as OpGraph; const template = h(Sequential, {}, h(Operation, { name: "nonexistent" }), ); const errors = validateTemplate(template, graph); expect(Array.isArray(errors)).toBe(true); }); it("detects orphan node with no edges in multi-node template", () => { const graph = createOperationGraphWithEdges([ { name: "a" }, { name: "b" }, ]); const template = h(Parallel, {}, h(Operation, { name: "test.a" }), h(Operation, { name: "test.b" }), ); const errors = validateTemplate(template, graph); const orphanErrors = errors.filter( (e) => e.type === "graph" && (e as { type: string; category: string }).category === "orphan-node" && JSON.stringify((e as { details: unknown }).details).includes("no edges"), ); expect(orphanErrors.length).toBeGreaterThan(0); }); });