From e2a7d889c6aa2e4a371f0d3747013fe521a2f8d1 Mon Sep 17 00:00:00 2001 From: "glm-5.1" Date: Thu, 21 May 2026 20:59:11 +0000 Subject: [PATCH] feat(schema): add edge attribute schemas and CallResult schema --- src/schema/edge.ts | 50 ++++++- src/schema/index.ts | 2 + test/schema/edge.test.ts | 277 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 test/schema/edge.test.ts diff --git a/src/schema/edge.ts b/src/schema/edge.ts index 8cec2e9..03f8487 100644 --- a/src/schema/edge.ts +++ b/src/schema/edge.ts @@ -1 +1,49 @@ -export {}; \ No newline at end of file +import { Type, type Static } from "@alkdev/typebox"; +import { NodeStatusEnum } from "./enums.js"; +import { OperationNodeAttrs } from "./node.js"; + +export const OperationEdgeAttrs = Type.Object({ + compatible: Type.Boolean(), + detail: Type.Optional(Type.String()), + mismatches: Type.Optional( + Type.Array( + Type.Object({ + path: Type.String(), + expected: Type.String(), + actual: Type.String(), + }), + ), + ), +}); +export type OperationEdgeAttrs = Static; + +export const TriggeredEdgeAttrs = Type.Object({}); +export type TriggeredEdgeAttrs = Static; + +export const DependencyEdgeAttrs = Type.Object({}); +export type DependencyEdgeAttrs = Static; + +export type CallEdgeAttrs = TriggeredEdgeAttrs | DependencyEdgeAttrs; + +export const TemplateEdgeAttrs = Type.Object({ + edgeType: Type.Union([Type.Literal("sequential"), Type.Literal("conditional")]), + condition: Type.Optional(Type.Unknown()), + negated: Type.Optional(Type.Boolean()), + dataFlow: Type.Optional(Type.Boolean({ default: false })), +}); +export type TemplateEdgeAttrs = Static; + +export type TemplateNodeAttrs = OperationNodeAttrs; + +export const CallResultSchema = Type.Object({ + status: NodeStatusEnum, + output: Type.Unknown(), + error: Type.Optional( + Type.Object({ + code: Type.String(), + message: Type.String(), + details: Type.Optional(Type.Unknown()), + }), + ), +}); +export type CallResult = Static; \ No newline at end of file diff --git a/src/schema/index.ts b/src/schema/index.ts index 31d47f2..dbf83d2 100644 --- a/src/schema/index.ts +++ b/src/schema/index.ts @@ -11,3 +11,5 @@ export { } from "./enums.js"; export * from "./node.js"; + +export * from "./edge.js"; \ No newline at end of file diff --git a/test/schema/edge.test.ts b/test/schema/edge.test.ts new file mode 100644 index 0000000..b7eed40 --- /dev/null +++ b/test/schema/edge.test.ts @@ -0,0 +1,277 @@ +import { describe, it, expect } from "vitest"; +import { Value } from "@alkdev/typebox/value"; +import { + OperationEdgeAttrs, + type OperationEdgeAttrs as OperationEdgeAttrsType, + TriggeredEdgeAttrs, + DependencyEdgeAttrs, + TemplateEdgeAttrs, + type TemplateEdgeAttrs as TemplateEdgeAttrsType, + CallResultSchema, + type CallResult, +} from "../../src/schema/edge"; +import { type OperationNodeAttrs } from "../../src/schema/node"; + +describe("OperationEdgeAttrs", () => { + const valid: OperationEdgeAttrsType = { + compatible: true, + }; + + it("accepts valid attributes with no optional fields", () => { + expect(Value.Check(OperationEdgeAttrs, valid)).toBe(true); + }); + + it("accepts compatible false", () => { + expect(Value.Check(OperationEdgeAttrs, { compatible: false })).toBe(true); + }); + + it("accepts valid attributes with all optional fields", () => { + const withOptional: OperationEdgeAttrsType = { + compatible: false, + detail: "Output schema mismatch", + mismatches: [ + { path: "/properties/name", expected: "string", actual: "number" }, + ], + }; + expect(Value.Check(OperationEdgeAttrs, withOptional)).toBe(true); + }); + + it("accepts empty mismatches array", () => { + expect( + Value.Check(OperationEdgeAttrs, { compatible: true, mismatches: [] }), + ).toBe(true); + }); + + it("rejects missing required compatible field", () => { + expect(Value.Check(OperationEdgeAttrs, {})).toBe(false); + }); + + it("rejects wrong type for compatible", () => { + expect(Value.Check(OperationEdgeAttrs, { compatible: "yes" })).toBe(false); + }); + + it("rejects wrong type for detail", () => { + expect( + Value.Check(OperationEdgeAttrs, { compatible: true, detail: 42 }), + ).toBe(false); + }); + + it("rejects mismatches with missing required fields", () => { + expect( + Value.Check(OperationEdgeAttrs, { + compatible: false, + mismatches: [{ path: "/x" }], + }), + ).toBe(false); + }); +}); + +describe("TriggeredEdgeAttrs", () => { + it("accepts empty object", () => { + expect(Value.Check(TriggeredEdgeAttrs, {})).toBe(true); + }); + + it("rejects non-object", () => { + expect(Value.Check(TriggeredEdgeAttrs, null)).toBe(false); + expect(Value.Check(TriggeredEdgeAttrs, "x")).toBe(false); + }); +}); + +describe("DependencyEdgeAttrs", () => { + it("accepts empty object", () => { + expect(Value.Check(DependencyEdgeAttrs, {})).toBe(true); + }); + + it("rejects non-object", () => { + expect(Value.Check(DependencyEdgeAttrs, null)).toBe(false); + expect(Value.Check(DependencyEdgeAttrs, 1)).toBe(false); + }); +}); + +describe("TemplateEdgeAttrs", () => { + const valid: TemplateEdgeAttrsType = { + edgeType: "sequential", + }; + + it("accepts valid sequential edge", () => { + expect(Value.Check(TemplateEdgeAttrs, valid)).toBe(true); + }); + + it("accepts conditional edge", () => { + expect( + Value.Check(TemplateEdgeAttrs, { edgeType: "conditional" }), + ).toBe(true); + }); + + it("rejected edgeType values are rejected", () => { + expect( + Value.Check(TemplateEdgeAttrs, { edgeType: "triggered" }), + ).toBe(false); + expect( + Value.Check(TemplateEdgeAttrs, { edgeType: "depends_on" }), + ).toBe(false); + expect( + Value.Check(TemplateEdgeAttrs, { edgeType: "typed" }), + ).toBe(false); + }); + + it("accepts optional condition (string)", () => { + expect( + Value.Check(TemplateEdgeAttrs, { + edgeType: "conditional", + condition: "fetch-data", + }), + ).toBe(true); + }); + + it("accepts optional condition (function)", () => { + expect( + Value.Check(TemplateEdgeAttrs, { + edgeType: "conditional", + condition: ((results: Record) => true) as unknown, + }), + ).toBe(true); + }); + + it("accepts optional negated", () => { + expect( + Value.Check(TemplateEdgeAttrs, { + edgeType: "conditional", + negated: true, + }), + ).toBe(true); + }); + + it("accepts optional dataFlow", () => { + expect( + Value.Check(TemplateEdgeAttrs, { + edgeType: "sequential", + dataFlow: true, + }), + ).toBe(true); + expect( + Value.Check(TemplateEdgeAttrs, { + edgeType: "sequential", + dataFlow: false, + }), + ).toBe(true); + }); + + it("accepts all optional fields together", () => { + const full: TemplateEdgeAttrsType = { + edgeType: "conditional", + condition: "fetch-data", + negated: true, + dataFlow: true, + }; + expect(Value.Check(TemplateEdgeAttrs, full)).toBe(true); + }); + + it("rejects missing required edgeType", () => { + expect(Value.Check(TemplateEdgeAttrs, {})).toBe(false); + }); + + it("rejects invalid edgeType", () => { + expect(Value.Check(TemplateEdgeAttrs, { edgeType: "parallel" })).toBe( + false, + ); + }); + + it("rejects wrong type for negated", () => { + expect( + Value.Check(TemplateEdgeAttrs, { edgeType: "sequential", negated: 1 }), + ).toBe(false); + }); + + it("rejects wrong type for dataFlow", () => { + expect( + Value.Check(TemplateEdgeAttrs, { + edgeType: "sequential", + dataFlow: "yes", + }), + ).toBe(false); + }); +}); + +describe("CallResultSchema", () => { + const valid: CallResult = { + status: "completed", + output: { label: "greeting" }, + }; + + it("accepts valid result without optional error", () => { + expect(Value.Check(CallResultSchema, valid)).toBe(true); + }); + + it("accepts valid result with error", () => { + const withError: CallResult = { + status: "failed", + output: null, + error: { code: "INTERNAL", message: "Something went wrong" }, + }; + expect(Value.Check(CallResultSchema, withError)).toBe(true); + }); + + it("accepts error with optional details", () => { + const withDetails: CallResult = { + status: "failed", + output: null, + error: { + code: "INTERNAL", + message: "Error", + details: { stack: "..." }, + }, + }; + expect(Value.Check(CallResultSchema, withDetails)).toBe(true); + }); + + it("accepts all valid statuses", () => { + for (const status of [ + "idle", + "waiting", + "ready", + "running", + "completed", + "failed", + "skipped", + "aborted", + ] as const) { + expect(Value.Check(CallResultSchema, { ...valid, status })).toBe(true); + } + }); + + it("rejects missing required status", () => { + expect(Value.Check(CallResultSchema, { output: {} })).toBe(false); + }); + + it("rejects invalid status", () => { + expect( + Value.Check(CallResultSchema, { status: "pending", output: null }), + ).toBe(false); + }); + + it("rejects error with missing required fields", () => { + expect( + Value.Check(CallResultSchema, { + status: "failed", + output: null, + error: { code: "X" }, + }), + ).toBe(false); + }); +}); + +describe("TemplateNodeAttrs type alias", () => { + it("TemplateNodeAttrs is an alias for OperationNodeAttrs type", () => { + const attrs: OperationNodeAttrs = { + name: "classify", + namespace: "task", + version: "1.0.0", + type: "query", + inputSchema: { type: "object" }, + outputSchema: { type: "object" }, + }; + expect(attrs.name).toBe("classify"); + expect(attrs.type).toBe("query"); + }); +}); \ No newline at end of file