chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { resolveDebugConfig } from "../debug";
|
||||
|
||||
describe("resolveDebugConfig", () => {
|
||||
it("returns all-off for undefined", () => {
|
||||
expect(resolveDebugConfig(undefined)).toEqual({
|
||||
enabled: false,
|
||||
events: false,
|
||||
lifecycle: false,
|
||||
verbose: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns all-off for false", () => {
|
||||
expect(resolveDebugConfig(false)).toEqual({
|
||||
enabled: false,
|
||||
events: false,
|
||||
lifecycle: false,
|
||||
verbose: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns events + lifecycle on but verbose off for true (PII safety)", () => {
|
||||
expect(resolveDebugConfig(true)).toEqual({
|
||||
enabled: true,
|
||||
events: true,
|
||||
lifecycle: true,
|
||||
verbose: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("allows explicit verbose opt-in via object shorthand", () => {
|
||||
expect(resolveDebugConfig({ verbose: true })).toEqual({
|
||||
enabled: true,
|
||||
events: true,
|
||||
lifecycle: true,
|
||||
verbose: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("allows explicit verbose opt-in with events", () => {
|
||||
expect(resolveDebugConfig({ events: true, verbose: true })).toEqual({
|
||||
enabled: true,
|
||||
events: true,
|
||||
lifecycle: true,
|
||||
verbose: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults events and lifecycle to true, verbose to false for empty object", () => {
|
||||
expect(resolveDebugConfig({})).toEqual({
|
||||
enabled: true,
|
||||
events: true,
|
||||
lifecycle: true,
|
||||
verbose: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("respects explicit events: true", () => {
|
||||
expect(resolveDebugConfig({ events: true })).toEqual({
|
||||
enabled: true,
|
||||
events: true,
|
||||
lifecycle: true,
|
||||
verbose: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("respects explicit events: false (lifecycle still defaults true)", () => {
|
||||
expect(resolveDebugConfig({ events: false })).toEqual({
|
||||
enabled: true,
|
||||
events: false,
|
||||
lifecycle: true,
|
||||
verbose: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("respects explicit lifecycle: false (events still defaults true)", () => {
|
||||
expect(resolveDebugConfig({ lifecycle: false })).toEqual({
|
||||
enabled: true,
|
||||
events: true,
|
||||
lifecycle: false,
|
||||
verbose: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("sets enabled to false when both events and lifecycle are false", () => {
|
||||
expect(resolveDebugConfig({ events: false, lifecycle: false })).toEqual({
|
||||
enabled: false,
|
||||
events: false,
|
||||
lifecycle: false,
|
||||
verbose: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("clamps verbose to false when enabled is false (events and lifecycle both false)", () => {
|
||||
expect(
|
||||
resolveDebugConfig({ events: false, lifecycle: false, verbose: true }),
|
||||
).toEqual({
|
||||
enabled: false,
|
||||
events: false,
|
||||
lifecycle: false,
|
||||
verbose: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("handles mixed config: events true, lifecycle false, verbose true", () => {
|
||||
expect(
|
||||
resolveDebugConfig({ events: true, lifecycle: false, verbose: true }),
|
||||
).toEqual({
|
||||
enabled: true,
|
||||
events: true,
|
||||
lifecycle: false,
|
||||
verbose: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { createLicenseContextValue } from "../index";
|
||||
import type { RuntimeLicenseStatus } from "../utils/types";
|
||||
|
||||
describe("createLicenseContextValue", () => {
|
||||
it("fails open when no status is known", () => {
|
||||
for (const status of [null, undefined] as const) {
|
||||
const ctx = createLicenseContextValue(status);
|
||||
expect(ctx.status).toBeNull();
|
||||
expect(ctx.license).toBeNull();
|
||||
expect(ctx.checkFeature("chat")).toBe(true);
|
||||
expect(ctx.getLimit("chat")).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["valid", "none", "expiring", "unknown"] as RuntimeLicenseStatus[])(
|
||||
"enables features for %s status",
|
||||
(status) => {
|
||||
const ctx = createLicenseContextValue(status);
|
||||
expect(ctx.status).toBe(status);
|
||||
expect(ctx.checkFeature("chat")).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["expired", "invalid"] as RuntimeLicenseStatus[])(
|
||||
"disables features for %s status",
|
||||
(status) => {
|
||||
const ctx = createLicenseContextValue(status);
|
||||
expect(ctx.status).toBe(status);
|
||||
expect(ctx.checkFeature("chat")).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
import { describe, it, expectTypeOf } from "vitest";
|
||||
import { z } from "zod";
|
||||
import * as v from "valibot";
|
||||
import { type } from "arktype";
|
||||
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
||||
import type { InferSchemaOutput } from "../standard-schema";
|
||||
|
||||
describe("InferSchemaOutput type inference", () => {
|
||||
describe("Zod schemas", () => {
|
||||
it("infers output type from a Zod object schema", () => {
|
||||
const schema = z.object({
|
||||
city: z.string(),
|
||||
count: z.number(),
|
||||
});
|
||||
|
||||
expectTypeOf<InferSchemaOutput<typeof schema>>().toEqualTypeOf<{
|
||||
city: string;
|
||||
count: number;
|
||||
}>();
|
||||
});
|
||||
|
||||
it("infers output type with optional fields", () => {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
age: z.number().optional(),
|
||||
});
|
||||
|
||||
expectTypeOf<InferSchemaOutput<typeof schema>>().toEqualTypeOf<{
|
||||
name: string;
|
||||
age?: number | undefined;
|
||||
}>();
|
||||
});
|
||||
|
||||
it("infers output type with enums", () => {
|
||||
const schema = z.object({
|
||||
unit: z.enum(["celsius", "fahrenheit"]),
|
||||
});
|
||||
|
||||
expectTypeOf<InferSchemaOutput<typeof schema>>().toEqualTypeOf<{
|
||||
unit: "celsius" | "fahrenheit";
|
||||
}>();
|
||||
});
|
||||
|
||||
it("infers output type with nested objects", () => {
|
||||
const schema = z.object({
|
||||
user: z.object({
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
expectTypeOf<InferSchemaOutput<typeof schema>>().toEqualTypeOf<{
|
||||
user: { name: string; email: string };
|
||||
}>();
|
||||
});
|
||||
|
||||
it("infers output type with arrays", () => {
|
||||
const schema = z.object({
|
||||
tags: z.array(z.string()),
|
||||
});
|
||||
|
||||
expectTypeOf<InferSchemaOutput<typeof schema>>().toEqualTypeOf<{
|
||||
tags: string[];
|
||||
}>();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Valibot schemas", () => {
|
||||
it("infers output type from a Valibot object schema", () => {
|
||||
const schema = v.object({
|
||||
query: v.string(),
|
||||
limit: v.number(),
|
||||
});
|
||||
|
||||
expectTypeOf<InferSchemaOutput<typeof schema>>().toEqualTypeOf<{
|
||||
query: string;
|
||||
limit: number;
|
||||
}>();
|
||||
});
|
||||
|
||||
it("infers output type with optional fields", () => {
|
||||
const schema = v.object({
|
||||
name: v.string(),
|
||||
age: v.optional(v.number()),
|
||||
});
|
||||
|
||||
expectTypeOf<InferSchemaOutput<typeof schema>>().toEqualTypeOf<{
|
||||
name: string;
|
||||
age?: number | undefined;
|
||||
}>();
|
||||
});
|
||||
|
||||
it("infers output type with nested objects", () => {
|
||||
const schema = v.object({
|
||||
user: v.object({
|
||||
name: v.string(),
|
||||
email: v.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
expectTypeOf<InferSchemaOutput<typeof schema>>().toEqualTypeOf<{
|
||||
user: { name: string; email: string };
|
||||
}>();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ArkType schemas", () => {
|
||||
it("infers output type from an ArkType object schema", () => {
|
||||
const schema = type({
|
||||
query: "string",
|
||||
limit: "number",
|
||||
});
|
||||
|
||||
expectTypeOf<InferSchemaOutput<typeof schema>>().toEqualTypeOf<{
|
||||
query: string;
|
||||
limit: number;
|
||||
}>();
|
||||
});
|
||||
|
||||
it("infers output type with optional fields", () => {
|
||||
const schema = type({
|
||||
name: "string",
|
||||
"age?": "number",
|
||||
});
|
||||
|
||||
expectTypeOf<InferSchemaOutput<typeof schema>>().toEqualTypeOf<{
|
||||
name: string;
|
||||
age?: number;
|
||||
}>();
|
||||
});
|
||||
});
|
||||
|
||||
describe("StandardSchemaV1 compatibility", () => {
|
||||
it("all schema types satisfy StandardSchemaV1", () => {
|
||||
expectTypeOf(
|
||||
z.object({ a: z.string() }),
|
||||
).toMatchTypeOf<StandardSchemaV1>();
|
||||
expectTypeOf(
|
||||
v.object({ a: v.string() }),
|
||||
).toMatchTypeOf<StandardSchemaV1>();
|
||||
expectTypeOf(type({ a: "string" })).toMatchTypeOf<StandardSchemaV1>();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,359 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { z } from "zod";
|
||||
import * as v from "valibot";
|
||||
import { toStandardJsonSchema } from "@valibot/to-json-schema";
|
||||
import { type } from "arktype";
|
||||
import { zodToJsonSchema } from "zod-to-json-schema";
|
||||
import { schemaToJsonSchema } from "../standard-schema";
|
||||
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
||||
|
||||
describe("schemaToJsonSchema", () => {
|
||||
describe("Zod schemas (via injected zodToJsonSchema)", () => {
|
||||
it("converts a simple zod object schema", () => {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
age: z.number(),
|
||||
});
|
||||
|
||||
const result = schemaToJsonSchema(schema, { zodToJsonSchema });
|
||||
|
||||
expect(result).toHaveProperty("type", "object");
|
||||
expect(result).toHaveProperty("properties.name.type", "string");
|
||||
expect(result).toHaveProperty("properties.age.type", "number");
|
||||
expect(result).toHaveProperty("required");
|
||||
expect(result.required).toContain("name");
|
||||
expect(result.required).toContain("age");
|
||||
});
|
||||
|
||||
it("converts a zod schema with optional fields", () => {
|
||||
const schema = z.object({
|
||||
city: z.string(),
|
||||
units: z.enum(["celsius", "fahrenheit"]).optional(),
|
||||
});
|
||||
|
||||
const result = schemaToJsonSchema(schema, { zodToJsonSchema });
|
||||
|
||||
expect(result).toHaveProperty("type", "object");
|
||||
expect(result).toHaveProperty("properties.city.type", "string");
|
||||
expect(result.required).toContain("city");
|
||||
expect(result.required).not.toContain("units");
|
||||
});
|
||||
|
||||
it("has vendor 'zod' on ~standard", () => {
|
||||
const schema = z.object({ foo: z.string() });
|
||||
expect(schema["~standard"].vendor).toBe("zod");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Valibot schemas (via toStandardJsonSchema wrapper)", () => {
|
||||
it("has vendor 'valibot' on ~standard", () => {
|
||||
const schema = v.object({ name: v.string() });
|
||||
expect(schema["~standard"].vendor).toBe("valibot");
|
||||
});
|
||||
|
||||
it("satisfies StandardSchemaV1", () => {
|
||||
const schema = v.object({ name: v.string() });
|
||||
const _: StandardSchemaV1 = schema;
|
||||
expect(_["~standard"].version).toBe(1);
|
||||
});
|
||||
|
||||
it("raw valibot schema throws (no ~standard.jsonSchema)", () => {
|
||||
const schema = v.object({ name: v.string() });
|
||||
expect(() => schemaToJsonSchema(schema)).toThrow(
|
||||
/Cannot convert schema to JSON Schema/,
|
||||
);
|
||||
});
|
||||
|
||||
it("converts a valibot schema wrapped with toStandardJsonSchema", () => {
|
||||
const schema = toStandardJsonSchema(
|
||||
v.object({
|
||||
name: v.string(),
|
||||
age: v.number(),
|
||||
}),
|
||||
);
|
||||
|
||||
const result = schemaToJsonSchema(schema);
|
||||
|
||||
expect(result).toHaveProperty("type", "object");
|
||||
expect(result).toHaveProperty("properties.name.type", "string");
|
||||
expect(result).toHaveProperty("properties.age.type", "number");
|
||||
expect(result.required).toContain("name");
|
||||
expect(result.required).toContain("age");
|
||||
});
|
||||
|
||||
it("converts a valibot schema with optional fields", () => {
|
||||
const schema = toStandardJsonSchema(
|
||||
v.object({
|
||||
city: v.string(),
|
||||
units: v.optional(v.picklist(["celsius", "fahrenheit"])),
|
||||
}),
|
||||
);
|
||||
|
||||
const result = schemaToJsonSchema(schema);
|
||||
|
||||
expect(result).toHaveProperty("type", "object");
|
||||
expect(result).toHaveProperty("properties.city");
|
||||
expect(result.required).toContain("city");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ArkType schemas (native ~standard.jsonSchema)", () => {
|
||||
it("has vendor 'arktype' on ~standard", () => {
|
||||
const schema = type({ name: "string" });
|
||||
expect(schema["~standard"].vendor).toBe("arktype");
|
||||
});
|
||||
|
||||
it("satisfies StandardSchemaV1", () => {
|
||||
const schema = type({ name: "string" });
|
||||
const _: StandardSchemaV1 = schema;
|
||||
expect(_["~standard"].version).toBe(1);
|
||||
});
|
||||
|
||||
it("natively implements ~standard.jsonSchema", () => {
|
||||
const schema = type({ name: "string" });
|
||||
const props = schema["~standard"];
|
||||
expect(props.jsonSchema).toBeDefined();
|
||||
expect(typeof props.jsonSchema.input).toBe("function");
|
||||
});
|
||||
|
||||
it("converts an arktype schema directly", () => {
|
||||
const schema = type({
|
||||
name: "string",
|
||||
age: "number",
|
||||
});
|
||||
|
||||
const result = schemaToJsonSchema(schema);
|
||||
|
||||
expect(result).toHaveProperty("type", "object");
|
||||
expect(result).toHaveProperty("properties.name.type", "string");
|
||||
expect(result).toHaveProperty("properties.age.type", "number");
|
||||
expect(result.required).toContain("name");
|
||||
expect(result.required).toContain("age");
|
||||
});
|
||||
|
||||
it("converts an arktype schema without needing zodToJsonSchema fallback", () => {
|
||||
const schema = type({ query: "string" });
|
||||
|
||||
// No zodToJsonSchema option needed — arktype uses Standard JSON Schema V1 natively
|
||||
const result = schemaToJsonSchema(schema);
|
||||
|
||||
expect(result).toHaveProperty("type", "object");
|
||||
expect(result).toHaveProperty("properties.query.type", "string");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Standard JSON Schema V1 protocol (mock)", () => {
|
||||
it("uses ~standard.jsonSchema.input() when present", () => {
|
||||
const mockSchema: StandardSchemaV1 & {
|
||||
"~standard": StandardSchemaV1["~standard"] & {
|
||||
jsonSchema: {
|
||||
input: (opts: { target: string }) => Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
} = {
|
||||
"~standard": {
|
||||
version: 1,
|
||||
vendor: "mock-lib",
|
||||
validate: (value: unknown) => ({ value }),
|
||||
jsonSchema: {
|
||||
input: (opts: { target: string }) => ({
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string" },
|
||||
},
|
||||
required: ["query"],
|
||||
$generatedBy: "mock-lib",
|
||||
$target: opts.target,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = schemaToJsonSchema(mockSchema);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: "object",
|
||||
properties: { query: { type: "string" } },
|
||||
required: ["query"],
|
||||
$generatedBy: "mock-lib",
|
||||
$target: "draft-07",
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers ~standard.jsonSchema over zodToJsonSchema fallback", () => {
|
||||
const mockSchema = {
|
||||
"~standard": {
|
||||
version: 1,
|
||||
vendor: "zod",
|
||||
validate: (value: unknown) => ({ value }),
|
||||
jsonSchema: {
|
||||
input: () => ({
|
||||
type: "object",
|
||||
properties: { fromJsonSchema: { type: "boolean" } },
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const zodFallback = () => ({
|
||||
type: "object",
|
||||
properties: { fromZodFallback: { type: "boolean" } },
|
||||
});
|
||||
|
||||
const result = schemaToJsonSchema(mockSchema, {
|
||||
zodToJsonSchema: zodFallback,
|
||||
});
|
||||
|
||||
// Should use ~standard.jsonSchema, not the zod fallback
|
||||
expect(result).toHaveProperty("properties.fromJsonSchema");
|
||||
expect(result).not.toHaveProperty("properties.fromZodFallback");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Zod v4 schemas (via toJSONSchema method)", () => {
|
||||
it("calls toJSONSchema() when the method exists on the schema", () => {
|
||||
const expectedOutput = {
|
||||
type: "object",
|
||||
properties: { name: { type: "string" } },
|
||||
required: ["name"],
|
||||
};
|
||||
|
||||
const mockZod4Schema: StandardSchemaV1 = {
|
||||
"~standard": {
|
||||
version: 1,
|
||||
vendor: "zod",
|
||||
validate: (value: unknown) => ({ value }),
|
||||
},
|
||||
toJSONSchema: () => expectedOutput,
|
||||
} as any;
|
||||
|
||||
const result = schemaToJsonSchema(mockZod4Schema);
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it("uses toJSONSchema() even without zodToJsonSchema option", () => {
|
||||
const mockZod4Schema: StandardSchemaV1 = {
|
||||
"~standard": {
|
||||
version: 1,
|
||||
vendor: "zod",
|
||||
validate: (value: unknown) => ({ value }),
|
||||
},
|
||||
toJSONSchema: () => ({
|
||||
type: "object",
|
||||
properties: { city: { type: "string" } },
|
||||
}),
|
||||
} as any;
|
||||
|
||||
// No options passed — toJSONSchema() should still work
|
||||
const result = schemaToJsonSchema(mockZod4Schema);
|
||||
expect(result).toHaveProperty("properties.city.type", "string");
|
||||
});
|
||||
|
||||
it("prefers toJSONSchema() over zodToJsonSchema fallback for Zod v4", () => {
|
||||
const mockZod4Schema: StandardSchemaV1 = {
|
||||
"~standard": {
|
||||
version: 1,
|
||||
vendor: "zod",
|
||||
validate: (value: unknown) => ({ value }),
|
||||
},
|
||||
toJSONSchema: () => ({
|
||||
type: "object",
|
||||
properties: { fromNative: { type: "boolean" } },
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const zodFallback = () => ({
|
||||
type: "object",
|
||||
properties: { fromFallback: { type: "boolean" } },
|
||||
});
|
||||
|
||||
const result = schemaToJsonSchema(mockZod4Schema, {
|
||||
zodToJsonSchema: zodFallback,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty("properties.fromNative");
|
||||
expect(result).not.toHaveProperty("properties.fromFallback");
|
||||
});
|
||||
|
||||
it("prefers ~standard.jsonSchema over toJSONSchema()", () => {
|
||||
const mockSchema = {
|
||||
"~standard": {
|
||||
version: 1,
|
||||
vendor: "zod",
|
||||
validate: (value: unknown) => ({ value }),
|
||||
jsonSchema: {
|
||||
input: () => ({
|
||||
type: "object",
|
||||
properties: { fromStandard: { type: "boolean" } },
|
||||
}),
|
||||
},
|
||||
},
|
||||
toJSONSchema: () => ({
|
||||
type: "object",
|
||||
properties: { fromToJSONSchema: { type: "boolean" } },
|
||||
}),
|
||||
};
|
||||
|
||||
const result = schemaToJsonSchema(mockSchema);
|
||||
|
||||
// Standard JSON Schema V1 should take priority
|
||||
expect(result).toHaveProperty("properties.fromStandard");
|
||||
expect(result).not.toHaveProperty("properties.fromToJSONSchema");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error handling", () => {
|
||||
it("throws when schema has no jsonSchema support and no zodToJsonSchema", () => {
|
||||
const mockSchema: StandardSchemaV1 = {
|
||||
"~standard": {
|
||||
version: 1,
|
||||
vendor: "unknown-lib",
|
||||
validate: (value: unknown) => ({ value }),
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => schemaToJsonSchema(mockSchema)).toThrow(
|
||||
/Cannot convert schema to JSON Schema/,
|
||||
);
|
||||
expect(() => schemaToJsonSchema(mockSchema)).toThrow(/unknown-lib/);
|
||||
});
|
||||
|
||||
it("throws for non-zod vendor when zodToJsonSchema is not provided", () => {
|
||||
const mockSchema: StandardSchemaV1 = {
|
||||
"~standard": {
|
||||
version: 1,
|
||||
vendor: "some-other-lib",
|
||||
validate: (value: unknown) => ({ value }),
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => schemaToJsonSchema(mockSchema)).toThrow(
|
||||
/no zodToJsonSchema fallback/,
|
||||
);
|
||||
});
|
||||
|
||||
it("uses zodToJsonSchema fallback for zod vendor schemas", () => {
|
||||
const mockZodSchema: StandardSchemaV1 = {
|
||||
"~standard": {
|
||||
version: 1,
|
||||
vendor: "zod",
|
||||
validate: (value: unknown) => ({ value }),
|
||||
},
|
||||
};
|
||||
|
||||
const fallback = () => ({
|
||||
type: "object",
|
||||
properties: { test: { type: "string" } },
|
||||
});
|
||||
|
||||
const result = schemaToJsonSchema(mockZodSchema, {
|
||||
zodToJsonSchema: fallback,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
type: "object",
|
||||
properties: { test: { type: "string" } },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* Regression tests proving that `schemaToJsonSchema` produces identical output
|
||||
* to a direct `zodToJsonSchema` call for all common Zod patterns.
|
||||
*
|
||||
* These tests exist to guarantee that the Standard Schema migration did not
|
||||
* change the JSON Schema output for existing Zod users.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { zodToJsonSchema } from "zod-to-json-schema";
|
||||
import { schemaToJsonSchema } from "../standard-schema";
|
||||
|
||||
/**
|
||||
* Helper: call zodToJsonSchema the same way the old code path did, then
|
||||
* compare with the new schemaToJsonSchema path.
|
||||
*/
|
||||
function expectIdenticalOutput(schema: z.ZodTypeAny) {
|
||||
const direct = zodToJsonSchema(schema, { $refStrategy: "none" });
|
||||
const migrated = schemaToJsonSchema(schema, { zodToJsonSchema });
|
||||
expect(migrated).toEqual(direct);
|
||||
}
|
||||
|
||||
describe("Zod regression — schemaToJsonSchema produces identical output to direct zodToJsonSchema", () => {
|
||||
describe("simple object schemas", () => {
|
||||
it("required string and number fields", () => {
|
||||
expectIdenticalOutput(z.object({ name: z.string(), age: z.number() }));
|
||||
});
|
||||
|
||||
it("optional fields", () => {
|
||||
expectIdenticalOutput(
|
||||
z.object({
|
||||
city: z.string(),
|
||||
units: z.enum(["celsius", "fahrenheit"]).optional(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("empty object", () => {
|
||||
expectIdenticalOutput(z.object({}));
|
||||
});
|
||||
|
||||
it("object with default values", () => {
|
||||
expectIdenticalOutput(
|
||||
z.object({
|
||||
page: z.number().default(1),
|
||||
perPage: z.number().default(20),
|
||||
query: z.string(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("nested objects", () => {
|
||||
it("single level nesting", () => {
|
||||
expectIdenticalOutput(
|
||||
z.object({
|
||||
user: z.object({ name: z.string(), email: z.string() }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("deep nesting", () => {
|
||||
expectIdenticalOutput(
|
||||
z.object({
|
||||
level1: z.object({
|
||||
level2: z.object({
|
||||
level3: z.object({
|
||||
value: z.boolean(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("arrays", () => {
|
||||
it("string array", () => {
|
||||
expectIdenticalOutput(z.object({ tags: z.array(z.string()) }));
|
||||
});
|
||||
|
||||
it("object array", () => {
|
||||
expectIdenticalOutput(
|
||||
z.object({
|
||||
items: z.array(z.object({ id: z.number(), label: z.string() })),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("nested arrays", () => {
|
||||
expectIdenticalOutput(z.object({ matrix: z.array(z.array(z.number())) }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("enums and literals", () => {
|
||||
it("z.enum", () => {
|
||||
expectIdenticalOutput(
|
||||
z.object({ status: z.enum(["active", "inactive", "pending"]) }),
|
||||
);
|
||||
});
|
||||
|
||||
it("z.literal string", () => {
|
||||
expectIdenticalOutput(z.object({ type: z.literal("user") }));
|
||||
});
|
||||
|
||||
it("z.literal number", () => {
|
||||
expectIdenticalOutput(z.object({ version: z.literal(2) }));
|
||||
});
|
||||
|
||||
it("z.nativeEnum", () => {
|
||||
enum Color {
|
||||
Red = "red",
|
||||
Green = "green",
|
||||
Blue = "blue",
|
||||
}
|
||||
expectIdenticalOutput(z.object({ color: z.nativeEnum(Color) }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("unions and discriminated unions", () => {
|
||||
it("z.union of primitives", () => {
|
||||
expectIdenticalOutput(
|
||||
z.object({ value: z.union([z.string(), z.number()]) }),
|
||||
);
|
||||
});
|
||||
|
||||
it("z.union of objects", () => {
|
||||
expectIdenticalOutput(
|
||||
z.object({
|
||||
result: z.union([
|
||||
z.object({ ok: z.literal(true), data: z.string() }),
|
||||
z.object({ ok: z.literal(false), error: z.string() }),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("z.discriminatedUnion", () => {
|
||||
expectIdenticalOutput(
|
||||
z.object({
|
||||
event: z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
type: z.literal("click"),
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
}),
|
||||
z.object({ type: z.literal("keypress"), key: z.string() }),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("nullable and nullish", () => {
|
||||
it("z.nullable", () => {
|
||||
expectIdenticalOutput(z.object({ bio: z.string().nullable() }));
|
||||
});
|
||||
|
||||
it("z.nullish (optional + nullable)", () => {
|
||||
expectIdenticalOutput(z.object({ nickname: z.string().nullish() }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("records and tuples", () => {
|
||||
it("z.record", () => {
|
||||
expectIdenticalOutput(
|
||||
z.object({ metadata: z.record(z.string(), z.unknown()) }),
|
||||
);
|
||||
});
|
||||
|
||||
it("z.tuple", () => {
|
||||
expectIdenticalOutput(
|
||||
z.object({ point: z.tuple([z.number(), z.number()]) }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("intersections", () => {
|
||||
it("z.intersection", () => {
|
||||
expectIdenticalOutput(
|
||||
z.intersection(
|
||||
z.object({ id: z.number() }),
|
||||
z.object({ name: z.string() }),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("string constraints", () => {
|
||||
it("z.string with min/max", () => {
|
||||
expectIdenticalOutput(z.object({ username: z.string().min(3).max(20) }));
|
||||
});
|
||||
|
||||
it("z.string with email", () => {
|
||||
expectIdenticalOutput(z.object({ email: z.string().email() }));
|
||||
});
|
||||
|
||||
it("z.string with url", () => {
|
||||
expectIdenticalOutput(z.object({ website: z.string().url() }));
|
||||
});
|
||||
|
||||
it("z.string with regex", () => {
|
||||
expectIdenticalOutput(
|
||||
z.object({ code: z.string().regex(/^[A-Z]{3}-\d{4}$/) }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("number constraints", () => {
|
||||
it("z.number with min/max", () => {
|
||||
expectIdenticalOutput(z.object({ score: z.number().min(0).max(100) }));
|
||||
});
|
||||
|
||||
it("z.number.int", () => {
|
||||
expectIdenticalOutput(z.object({ count: z.number().int() }));
|
||||
});
|
||||
|
||||
it("z.number.positive", () => {
|
||||
expectIdenticalOutput(z.object({ amount: z.number().positive() }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("catchall and passthrough", () => {
|
||||
it("z.object with catchall", () => {
|
||||
expectIdenticalOutput(z.object({}).catchall(z.string()));
|
||||
});
|
||||
|
||||
it("z.object with passthrough", () => {
|
||||
expectIdenticalOutput(z.object({ id: z.number() }).passthrough());
|
||||
});
|
||||
});
|
||||
|
||||
describe("z.any and z.unknown", () => {
|
||||
it("z.any", () => {
|
||||
expectIdenticalOutput(z.any());
|
||||
});
|
||||
|
||||
it("z.unknown", () => {
|
||||
expectIdenticalOutput(z.unknown());
|
||||
});
|
||||
|
||||
it("object with z.any field", () => {
|
||||
expectIdenticalOutput(z.object({ data: z.any() }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("complex real-world schemas", () => {
|
||||
it("tool-like schema with multiple field types", () => {
|
||||
expectIdenticalOutput(
|
||||
z.object({
|
||||
query: z.string().describe("Search query"),
|
||||
filters: z
|
||||
.object({
|
||||
category: z.enum(["books", "movies", "music"]).optional(),
|
||||
minRating: z.number().min(0).max(5).optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
pagination: z
|
||||
.object({
|
||||
page: z.number().int().positive().default(1),
|
||||
perPage: z.number().int().positive().default(20),
|
||||
})
|
||||
.optional(),
|
||||
sortBy: z.enum(["relevance", "date", "rating"]).default("relevance"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("CopilotKit-like weather tool schema", () => {
|
||||
expectIdenticalOutput(
|
||||
z.object({
|
||||
city: z.string().describe("The city to get weather for"),
|
||||
units: z
|
||||
.enum(["celsius", "fahrenheit"])
|
||||
.optional()
|
||||
.describe("Temperature units"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("agent-like action schema with nested results", () => {
|
||||
expectIdenticalOutput(
|
||||
z.object({
|
||||
action: z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
type: z.literal("search"),
|
||||
query: z.string(),
|
||||
maxResults: z.number().int().optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("navigate"),
|
||||
url: z.string().url(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("execute"),
|
||||
code: z.string(),
|
||||
language: z.enum(["javascript", "python", "shell"]),
|
||||
}),
|
||||
]),
|
||||
context: z.record(z.string(), z.unknown()).optional(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("described schemas (common in tool definitions)", () => {
|
||||
it("preserves .describe() metadata", () => {
|
||||
expectIdenticalOutput(
|
||||
z
|
||||
.object({
|
||||
city: z.string().describe("The city name"),
|
||||
count: z.number().describe("Number of results"),
|
||||
})
|
||||
.describe("Weather tool parameters"),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user