import { describe, expect, test } from "bun:test" import { Effect, Schema, SchemaGetter } from "effect" import { CodeMode, Tool } from "./lib/index.js" import { decodeInput, inputProperties, inputTypeScript, jsonSchemaToTypeScript, outputTypeScript, } from "../src/tool-schema.js" // A raw JSON Schema tool in the shape an MCP adapter produces: render-only input schema // whose property descriptions and constraints must surface as JSDoc in pretty signatures. const listIssues = Tool.make({ description: "List issues in a repository", input: { type: "object", properties: { owner: { type: "string", description: "Repository owner" }, after: { type: "string", description: "Cursor from the previous response's pageInfo" }, perPage: { type: "number", description: "Results per page", default: 30 }, labels: { type: "array", items: { type: "string" }, description: "Filter by labels", minItems: 1, maxItems: 10 }, state: { type: "string", enum: ["open", "closed"] }, }, required: ["owner"], }, output: {}, execute: () => Effect.succeed("[]"), }) // An Effect Schema tool whose field annotations must flow through the emitted JSON Schema. const lookupOrder = Tool.make({ description: "Look up an order", input: Schema.Struct({ id: Schema.String.annotate({ description: "Order identifier" }), verbose: Schema.optionalKey(Schema.Boolean), }), output: Schema.Struct({ status: Schema.String.annotate({ description: "Current order status" }), }), execute: () => Effect.succeed({ status: "open" }), }) describe("pretty signature rendering", () => { test("described fields get compact JSDoc; undescribed and unconstrained fields get none", () => { expect(inputTypeScript(listIssues, true)).toBe( [ "{", " /** Repository owner */", " owner: string,", " /** Cursor from the previous response's pageInfo */", " after?: string,", " /** Results per page. @default 30 */", " perPage?: number,", " /** Filter by labels. @minItems 1 @maxItems 10 */", " labels?: Array,", ' state?: "open" | "closed",', "}", ].join("\n"), ) }) test("compact mode output is unchanged by the pretty machinery", () => { expect(inputTypeScript(listIssues)).toBe( '{ owner: string; after?: string; perPage?: number; labels?: Array; state?: "open" | "closed" }', ) expect(inputTypeScript(lookupOrder)).toBe("{ id: string; verbose?: boolean }") expect(outputTypeScript(lookupOrder)).toBe("{ status: string }") }) test("nested objects recurse with increasing indent and their own JSDoc", () => { const pretty = jsonSchemaToTypeScript( { type: "object", properties: { filter: { type: "object", description: "Search filter", properties: { state: { type: "string", description: "Issue state" } }, }, }, }, true, ) expect(pretty).toBe( [ "{", " /** Search filter */", " filter?: {", " /** Issue state */", " state?: string,", " },", "}", ].join("\n"), ) }) test("Effect Schema annotations become JSDoc on input and output fields", () => { expect(inputTypeScript(lookupOrder, true)).toBe( ["{", " /** Order identifier */", " id: string,", " verbose?: boolean,", "}"].join("\n"), ) expect(outputTypeScript(lookupOrder, true)).toBe( ["{", " /** Current order status */", " status: string,", "}"].join("\n"), ) }) test("constraints and annotations share compact tagged JSDoc", () => { const pretty = jsonSchemaToTypeScript( { type: "object", properties: { legacy: { type: "string", deprecated: true }, homepage: { type: "string", format: "uri" }, tags: { type: "array", items: { type: "string" }, minItems: 2, maxItems: 5, default: ["a", "b"] }, }, }, true, ) expect(pretty).toContain(" /** @deprecated */\n legacy?: string") expect(pretty).toContain(" /** @format uri */\n homepage?: string") expect(pretty).toContain(' /** @default ["a","b"] @minItems 2 @maxItems 5 */\n tags?: Array') }) test("skips an unserializable default rather than emitting a broken summary", () => { const pretty = jsonSchemaToTypeScript( { type: "object", properties: { size: { type: "number", default: 1n } } }, true, ) expect(pretty).toBe(["{", " size?: number,", "}"].join("\n")) }) test("labels immediate item and dictionary value metadata above the field", () => { const schema = { properties: { recipients: { type: "array", description: "People to notify", minItems: 1, items: { type: "string", description: "Email address", format: "email" }, }, scores: { type: "object", additionalProperties: { type: "number", minimum: 0 } }, names: { type: "array", items: { type: "string", description: "Display name" } }, plain: { type: "object", additionalProperties: { type: "string" } }, }, } expect(jsonSchemaToTypeScript(schema, true)).toBe( [ "{", " /**", " * People to notify. @minItems 1", " * Each item: Email address. @format email", " */", " recipients?: Array,", " /** Each value: @minimum 0 */", " scores?: {", " [key: string]: number,", " },", " /** Each item: Display name */", " names?: Array,", " plain?: {", " [key: string]: string,", " },", "}", ].join("\n"), ) expect(jsonSchemaToTypeScript(schema)).toBe( "{ recipients?: Array; scores?: { [key: string]: number }; names?: Array; plain?: { [key: string]: string } }", ) }) test("does not apply additional property metadata to named properties", () => { expect( jsonSchemaToTypeScript( { properties: { scores: { type: "object", properties: { title: { type: "string", description: "Score title" } }, additionalProperties: { type: "number", minimum: 0 }, }, }, }, true, ), ).toBe( [ "{", " /** Each additional value: @minimum 0 */", " scores?: {", " /** Score title */", " title?: string,", " [key: string]: number,", " },", "}", ].join("\n"), ) }) test("labels array and dictionary contents in type-array unions", () => { expect( jsonSchemaToTypeScript( { properties: { recipients: { type: ["array", "null"], items: { type: "string", format: "email" } }, scores: { type: ["object", "null"], additionalProperties: { type: "number", minimum: 0 } }, either: { type: ["array", "object"], items: { type: "string", minLength: 1 }, additionalProperties: { type: "number", minimum: 0 }, }, }, }, true, ), ).toBe( [ "{", " /** Each item: @format email */", " recipients?: Array | null,", " /** Each value: @minimum 0 */", " scores?: {", " [key: string]: number,", " } | null,", " /**", " * Each item: @minLength 1", " * Each value: @minimum 0", " */", " either?: Array | {", " [key: string]: number,", " },", "}", ].join("\n"), ) }) test("keeps multiline item metadata indented under its label and escapes terminators", () => { expect( jsonSchemaToTypeScript( { properties: { values: { type: "array", minItems: 1, items: { type: "string", description: "\nFirst */ line\n\nSecond line\n", pattern: "^\\d+\n*/$" }, }, }, }, true, ), ).toBe( [ "{", " /**", " * @minItems 1", " * Each item: First * / line", " *", " * Second line", " * @pattern ^\\d+", " * * /$", " */", " values?: Array,", "}", ].join("\n"), ) }) test("does not flatten nested containers, reference targets, or branches into item metadata", () => { expect( jsonSchemaToTypeScript( { $defs: { Email: { type: "string", format: "email" } }, properties: { matrix: { type: "array", items: { type: "array", minItems: 2, items: { type: "integer", minimum: 0 } } }, refs: { type: "array", items: { $ref: "#/$defs/Email", description: "Recipient" } }, choices: { type: "array", items: { anyOf: [ { type: "string", minLength: 1 }, { type: "number", minimum: 0 }, ], }, }, }, }, true, ), ).toBe( [ "{", " /** Each item: @minItems 2 */", " matrix?: Array>,", " /** Each item: Recipient */", " refs?: Array,", " choices?: Array,", "}", ].join("\n"), ) }) test.each([ [{ type: "number", minimum: 0 }, "@minimum 0", "number"], [{ type: "number", maximum: 0 }, "@maximum 0", "number"], [{ type: "number", exclusiveMinimum: 0 }, "@exclusiveMinimum 0", "number"], [{ type: "number", exclusiveMaximum: 0 }, "@exclusiveMaximum 0", "number"], [{ type: "number", multipleOf: 0.25 }, "@multipleOf 0.25", "number"], [{ type: "string", minLength: 0 }, "@minLength 0", "string"], [{ type: "string", maxLength: 0 }, "@maxLength 0", "string"], [{ type: "string", pattern: "^[a-z]+$" }, "@pattern ^[a-z]+$", "string"], [{ type: "array", minItems: 0 }, "@minItems 0", "Array"], [{ type: "array", maxItems: 0 }, "@maxItems 0", "Array"], [{ type: "array", uniqueItems: true }, "@uniqueItems true", "Array"], [{ type: "string", minLength: 0, maxLength: 0 }, "@minLength 0 @maxLength 0", "string"], [{ type: "array", minItems: 0, maxItems: 0 }, "@minItems 0 @maxItems 0", "Array"], [ { type: "array", minItems: 1, maxItems: 10, uniqueItems: true }, "@minItems 1 @maxItems 10 @uniqueItems true", "Array", ], ] as const)("renders constraint %j without changing the compact type", (value, summary, type) => { const schema = { type: "object", properties: { value } } expect(jsonSchemaToTypeScript(schema, true)).toBe( ["{", ` /** ${summary} */`, ` value?: ${type},`, "}"].join("\n"), ) expect(jsonSchemaToTypeScript(schema)).toBe(`{ value?: ${type} }`) }) test("documents integer numbers without adding redundant types or requiring uniqueness when false", () => { expect( jsonSchemaToTypeScript( { type: "object", properties: { count: { type: "integer" }, amount: { type: "number" }, name: { type: "string" }, enabled: { type: "boolean" }, values: { type: "array", uniqueItems: false }, choice: { type: ["integer", "string"] }, }, }, true, ), ).toBe( [ "{", " /** @integer */", " count?: number,", " amount?: number,", " name?: string,", " enabled?: boolean,", " values?: Array,", " choice?: number | string,", "}", ].join("\n"), ) }) test.each([false, null, ""])("preserves default %j alongside constraints", (value) => { expect(jsonSchemaToTypeScript({ properties: { value: { default: value, minLength: 0 } } }, true)).toContain( ` /** @default ${JSON.stringify(value)} @minLength 0 */\n`, ) }) test("escapes comment terminators in summary values", () => { expect( jsonSchemaToTypeScript( { properties: { value: { type: "string", default: "*/", format: "*/", pattern: "^a*/b$" } } }, true, ), ).toBe(["{", ' /** @default "* /" @format * / @pattern ^a* /b$ */', " value?: string,", "}"].join("\n")) }) test("neutralizes */ inside descriptions so nothing closes the comment early", () => { const pretty = jsonSchemaToTypeScript( { type: "object", properties: { note: { type: "string", description: "Ends */ early" } } }, true, ) expect(pretty).toContain(" /** Ends * / early */") expect(pretty).not.toContain("Ends */") }) test("multiline descriptions become *-prefixed blocks with blank edges trimmed", () => { const pretty = jsonSchemaToTypeScript( { type: "object", properties: { query: { type: "string", description: "\nFirst line\n\nSecond line\n" } }, }, true, ) expect(pretty).toBe( ["{", " /**", " * First line", " *", " * Second line", " */", " query?: string,", "}"].join("\n"), ) }) test("preserves inclusive and exclusive numeric bounds together", () => { expect( jsonSchemaToTypeScript( { properties: { value: { type: "integer", minimum: -10, maximum: 10, exclusiveMinimum: -5, exclusiveMaximum: 5, multipleOf: 2, }, }, }, true, ), ).toContain( " /** @integer @minimum -10 @maximum 10 @exclusiveMinimum -5 @exclusiveMaximum 5 @multipleOf 2 */\n value?: number,", ) }) test.each([ ["Maximum attempts", "Maximum attempts."], ["Maximum attempts.", "Maximum attempts."], ["Maximum attempts!", "Maximum attempts!."], ])("combines a short description (%s) with its summary", (description, expected) => { expect( jsonSchemaToTypeScript( { properties: { attempts: { description, type: "integer", minimum: 1, default: 3 } } }, true, ), ).toContain(` /** ${expected} @default 3 @integer @minimum 1 */\n`) }) test("keeps multiline descriptions intact and appends a compact summary", () => { expect( jsonSchemaToTypeScript( { properties: { attempts: { description: "\nMaximum attempts\n\nIncludes the initial request.\n", type: "integer", minimum: 1, }, }, }, true, ), ).toBe( [ "{", " /**", " * Maximum attempts", " *", " * Includes the initial request.", " * @integer @minimum 1", " */", " attempts?: number,", "}", ].join("\n"), ) }) test("uses a block for long descriptions without truncating or rewriting them", () => { const description = "A detailed description. ".repeat(8).trim() expect(jsonSchemaToTypeScript({ properties: { name: { type: "string", description, minLength: 1 } } }, true)).toBe( ["{", " /**", ` * ${description}`, " * @minLength 1", " */", " name?: string,", "}"].join("\n"), ) }) test("preserves pattern backslashes and prefixes every line of multiline summary values", () => { expect( jsonSchemaToTypeScript( { properties: { value: { type: "string", pattern: "^\\d+\n*/$", default: "a\nb" } } }, true, ), ).toBe( ["{", " /**", ' * @default "a\\nb" @pattern ^\\d+', " * * /$", " */", " value?: string,", "}"].join("\n"), ) }) test("stays total on cyclic $refs and pathological nesting in both modes", () => { const cyclic = { $ref: "#/$defs/Node", $defs: { Node: { type: "object", properties: { child: { $ref: "#/$defs/Node" }, name: { type: "string" } } } }, } as const expect(jsonSchemaToTypeScript(cyclic)).toBe("{ child?: unknown; name?: string }") expect(jsonSchemaToTypeScript(cyclic, true)).toContain("child?: unknown") let deep: Record = { type: "string" } for (let level = 0; level < 12; level += 1) deep = { type: "object", properties: { next: deep } } for (const pretty of [false, true]) { const rendered = jsonSchemaToTypeScript(deep, pretty) expect(rendered).toContain("unknown") expect(rendered).toContain("next?:") } }) test("intersects ref and union siblings instead of discarding them", () => { expect( jsonSchemaToTypeScript({ $ref: "#/$defs/User", properties: { active: { type: "boolean" } }, required: ["active"], $defs: { User: { type: "object", properties: { id: { type: "string" } }, required: ["id"] }, }, }), ).toBe("{ id: string } & { active: boolean }") expect( jsonSchemaToTypeScript({ type: "object", properties: { common: { type: "boolean" } }, required: ["common"], anyOf: [ { type: "object", properties: { name: { type: "string" } }, required: ["name"] }, { type: "object", properties: { count: { type: "number" } }, required: ["count"] }, ], }), ).toBe("({ name: string } | { count: number }) & { common: boolean }") expect(jsonSchemaToTypeScript({ $ref: "https://example.com/schema.json" })).toBe("unknown") expect( jsonSchemaToTypeScript({ $ref: "#/$defs/User/properties/id", $defs: { User: { type: "object" }, id: { type: "string" } }, }), ).toBe("unknown") expect( jsonSchemaToTypeScript({ type: ["object", "null"], properties: { name: { type: "string" } }, }), ).toBe("{ name?: string } | null") }) }) describe("JSON Schema definition scope", () => { test.each(["Lookup Input", "café", "A/B~C% D#E"])( "resolves Effect definition %s in signatures and input metadata", (identifier) => { const tool = Tool.make({ description: "Named tool", input: Schema.Struct({ city: Schema.String }).annotate({ identifier }), output: Schema.Struct({ city: Schema.String }).annotate({ identifier }), execute: (input) => Effect.succeed(input), }) expect(inputTypeScript(tool)).toBe("{ city: string }") expect(outputTypeScript(tool)).toBe("{ city: string }") expect(inputProperties(tool)).toEqual([{ name: "city", description: undefined, required: true }]) }, ) test.each(["#/$defs/%", "#/$defs/A~2B", "#/$defs/Missing", "#/$defs/Loop", "https://example.test/schema"])( "keeps malformed, unresolved, and recursive reference %s unknown", ($ref) => { const tool = Tool.make({ description: "Unresolved tool", input: { $ref, $defs: { Loop: { $ref: "#/$defs/Loop" } } }, execute: () => Effect.succeed(null), }) expect(inputTypeScript(tool)).toBe("unknown") expect(inputProperties(tool)).toEqual([]) }, ) test.each(["definitions", "$defs"])("resolves root %s and lets $defs take precedence", (key) => { const schema = { $ref: `#/${key}/Value`, [key]: { Value: { type: "string" } } } expect(jsonSchemaToTypeScript(schema)).toBe("string") expect(jsonSchemaToTypeScript(schema, true)).toBe("string") const overridden = { ...schema, $defs: { Value: { type: "number" } } } expect(jsonSchemaToTypeScript(overridden)).toBe("number") expect(jsonSchemaToTypeScript(overridden, true)).toBe("number") }) test.each(["definitions", "$defs"])("nested %s shadow inherited definitions without affecting siblings", (key) => { const schema = { type: "object", definitions: { Inherited: { type: "string" } }, $defs: { Value: { type: "number" } }, properties: { nested: { $ref: `#/${key}/Value`, [key]: { Value: { type: "boolean" } } }, inherited: { $ref: "#/definitions/Inherited" }, sibling: { $ref: "#/$defs/Value" }, }, } expect(jsonSchemaToTypeScript(schema)).toBe("{ nested?: boolean; inherited?: string; sibling?: number }") expect(jsonSchemaToTypeScript(schema, true)).toBe( ["{", " nested?: boolean,", " inherited?: string,", " sibling?: number,", "}"].join("\n"), ) }) }) describe("non-identifier property names render as quoted keys", () => { // MCP-style schemas routinely carry property names that are not bare TS identifiers // (`foo-bar`, `@type`, dotted names); the rendered signature must quote them so the // model sees a valid TypeScript object type. Bare identifiers stay unquoted. const rawSchema = { type: "object", properties: { "foo-bar": { type: "string" }, "@type": { type: "string" }, "x.y": { type: "number", description: "Dotted name" }, "123": { type: "number" }, plain: { type: "boolean" }, }, required: ["@type"], } as const test("compact rendering quotes non-identifier keys and leaves identifiers bare", () => { expect(jsonSchemaToTypeScript(rawSchema)).toBe( '{ "123"?: number; "foo-bar"?: string; "@type": string; "x.y"?: number; plain?: boolean }', ) }) test("pretty rendering quotes non-identifier keys and keeps their JSDoc", () => { expect(jsonSchemaToTypeScript(rawSchema, true)).toBe( [ "{", ' "123"?: number,', ' "foo-bar"?: string,', ' "@type": string,', " /** Dotted name */", ' "x.y"?: number,', " plain?: boolean,", "}", ].join("\n"), ) }) test("JSON Schema input and output signatures of a tool both quote", () => { const tool = Tool.make({ description: "Adapter tool with awkward field names", input: rawSchema, output: { type: "object", properties: { "content-type": { type: "string" } }, required: ["content-type"], } as const, execute: () => Effect.succeed({ "content-type": "text/plain" }), }) expect(inputTypeScript(tool)).toContain('"foo-bar"?: string') expect(outputTypeScript(tool)).toBe('{ "content-type": string }') expect(outputTypeScript(tool, true)).toBe(["{", ' "content-type": string,', "}"].join("\n")) }) test("Effect Schema structs with non-identifier field names quote too", () => { const tool = Tool.make({ description: "Schema tool with awkward field names", input: Schema.Struct({ "foo-bar": Schema.String, plain: Schema.optionalKey(Schema.Number) }), execute: () => Effect.succeed(null), }) expect(inputTypeScript(tool)).toBe('{ "foo-bar": string; plain?: number }') expect(inputTypeScript(tool, true)).toBe(["{", ' "foo-bar": string,', " plain?: number,", "}"].join("\n")) }) }) describe("union schemas render every alternative", () => { test("Effect numbers advertise the numeric values accepted by the input decoder", () => { const tool = Tool.make({ description: "Numeric tool", input: Schema.Struct({ amount: Schema.Number }), output: Schema.Number, execute: (input) => Effect.succeed(input.amount), }) expect(inputTypeScript(tool)).toBe("{ amount: number }") expect(inputTypeScript(tool, true)).toBe("{\n amount: number,\n}") expect(outputTypeScript(tool)).toBe("number") expect(outputTypeScript(tool, true)).toBe("number") expect(decodeInput(tool, { amount: 42 })).toEqual({ amount: 42 }) for (const amount of ["NaN", "Infinity", "-Infinity"]) { expect(() => decodeInput(tool, { amount })).toThrow("Expected number") } }) test("Effect number encodings render as number wherever they appear", () => { const Amount = Schema.Number.annotate({ identifier: "Amount" }) const schema = Schema.Struct({ plain: Schema.Number, count: Schema.Int, bounded: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 10 })), positive: Schema.Number.check(Schema.isGreaterThan(0)), maybe: Schema.optionalKey(Schema.Number), nullable: Schema.NullOr(Schema.Number), list: Schema.Array(Schema.Number), scores: Schema.Record(Schema.String, Schema.Number), nested: Schema.Struct({ first: Amount, second: Amount }), }) const tool = Tool.make({ description: "Numbers", input: schema, output: schema, execute: Effect.succeed }) const compact = [ "{ plain: number", "count: number", "bounded: number", "positive: number", "maybe?: number", "nullable: number | null", "list: Array", "scores: { [key: string]: number }", "nested: { first: number; second: number } }", ].join("; ") expect(inputTypeScript(tool)).toBe(compact) expect(outputTypeScript(tool)).toBe(compact) for (const rendered of [inputTypeScript(tool, true), outputTypeScript(tool, true)]) { expect(rendered).toContain(" /** @integer */\n count: number,") expect(rendered).toContain(" /** @integer @minimum 1 @maximum 10 */\n bounded: number,") expect(rendered).not.toContain("Infinity") } const named = Tool.make({ description: "n", input: Amount, output: Amount, execute: Effect.succeed }) expect(inputTypeScript(named)).toBe("number") expect(outputTypeScript(named)).toBe("number") }) test("strings that only spell non-finite numbers render as number", () => { const literals = ["NaN", "Infinity", "-Infinity"] as const const union = Schema.Union([Schema.Finite, Schema.Literals(literals)]) const tool = Tool.make({ description: "n", input: union, output: union, execute: Effect.succeed }) expect(inputTypeScript(tool)).toBe("number") expect(outputTypeScript(tool)).toBe("number") // The signature only narrows what the model is told; the strings still decode. for (const value of literals) expect(decodeInput(tool, value)).toBe(value) expect(jsonSchemaToTypeScript({ anyOf: [{ type: "number" }, { type: "string", enum: [...literals] }] })).toBe( "number", ) expect( jsonSchemaToTypeScript({ anyOf: [{ type: "number" }, ...literals.map((value) => ({ type: "string", enum: [value] }))], }), ).toBe("number") }) test("keeps string enums with other values alongside numbers", () => { expect( jsonSchemaToTypeScript({ anyOf: [{ type: "number" }, { type: "string", enum: ["NaN", "Infinity", "unknown"] }], }), ).toBe('number | "NaN" | "Infinity" | "unknown"') const tool = Tool.make({ description: "Number or status", input: Schema.Union([Schema.Number, Schema.Literal("unknown")]), output: Schema.Union([Schema.Number, Schema.Literal("unknown")]), execute: (input) => Effect.succeed(input), }) expect(inputTypeScript(tool)).toContain('"unknown"') expect(outputTypeScript(tool)).toContain('"unknown"') expect(decodeInput(tool, "unknown")).toBe("unknown") }) test("anyOf with a number branch keeps sibling alternatives", () => { const schema = { anyOf: [{ type: "string" }, { type: "number" }], } as const expect(jsonSchemaToTypeScript(schema)).toBe("string | number") expect(jsonSchemaToTypeScript(schema, true)).toBe("string | number") }) test("nullable numeric unions keep null", () => { const schema = { oneOf: [{ type: "number" }, { type: "null" }], } as const expect(jsonSchemaToTypeScript(schema)).toBe("number | null") expect(jsonSchemaToTypeScript(schema, true)).toBe("number | null") }) test("tool input and output signatures preserve numeric unions", () => { const tool = Tool.make({ description: "Tool with numeric unions", input: { type: "object", properties: { value: { anyOf: [{ type: "string" }, { type: "number" }] }, }, } as const, output: { anyOf: [{ type: "number" }, { type: "boolean" }] } as const, execute: () => Effect.succeed(1), }) expect(inputTypeScript(tool)).toBe("{ value?: string | number }") expect(outputTypeScript(tool)).toBe("number | boolean") }) test("allOf keeps siblings and parenthesized union members in order", () => { const schema = { properties: { common: { type: "boolean" } }, allOf: [{ type: "object", properties: { id: { type: "string" } } }, { type: ["string", "null"] }], } as const expect(jsonSchemaToTypeScript(schema)).toBe("{ common?: boolean } & { id?: string } & (string | null)") expect(jsonSchemaToTypeScript(schema, true)).toBe( ["{", " common?: boolean,", " } & {", " id?: string,", " } & (string | null)"].join("\n"), ) }) test.each([false, true])("allOf does not discard an unresolved constraint (pretty=%s)", (pretty) => { for (const $ref of ["#/$defs/Missing", "#/definitions/Missing", "https://example.com/external.json"]) { expect(jsonSchemaToTypeScript({ allOf: [{ type: "string" }, { $ref }] }, pretty)).toBe("unknown") expect(jsonSchemaToTypeScript({ allOf: [{ type: "string" }, { allOf: [{ $ref }] }] }, pretty)).toBe("unknown") expect( jsonSchemaToTypeScript({ allOf: [{ properties: { nested: { $ref } } }, { type: "string" }] }, pretty), ).toBe("unknown") } expect( jsonSchemaToTypeScript( { type: "string", allOf: [{ $ref: "#/$defs/Constraint" }], $defs: { Constraint: { description: "TypeScript-neutral constraint" } }, }, pretty, ), ).toBe("string") }) }) describe("JSDoc signatures in catalogs and search results", () => { test.each([ { source: "JSON Schema", schema: { type: "object", properties: { count: { type: "integer", minimum: 0, maximum: 10 }, name: { type: "string", minLength: 1, maxLength: 20, pattern: "^[a-z]+$" }, labels: { type: "array", items: { type: "string", minLength: 1 }, minItems: 1, maxItems: 5 }, scores: { type: "object", additionalProperties: { type: "integer", minimum: 0 } }, }, required: ["count", "name", "labels", "scores"], }, }, { source: "Effect", schema: Schema.Struct({ count: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(10)), name: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(20), Schema.isPattern(/^[a-z]+$/u)), labels: Schema.Array(Schema.String.check(Schema.isMinLength(1))).check( Schema.isMinLength(1), Schema.isMaxLength(5), ), scores: Schema.Record(Schema.String, Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))), }), }, ])("$source constraints survive input/output catalog and search signatures", async ({ schema }) => { const runtime = CodeMode.make({ tools: { constrained: Tool.make({ description: "Constrained tool", input: schema, output: schema, execute: () => Effect.succeed({ count: 1, name: "test", labels: ["test"], scores: { test: 1 } }), }), }, }) const type = [ "{", " /** @integer @minimum 0 @maximum 10 */", " count: number,", " /** @minLength 1 @maxLength 20 @pattern ^[a-z]+$ */", " name: string,", " /**", " * @minItems 1 @maxItems 5", " * Each item: @minLength 1", " */", " labels: Array,", " /** Each value: @integer @minimum 0 */", " scores: {", " [key: string]: number,", " },", "}", ].join("\n") const signature = `tools.constrained(${type}): Promise<${type}>` expect(runtime.catalog[0]?.signature).toBe(signature) const result = await Effect.runPromise(runtime.execute('return search({ query: "tools.constrained" })')) expect(result.ok).toBe(true) if (!result.ok) throw new Error("search failed") expect(result.value).toMatchObject({ items: [{ signature }] }) }) const runtime = CodeMode.make({ tools: { github: { list_issues: listIssues }, orders: { lookup: lookupOrder } } }) const search = async (query: string) => { const result = await Effect.runPromise(runtime.execute(`return search({ query: ${JSON.stringify(query)} })`)) expect(result.ok).toBe(true) if (!result.ok) throw new Error("search failed") return result.value as { items: Array<{ path: string; signature: string }>; remaining: number } } test("a raw JSON Schema (MCP-style) tool's result signature carries field JSDoc and summaries", async () => { const { items } = await search("list issues repository") const item = items.find(({ path }) => path === "tools.github.list_issues")! expect(item.signature).toBe( [ "tools.github.list_issues({", " /** Repository owner */", " owner: string,", " /** Cursor from the previous response's pageInfo */", " after?: string,", " /** Results per page. @default 30 */", " perPage?: number,", " /** Filter by labels. @minItems 1 @maxItems 10 */", " labels?: Array,", ' state?: "open" | "closed",', "}): Promise", ].join("\n"), ) }) test("an annotated Effect Schema tool's result signature carries field JSDoc (exact-path lookup too)", async () => { for (const query of ["look up order", "tools.orders.lookup"]) { const { items } = await search(query) const item = items.find(({ path }) => path === "tools.orders.lookup")! expect(item.signature).toBe( [ "tools.orders.lookup({", " /** Order identifier */", " id: string,", " verbose?: boolean,", "}): Promise<{", " /** Current order status */", " status: string,", "}>", ].join("\n"), ) } }) test("the catalog uses the same JSDoc signatures as search", async () => { const catalog = runtime.catalog const github = (await search("list issues repository")).items.find( ({ path }) => path === "tools.github.list_issues", )! const orders = (await search("look up order")).items.find(({ path }) => path === "tools.orders.lookup")! expect(catalog.map(({ signature }) => signature)).toContain(github.signature) expect(catalog.map(({ signature }) => signature)).toContain(orders.signature) expect(github.signature).toContain("/** Repository owner */") }) }) describe("empty input signatures agree with decoding", () => { test.each([ { name: "a plain empty struct", input: Schema.Struct({}) }, { name: "an annotated empty struct", input: Schema.Struct({}).annotate({ identifier: "Empty", description: "No input" }), }, { name: "a checked empty struct", input: Schema.Struct({}).check(Schema.makeFilter(() => true)) }, { name: "an empty encoded struct transformed to a non-empty struct", input: Schema.Struct({}).pipe( Schema.decodeTo(Schema.Struct({ token: Schema.String }), { decode: SchemaGetter.transform(() => ({ token: "x" })), encode: SchemaGetter.transform(() => ({})), }), ), }, { name: "a raw JSON Schema empty object", input: { type: "object", properties: {} } }, ])("$name advertises () and runs with zero arguments", async ({ input }) => { const runtime = CodeMode.make({ tools: { ping: Tool.make({ description: "Ping", input, output: Schema.String, execute: () => Effect.succeed("pong") }), }, }) const signature = "tools.ping(): Promise" expect(runtime.catalog[0]?.signature).toBe(signature) const result = await Effect.runPromise(runtime.execute('return search({ query: "tools.ping" })')) expect(result.ok).toBe(true) if (!result.ok) throw new Error("search failed") expect(result.value).toMatchObject({ items: [{ signature }] }) expect(await Effect.runPromise(runtime.execute("return await tools.ping()"))).toMatchObject({ ok: true, value: "pong", }) }) test.each([ { name: "a record of JSON values", input: Schema.Record(Schema.String, Schema.Json), call: "{ a: 1 }" }, { name: "a record of unknown values", input: Schema.Record(Schema.String, Schema.Unknown), call: "{ a: 1 }" }, { name: "a pattern-keyed record", input: Schema.Record(Schema.TemplateLiteral(["x-", Schema.String]), Schema.String), call: '{ "x-a": "1" }', }, { name: "a constrained empty struct", input: Schema.Struct({}).check(Schema.isMinProperties(1)), call: "{ a: 1 }" }, ])("$name is not advertised as a zero-argument call", async ({ input, call }) => { const runtime = CodeMode.make({ tools: { take: Tool.make({ description: "Take", input, output: Schema.String, execute: () => Effect.succeed("taken") }), }, }) expect(runtime.catalog[0]?.signature).not.toBe("tools.take(): Promise") expect(await Effect.runPromise(runtime.execute(`return await tools.take(${call})`))).toMatchObject({ ok: true, value: "taken", }) }) test("a required encoded input transformed to an empty struct keeps its required fields", async () => { const consume = Tool.make({ description: "Consume token", input: Schema.Struct({ token: Schema.String }).pipe( Schema.decodeTo(Schema.Struct({}), { decode: SchemaGetter.transform(() => ({})), encode: SchemaGetter.transform(() => ({ token: "x" })), }), ), output: Schema.String, execute: () => Effect.succeed("consumed"), }) const runtime = CodeMode.make({ tools: { consume } }) const signature = "tools.consume({\n token: string,\n}): Promise" expect(() => decodeInput(consume, {})).toThrow() expect(runtime.catalog[0]?.signature).toBe(signature) const result = await Effect.runPromise(runtime.execute('return search({ query: "tools.consume" })')) expect(result.ok).toBe(true) if (!result.ok) throw new Error("search failed") expect(result.value).toMatchObject({ items: [{ signature }] }) expect(await Effect.runPromise(runtime.execute("return await tools.consume()"))).toMatchObject({ ok: false, error: { kind: "InvalidToolInput" }, }) expect(await Effect.runPromise(runtime.execute('return await tools.consume({ token: "t" })'))).toMatchObject({ ok: true, value: "consumed", }) }) }) describe("empty object signatures", () => { const output = (schema: Schema.Decoder) => Tool.make({ description: "Inspect", input: Schema.Struct({}), output: schema, execute: () => Effect.succeed({}) }) test("an empty struct output renders as {}", () => { const tool = output(Schema.Struct({})) expect(outputTypeScript(tool)).toBe("{}") expect(outputTypeScript(tool, true)).toBe("{}") expect(CodeMode.make({ tools: { inspect: tool } }).catalog[0]?.signature).toBe("tools.inspect(): Promise<{}>") }) test("nested empty structs render as {}", () => { const tool = output( Schema.Struct({ meta: Schema.Struct({}), extra: Schema.optionalKey(Schema.Struct({}).annotate({ description: "Extra data" })), }), ) expect(outputTypeScript(tool)).toBe("{ meta: {}; extra?: {} }") expect(outputTypeScript(tool, true)).toBe( ["{", " meta: {},", " /** Extra data */", " extra?: {},", "}"].join("\n"), ) }) test("raw schemas render only the exact empty-struct shapes as {}", () => { expect(jsonSchemaToTypeScript({ not: { type: "null" } })).toBe("{}") expect(jsonSchemaToTypeScript({ anyOf: [{ type: "object" }, { type: "array" }] })).toBe("{}") expect(jsonSchemaToTypeScript({ type: "string", not: { type: "null" } })).toBe("string") expect(jsonSchemaToTypeScript({ not: { type: "string" } })).toBe("unknown") expect(jsonSchemaToTypeScript({ not: { type: "null", description: "Not null" } })).toBe("unknown") }) }) describe("non-identifier tool paths", () => { const resolveLibrary = Tool.make({ description: "Resolve a Context7 library ID", input: { type: "object", properties: { query: { type: "string" }, libraryName: { type: "string" }, }, required: ["query", "libraryName"], } as const, output: {}, execute: () => Effect.succeed("/reactjs/react.dev"), }) const runtime = CodeMode.make({ tools: { context7: { "resolve-library-id": resolveLibrary } } }) test("catalog signatures use bracket notation for dashed tool names", () => { expect(runtime.catalog[0]?.signature).toBe( 'tools.context7["resolve-library-id"]({\n query: string,\n libraryName: string,\n}): Promise', ) }) test("search results return callable bracket-notation paths and signatures", async () => { const result = await Effect.runPromise(runtime.execute(`return search({ query: "resolve library" })`)) expect(result.ok).toBe(true) if (!result.ok) throw new Error("search failed") const value = result.value as { items: Array<{ path: string; signature: string }> } expect(value.items[0]?.path).toBe('tools.context7["resolve-library-id"]') expect(value.items[0]?.signature).toContain('tools.context7["resolve-library-id"]({') }) })