Relocate buildTypeEdges from construction.ts to type-compat.ts per architecture spec. construction.ts re-exports it for backward compatibility. Add 10 unit tests for buildTypeEdges covering compatible edges, incompatible edges with mismatches, unknown schema passthrough, incremental construction, and edge deduplication.
523 lines
22 KiB
TypeScript
523 lines
22 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { Type, type TSchema } from "@alkdev/typebox";
|
|
import { typeCompat, buildTypeEdges, type TypeCompatResult, type TypeMismatch } from "../../src/analysis/type-compat.js";
|
|
import { FlowGraph } from "../../src/graph/construction.js";
|
|
import type { OperationSpec } from "../../src/graph/construction.js";
|
|
|
|
describe("typeCompat", () => {
|
|
describe("exact match", () => {
|
|
it("identical string schemas are compatible", () => {
|
|
const result = typeCompat(Type.String(), Type.String());
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(true);
|
|
});
|
|
|
|
it("identical number schemas are compatible", () => {
|
|
const result = typeCompat(Type.Number(), Type.Number());
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(true);
|
|
});
|
|
|
|
it("identical boolean schemas are compatible", () => {
|
|
const result = typeCompat(Type.Boolean(), Type.Boolean());
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(true);
|
|
});
|
|
|
|
it("identical object schemas are compatible", () => {
|
|
const output = Type.Object({ name: Type.String(), age: Type.Number() });
|
|
const input = Type.Object({ name: Type.String(), age: Type.Number() });
|
|
const result = typeCompat(output, input);
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("superset (output has extra fields)", () => {
|
|
it("output with extra fields is still compatible", () => {
|
|
const output = Type.Object({ name: Type.String(), age: Type.Number(), email: Type.String() });
|
|
const input = Type.Object({ name: Type.String(), age: Type.Number() });
|
|
const result = typeCompat(output, input);
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(true);
|
|
expect(result!.detail).toBe("output has extra fields beyond input requirements");
|
|
});
|
|
|
|
it("output with many extra fields is compatible", () => {
|
|
const output = Type.Object({ a: Type.String(), b: Type.Number(), c: Type.Boolean(), d: Type.String() });
|
|
const input = Type.Object({ a: Type.String() });
|
|
const result = typeCompat(output, input);
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("subset (output missing required fields)", () => {
|
|
it("output missing a required field is incompatible", () => {
|
|
const output = Type.Object({ name: Type.String() });
|
|
const input = Type.Object({ name: Type.String(), age: Type.Number() });
|
|
const result = typeCompat(output, input);
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(false);
|
|
expect(result!.mismatches).toBeDefined();
|
|
expect(result!.mismatches!.length).toBeGreaterThan(0);
|
|
const ageMismatch = result!.mismatches!.find((m) => m.path === "/age");
|
|
expect(ageMismatch).toBeDefined();
|
|
expect(ageMismatch!.expected).toBe("number");
|
|
expect(ageMismatch!.actual).toBe("missing");
|
|
});
|
|
|
|
it("output missing multiple required fields reports all mismatches", () => {
|
|
const output = Type.Object({ name: Type.String() });
|
|
const input = Type.Object({ name: Type.String(), age: Type.Number(), email: Type.String() });
|
|
const result = typeCompat(output, input);
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(false);
|
|
const paths = result!.mismatches!.map((m) => m.path);
|
|
expect(paths).toContain("/age");
|
|
expect(paths).toContain("/email");
|
|
});
|
|
});
|
|
|
|
describe("type mismatch", () => {
|
|
it("string output vs number input is incompatible", () => {
|
|
const output = Type.Object({ value: Type.String() });
|
|
const input = Type.Object({ value: Type.Number() });
|
|
const result = typeCompat(output, input);
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(false);
|
|
const mismatch = result!.mismatches!.find((m) => m.path === "/value");
|
|
expect(mismatch).toBeDefined();
|
|
expect(mismatch!.expected).toBe("number");
|
|
expect(mismatch!.actual).toBe("string");
|
|
});
|
|
|
|
it("boolean output vs string input is incompatible", () => {
|
|
const output = Type.Object({ flag: Type.Boolean() });
|
|
const input = Type.Object({ flag: Type.String() });
|
|
const result = typeCompat(output, input);
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("unknown passthrough", () => {
|
|
it("output is Type.Unknown() returns undefined", () => {
|
|
const result = typeCompat(Type.Unknown(), Type.String());
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it("input is Type.Unknown() returns undefined", () => {
|
|
const result = typeCompat(Type.String(), Type.Unknown());
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it("both schemas are Type.Unknown() returns undefined", () => {
|
|
const result = typeCompat(Type.Unknown(), Type.Unknown());
|
|
expect(result).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("nested objects (recursive)", () => {
|
|
it("deeply nested objects with exact match are compatible", () => {
|
|
const output = Type.Object({
|
|
address: Type.Object({
|
|
city: Type.String(),
|
|
zip: Type.String(),
|
|
}),
|
|
});
|
|
const input = Type.Object({
|
|
address: Type.Object({
|
|
city: Type.String(),
|
|
zip: Type.String(),
|
|
}),
|
|
});
|
|
const result = typeCompat(output, input);
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(true);
|
|
});
|
|
|
|
it("nested object with superset fields is compatible", () => {
|
|
const output = Type.Object({
|
|
address: Type.Object({
|
|
city: Type.String(),
|
|
zip: Type.String(),
|
|
country: Type.String(),
|
|
}),
|
|
});
|
|
const input = Type.Object({
|
|
address: Type.Object({
|
|
city: Type.String(),
|
|
zip: Type.String(),
|
|
}),
|
|
});
|
|
const result = typeCompat(output, input);
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(true);
|
|
});
|
|
|
|
it("nested object missing required field is incompatible", () => {
|
|
const output = Type.Object({
|
|
address: Type.Object({
|
|
city: Type.String(),
|
|
}),
|
|
});
|
|
const input = Type.Object({
|
|
address: Type.Object({
|
|
city: Type.String(),
|
|
zip: Type.String(),
|
|
}),
|
|
});
|
|
const result = typeCompat(output, input);
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(false);
|
|
const mismatch = result!.mismatches!.find((m) => m.path === "/address/zip");
|
|
expect(mismatch).toBeDefined();
|
|
});
|
|
|
|
it("nested object with type mismatch reports correct path", () => {
|
|
const output = Type.Object({
|
|
address: Type.Object({
|
|
city: Type.Number(),
|
|
}),
|
|
});
|
|
const input = Type.Object({
|
|
address: Type.Object({
|
|
city: Type.String(),
|
|
}),
|
|
});
|
|
const result = typeCompat(output, input);
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(false);
|
|
const mismatch = result!.mismatches!.find((m) => m.path === "/address/city");
|
|
expect(mismatch).toBeDefined();
|
|
expect(mismatch!.expected).toBe("string");
|
|
expect(mismatch!.actual).toBe("number");
|
|
});
|
|
});
|
|
|
|
describe("arrays", () => {
|
|
it("identical array element types are compatible", () => {
|
|
const result = typeCompat(Type.Array(Type.String()), Type.Array(Type.String()));
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(true);
|
|
});
|
|
|
|
it("array with incompatible element types is not compatible", () => {
|
|
const result = typeCompat(Type.Array(Type.Number()), Type.Array(Type.String()));
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(false);
|
|
expect(result!.mismatches!.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("output array with union element type is not compatible with string array input", () => {
|
|
const result = typeCompat(
|
|
Type.Array(Type.Union([Type.String(), Type.Number()])),
|
|
Type.Array(Type.String()),
|
|
);
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(false);
|
|
});
|
|
|
|
it("output array with string element is compatible with union array input", () => {
|
|
const result = typeCompat(
|
|
Type.Array(Type.String()),
|
|
Type.Array(Type.Union([Type.String(), Type.Number()])),
|
|
);
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("optional fields", () => {
|
|
it("optional field in input that is present in output is compatible", () => {
|
|
const output = Type.Object({ name: Type.String(), email: Type.String() });
|
|
const input = Type.Object({ name: Type.String(), email: Type.Optional(Type.String()) });
|
|
const result = typeCompat(output, input);
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(true);
|
|
});
|
|
|
|
it("optional field in input that is absent in output is compatible", () => {
|
|
const output = Type.Object({ name: Type.String() });
|
|
const input = Type.Object({ name: Type.String(), email: Type.Optional(Type.String()) });
|
|
const result = typeCompat(output, input);
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(true);
|
|
});
|
|
|
|
it("required field in input that is absent in output is not compatible", () => {
|
|
const output = Type.Object({ name: Type.String() });
|
|
const input = Type.Object({ name: Type.String(), age: Type.Number() });
|
|
const result = typeCompat(output, input);
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("union types (subtype checking)", () => {
|
|
it("string output is compatible with string|number input", () => {
|
|
const result = typeCompat(Type.String(), Type.Union([Type.String(), Type.Number()]));
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(true);
|
|
});
|
|
|
|
it("string|number output is NOT compatible with string input", () => {
|
|
const result = typeCompat(Type.Union([Type.String(), Type.Number()]), Type.String());
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(false);
|
|
});
|
|
|
|
it("number output is compatible with string|number input", () => {
|
|
const result = typeCompat(Type.Number(), Type.Union([Type.String(), Type.Number()]));
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(true);
|
|
});
|
|
|
|
it("literal output is compatible with matching union input", () => {
|
|
const result = typeCompat(Type.Literal("hello"), Type.Union([Type.Literal("hello"), Type.Literal("world")]));
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(true);
|
|
});
|
|
|
|
it("literal output is NOT compatible with non-matching union input", () => {
|
|
const result = typeCompat(Type.Literal("other"), Type.Union([Type.Literal("hello"), Type.Literal("world")]));
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("literal types", () => {
|
|
it("identical string literals are compatible", () => {
|
|
const result = typeCompat(Type.Literal("hello"), Type.Literal("hello"));
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(true);
|
|
});
|
|
|
|
it("different string literals are not compatible", () => {
|
|
const result = typeCompat(Type.Literal("hello"), Type.Literal("world"));
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(false);
|
|
});
|
|
|
|
it("string literal output is compatible with string input", () => {
|
|
const result = typeCompat(Type.Literal("hello"), Type.String());
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(true);
|
|
});
|
|
|
|
it("number literal output is compatible with number input", () => {
|
|
const result = typeCompat(Type.Literal(42), Type.Number());
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("integer and number compatibility", () => {
|
|
it("integer output is compatible with number input", () => {
|
|
const result = typeCompat(Type.Integer(), Type.Number());
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(true);
|
|
});
|
|
|
|
it("number output is NOT compatible with integer input", () => {
|
|
const result = typeCompat(Type.Number(), Type.Integer());
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("complex realistic schemas", () => {
|
|
it("realistic operation schemas - classify output compatible with enrich input", () => {
|
|
const classifyOutput = Type.Object({
|
|
categories: Type.Array(Type.String()),
|
|
confidence: Type.Number(),
|
|
metadata: Type.Optional(Type.Object({
|
|
model: Type.String(),
|
|
version: Type.String(),
|
|
})),
|
|
});
|
|
const enrichInput = Type.Object({
|
|
categories: Type.Array(Type.String()),
|
|
confidence: Type.Number(),
|
|
});
|
|
const result = typeCompat(classifyOutput, enrichInput);
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(true);
|
|
});
|
|
|
|
it("realistic operation schemas - incompatible due to field type", () => {
|
|
const fetchOutput = Type.Object({
|
|
items: Type.Array(Type.Object({ id: Type.String(), name: Type.String() })),
|
|
total: Type.Number(),
|
|
});
|
|
const processInput = Type.Object({
|
|
items: Type.Array(Type.Object({ id: Type.Number(), name: Type.String() })),
|
|
total: Type.Number(),
|
|
});
|
|
const result = typeCompat(fetchOutput, processInput);
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(false);
|
|
const mismatch = result!.mismatches!.find((m) => m.path.includes("id"));
|
|
expect(mismatch).toBeDefined();
|
|
});
|
|
|
|
it("multiple mismatches at different depths", () => {
|
|
const output = Type.Object({
|
|
name: Type.Number(),
|
|
address: Type.Object({
|
|
city: Type.Number(),
|
|
zip: Type.Boolean(),
|
|
}),
|
|
});
|
|
const input = Type.Object({
|
|
name: Type.String(),
|
|
address: Type.Object({
|
|
city: Type.String(),
|
|
zip: Type.String(),
|
|
}),
|
|
});
|
|
const result = typeCompat(output, input);
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(false);
|
|
expect(result!.mismatches!.length).toBeGreaterThanOrEqual(3);
|
|
});
|
|
});
|
|
|
|
describe("TypeCompatResult shape", () => {
|
|
it("compatible result has no mismatches", () => {
|
|
const result = typeCompat(Type.String(), Type.String());
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(true);
|
|
expect(result!.mismatches).toBeUndefined();
|
|
});
|
|
|
|
it("incompatible result has mismatches array", () => {
|
|
const result = typeCompat(Type.String(), Type.Number());
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(false);
|
|
expect(Array.isArray(result!.mismatches)).toBe(true);
|
|
expect(result!.mismatches!.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("TypeMismatch has correct shape", () => {
|
|
const result = typeCompat(
|
|
Type.Object({ value: Type.String() }),
|
|
Type.Object({ value: Type.Number() }),
|
|
);
|
|
expect(result).toBeDefined();
|
|
expect(result!.compatible).toBe(false);
|
|
const mismatch: TypeMismatch = result!.mismatches![0]!;
|
|
expect(typeof mismatch.path).toBe("string");
|
|
expect(typeof mismatch.expected).toBe("string");
|
|
expect(typeof mismatch.actual).toBe("string");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("buildTypeEdges", () => {
|
|
it("adds compatible edges for matching output→input schemas", () => {
|
|
const fg = new FlowGraph();
|
|
fg.addOperation({ name: "extract", namespace: "task", version: "1.0.0", type: "query", inputSchema: Type.Object({ raw: Type.String() }), outputSchema: Type.Object({ text: Type.String() }) });
|
|
fg.addOperation({ name: "classify", namespace: "task", version: "1.0.0", type: "query", inputSchema: Type.Object({ text: Type.String() }), outputSchema: Type.Object({ label: Type.String(), score: Type.Number() }) });
|
|
buildTypeEdges(fg);
|
|
expect(fg.hasEdge("task.extract", "task.classify")).toBe(true);
|
|
const attrs = fg.getEdgeAttributes("task.extract", "task.classify") as Record<string, unknown>;
|
|
expect(attrs.edgeType).toBe("typed");
|
|
expect(attrs.compatible).toBe(true);
|
|
});
|
|
|
|
it("adds incompatible edges when schemas mismatch", () => {
|
|
const fg = new FlowGraph();
|
|
fg.addOperation({ name: "classify", namespace: "task", version: "1.0.0", type: "query", inputSchema: Type.Object({ text: Type.String() }), outputSchema: Type.Object({ label: Type.String(), score: Type.Number() }) });
|
|
fg.addOperation({ name: "count", namespace: "task", version: "1.0.0", type: "query", inputSchema: Type.Object({ count: Type.Number() }), outputSchema: Type.Object({ result: Type.Number() }) });
|
|
buildTypeEdges(fg);
|
|
expect(fg.hasEdge("task.classify", "task.count")).toBe(true);
|
|
const attrs = fg.getEdgeAttributes("task.classify", "task.count") as Record<string, unknown>;
|
|
expect(attrs.edgeType).toBe("typed");
|
|
expect(attrs.compatible).toBe(false);
|
|
expect(attrs.mismatches).toBeDefined();
|
|
});
|
|
|
|
it("incompatible edges include mismatches array", () => {
|
|
const fg = new FlowGraph();
|
|
fg.addOperation({ name: "a", namespace: "op", version: "1.0.0", type: "query", inputSchema: Type.Object({ x: Type.String() }), outputSchema: Type.Object({ value: Type.String() }) });
|
|
fg.addOperation({ name: "b", namespace: "op", version: "1.0.0", type: "query", inputSchema: Type.Object({ value: Type.Number() }), outputSchema: Type.Object({ z: Type.Boolean() }) });
|
|
buildTypeEdges(fg);
|
|
const attrs = fg.getEdgeAttributes("op.a", "op.b") as Record<string, unknown>;
|
|
expect(attrs.compatible).toBe(false);
|
|
expect(Array.isArray(attrs.mismatches)).toBe(true);
|
|
expect((attrs.mismatches as Array<unknown>).length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("does not add edges when either schema is Unknown", () => {
|
|
const fg = new FlowGraph();
|
|
fg.addOperation({ name: "unk_out", namespace: "op", version: "1.0.0", type: "query", inputSchema: Type.Object({ x: Type.String() }), outputSchema: Type.Unknown() });
|
|
fg.addOperation({ name: "unk_in", namespace: "op", version: "1.0.0", type: "query", inputSchema: Type.Unknown(), outputSchema: Type.Object({ y: Type.String() }) });
|
|
fg.addOperation({ name: "normal", namespace: "op", version: "1.0.0", type: "query", inputSchema: Type.Object({ y: Type.String() }), outputSchema: Type.Object({ x: Type.String() }) });
|
|
buildTypeEdges(fg);
|
|
expect(fg.hasEdge("op.unk_out", "op.unk_in")).toBe(false);
|
|
expect(fg.hasEdge("op.unk_out", "op.normal")).toBe(false);
|
|
expect(fg.hasEdge("op.normal", "op.unk_in")).toBe(false);
|
|
});
|
|
|
|
it("sets detail to namespace.name.output → namespace.name.input for compatible edges", () => {
|
|
const fg = new FlowGraph();
|
|
fg.addOperation({ name: "extract", namespace: "task", version: "1.0.0", type: "query", inputSchema: Type.Object({ raw: Type.String() }), outputSchema: Type.Object({ text: Type.String() }) });
|
|
fg.addOperation({ name: "classify", namespace: "task", version: "1.0.0", type: "query", inputSchema: Type.Object({ text: Type.String() }), outputSchema: Type.Object({ label: Type.String() }) });
|
|
buildTypeEdges(fg);
|
|
const attrs = fg.getEdgeAttributes("task.extract", "task.classify") as Record<string, unknown>;
|
|
expect(attrs.detail).toContain("task.extract.output → task.classify.input");
|
|
});
|
|
|
|
it("is callable after incremental addOperation calls", () => {
|
|
const fg = new FlowGraph();
|
|
fg.addOperation({ name: "extract", namespace: "op", version: "1.0.0", type: "query", inputSchema: Type.Object({ raw: Type.String() }), outputSchema: Type.Object({ text: Type.String() }) });
|
|
buildTypeEdges(fg);
|
|
expect(fg.size).toBe(0);
|
|
fg.addOperation({ name: "classify", namespace: "op", version: "1.0.0", type: "query", inputSchema: Type.Object({ text: Type.String() }), outputSchema: Type.Object({ label: Type.String() }) });
|
|
buildTypeEdges(fg);
|
|
expect(fg.hasEdge("op.extract", "op.classify")).toBe(true);
|
|
});
|
|
|
|
it("produces edges for three operations in a pipeline", () => {
|
|
const fg = new FlowGraph();
|
|
fg.addOperation({ name: "extract", namespace: "task", version: "1.0.0", type: "query", inputSchema: Type.Object({ raw: Type.String() }), outputSchema: Type.Object({ text: Type.String() }) });
|
|
fg.addOperation({ name: "classify", namespace: "task", version: "1.0.0", type: "query", inputSchema: Type.Object({ text: Type.String() }), outputSchema: Type.Object({ label: Type.String() }) });
|
|
fg.addOperation({ name: "enrich", namespace: "task", version: "1.0.0", type: "query", inputSchema: Type.Object({ label: Type.String() }), outputSchema: Type.Object({ enriched: Type.String() }) });
|
|
buildTypeEdges(fg);
|
|
expect(fg.hasEdge("task.extract", "task.classify")).toBe(true);
|
|
expect(fg.hasEdge("task.classify", "task.enrich")).toBe(true);
|
|
expect(fg.hasEdge("task.extract", "task.enrich")).toBe(true);
|
|
const e2c = fg.getEdgeAttributes("task.extract", "task.classify") as Record<string, unknown>;
|
|
const c2e = fg.getEdgeAttributes("task.classify", "task.enrich") as Record<string, unknown>;
|
|
const e2e = fg.getEdgeAttributes("task.extract", "task.enrich") as Record<string, unknown>;
|
|
expect(e2c.compatible).toBe(true);
|
|
expect(c2e.compatible).toBe(true);
|
|
expect(e2e.compatible).toBe(false);
|
|
});
|
|
|
|
it("does not add self-loops", () => {
|
|
const fg = new FlowGraph();
|
|
fg.addOperation({ name: "a", namespace: "op", version: "1.0.0", type: "query", inputSchema: Type.Object({ x: Type.String() }), outputSchema: Type.Object({ x: Type.String() }) });
|
|
buildTypeEdges(fg);
|
|
expect(fg.size).toBe(0);
|
|
});
|
|
|
|
it("returns empty graph with no edges for empty graph", () => {
|
|
const fg = new FlowGraph();
|
|
buildTypeEdges(fg);
|
|
expect(fg.order).toBe(0);
|
|
expect(fg.size).toBe(0);
|
|
});
|
|
|
|
it("skips edges that would already exist", () => {
|
|
const fg = new FlowGraph();
|
|
fg.addOperation({ name: "a", namespace: "op", version: "1.0.0", type: "query", inputSchema: Type.Object({ x: Type.String() }), outputSchema: Type.Object({ y: Type.String() }) });
|
|
fg.addOperation({ name: "b", namespace: "op", version: "1.0.0", type: "query", inputSchema: Type.Object({ y: Type.String() }), outputSchema: Type.Object({ z: Type.String() }) });
|
|
buildTypeEdges(fg);
|
|
const sizeAfterFirst = fg.size;
|
|
buildTypeEdges(fg);
|
|
expect(fg.size).toBe(sizeAfterFirst);
|
|
});
|
|
}); |