Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557import { describe, expect, test } from "bun:test"import { Effect, Layer, Option } from "effect"import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"import { CodeMode, OpenAPI, Tool } from "../src/index.js"import { inputTypeScript, outputTypeScript } from "../src/tool-schema.js"
const baseUrl = "http://localhost:4096"type Document = OpenAPI.Document
type Recorded = { readonly method: string readonly url: string readonly headers: Record<string, string> readonly body: unknown}
const transportSpec = async (): Promise<Document> => { return Bun.file(new URL("./fixtures/openapi-transports.json", import.meta.url)).json() as Promise<Document>}
const happyPathSpec = async (): Promise<Document> => { return Bun.file(new URL("./fixtures/openapi-happy-path.json", import.meta.url)).json() as Promise<Document>}
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value)
const toolAt = (tools: OpenAPI.Tools, name: string) => name .split(".") .reduce< Tool.Tool<HttpClient.HttpClient> | OpenAPI.Tools | undefined >((current, segment) => (current !== undefined && !Tool.isTool(current) ? current[segment] : undefined), tools)
const recordingClient = (respond: (request: HttpClientRequest.HttpClientRequest) => Response) => { const requests: Array<Recorded> = [] const layer = Layer.succeed(HttpClient.HttpClient)( HttpClient.make((request) => Effect.gen(function* () { const body = request.body._tag === "Uint8Array" ? JSON.parse(new TextDecoder().decode(request.body.body)) : undefined const url = Option.map(HttpClientRequest.toUrl(request), (resolved) => resolved.toString()) requests.push({ method: request.method, url: Option.getOrElse(url, () => request.url), headers: { ...request.headers }, body, }) return HttpClientResponse.fromWeb(request, respond(request)) }), ), ) return { requests, layer }}
const json = (value: unknown, status = 200) => new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } })
const singleOperation = (operation: Record<string, unknown>, method = "get"): Document => ({ openapi: "3.1.0", paths: { "/test": { [method]: { operationId: "test", responses: { 200: { description: "Success" } }, ...operation } }, },})
const directionalSpec = (openapi: string): Document => ({ openapi, paths: { "/users": { post: { operationId: "users.create", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/User" } } }, }, responses: { 200: { description: "Created", content: { "application/json": { schema: { $ref: "#/components/schemas/User" } } }, }, }, }, }, }, components: { schemas: { ReadOnlyID: { type: "string", readOnly: true }, User: { type: "object", additionalProperties: false, required: ["id", "name", "password", "profile", "generated"], properties: { id: { type: "string", readOnly: true }, name: { type: "string" }, password: { type: "string", writeOnly: true }, profile: { type: "object", additionalProperties: false, required: ["createdAt", "secret", "label"], properties: { createdAt: { type: "string", readOnly: true }, secret: { type: "string", writeOnly: true }, label: { type: "string" }, }, }, generated: { $ref: "#/components/schemas/ReadOnlyID" }, }, }, }, },})
describe("OpenAPI.fromSpec", () => { test("covers a representative API from generation through execution", async () => { const resolutions: Array<string> = [] const client = recordingClient((request) => { const url = Option.getOrElse(HttpClientRequest.toUrl(request), () => new URL(request.url)) if (request.method === "POST") { return new Response( JSON.stringify({ id: "user-2", name: "Grace", email: "grace@example.test", role: "admin" }), { status: 201, headers: { "content-type": "application/vnd.example+json" } }, ) } if (request.method === "DELETE") return new Response(null, { status: 204 }) if (url.pathname === "/search") { return new Response("2 matches", { headers: { "content-type": "text/plain" } }) } return json({ id: "user-1", name: "Ada", email: "ada@example.test", role: "member" }) }) const api = OpenAPI.fromSpec({ spec: await happyPathSpec(), baseUrl, auth: { resolve: ({ name }) => { resolutions.push(name) return Effect.succeed( name === "BearerAuth" ? { type: "bearer", token: "bearer-secret" } : { type: "apiKey", value: "api-secret" }, ) }, }, }) const get = toolAt(api.tools, "users.get") const create = toolAt(api.tools, "users.create") const search = toolAt(api.tools, "search.run") const remove = toolAt(api.tools, "users.remove")
expect(api.skipped).toEqual([]) if (!Tool.isTool(get) || !Tool.isTool(create) || !Tool.isTool(search) || !Tool.isTool(remove)) { throw new Error("happy-path fixture did not generate every operation") } expect(inputTypeScript(get)).toBe( '{ userId: string; include?: Array<string>; verbose?: boolean; "X-Trace-ID"?: string }', ) expect(inputTypeScript(create)).toBe('{ name: string; email: string; role?: "admin" | "member" }') expect(inputTypeScript(search)).toBe("{ filter?: { query: string; page?: number }; tags?: Array<string> }") expect(inputTypeScript(remove)).toBe("{ userId: string }") expect(outputTypeScript(get)).toContain("id: string") expect(outputTypeScript(create)).toContain('role?: "admin" | "member"') expect(outputTypeScript(search)).toBe("string") expect(outputTypeScript(remove)).toBe("null")
const result = await Effect.runPromise( CodeMode.make({ tools: { api: api.tools } }) .execute( ` const user = await tools.api.users.get({ userId: "user-1", include: ["profile", "permissions"], verbose: true, "X-Trace-ID": "trace-1", }) const created = await tools.api.users.create({ name: "Grace", email: "grace@example.test", role: "admin", }) const summary = await tools.api.search.run({ filter: { query: "effect", page: 2 }, tags: ["typescript", "runtime"], }) const removed = await tools.api.users.remove({ userId: "user-1" }) return { user, created, summary, removed } `, ) .pipe(Effect.provide(client.layer)), )
expect(result).toMatchObject({ ok: true, value: { user: { id: "user-1", name: "Ada" }, created: { id: "user-2", name: "Grace" }, summary: "2 matches", removed: null, }, }) expect(resolutions).toEqual(["BearerAuth", "ApiKey", "BearerAuth"]) expect(client.requests).toHaveLength(4)
const getUrl = new URL(client.requests[0]!.url) expect(getUrl.pathname).toBe("/users/user-1") expect(getUrl.searchParams.get("include")).toBe("profile,permissions") expect(getUrl.searchParams.get("verbose")).toBe("true") expect(client.requests[0]!.headers["x-trace-id"]).toBe("trace-1") expect(client.requests[0]!.headers.authorization).toBe("Bearer bearer-secret")
const createUrl = new URL(client.requests[1]!.url) expect(createUrl.searchParams.get("api_key")).toBe("api-secret") expect(client.requests[1]!.body).toEqual({ name: "Grace", email: "grace@example.test", role: "admin" })
const searchUrl = new URL(client.requests[2]!.url) expect(searchUrl.searchParams.get("filter[query]")).toBe("effect") expect(searchUrl.searchParams.get("filter[page]")).toBe("2") expect(searchUrl.searchParams.getAll("tags")).toEqual(["typescript", "runtime"]) expect(client.requests[2]!.headers.authorization).toBeUndefined() expect(new URL(client.requests[3]!.url).pathname).toBe("/users/user-1") expect(client.requests[3]!.headers.authorization).toBe("Bearer bearer-secret") })
test("generates supported operations and reports unsupported transports", async () => { const spec = await transportSpec() const result = OpenAPI.fromSpec({ spec, baseUrl })
expect(result.skipped).toEqual([ { method: "GET", path: "/events", reason: "SSE operations are not supported", }, { method: "GET", path: "/files/{path}", reason: "binary responses are not supported", }, { method: "PUT", path: "/files/{path}", reason: "request body has no JSON content (declared: application/octet-stream)", }, { method: "GET", path: "/terminals/{terminalID}/connect", reason: "WebSocket operations are not supported", }, ])
const get = toolAt(result.tools, "records.get") expect(Tool.isTool(get)).toBe(true) if (!Tool.isTool(get)) throw new Error("records.get was not generated") expect(inputTypeScript(get)).toBe("{ recordID: string }") expect(outputTypeScript(get)).toBe("{ id: string; value: string }") expect(toolAt(result.tools, "events.subscribe")).toBeUndefined() expect(toolAt(result.tools, "files.read")).toBeUndefined() expect(toolAt(result.tools, "files.write")).toBeUndefined() expect(toolAt(result.tools, "terminals.connect")).toBeUndefined() })
test("preserves operation path sanitization and collision handling", () => { const response = { responses: { 200: { description: "Success" } } } const result = OpenAPI.fromSpec({ baseUrl, spec: { openapi: "3.1.0", paths: { "/first": { get: { ...response, operationId: "group.item" } }, "/second": { get: { ...response, operationId: "group.item" } }, "/third": { get: { ...response, operationId: "group..other" } }, }, }, })
expect(Tool.isTool(toolAt(result.tools, "group.item"))).toBe(true) expect(Tool.isTool(toolAt(result.tools, "group_item_2"))).toBe(true) expect(Tool.isTool(toolAt(result.tools, "group.operation.other"))).toBe(true) })
test("does not reserve names for unsupported operations between duplicate operation IDs", () => { const operation = { operationId: "group.item", responses: { 200: { description: "Success" } } } for (const unsupported of [false, true]) { const result = OpenAPI.fromSpec({ baseUrl, spec: { openapi: "3.1.0", paths: { "/first": { get: operation }, ...(unsupported ? { "/unsupported": { get: { ...operation, "x-websocket": true } } } : {}), "/last": { get: operation }, }, }, })
expect(Object.keys(result.tools)).toEqual(["group", "group_item_2"]) expect(toolAt(result.tools, "group.item")).toMatchObject({ _tag: "CodeModeTool", description: "GET /first" }) expect(toolAt(result.tools, "group_item_2")).toMatchObject({ _tag: "CodeModeTool", description: "GET /last" }) expect(result.skipped).toEqual( unsupported ? [{ method: "GET", path: "/unsupported", reason: "WebSocket operations are not supported" }] : [], ) } })
test("synthesizes flat operation IDs from methods and paths", () => { const response = { responses: { 200: { description: "Success" } } } const tools = OpenAPI.fromSpec({ baseUrl, spec: { openapi: "3.1.0", paths: { "/users": { get: response, post: response }, "/users/{id}": { get: response, patch: response, delete: response }, "/organizations/{organizationId}/users/{id}": { get: response }, }, }, }).tools
for (const path of [ "getUsers", "postUsers", "getUsersById", "patchUsersById", "deleteUsersById", "getOrganizationsByOrganizationidUsersById", ]) { expect(Tool.isTool(toolAt(tools, path))).toBe(true) } })
test("lets operation parameters override matching path parameters", () => { const tool = toolAt( OpenAPI.fromSpec({ baseUrl, spec: { openapi: "3.1.0", paths: { "/test": { parameters: [{ name: "limit", in: "query", schema: { type: "string" } }], get: { operationId: "test", parameters: [ { name: "limit", in: "query", schema: { type: "boolean" } }, { name: "limit", in: "query", required: true, schema: { type: "number" } }, ], responses: { 200: { description: "Success" } }, }, }, }, }, }).tools, "test", )
if (!Tool.isTool(tool)) throw new Error("test was not generated") expect(inputTypeScript(tool)).toBe("{ limit: number }") })
test("normalizes OpenAPI 3.0 schemas with Effect", () => { const result = OpenAPI.fromSpec({ baseUrl, spec: { openapi: "3.0.3", paths: { "/search": { get: { operationId: "search", parameters: [ { in: "query", name: "value", schema: { type: "string", nullable: true, minLength: 2 }, }, ], responses: { 200: { description: "Success" } }, }, }, }, }, }) const search = toolAt(result.tools, "search")
expect(Tool.isTool(search)).toBe(true) if (!Tool.isTool(search)) throw new Error("search was not generated") expect(inputTypeScript(search)).toBe("{ value?: string | null }") const schema: unknown = search.input const input = isRecord(schema) ? schema : {} const properties = isRecord(input.properties) ? input.properties : {} const value = isRecord(properties.value) ? properties.value : {} expect(value.minLength).toBe(2) })
test("preserves schema-local definitions alongside component definitions", () => { const tool = toolAt( OpenAPI.fromSpec({ baseUrl, spec: { openapi: "3.1.0", paths: { "/test": { get: { operationId: "test", responses: { 200: { description: "Success", content: { "application/json": { schema: { $ref: "#/$defs/Local", $defs: { Local: { type: "string" } } }, }, }, }, }, }, }, }, components: { schemas: { Global: { type: "number" } } }, }, }).tools, "test", )
if (!Tool.isTool(tool) || !isRecord(tool.output)) throw new Error("test output was not generated") expect(tool.output.$defs).toMatchObject({ Local: { type: "string" }, Global: { type: "number" } }) })
test("projects read-only and write-only properties by schema direction", () => { for (const version of ["3.0.3", "3.1.0"]) { const tool = toolAt(OpenAPI.fromSpec({ baseUrl, spec: directionalSpec(version) }).tools, "users.create") if (!Tool.isTool(tool) || !isRecord(tool.input) || !isRecord(tool.output)) { throw new Error(`users.create was not generated for OpenAPI ${version}`) }
expect(inputTypeScript(tool)).toBe( "{ name: string; password: string; profile: { secret: string; label: string } }", ) expect(outputTypeScript(tool)).toBe( "{ id: string; name: string; profile: { createdAt: string; label: string }; generated: string }", )
const requestDefinitions = isRecord(tool.input.$defs) ? tool.input.$defs : {} const responseDefinitions = isRecord(tool.output.$defs) ? tool.output.$defs : {} const requestUser = isRecord(requestDefinitions.User) ? requestDefinitions.User : {} const responseUser = isRecord(responseDefinitions.User) ? responseDefinitions.User : {} expect(Object.keys(isRecord(requestUser.properties) ? requestUser.properties : {})).toEqual([ "name", "password", "profile", ]) expect(requestUser.required).toEqual(["name", "password", "profile"]) expect(Object.keys(isRecord(responseUser.properties) ? responseUser.properties : {})).toEqual([ "id", "name", "profile", "generated", ]) expect(responseUser.required).toEqual(["id", "name", "profile", "generated"]) } })
test("projects directional annotations through local refs and allOf composition", () => { const tool = toolAt( OpenAPI.fromSpec({ baseUrl, spec: singleOperation( { requestBody: { required: true, content: { "application/json": { schema: { type: "object", additionalProperties: false, required: ["local", "composed", "name"], properties: { local: { $ref: "#/$defs/ReadOnlyValue" }, composed: { allOf: [{ $ref: "#/$defs/ReadOnlyValue" }] }, name: { type: "string" }, }, $defs: { ReadOnlyValue: { type: "string", readOnly: true }, }, }, }, }, }, }, "post", ), }).tools, "test", ) if (!Tool.isTool(tool)) throw new Error("test was not generated")
expect(inputTypeScript(tool)).toBe("{ name: string }") })
test("honors declarations that are siblings of a $ref", () => { const tool = toolAt( OpenAPI.fromSpec({ baseUrl, spec: { openapi: "3.1.0", paths: { "/test": { post: { operationId: "test", responses: { 200: { description: "Success" } }, requestBody: { required: true, content: { "application/json": { schema: { type: "object", additionalProperties: false, required: ["record"], properties: { record: { $ref: "#/components/schemas/Base", properties: { extra: { type: "string", readOnly: true }, note: { type: "string" } }, required: ["extra", "note", "id"], }, }, }, }, }, }, }, }, }, components: { schemas: { Base: { type: "object", required: ["id", "name"], properties: { id: { type: "string", readOnly: true }, name: { type: "string" } }, }, }, }, }, }).tools, "test", ) if (!Tool.isTool(tool) || !isRecord(tool.input)) throw new Error("test was not generated") const properties = isRecord(tool.input.properties) ? tool.input.properties : {} const record = isRecord(properties.record) ? properties.record : {} const definitions = isRecord(tool.input.$defs) ? tool.input.$defs : {} const base = isRecord(definitions.Base) ? definitions.Base : {}
expect(Object.keys(isRecord(record.properties) ? record.properties : {})).toEqual(["note"]) expect(record.required).toEqual(["note"]) expect(Object.keys(isRecord(base.properties) ? base.properties : {})).toEqual(["name"]) expect(base.required).toEqual(["name"]) })
test("honors directional declarations on intermediate reference hops", () => { const tool = toolAt( OpenAPI.fromSpec({ baseUrl, spec: { ...singleOperation( { requestBody: { required: true, content: { "application/json": { schema: { type: "object", additionalProperties: false, required: ["secret", "name"], properties: { // Hidden only by the sibling declaration on the middle hop. secret: { $ref: "#/components/schemas/Middle" }, name: { type: "string" }, }, }, }, }, }, }, "post", ), components: { schemas: { Middle: { $ref: "#/components/schemas/Plain", readOnly: true }, Plain: { type: "string" }, }, }, }, }).tools, "test", ) if (!Tool.isTool(tool)) throw new Error("test was not generated")
expect(inputTypeScript(tool)).toBe("{ name: string }") })
test("projects cyclic component references without hanging", () => { const tool = toolAt( OpenAPI.fromSpec({ baseUrl, spec: { openapi: "3.1.0", paths: { "/test": { post: { operationId: "test", responses: { 200: { description: "Success" } }, requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/Node" } } }, }, }, }, }, components: { schemas: { Node: { type: "object", required: ["id", "name", "child"], properties: { id: { type: "string", readOnly: true }, name: { type: "string" }, child: { $ref: "#/components/schemas/Node" }, }, }, }, }, }, }).tools, "test", ) if (!Tool.isTool(tool) || !isRecord(tool.input)) throw new Error("test was not generated") const definitions = isRecord(tool.input.$defs) ? tool.input.$defs : {} const node = isRecord(definitions.Node) ? definitions.Node : {}
expect(Object.keys(isRecord(node.properties) ? node.properties : {})).toEqual(["name", "child"]) expect(node.required).toEqual(["name", "child"]) })
test("projects diamond-shaped reference graphs in linear time", () => { // Each component references the next twice; without memoized hidden-ness this is 2^30 work. const depth = 30 const schemas = Object.fromEntries( Array.from({ length: depth }, (_, index) => [ `C${index}`, index === depth - 1 ? { type: "object", properties: { id: { type: "string", readOnly: true }, name: { type: "string" } } } : { allOf: [{ $ref: `#/components/schemas/C${index + 1}` }, { $ref: `#/components/schemas/C${index + 1}` }] }, ]), ) const tool = toolAt( OpenAPI.fromSpec({ baseUrl, spec: { openapi: "3.1.0", paths: { "/test": { post: { operationId: "test", responses: { 200: { description: "Success" } }, requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/C0" } } }, }, }, }, }, components: { schemas }, }, }).tools, "test", ) if (!Tool.isTool(tool) || !isRecord(tool.input)) throw new Error("test was not generated") const definitions = isRecord(tool.input.$defs) ? tool.input.$defs : {} const leaf = isRecord(definitions[`C${depth - 1}`]) ? definitions[`C${depth - 1}`] : {}
expect(Object.keys(isRecord(leaf.properties) ? leaf.properties : {})).toEqual(["name"]) })
test("resolves hiding through reference cycles regardless of evaluation order", () => { // `Wrap` is hidden only through the cycle member `Loop`; evaluating a property that // enters the cycle at `Loop` first must not freeze a provisional result for `Wrap`. const schemas = { Wrap: { allOf: [{ $ref: "#/components/schemas/Loop" }] }, Loop: { allOf: [{ $ref: "#/components/schemas/Wrap" }, { readOnly: true }] }, } const body = (properties: Record<string, unknown>) => ({ required: true, content: { "application/json": { schema: { type: "object", additionalProperties: false, required: [...Object.keys(properties), "name"], properties: { ...properties, name: { type: "string" } }, }, }, }, }) for (const properties of [ { a: { $ref: "#/components/schemas/Loop" }, b: { $ref: "#/components/schemas/Wrap" } }, { a: { $ref: "#/components/schemas/Wrap" }, b: { $ref: "#/components/schemas/Loop" } }, ]) { const tool = toolAt( OpenAPI.fromSpec({ baseUrl, spec: { ...singleOperation({ requestBody: body(properties) }, "post"), components: { schemas } }, }).tools, "test", ) if (!Tool.isTool(tool)) throw new Error("test was not generated")
expect(inputTypeScript(tool)).toBe("{ name: string }") } })
test("keeps not, if, and contains subschemas unprojected", () => { const tool = toolAt( OpenAPI.fromSpec({ baseUrl, spec: singleOperation( { requestBody: { required: true, content: { "application/json": { schema: { type: "object", additionalProperties: false, required: ["record"], properties: { record: { type: "object", // Removing `secret` here would turn `not` unsatisfiable and // flip which branch of `if` applies; both must pass through. not: { required: ["secret"], properties: { secret: { type: "string", readOnly: true } } }, if: { required: ["kind"], properties: { kind: { type: "string", readOnly: true } } }, }, }, }, }, }, }, }, "post", ), }).tools, "test", ) if (!Tool.isTool(tool) || !isRecord(tool.input)) throw new Error("test was not generated") const properties = isRecord(tool.input.properties) ? tool.input.properties : {} const record: Record<string, unknown> = isRecord(properties.record) ? properties.record : {}
expect(record.not).toEqual({ required: ["secret"], properties: { secret: { type: "string", readOnly: true } } }) expect(record.if).toEqual({ required: ["kind"], properties: { kind: { type: "string", readOnly: true } } }) })
test("does not hide properties whose direction is declared only in anyOf or oneOf alternatives", () => { // Deliberate scope bound: alternatives may apply, so a directional declaration on // one alternative does not hide the property; the annotation is preserved as-is. const tool = toolAt( OpenAPI.fromSpec({ baseUrl, spec: singleOperation( { requestBody: { required: true, content: { "application/json": { schema: { type: "object", additionalProperties: false, required: ["choice", "pick"], properties: { choice: { anyOf: [{ type: "string", readOnly: true }, { type: "number" }] }, pick: { oneOf: [{ type: "string", readOnly: true }, { type: "number" }] }, }, }, }, }, }, }, "post", ), }).tools, "test", ) if (!Tool.isTool(tool) || !isRecord(tool.input)) throw new Error("test was not generated") const properties = isRecord(tool.input.properties) ? tool.input.properties : {} const choice: Record<string, unknown> = isRecord(properties.choice) ? properties.choice : {} const pick: Record<string, unknown> = isRecord(properties.pick) ? properties.pick : {}
expect(Object.keys(properties)).toEqual(["choice", "pick"]) expect(choice.anyOf).toEqual([{ type: "string", readOnly: true }, { type: "number" }]) expect(pick.oneOf).toEqual([{ type: "string", readOnly: true }, { type: "number" }]) })
test("does not misresolve shadowed local $defs when flattening body fields", () => { const tool = toolAt( OpenAPI.fromSpec({ baseUrl, spec: singleOperation( { requestBody: { required: true, content: { "application/json": { schema: { type: "object", additionalProperties: false, required: ["record"], $defs: { Value: { type: "string" } }, properties: { record: { type: "object", required: ["x"], properties: { x: { $ref: "#/$defs/Value" } }, // Shadows the body-level Value; must not affect the body-rooted projection. $defs: { Value: { type: "string", readOnly: true } }, }, }, }, }, }, }, }, "post", ), }).tools, "test", ) if (!Tool.isTool(tool) || !isRecord(tool.input)) throw new Error("test was not generated") const properties = isRecord(tool.input.properties) ? tool.input.properties : {} const record = isRecord(properties.record) ? properties.record : {}
expect(Object.keys(isRecord(record.properties) ? record.properties : {})).toEqual(["x"]) expect(record.required).toEqual(["x"]) })
test("projects directional annotations inside parameter schemas", () => { const tool = toolAt( OpenAPI.fromSpec({ baseUrl, spec: singleOperation({ parameters: [ { name: "filter", in: "query", required: true, schema: { type: "object", required: ["state", "id"], properties: { state: { type: "string" }, id: { type: "string", readOnly: true } }, }, }, ], }), }).tools, "test", ) if (!Tool.isTool(tool)) throw new Error("test was not generated")
expect(inputTypeScript(tool)).toBe("{ filter: { state: string } }") })
test("ignores inherited directional annotations", () => { const inherited: Record<string, unknown> = { type: "string" } Object.setPrototypeOf(inherited, { readOnly: true }) const tool = toolAt( OpenAPI.fromSpec({ baseUrl, spec: singleOperation({ parameters: [ { name: "filter", in: "query", required: true, schema: { type: "object", // The own annotation on `id` keeps projection active for the document, // so `value` pins that prototype-inherited annotations are not read. properties: { value: inherited, id: { type: "string", readOnly: true } }, required: ["value", "id"], }, }, ], }), }).tools, "test", ) if (!Tool.isTool(tool)) throw new Error("test was not generated")
expect(inputTypeScript(tool)).toBe("{ filter: { value: string } }") })
test("cleans required properties across allOf branches", () => { const tool = toolAt( OpenAPI.fromSpec({ baseUrl, spec: singleOperation( { requestBody: { required: true, content: { "application/json": { schema: { type: "object", required: ["id", "name"], allOf: [ { type: "object", required: ["id", "name"], properties: { id: { type: "string", readOnly: true }, name: { type: "string" } }, }, ], }, }, }, }, }, "post", ), }).tools, "test", ) if (!Tool.isTool(tool) || !isRecord(tool.input)) throw new Error("test was not generated") const properties = isRecord(tool.input.properties) ? tool.input.properties : {} const body = isRecord(properties.body) ? properties.body : {} const allOf = Array.isArray(body.allOf) ? body.allOf : [] const branch = isRecord(allOf[0]) ? allOf[0] : {}
expect(body.required).toEqual(["name"]) expect(branch.required).toEqual(["name"]) expect(Object.keys(isRecord(branch.properties) ? branch.properties : {})).toEqual(["name"]) })
test("keeps directional schemas model-facing while preserving runtime pass-through", async () => { const client = recordingClient(() => json({ id: "server-id", name: "Ada", password: "returned-by-server", profile: { createdAt: "today", secret: "returned-secret", label: "primary" }, generated: "generated-id", }), ) const tool = toolAt(OpenAPI.fromSpec({ baseUrl, spec: directionalSpec("3.1.0") }).tools, "users.create") if (!Tool.isTool(tool)) throw new Error("users.create was not generated")
const result = await Effect.runPromise( tool .execute({ id: "ignored-top-level", generated: "ignored-generated", name: "Ada", password: "request-secret", profile: { createdAt: "sent-nested", secret: "nested-secret", label: "primary" }, }) .pipe(Effect.provide(client.layer)), )
expect(client.requests[0]?.body).toEqual({ name: "Ada", password: "request-secret", profile: { createdAt: "sent-nested", secret: "nested-secret", label: "primary" }, }) expect(result).toMatchObject({ password: "returned-by-server", profile: { secret: "returned-secret" } }) })
test("exposes generated operations through CodeMode discovery", async () => { const { layer } = recordingClient(() => json({})) const runtime = CodeMode.make({ tools: { api: OpenAPI.fromSpec({ spec: await happyPathSpec(), baseUrl }).tools }, }) const result = await Effect.runPromise( runtime .execute( ` return search({ query: "get a user", namespace: "api", limit: 1 }) `, ) .pipe(Effect.provide(layer)), )
expect(result).toMatchObject({ ok: true }) if (!result.ok) return expect(result.value).toMatchObject({ items: [ { path: "tools.api.users.get", description: "Get a user", }, ], }) expect(JSON.stringify(result.value)).toContain("userId: string") })
test("serializes supported simple and form parameter shapes", async () => { const client = recordingClient(() => json({ ok: true })) const result = OpenAPI.fromSpec({ baseUrl, spec: { openapi: "3.1.0", paths: { "/items/{keys}": { get: { operationId: "items", parameters: [ { name: "keys", in: "path", required: true, schema: { type: "array", items: { type: "string" } } }, { name: "tags", in: "query", style: "form", explode: false, schema: { type: "array" } }, { name: "filter", in: "query", style: "form", explode: true, schema: { type: "object" } }, { name: "nullable", in: "query", required: true, schema: { type: ["string", "null"] } }, { name: "constructor", in: "query", schema: { type: "string" } }, { name: "meta", in: "header", style: "simple", explode: true, schema: { type: "object" } }, ], responses: { 200: { description: "Success" } }, }, }, }, }, }) const tool = toolAt(result.tools, "items") if (!Tool.isTool(tool)) throw new Error("items was not generated")
await Effect.runPromise( tool .execute({ keys: ["a!", "b*"], tags: ["x", "y"], filter: { state: "open", page: 2 }, nullable: null, constructor: "safe", meta: { a: "b", c: "d" }, }) .pipe(Effect.provide(client.layer)), )
const url = new URL(client.requests[0]!.url) expect(url.pathname).toBe("/items/a%21,b%2A") expect(url.searchParams.get("tags")).toBe("x,y") expect(url.searchParams.get("state")).toBe("open") expect(url.searchParams.get("page")).toBe("2") expect(url.searchParams.get("nullable")).toBe("null") expect(url.searchParams.get("constructor")).toBe("safe") expect(client.requests[0]!.headers.meta).toBe("a=b,c=d") await expect( Effect.runPromise(tool.execute({ keys: [undefined] }).pipe(Effect.provide(client.layer))), ).rejects.toThrow("unsupported nested value") })
test("preserves ordered exploded and deep-object query parameters", async () => { const client = recordingClient(() => json({ ok: true })) const tool = toolAt( OpenAPI.fromSpec({ baseUrl, spec: singleOperation({ parameters: [ { name: "tags", in: "query", style: "form", explode: true, schema: { type: "array" } }, { name: "filter", in: "query", style: "form", explode: true, schema: { type: "object" } }, { name: "location", in: "query", style: "deepObject", explode: true, schema: { type: "object" } }, ], }), }).tools, "test", ) if (!Tool.isTool(tool)) throw new Error("test was not generated")
await Effect.runPromise( tool .execute({ tags: ["first value", "second&value"], filter: { state: "open now", page: 2 }, location: { directory: "/tmp/a b", workspace: "work&1" }, }) .pipe(Effect.provide(client.layer)), )
expect(client.requests[0]?.url).toBe( `${baseUrl}/test?tags=first+value&tags=second%26value&state=open+now&page=2&location%5Bdirectory%5D=%2Ftmp%2Fa+b&location%5Bworkspace%5D=work%261`, ) await expect(Effect.runPromise(tool.execute({ tags: [{}] }).pipe(Effect.provide(client.layer)))).rejects.toThrow( "Parameter 'tags' contains an unsupported nested value.", ) await expect( Effect.runPromise(tool.execute({ filter: { state: {} } }).pipe(Effect.provide(client.layer))), ).rejects.toThrow("Query parameter 'filter' contains an unsupported nested value.") await expect( Effect.runPromise(tool.execute({ location: { directory: [] } }).pipe(Effect.provide(client.layer))), ).rejects.toThrow("Deep-object parameter 'location' contains an unsupported nested value.") expect(client.requests).toHaveLength(1) })
test("skips unsupported parameter encodings and malformed security", () => { const result = OpenAPI.fromSpec({ baseUrl, spec: { openapi: "3.1.0", security: [{ bearer: [] }], paths: { "/cookie": { get: { operationId: "cookie", parameters: [{ name: "session", in: "cookie", schema: { type: "string" } }], responses: { 200: { description: "Success" } }, }, }, "/reserved": { get: { operationId: "reserved", parameters: [{ name: "query", in: "query", allowReserved: true, schema: { type: "string" } }], responses: { 200: { description: "Success" } }, }, }, "/invalid-style": { get: { operationId: "invalidStyle", parameters: [{ name: "query", in: "query", style: 42, schema: { type: "string" } }], responses: { 200: { description: "Success" } }, }, }, "/security": { get: { operationId: "security", security: null, responses: { 200: { description: "Success" } } }, }, }, }, })
expect(result.tools).toEqual({}) expect(result.skipped.map((item) => item.reason)).toEqual([ "cookie parameter 'session' is not supported", "parameter 'query' uses unsupported allowReserved encoding", "parameter 'query' has an invalid style", "security declaration is not an array", ]) })
test("fails closed on prototype-named missing security schemes", () => { const result = OpenAPI.fromSpec({ baseUrl, spec: singleOperation({ security: [JSON.parse('{"__proto__":[]}')] }), })
expect(result.tools).toEqual({}) expect(result.skipped[0]?.reason).toBe("security requirement references missing or malformed scheme: __proto__") })
test("resolves bearer authentication without exposing it as input", async () => { const contexts: Array<Parameters<OpenAPI.AuthResolver>[0]> = [] const client = recordingClient(() => json({ ok: true })) const spec = { ...singleOperation({ operationId: undefined }), security: [{ bearer: [] }], components: { securitySchemes: { bearer: { type: "http", scheme: "bearer" } } }, } satisfies Document const tool = toolAt( OpenAPI.fromSpec({ baseUrl, spec, auth: { resolve: (context) => { contexts.push(context) return Effect.succeed({ type: "bearer", token: "secret" }) }, }, }).tools, "getTest", ) if (!Tool.isTool(tool)) throw new Error("test was not generated")
await Effect.runPromise(tool.execute({}).pipe(Effect.provide(client.layer)))
expect(inputTypeScript(tool)).toBe("{}") expect(client.requests[0]!.headers.authorization).toBe("Bearer secret") expect(contexts).toEqual([ { name: "bearer", definition: { type: "http", scheme: "bearer" }, scopes: [], operation: { operationId: undefined, method: "GET", path: "/test", summary: undefined, description: undefined, }, }, ]) })
test("applies authentication carriers without prototype or collision loss", async () => { const client = recordingClient(() => json({ ok: true })) const authenticated = ( security: ReadonlyArray<Record<string, ReadonlyArray<string>>>, schemes: Record<string, unknown>, ) => OpenAPI.fromSpec({ baseUrl, spec: { ...singleOperation({}), security, components: { securitySchemes: schemes } }, auth: { resolve: () => Effect.succeed({ type: "apiKey", value: "secret" }) }, }) const prototype = toolAt( authenticated([{ key: [] }], { key: { type: "apiKey", in: "query", name: "__proto__" } }).tools, "test", ) if (!Tool.isTool(prototype)) throw new Error("prototype auth tool was not generated")
await Effect.runPromise(prototype.execute({}).pipe(Effect.provide(client.layer))) expect(new URL(client.requests[0]!.url).searchParams.get("__proto__")).toBe("secret")
const duplicate = toolAt( authenticated([{ first: [], second: [] }], { first: { type: "apiKey", in: "header", name: "x-key" }, second: { type: "apiKey", in: "header", name: "x-key" }, }).tools, "test", ) if (!Tool.isTool(duplicate)) throw new Error("duplicate auth tool was not generated") await expect(Effect.runPromise(duplicate.execute({}).pipe(Effect.provide(client.layer)))).rejects.toThrow( "multiple credentials", )
const cookie = authenticated([{ key: [] }], { key: { type: "apiKey", in: "cookie", name: "session" } }) expect(cookie.tools).toEqual({}) expect(cookie.skipped[0]?.reason).toBe("cookie authentication 'key' is not supported")
const alternative = OpenAPI.fromSpec({ baseUrl, spec: { ...singleOperation({}), security: [{ cookie: [] }, { bearer: [] }], components: { securitySchemes: { cookie: { type: "apiKey", in: "cookie", name: "session" }, bearer: { type: "http", scheme: "bearer" }, }, }, }, auth: { resolve: ({ name }) => Effect.succeed(name === "bearer" ? { type: "bearer", token: "secret" } : undefined), }, }) const alternativeTool = toolAt(alternative.tools, "test") if (!Tool.isTool(alternativeTool)) throw new Error("supported auth alternative was not generated") await Effect.runPromise(alternativeTool.execute({}).pipe(Effect.provide(client.layer))) expect(client.requests.at(-1)?.headers.authorization).toBe("Bearer secret") })
test("honors server precedence and rejects ambiguous base URLs", async () => { const client = recordingClient(() => json({ ok: true })) const spec = { ...singleOperation({ servers: [{ url: "https://operation.example/v1" }] }), servers: [{ url: "https://document.example" }], } satisfies Document const tool = toolAt(OpenAPI.fromSpec({ spec }).tools, "test") if (!Tool.isTool(tool)) throw new Error("test was not generated")
await Effect.runPromise(tool.execute({}).pipe(Effect.provide(client.layer))) expect(client.requests[0]?.url).toBe("https://operation.example/v1/test")
const invalid = OpenAPI.fromSpec({ spec, baseUrl: "https://example.com/api?tenant=one" }) expect(invalid.tools).toEqual({}) expect(invalid.skipped[0]?.reason).toContain("unsupported query string or fragment")
const malformed = OpenAPI.fromSpec({ spec, baseUrl: "https:/example.com" }) expect(malformed.tools).toEqual({}) expect(malformed.skipped[0]?.reason).toContain("not an absolute HTTP(S) URL") })
test("resolves chained response refs before detecting unsupported transports", () => { const result = OpenAPI.fromSpec({ baseUrl, spec: { ...singleOperation({ responses: { 200: { $ref: "#/components/responses/First" } } }), components: { responses: { First: { $ref: "#/components/responses/Stream" }, Stream: { content: { "text/event-stream": { schema: { type: "string" } } } }, }, }, }, })
expect(result.tools).toEqual({}) expect(result.skipped[0]?.reason).toBe("SSE operations are not supported") })
test("resolves response schemas before detecting binary output", () => { const result = OpenAPI.fromSpec({ baseUrl, spec: { ...singleOperation({ responses: { 200: { content: { "text/plain": { schema: { $ref: "#/components/schemas/File" } } }, }, }, }), components: { schemas: { File: { type: "string", format: "binary" } } }, }, })
expect(result.tools).toEqual({}) expect(result.skipped[0]?.reason).toBe("binary responses are not supported") })
test("validates composite parameters before resolving auth", async () => { const resolutions: Array<string> = [] const client = recordingClient(() => json({ ok: true })) const tool = toolAt( OpenAPI.fromSpec({ baseUrl, spec: { ...singleOperation({ parameters: [{ name: "filter", in: "query", style: "form", explode: true, schema: { type: "object" } }], }), security: [{ bearer: [] }], components: { securitySchemes: { bearer: { type: "http", scheme: "bearer" } } }, }, auth: { resolve: ({ name }) => { resolutions.push(name) return Effect.succeed({ type: "bearer", token: "secret" }) }, }, }).tools, "test", ) if (!Tool.isTool(tool)) throw new Error("test was not generated")
await expect( Effect.runPromise(tool.execute({ filter: { value: undefined } }).pipe(Effect.provide(client.layer))), ).rejects.toThrow("unsupported nested value") expect(resolutions).toEqual([]) expect(client.requests).toEqual([]) })
test("preserves JSON media types and rejects unencodable bodies", async () => { const client = recordingClient(() => json({ ok: true })) const tool = toolAt( OpenAPI.fromSpec({ baseUrl, spec: singleOperation( { requestBody: { required: true, content: { "application/merge-patch+json": { schema: { type: "object" } } }, }, }, "post", ), }).tools, "test", ) if (!Tool.isTool(tool)) throw new Error("test was not generated")
await Effect.runPromise(tool.execute({ body: { name: "updated" } }).pipe(Effect.provide(client.layer))) expect(client.requests[0]!.headers["content-type"]).toBe("application/merge-patch+json") const cyclic: Record<string, unknown> = {} cyclic.self = cyclic await expect(Effect.runPromise(tool.execute({ body: cyclic }).pipe(Effect.provide(client.layer)))).rejects.toThrow( "Invalid JSON body", ) })
test("rejects oversized and malformed JSON responses", async () => { const tool = toolAt(OpenAPI.fromSpec({ baseUrl, spec: singleOperation({}) }).tools, "test") if (!Tool.isTool(tool)) throw new Error("test was not generated") const oversized = recordingClient( () => new Response(null, { headers: { "content-length": String(50 * 1024 * 1024 + 1) } }), ) const malformed = recordingClient(() => new Response("{", { headers: { "content-type": "application/json" } })) const chunked = recordingClient(() => new Response(new Uint8Array(50 * 1024 * 1024 + 1)))
await expect(Effect.runPromise(tool.execute({}).pipe(Effect.provide(oversized.layer)))).rejects.toThrow( "response exceeds 50 MiB", ) await expect(Effect.runPromise(tool.execute({}).pipe(Effect.provide(malformed.layer)))).rejects.toThrow( "returned malformed JSON", ) await expect(Effect.runPromise(tool.execute({}).pipe(Effect.provide(chunked.layer)))).rejects.toThrow( "response exceeds 50 MiB", ) })
test("keeps non-JSON responses raw and unions every success output", async () => { const spec = singleOperation({ responses: { 200: { description: "Text", content: { "text/plain": { schema: { type: "string" } } } }, 204: { description: "Empty" }, }, }) const tool = toolAt(OpenAPI.fromSpec({ baseUrl, spec }).tools, "test") if (!Tool.isTool(tool)) throw new Error("test was not generated") const client = recordingClient(() => new Response("123", { headers: { "content-type": "text/plain" } }))
expect(outputTypeScript(tool)).toBe("string | null") await expect(Effect.runPromise(tool.execute({}).pipe(Effect.provide(client.layer)))).resolves.toBe("123") })
test("fails missing required parameters before auth and network", async () => { const { requests, layer } = recordingClient(() => json({})) const runtime = CodeMode.make({ tools: { api: OpenAPI.fromSpec({ spec: await transportSpec(), baseUrl }).tools }, })
const result = await Effect.runPromise( runtime.execute("return await tools.api.records.get({})").pipe(Effect.provide(layer)), )
expect(result).toMatchObject({ ok: false }) expect(JSON.stringify(result)).toContain("Missing required path parameter 'recordID'") expect(requests).toHaveLength(0) })
test("prefixes cross-location collisions and reconstructs the HTTP request", async () => { const spec = { openapi: "3.1.0", info: { title: "collision", version: "1.0.0" }, paths: { "/echo": { post: { operationId: "echo", requestBody: { required: true, content: { "application/json": { schema: { type: "string" } } }, }, responses: { "204": { description: "Echoed" } }, }, }, "/things/{id}": { post: { operationId: "things.update", parameters: [ { name: "id", in: "path", required: true, schema: { type: "string" } }, { name: "id", in: "query", required: true, schema: { type: "string" } }, { name: "path_id", in: "query", schema: { type: "string" } }, { name: "id", in: "header", required: true, schema: { type: "string" } }, ], requestBody: { required: true, content: { "application/json": { schema: { type: "object", properties: { id: { type: "string" } }, required: ["id"], additionalProperties: false, }, }, }, }, responses: { "204": { description: "Updated" } }, }, }, }, } satisfies Document const { requests, layer } = recordingClient(() => new Response(null, { status: 204 })) const tools = OpenAPI.fromSpec({ spec, baseUrl }).tools const update = toolAt(tools, "things.update") const echo = toolAt(tools, "echo")
expect(Tool.isTool(update)).toBe(true) if (!Tool.isTool(update)) throw new Error("things.update was not generated") expect(inputTypeScript(update)).toBe( "{ path_id: string; query_id: string; path_id_2?: string; header_id: string; body_id: string }", ) expect(Tool.isTool(echo)).toBe(true) if (!Tool.isTool(echo)) throw new Error("echo was not generated") expect(inputTypeScript(echo)).toBe("{ body: string }")
const runtime = CodeMode.make({ tools }) const result = await Effect.runPromise( runtime .execute( ` const updated = await tools.things.update({ path_id: "path", query_id: "query", path_id_2: "literal", header_id: "header", body_id: "body" }) const echoed = await tools.echo({ body: "hello" }) return { updated, echoed } `, ) .pipe(Effect.provide(layer)), )
expect(result).toMatchObject({ ok: true }) expect(requests).toHaveLength(2) expect(new URL(requests[0]!.url).pathname).toBe("/things/path") expect(new URL(requests[0]!.url).searchParams.get("id")).toBe("query") expect(new URL(requests[0]!.url).searchParams.get("path_id")).toBe("literal") expect(requests[0]!.headers.id).toBe("header") expect(requests[0]!.body).toStrictEqual({ id: "body" }) expect(requests[1]!.body).toBe("hello") })
test("keeps bodies nested when flattening would lose schema semantics", () => { const body = (schema: Record<string, unknown>, required = true) => ({ required, content: { "application/json": { schema } }, }) const spec = { openapi: "3.1.0", info: { title: "bodies", version: "1.0.0" }, paths: Object.fromEntries( [ [ "optional", body( { type: "object", properties: { name: { type: "string" } }, required: ["name"], additionalProperties: false, }, false, ), ], ["dictionary", body({ type: "object", additionalProperties: { type: "string" } })], [ "composed", body({ type: "object", allOf: [{ type: "object", properties: { name: { type: "string" } }, required: ["name"] }], additionalProperties: false, }), ], [ "nullable", body({ type: ["object", "null"], properties: { name: { type: "string" } }, additionalProperties: false, }), ], ].map(([name, requestBody]) => [ `/body/${name}`, { post: { operationId: `body.${name}`, requestBody, responses: { "204": { description: "Accepted" } }, }, }, ]), ), } satisfies Document const tools = OpenAPI.fromSpec({ spec, baseUrl }).tools
for (const name of ["optional", "dictionary", "composed", "nullable"]) { const tool = toolAt(tools, `body.${name}`) expect(Tool.isTool(tool)).toBe(true) if (!Tool.isTool(tool)) throw new Error(`body.${name} was not generated`) const input = isRecord(tool.input) ? tool.input : {} expect(Object.keys(isRecord(input.properties) ? input.properties : {})).toStrictEqual(["body"]) } const optional = toolAt(tools, "body.optional") if (!Tool.isTool(optional)) throw new Error("body.optional was not generated") expect(inputTypeScript(optional)).toBe("{ body?: { name: string } }") })})