Files
flowgraph/test/schema/graph.test.ts

379 lines
9.7 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { Value } from "@alkdev/typebox/value";
import { Type } from "@alkdev/typebox";
import {
SerializedGraph,
OperationGraphSerialized,
type OperationGraphSerialized as OperationGraphSerializedType,
CallGraphSerialized,
type CallGraphSerialized as CallGraphSerializedType,
type FlowGraphSerialized,
} from "../../src/schema/graph";
describe("SerializedGraph", () => {
it("produces a valid TypeBox schema with custom node/edge/graph attrs", () => {
const schema = SerializedGraph(
Type.Object({ label: Type.String() }),
Type.Object({ weight: Type.Number() }),
Type.Object({ name: Type.String() }),
);
const valid = {
attributes: { name: "test-graph" },
options: { type: "directed", multi: false, allowSelfLoops: false },
nodes: [{ key: "a", attributes: { label: "node-a" } }],
edges: [
{ key: "a->b", source: "a", target: "b", attributes: { weight: 1 } },
],
};
expect(Value.Check(schema, valid)).toBe(true);
});
it("rejects missing required fields", () => {
const schema = SerializedGraph(
Type.Object({ label: Type.String() }),
Type.Object({}),
Type.Object({}),
);
expect(Value.Check(schema, {})).toBe(false);
expect(Value.Check(schema, { attributes: {} })).toBe(false);
expect(Value.Check(schema, { attributes: {}, options: {} })).toBe(false);
});
it("rejects wrong options type", () => {
const schema = SerializedGraph(
Type.Object({}),
Type.Object({}),
Type.Object({}),
);
expect(
Value.Check(schema, {
attributes: {},
options: { type: "undirected", multi: false, allowSelfLoops: false },
nodes: [],
edges: [],
}),
).toBe(false);
expect(
Value.Check(schema, {
attributes: {},
options: { type: "directed", multi: true, allowSelfLoops: false },
nodes: [],
edges: [],
}),
).toBe(false);
expect(
Value.Check(schema, {
attributes: {},
options: { type: "directed", multi: false, allowSelfLoops: true },
nodes: [],
edges: [],
}),
).toBe(false);
});
it("rejects invalid node attributes", () => {
const schema = SerializedGraph(
Type.Object({ label: Type.String() }),
Type.Object({}),
Type.Object({}),
);
expect(
Value.Check(schema, {
attributes: {},
options: { type: "directed", multi: false, allowSelfLoops: false },
nodes: [{ key: "a", attributes: { label: 42 } }],
edges: [],
}),
).toBe(false);
});
it("rejects invalid edge attributes", () => {
const schema = SerializedGraph(
Type.Object({}),
Type.Object({ weight: Type.Number() }),
Type.Object({}),
);
expect(
Value.Check(schema, {
attributes: {},
options: { type: "directed", multi: false, allowSelfLoops: false },
nodes: [],
edges: [
{ key: "a->b", source: "a", target: "b", attributes: { weight: "heavy" } },
],
}),
).toBe(false);
});
it("rejects missing edge key/source/target", () => {
const schema = SerializedGraph(
Type.Object({}),
Type.Object({}),
Type.Object({}),
);
expect(
Value.Check(schema, {
attributes: {},
options: { type: "directed", multi: false, allowSelfLoops: false },
nodes: [],
edges: [{ source: "a", target: "b", attributes: {} }],
}),
).toBe(false);
expect(
Value.Check(schema, {
attributes: {},
options: { type: "directed", multi: false, allowSelfLoops: false },
nodes: [],
edges: [{ key: "a->b", target: "b", attributes: {} }],
}),
).toBe(false);
});
it("accepts empty nodes and edges arrays", () => {
const schema = SerializedGraph(
Type.Object({}),
Type.Object({}),
Type.Object({}),
);
expect(
Value.Check(schema, {
attributes: {},
options: { type: "directed", multi: false, allowSelfLoops: false },
nodes: [],
edges: [],
}),
).toBe(true);
});
});
describe("OperationGraphSerialized", () => {
const valid: OperationGraphSerializedType = {
attributes: {},
options: { type: "directed", multi: false, allowSelfLoops: false },
nodes: [
{
key: "task.classify",
attributes: {
name: "classify",
namespace: "task",
version: "1.0.0",
type: "query",
inputSchema: { type: "object" },
outputSchema: { type: "object" },
},
},
],
edges: [
{
key: "task.classify->task.enrich",
source: "task.classify",
target: "task.enrich",
attributes: { compatible: true },
},
],
};
it("accepts valid operation graph serialization", () => {
expect(Value.Check(OperationGraphSerialized, valid)).toBe(true);
});
it("accepts operation graph with empty nodes and edges", () => {
expect(
Value.Check(OperationGraphSerialized, {
attributes: {},
options: { type: "directed", multi: false, allowSelfLoops: false },
nodes: [],
edges: [],
}),
).toBe(true);
});
it("rejects operation node with missing required fields", () => {
expect(
Value.Check(OperationGraphSerialized, {
attributes: {},
options: { type: "directed", multi: false, allowSelfLoops: false },
nodes: [{ key: "x", attributes: { name: "x" } }],
edges: [],
}),
).toBe(false);
});
it("rejects operation edge missing required compatible field", () => {
expect(
Value.Check(OperationGraphSerialized, {
attributes: {},
options: { type: "directed", multi: false, allowSelfLoops: false },
nodes: [],
edges: [
{
key: "a->b",
source: "a",
target: "b",
attributes: {},
},
],
}),
).toBe(false);
});
it("rejects wrong options type", () => {
expect(
Value.Check(OperationGraphSerialized, {
attributes: {},
options: { type: "undirected", multi: false, allowSelfLoops: false },
nodes: [],
edges: [],
}),
).toBe(false);
});
});
describe("CallGraphSerialized", () => {
const valid: CallGraphSerializedType = {
attributes: {},
options: { type: "directed", multi: false, allowSelfLoops: false },
nodes: [
{
key: "req_abc123",
attributes: {
requestId: "req_abc123",
operationId: "task.classify",
status: "pending",
input: { text: "hello" },
},
},
],
edges: [
{
key: "req_abc123->req_def456",
source: "req_abc123",
target: "req_def456",
attributes: {},
},
],
};
it("accepts valid call graph serialization", () => {
expect(Value.Check(CallGraphSerialized, valid)).toBe(true);
});
it("accepts call graph with depends_on edge key convention", () => {
const withDepends: CallGraphSerializedType = {
attributes: {},
options: { type: "directed", multi: false, allowSelfLoops: false },
nodes: [
{
key: "req_abc123",
attributes: {
requestId: "req_abc123",
operationId: "task.classify",
status: "completed",
input: {},
},
},
{
key: "req_def456",
attributes: {
requestId: "req_def456",
operationId: "task.enrich",
status: "running",
input: {},
},
},
],
edges: [
{
key: "req_abc123->req_def456",
source: "req_abc123",
target: "req_def456",
attributes: {},
},
{
key: "req_abc123->req_def456:depends_on",
source: "req_abc123",
target: "req_def456",
attributes: {},
},
],
};
expect(Value.Check(CallGraphSerialized, withDepends)).toBe(true);
});
it("accepts call graph with empty nodes and edges", () => {
expect(
Value.Check(CallGraphSerialized, {
attributes: {},
options: { type: "directed", multi: false, allowSelfLoops: false },
nodes: [],
edges: [],
}),
).toBe(true);
});
it("rejects call node with missing required fields", () => {
expect(
Value.Check(CallGraphSerialized, {
attributes: {},
options: { type: "directed", multi: false, allowSelfLoops: false },
nodes: [{ key: "x", attributes: { requestId: "x" } }],
edges: [],
}),
).toBe(false);
});
it("rejects call node with invalid status", () => {
expect(
Value.Check(CallGraphSerialized, {
attributes: {},
options: { type: "directed", multi: false, allowSelfLoops: false },
nodes: [
{
key: "req_1",
attributes: {
requestId: "req_1",
operationId: "task.x",
status: "idle",
input: {},
},
},
],
edges: [],
}),
).toBe(false);
});
it("rejects wrong options type", () => {
expect(
Value.Check(CallGraphSerialized, {
attributes: {},
options: { type: "undirected", multi: false, allowSelfLoops: false },
nodes: [],
edges: [],
}),
).toBe(false);
});
});
describe("FlowGraphSerialized", () => {
it("FlowGraphSerialized is a type alias for the union", () => {
const op: FlowGraphSerialized = {
attributes: {},
options: { type: "directed", multi: false, allowSelfLoops: false },
nodes: [],
edges: [],
};
expect(op.options.type).toBe("directed");
});
});