Implement FlowgraphError hierarchy with all error classes and validation result types
This commit is contained in:
@@ -1 +1,125 @@
|
|||||||
export {};
|
type CallStatus = "pending" | "running" | "completed" | "failed" | "aborted";
|
||||||
|
|
||||||
|
class FlowgraphError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "FlowgraphError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConstructionError extends FlowgraphError {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "ConstructionError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DuplicateNodeError extends ConstructionError {
|
||||||
|
readonly key: string;
|
||||||
|
constructor(key: string) {
|
||||||
|
super(`Node with key "${key}" already exists`);
|
||||||
|
this.name = "DuplicateNodeError";
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DuplicateEdgeError extends ConstructionError {
|
||||||
|
readonly source: string;
|
||||||
|
readonly target: string;
|
||||||
|
constructor(source: string, target: string) {
|
||||||
|
super(`Edge "${source} -> ${target}" already exists`);
|
||||||
|
this.name = "DuplicateEdgeError";
|
||||||
|
this.source = source;
|
||||||
|
this.target = target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NodeNotFoundError extends ConstructionError {
|
||||||
|
readonly key: string;
|
||||||
|
constructor(key: string) {
|
||||||
|
super(`Node "${key}" not found in graph`);
|
||||||
|
this.name = "NodeNotFoundError";
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CycleError extends ConstructionError {
|
||||||
|
readonly cycles: string[][];
|
||||||
|
constructor(cycles: string[][]) {
|
||||||
|
super(`Adding this edge would create a cycle: ${JSON.stringify(cycles)}`);
|
||||||
|
this.name = "CycleError";
|
||||||
|
this.cycles = cycles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvalidInputError extends ConstructionError {
|
||||||
|
readonly errors: ValidationError[];
|
||||||
|
constructor(errors: ValidationError[]) {
|
||||||
|
super(`Invalid input: ${errors.length} validation error(s)`);
|
||||||
|
this.name = "InvalidInputError";
|
||||||
|
this.errors = errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvalidTransitionError extends FlowgraphError {
|
||||||
|
readonly requestId: string;
|
||||||
|
readonly from: CallStatus;
|
||||||
|
readonly to: CallStatus;
|
||||||
|
constructor(requestId: string, from: CallStatus, to: CallStatus) {
|
||||||
|
super(`Invalid status transition for call ${requestId}: ${from} → ${to}`);
|
||||||
|
this.name = "InvalidTransitionError";
|
||||||
|
this.requestId = requestId;
|
||||||
|
this.from = from;
|
||||||
|
this.to = to;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ValidationError {
|
||||||
|
type: "schema";
|
||||||
|
nodeKey: string;
|
||||||
|
field: string;
|
||||||
|
message: string;
|
||||||
|
value?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GraphValidationError {
|
||||||
|
type: "graph";
|
||||||
|
category: "cycle" | "dangling-reference" | "orphan-node" | "status-inconsistency";
|
||||||
|
details: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TypeMismatch {
|
||||||
|
path: string;
|
||||||
|
expected: string;
|
||||||
|
actual: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TypeIncompatError {
|
||||||
|
type: "type-compat";
|
||||||
|
sourceKey: string;
|
||||||
|
targetKey: string;
|
||||||
|
compatible: false;
|
||||||
|
mismatches: TypeMismatch[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnyValidationError = ValidationError | GraphValidationError | TypeIncompatError;
|
||||||
|
|
||||||
|
export {
|
||||||
|
FlowgraphError,
|
||||||
|
ConstructionError,
|
||||||
|
DuplicateNodeError,
|
||||||
|
DuplicateEdgeError,
|
||||||
|
NodeNotFoundError,
|
||||||
|
CycleError,
|
||||||
|
InvalidInputError,
|
||||||
|
InvalidTransitionError,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type {
|
||||||
|
CallStatus,
|
||||||
|
ValidationError,
|
||||||
|
GraphValidationError,
|
||||||
|
TypeMismatch,
|
||||||
|
TypeIncompatError,
|
||||||
|
AnyValidationError,
|
||||||
|
};
|
||||||
@@ -1 +1 @@
|
|||||||
export {};
|
export * from "./error/index.js";
|
||||||
@@ -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', () => {
|
describe("FlowgraphError", () => {
|
||||||
it('placeholder', () => {
|
it("constructs with correct name and message", () => {
|
||||||
expect(true).toBe(true);
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user