import { describe, it, expect } from "vitest"; import { FlowgraphError, ConstructionError, DuplicateNodeError, DuplicateEdgeError, NodeNotFoundError, CycleError, InvalidInputError, InvalidTransitionError, } from "../../src/error/index.js"; import type { CallStatus, ValidationError, GraphValidationError, TypeMismatch, TypeIncompatError, AnyValidationError, } from "../../src/error/index.js"; describe("FlowgraphError", () => { it("constructs with correct name and message", () => { const err = new FlowgraphError("test"); expect(err).toBeInstanceOf(Error); expect(err).toBeInstanceOf(FlowgraphError); expect(err.name).toBe("FlowgraphError"); expect(err.message).toBe("test"); }); }); describe("ConstructionError", () => { it("extends FlowgraphError", () => { const err = new ConstructionError("construction failed"); expect(err).toBeInstanceOf(Error); expect(err).toBeInstanceOf(FlowgraphError); expect(err).toBeInstanceOf(ConstructionError); expect(err.name).toBe("ConstructionError"); expect(err.message).toBe("construction failed"); }); }); describe("DuplicateNodeError", () => { it("has key property and correct name", () => { const err = new DuplicateNodeError("nodeA"); expect(err).toBeInstanceOf(ConstructionError); expect(err).toBeInstanceOf(FlowgraphError); expect(err.name).toBe("DuplicateNodeError"); expect(err.key).toBe("nodeA"); expect(err.message).toBe('Node with key "nodeA" already exists'); }); }); describe("DuplicateEdgeError", () => { it("has source and target properties", () => { const err = new DuplicateEdgeError("A", "B"); expect(err).toBeInstanceOf(ConstructionError); expect(err.name).toBe("DuplicateEdgeError"); expect(err.source).toBe("A"); expect(err.target).toBe("B"); expect(err.message).toBe('Edge "A -> B" already exists'); }); }); describe("NodeNotFoundError", () => { it("has key property", () => { const err = new NodeNotFoundError("missing"); expect(err).toBeInstanceOf(ConstructionError); expect(err.name).toBe("NodeNotFoundError"); expect(err.key).toBe("missing"); expect(err.message).toBe('Node "missing" not found in graph'); }); }); describe("CycleError", () => { it("has cycles property", () => { const cycles = [["A", "B", "C", "A"]]; const err = new CycleError(cycles); expect(err).toBeInstanceOf(ConstructionError); expect(err.name).toBe("CycleError"); expect(err.cycles).toEqual(cycles); expect(err.message).toContain("cycle"); }); }); describe("InvalidInputError", () => { it("has errors property with ValidationError array", () => { const validationErrors: ValidationError[] = [ { type: "schema", nodeKey: "n1", field: "status", message: "invalid status" }, ]; const err = new InvalidInputError(validationErrors); expect(err).toBeInstanceOf(ConstructionError); expect(err.name).toBe("InvalidInputError"); expect(err.errors).toEqual(validationErrors); expect(err.message).toBe("Invalid input: 1 validation error(s)"); }); }); describe("InvalidTransitionError", () => { it("has requestId, from, to properties", () => { const err = new InvalidTransitionError("req-1", "completed", "running"); expect(err).toBeInstanceOf(FlowgraphError); expect(err).not.toBeInstanceOf(ConstructionError); expect(err.name).toBe("InvalidTransitionError"); expect(err.requestId).toBe("req-1"); expect(err.from).toBe("completed"); expect(err.to).toBe("running"); expect(err.message).toContain("req-1"); expect(err.message).toContain("completed"); expect(err.message).toContain("running"); }); }); describe("CallStatus type", () => { it("accepts valid statuses", () => { const statuses: CallStatus[] = ["pending", "running", "completed", "failed", "aborted"]; expect(statuses).toHaveLength(5); }); }); describe("ValidationError interface", () => { it("can be constructed as a plain object", () => { const ve: ValidationError = { type: "schema", nodeKey: "n1", field: "status", message: "invalid", }; expect(ve.type).toBe("schema"); expect(ve.nodeKey).toBe("n1"); expect(ve.field).toBe("status"); expect(ve.message).toBe("invalid"); }); it("accepts optional value", () => { const ve: ValidationError = { type: "schema", nodeKey: "n1", field: "status", message: "invalid", value: 42, }; expect(ve.value).toBe(42); }); }); describe("GraphValidationError interface", () => { it("supports cycle category", () => { const ge: GraphValidationError = { type: "graph", category: "cycle", details: { cycles: [["A", "B", "A"]] }, }; expect(ge.type).toBe("graph"); expect(ge.category).toBe("cycle"); }); it("supports dangling-reference category", () => { const ge: GraphValidationError = { type: "graph", category: "dangling-reference", details: { source: "A", target: "Z" }, }; expect(ge.category).toBe("dangling-reference"); }); it("supports orphan-node category", () => { const ge: GraphValidationError = { type: "graph", category: "orphan-node", details: { nodeKey: "lonely" }, }; expect(ge.category).toBe("orphan-node"); }); it("supports status-inconsistency category", () => { const ge: GraphValidationError = { type: "graph", category: "status-inconsistency", details: { nodeKey: "n1", parentKey: "p1", nodeStatus: "running", parentStatus: "completed" }, }; expect(ge.category).toBe("status-inconsistency"); }); }); describe("TypeMismatch interface", () => { it("has path, expected, actual", () => { const tm: TypeMismatch = { path: "$.input.name", expected: "string", actual: "number" }; expect(tm.path).toBe("$.input.name"); expect(tm.expected).toBe("string"); expect(tm.actual).toBe("number"); }); }); describe("TypeIncompatError interface", () => { it("has required fields with compatible: false", () => { const tie: TypeIncompatError = { type: "type-compat", sourceKey: "A", targetKey: "B", compatible: false, mismatches: [{ path: "$.name", expected: "string", actual: "number" }], }; expect(tie.type).toBe("type-compat"); expect(tie.compatible).toBe(false); expect(tie.mismatches).toHaveLength(1); }); }); describe("AnyValidationError union", () => { it("accepts ValidationError", () => { const e: AnyValidationError = { type: "schema", nodeKey: "n1", field: "f", message: "bad" }; expect(e.type).toBe("schema"); }); it("accepts GraphValidationError", () => { const e: AnyValidationError = { type: "graph", category: "cycle", details: {} }; expect(e.type).toBe("graph"); }); it("accepts TypeIncompatError", () => { const e: AnyValidationError = { type: "type-compat", sourceKey: "A", targetKey: "B", compatible: false, mismatches: [], }; expect(e.type).toBe("type-compat"); }); }); describe("instanceof checks", () => { it("DuplicateNodeError instanceof FlowgraphError", () => { expect(new DuplicateNodeError("x")).toBeInstanceOf(FlowgraphError); }); it("DuplicateEdgeError instanceof ConstructionError", () => { expect(new DuplicateEdgeError("a", "b")).toBeInstanceOf(ConstructionError); }); it("NodeNotFoundError instanceof ConstructionError", () => { expect(new NodeNotFoundError("x")).toBeInstanceOf(ConstructionError); }); it("CycleError instanceof ConstructionError", () => { expect(new CycleError([["a", "b", "a"]])).toBeInstanceOf(ConstructionError); }); it("InvalidInputError instanceof ConstructionError", () => { expect(new InvalidInputError([])).toBeInstanceOf(ConstructionError); }); it("InvalidTransitionError instanceof FlowgraphError but not ConstructionError", () => { const err = new InvalidTransitionError("r1", "completed", "running"); expect(err).toBeInstanceOf(FlowgraphError); expect(err).not.toBeInstanceOf(ConstructionError); }); });