Implement graph validation functions (validateSchema, validateGraph, validate)

This commit is contained in:
2026-05-21 21:16:34 +00:00
parent d63ef886d8
commit 48b389841e
6 changed files with 429 additions and 8 deletions

View File

@@ -1,7 +1,256 @@
import { describe, it, expect } from 'vitest';
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";
describe('graph validation', () => {
it('placeholder', () => {
expect(true).toBe(true);
const SimpleNodeSchema = Type.Object({
name: Type.String(),
value: Type.Number(),
});
type SimpleNode = Static<typeof SimpleNodeSchema>;
function createValidGraph(): FlowGraph<typeof SimpleNodeSchema, TSchema> {
const g = new FlowGraph<typeof SimpleNodeSchema, TSchema>();
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<TSchema, TSchema>();
g.graph.addNode("a", { name: "alpha" });
const errors = validateSchema(
g as FlowGraph<typeof SimpleNodeSchema, TSchema>,
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<TSchema, TSchema>();
g.graph.addNode("a", { name: "alpha", value: "not-a-number" });
const errors = validateSchema(
g as FlowGraph<typeof SimpleNodeSchema, TSchema>,
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<TSchema, TSchema>();
g.graph.addNode("a", { name: "alpha" });
g.graph.addNode("b", { value: 1 });
const errors = validateSchema(
g as FlowGraph<typeof SimpleNodeSchema, TSchema>,
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<typeof SimpleNodeSchema, TSchema>();
const errors = validateSchema(g, SimpleNodeSchema);
expect(errors).toEqual([]);
});
it("never throws on invalid data", () => {
const g = new FlowGraph<TSchema, TSchema>();
g.graph.addNode("a", {});
expect(() =>
validateSchema(g as FlowGraph<typeof SimpleNodeSchema, TSchema>, 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<TSchema, TSchema>();
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<TSchema, TSchema>();
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<TSchema, TSchema>();
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<TSchema, TSchema>();
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<TSchema, TSchema>();
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<TSchema, TSchema>();
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<TSchema, TSchema>();
g.graph.addNode("orphan", { name: "orphan" });
g.graph.addNode("invalid", { name: 123 });
const errors = validate(
g as FlowGraph<typeof SimpleNodeSchema, TSchema>,
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<TSchema, TSchema>();
g.graph.addNode("a", { name: "wrong-type", value: "not-a-number" });
const errors = validate(
g as FlowGraph<typeof SimpleNodeSchema, TSchema>,
SimpleNodeSchema,
);
for (const error of errors) {
expect(["schema", "graph", "type-compat"]).toContain(error.type);
}
});
it("empty graph returns empty errors", () => {
const g = new FlowGraph<typeof SimpleNodeSchema, TSchema>();
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<TSchema, TSchema>();
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<TSchema, TSchema>();
g.addNode("orphan", { name: "orphan" });
const errors = g.validate(SimpleNodeSchema);
const graphErrors = errors.filter((e) => e.type === "graph");
expect(graphErrors.length).toBeGreaterThan(0);
});
});