From 3a8a54ccd6e4d9567e0f2732ad40f1a6048095ea Mon Sep 17 00:00:00 2001 From: "glm-5.1" Date: Thu, 21 May 2026 20:51:19 +0000 Subject: [PATCH] Implement FlowgraphError hierarchy with all error classes and validation result types --- src/error/index.ts | 126 ++++++++++++++++++- src/index.ts | 2 +- test/error/errors.test.ts | 259 +++++++++++++++++++++++++++++++++++++- 3 files changed, 381 insertions(+), 6 deletions(-) diff --git a/src/error/index.ts b/src/error/index.ts index 8cec2e9..41722ee 100644 --- a/src/error/index.ts +++ b/src/error/index.ts @@ -1 +1,125 @@ -export {}; \ No newline at end of file +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, +}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 8cec2e9..b9b83cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1 @@ -export {}; \ No newline at end of file +export * from "./error/index.js"; \ No newline at end of file diff --git a/test/error/errors.test.ts b/test/error/errors.test.ts index 08d25f9..c1c9529 100644 --- a/test/error/errors.test.ts +++ b/test/error/errors.test.ts @@ -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); }); }); \ No newline at end of file