379 lines
9.7 KiB
TypeScript
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");
|
|
});
|
|
}); |