Rhys Sullivanfix naming and release7b942677 months agoHistory
351 lines · 10.5 KB · TypeScript
RawDownload
import { readFileSync } from "node:fs"; import { describe, expect, it } from "@effect/vitest";import * as Effect from "effect/Effect"; import type { OpenApiToolDefinition } from "./openapi-definitions";import { compileOpenApiToolDefinitions } from "./openapi-definitions";import { extractOpenApiManifest } from "./openapi-extraction";import { buildOpenApiToolPresentation } from "./openapi-tool-presentation"; const readFixture = (name: string): string =>  readFileSync(    new URL(`../../control-plane/src/runtime/fixtures/${name}`, import.meta.url),    "utf8",  ); describe("buildOpenApiToolPresentation", () => {  it("adds parameter descriptions to flattened call schemas", () => {    const definition: OpenApiToolDefinition = {      toolId: "accessGroups.deleteProject",      rawToolId: "deleteAccessGroupProject",      name: "Delete an access group project",      description: "Allows deletion of an access group project",      group: "accessGroups",      leaf: "deleteProject",      tags: [],      versionSegment: "v1",      method: "delete",      path: "/v1/access-groups/{accessGroupIdOrName}/projects/{projectId}",      invocation: {        method: "delete",        pathTemplate: "/v1/access-groups/{accessGroupIdOrName}/projects/{projectId}",        parameters: [          {            name: "accessGroupIdOrName",            location: "path",            required: true,          },          {            name: "projectId",            location: "path",            required: true,          },          {            name: "teamId",            location: "query",            required: false,          },          {            name: "slug",            location: "query",            required: false,          },        ],        requestBody: null,      },      operationHash: "deadbeefcafebabe",      typing: {        inputSchema: {          type: "object",          properties: {            accessGroupIdOrName: { type: "string" },            projectId: { type: "string" },            teamId: { type: "string" },            slug: { type: "string" },          },          required: ["accessGroupIdOrName", "projectId"],          additionalProperties: false,        },      },      documentation: {        summary: "Delete an access group project",        parameters: [          {            name: "accessGroupIdOrName",            location: "path",            required: true,            description: "The access group identifier or slug.",          },          {            name: "projectId",            location: "path",            required: true,            description: "The project identifier.",          },          {            name: "teamId",            location: "query",            required: false,            description: "The Team identifier to perform the request on behalf of.",          },          {            name: "slug",            location: "query",            required: false,            description: "The Team slug to perform the request on behalf of.",          },        ],      },    };     const presentation = buildOpenApiToolPresentation({ definition });    const schema = presentation.inputSchema as {      properties?: Record<string, { description?: string }>;    };     expect(schema.properties?.accessGroupIdOrName?.description).toBe(      "The access group identifier or slug.",    );    expect(schema.properties?.projectId?.description).toBe(      "The project identifier.",    );    expect(schema.properties?.teamId?.description).toBe(      "The Team identifier to perform the request on behalf of.",    );    expect(schema.properties?.slug?.description).toBe(      "The Team slug to perform the request on behalf of.",    );  });   it("uses the request body property schema instead of nesting the full input schema", () => {    const definition: OpenApiToolDefinition = {      toolId: "projects.update",      rawToolId: "updateProject",      name: "Update project",      description: "Updates a project.",      group: "projects",      leaf: "update",      tags: [],      versionSegment: "v1",      method: "patch",      path: "/v1/projects/{projectId}",      invocation: {        method: "patch",        pathTemplate: "/v1/projects/{projectId}",        parameters: [          {            name: "projectId",            location: "path",            required: true,          },        ],        requestBody: {          required: true,          contentTypes: ["application/json"],        },      },      operationHash: "feedfacecafebeef",      typing: {        inputSchema: {          type: "object",          properties: {            projectId: {              type: "string",            },            body: {              type: "object",              properties: {                name: {                  type: "string",                },              },              required: ["name"],              additionalProperties: false,            },          },          required: ["projectId", "body"],          additionalProperties: false,        },      },      documentation: {        parameters: [          {            name: "projectId",            location: "path",            required: true,            description: "The project identifier.",          },        ],        requestBody: {          description: "Project update payload.",        },      },    };     const presentation = buildOpenApiToolPresentation({ definition });    const schema = presentation.inputSchema as {      properties?: Record<string, unknown>;    };    const body = schema.properties?.body as {      description?: string;      properties?: Record<string, unknown>;    };     expect(body.description).toBe("Project update payload.");    expect(body.properties?.name).toEqual({ type: "string" });    expect(body.properties?.body).toBeUndefined();  });   it.effect(    "resolves request and response schemas from ref hints for the real Neon OpenAPI spec",    () =>      Effect.gen(function* () {        const specText = readFixture("neon-openapi.json");        const manifest = yield* extractOpenApiManifest("neon", specText);        const definition = compileOpenApiToolDefinitions(manifest).find(          (candidate) => candidate.toolId === "apiKey.createApiKey",        );         expect(definition).toBeDefined();         const presentation = buildOpenApiToolPresentation({          definition: definition!,          refHintTable: manifest.refHintTable,        });         expect(presentation.inputTypePreview).toContain("body");        expect(presentation.inputTypePreview).toContain("key_name");        expect(presentation.outputTypePreview).toContain("key");        expect(presentation.inputSchema).toMatchObject({          type: "object",          properties: {            body: {              type: "object",              properties: {                key_name: {                  type: "string",                },              },              required: ["key_name"],            },          },          required: ["body"],        });        expect(presentation.outputSchema).toMatchObject({          type: "object",          properties: {            id: {              type: "integer",            },            key: {              type: "string",            },            name: {              type: "string",            },          },        });      }),    120_000,  );   it.effect(    "preserves response wrappers for response-only Neon operations",    () =>      Effect.gen(function* () {        const specText = readFixture("neon-openapi.json");        const manifest = yield* extractOpenApiManifest("neon", specText);        const definition = compileOpenApiToolDefinitions(manifest).find(          (candidate) => candidate.toolId === "apiKey.listApiKeys",        );         expect(definition).toBeDefined();         const presentation = buildOpenApiToolPresentation({          definition: definition!,          refHintTable: manifest.refHintTable,        });         expect(presentation.inputTypePreview).toBe("unknown");        expect(presentation.outputTypePreview).toContain("id");        expect(presentation.outputTypePreview).toContain("name");        expect(presentation.outputSchema).toMatchObject({          type: "array",          items: {            type: "object",            properties: {              id: {                type: "integer",              },              name: {                type: "string",              },            },          },        });      }),    120_000,  );   it.effect(    "extracts parameter schemas and descriptions for parameter-only Vercel operations",    () =>      Effect.gen(function* () {        const specText = readFixture("vercel-openapi.json");        const manifest = yield* extractOpenApiManifest("vercel", specText);        const definition = compileOpenApiToolDefinitions(manifest).find(          (candidate) => candidate.toolId === "accessGroups.listAccessGroupProjects",        );         expect(definition).toBeDefined();         const presentation = buildOpenApiToolPresentation({          definition: definition!,          refHintTable: manifest.refHintTable,        });         expect(definition?.documentation?.parameters).toEqual(          expect.arrayContaining([            expect.objectContaining({              name: "idOrName",              description: "The ID or name of the Access Group.",            }),            expect.objectContaining({              name: "limit",              description: "Limit how many access group projects should be returned.",            }),          ]),        );        expect(presentation.inputSchema).toMatchObject({          type: "object",          properties: {            idOrName: {              type: "string",              description: "The ID or name of the Access Group.",            },            limit: {              type: "integer",              description: "Limit how many access group projects should be returned.",              minimum: 1,              maximum: 100,            },            next: {              type: "string",              description: "Continuation cursor to retrieve the next page of results.",            },            teamId: {              type: "string",              description: "The Team identifier to perform the request on behalf of.",            },            slug: {              type: "string",              description: "The Team slug to perform the request on behalf of.",            },          },          required: ["idOrName"],        });      }),    120_000,  );});