|
|
|
|
@@ -1,6 +1,8 @@
|
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
|
|
|
import { Type, type TSchema } from "@alkdev/typebox";
|
|
|
|
|
import { typeCompat, type TypeCompatResult, type TypeMismatch } from "../../src/analysis/type-compat.js";
|
|
|
|
|
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", () => {
|
|
|
|
|
@@ -411,4 +413,111 @@ describe("typeCompat", () => {
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
});
|