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