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";
|
||||
|
||||
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