feat(schema): add edge attribute schemas and CallResult schema
This commit is contained in:
@@ -1 +1,49 @@
|
|||||||
export {};
|
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<typeof OperationEdgeAttrs>;
|
||||||
|
|
||||||
|
export const TriggeredEdgeAttrs = Type.Object({});
|
||||||
|
export type TriggeredEdgeAttrs = Static<typeof TriggeredEdgeAttrs>;
|
||||||
|
|
||||||
|
export const DependencyEdgeAttrs = Type.Object({});
|
||||||
|
export type DependencyEdgeAttrs = Static<typeof DependencyEdgeAttrs>;
|
||||||
|
|
||||||
|
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<typeof TemplateEdgeAttrs>;
|
||||||
|
|
||||||
|
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<typeof CallResultSchema>;
|
||||||
@@ -11,3 +11,5 @@ export {
|
|||||||
} from "./enums.js";
|
} from "./enums.js";
|
||||||
|
|
||||||
export * from "./node.js";
|
export * from "./node.js";
|
||||||
|
|
||||||
|
export * from "./edge.js";
|
||||||
277
test/schema/edge.test.ts
Normal file
277
test/schema/edge.test.ts
Normal file
@@ -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<string, unknown>) => 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user