import { describe, it, expect } from "vitest"; import { Type, type Static, type TSchema } from "@alkdev/typebox"; import { FlowGraph } from "../../src/graph/construction.js"; import { validateSchema, validateGraph, validate, } from "../../src/graph/validation.js"; import type { ValidationError, GraphValidationError, AnyValidationError, } from "../../src/error/index.js"; const SimpleNodeSchema = Type.Object({ name: Type.String(), value: Type.Number(), }); type SimpleNode = Static; function createValidGraph(): FlowGraph { const g = new FlowGraph(); g.addNode("a", { name: "alpha", value: 1 }); g.addNode("b", { name: "beta", value: 2 }); g.addEdge("a", "b"); return g; } describe("validateSchema", () => { it("returns empty array for valid nodes", () => { const g = createValidGraph(); const errors = validateSchema(g, SimpleNodeSchema); expect(errors).toEqual([]); }); it("detects missing required field", () => { const g = new FlowGraph(); g.graph.addNode("a", { name: "alpha" }); const errors = validateSchema( g as FlowGraph, SimpleNodeSchema, ); expect(errors.length).toBeGreaterThan(0); expect(errors[0]!.type).toBe("schema"); expect(errors[0]!.nodeKey).toBe("a"); }); it("detects wrong type for field", () => { const g = new FlowGraph(); g.graph.addNode("a", { name: "alpha", value: "not-a-number" }); const errors = validateSchema( g as FlowGraph, SimpleNodeSchema, ); expect(errors.length).toBeGreaterThan(0); const valueErrors = errors.filter((e) => e.field.includes("value")); expect(valueErrors.length).toBeGreaterThan(0); }); it("collects errors from multiple nodes", () => { const g = new FlowGraph(); g.graph.addNode("a", { name: "alpha" }); g.graph.addNode("b", { value: 1 }); const errors = validateSchema( g as FlowGraph, SimpleNodeSchema, ); const aErrors = errors.filter((e) => e.nodeKey === "a"); const bErrors = errors.filter((e) => e.nodeKey === "b"); expect(aErrors.length).toBeGreaterThan(0); expect(bErrors.length).toBeGreaterThan(0); }); it("returns empty array for empty graph", () => { const g = new FlowGraph(); const errors = validateSchema(g, SimpleNodeSchema); expect(errors).toEqual([]); }); it("never throws on invalid data", () => { const g = new FlowGraph(); g.graph.addNode("a", {}); expect(() => validateSchema(g as FlowGraph, SimpleNodeSchema), ).not.toThrow(); }); }); describe("validateGraph", () => { it("returns empty array for valid DAG", () => { const g = createValidGraph(); const errors = validateGraph(g); expect(errors).toEqual([]); }); it("returns empty array for empty graph", () => { const g = new FlowGraph(); const errors = validateGraph(g); expect(errors).toEqual([]); }); it("detects cycles via direct graph access", () => { const g = new FlowGraph(); g.graph.addNode("a", { name: "a" }); g.graph.addNode("b", { name: "b" }); g.graph.addEdgeWithKey("a->b", "a", "b", {}); g.graph.addEdgeWithKey("b->a", "b", "a", {}); const errors = validateGraph(g); const cycleErrors = errors.filter( (e) => e.category === "cycle", ) as GraphValidationError[]; expect(cycleErrors.length).toBeGreaterThan(0); const details = cycleErrors[0]!.details as { cycles: string[][] }; expect(details.cycles.length).toBeGreaterThan(0); }); it("detects orphan nodes", () => { const g = new FlowGraph(); g.addNode("a", { name: "a" }); g.addNode("b", { name: "b" }); g.addNode("c", { name: "c" }); g.addEdge("a", "b"); const errors = validateGraph(g); const orphanErrors = errors.filter( (e) => e.category === "orphan-node", ) as GraphValidationError[]; expect(orphanErrors.length).toBe(1); const details = orphanErrors[0]!.details as { nodeKey: string }; expect(details.nodeKey).toBe("c"); }); it("detects status inconsistency", () => { const g = new FlowGraph(); g.graph.addNode("parent", { name: "parent", status: "completed" }); g.graph.addNode("child", { name: "child", status: "running" }); g.graph.addEdgeWithKey("parent->child", "parent", "child", { edgeType: "triggered" }); const errors = validateGraph(g); const statusErrors = errors.filter( (e) => e.category === "status-inconsistency", ) as GraphValidationError[]; expect(statusErrors.length).toBe(1); const details = statusErrors[0]!.details as { nodeKey: string; parentKey: string; nodeStatus: string; parentStatus: string; }; expect(details.nodeKey).toBe("child"); expect(details.parentKey).toBe("parent"); expect(details.nodeStatus).toBe("running"); expect(details.parentStatus).toBe("completed"); }); it("does not report status inconsistency for consistent statuses", () => { const g = new FlowGraph(); g.graph.addNode("parent", { name: "parent", status: "running" }); g.graph.addNode("child", { name: "child", status: "running" }); g.graph.addEdgeWithKey("parent->child", "parent", "child", { edgeType: "triggered" }); const errors = validateGraph(g); const statusErrors = errors.filter( (e) => e.category === "status-inconsistency", ); expect(statusErrors.length).toBe(0); }); it("collects multiple error types simultaneously", () => { const g = new FlowGraph(); g.graph.addNode("orphan", { name: "orphan" }); g.graph.addNode("a", { name: "a", status: "completed" }); g.graph.addNode("b", { name: "b", status: "running" }); g.graph.addEdgeWithKey("a->b", "a", "b", { edgeType: "triggered" }); const errors = validateGraph(g); expect(errors.length).toBeGreaterThanOrEqual(2); const categories = new Set(errors.map((e) => e.category)); expect(categories.has("orphan-node")).toBe(true); expect(categories.has("status-inconsistency")).toBe(true); }); it("never throws", () => { const g = new FlowGraph(); g.graph.addNode("a", { name: "a" }); g.graph.addNode("b", { name: "b" }); g.graph.addEdgeWithKey("a->b", "a", "b", {}); g.graph.addEdgeWithKey("b->a", "b", "a", {}); expect(() => validateGraph(g)).not.toThrow(); }); }); describe("validate (combined)", () => { it("returns empty array for valid graph with matching schema", () => { const g = createValidGraph(); const errors = validate(g, SimpleNodeSchema); expect(errors).toEqual([]); }); it("combines schema and graph errors", () => { const g = new FlowGraph(); g.graph.addNode("orphan", { name: "orphan" }); g.graph.addNode("invalid", { name: 123 }); const errors = validate( g as FlowGraph, SimpleNodeSchema, ); const schemaErrors = errors.filter( (e) => e.type === "schema", ) as ValidationError[]; const graphErrors = errors.filter( (e) => e.type === "graph", ) as GraphValidationError[]; expect(schemaErrors.length).toBeGreaterThan(0); expect(graphErrors.length).toBeGreaterThan(0); }); it("returns AnyValidationError union type", () => { const g = new FlowGraph(); g.graph.addNode("a", { name: "wrong-type", value: "not-a-number" }); const errors = validate( g as FlowGraph, SimpleNodeSchema, ); for (const error of errors) { expect(["schema", "graph", "type-compat"]).toContain(error.type); } }); it("empty graph returns empty errors", () => { const g = new FlowGraph(); const errors = validate(g, SimpleNodeSchema); expect(errors).toEqual([]); }); }); describe("FlowGraph.validate() convenience method", () => { it("delegates to standalone validate", () => { const g = createValidGraph(); const errors = g.validate(SimpleNodeSchema); expect(errors).toEqual([]); }); it("detects schema errors via convenience method", () => { const g = new FlowGraph(); g.graph.addNode("a", { name: "wrong-type", value: "not-a-number" }); const errors = g.validate(SimpleNodeSchema); const schemaErrors = errors.filter((e) => e.type === "schema"); expect(schemaErrors.length).toBeGreaterThan(0); }); it("detects graph errors via convenience method", () => { const g = new FlowGraph(); g.addNode("orphan", { name: "orphan" }); const errors = g.validate(SimpleNodeSchema); const graphErrors = errors.filter((e) => e.type === "graph"); expect(graphErrors.length).toBeGreaterThan(0); }); });