Implement FlowgraphError hierarchy with all error classes and validation result types

This commit is contained in:
2026-05-21 20:51:19 +00:00
parent 0886ba1f00
commit 3a8a54ccd6
3 changed files with 381 additions and 6 deletions

View File

@@ -1,7 +1,258 @@
import { describe, it, expect } from 'vitest';
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('errors', () => {
it('placeholder', () => {
expect(true).toBe(true);
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);
});
});