Rhys Sullivantrim sizes71e33a97 months agoHistory
1230 lines · 37.2 KB · TypeScript
RawDownload
import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; import {  HttpApi,  HttpApiBuilder,  HttpApiEndpoint,  HttpApiGroup,  HttpApiSchema,  HttpClient,  HttpClientResponse,  OpenApi,} from "@effect/platform";import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";import { describe, expect, it } from "@effect/vitest";import { assertInclude, assertTrue } from "@effect/vitest/utils"; import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import { Schema } from "effect"; import type { ToolDefinition, ToolInput, ToolMap } from "@executor/codemode-core"; import { extractOpenApiManifest } from "./openapi-extraction";import { createOpenApiToolsFromManifest, createOpenApiToolsFromSpec } from "./openapi-tools"; type TestServer = {  baseUrl: string;  requests: Array<{    method: string;    path: string;    query: string;    body: string;    headers: Record<string, string>;  }>;  close: () => Promise<void>;}; type EffectServerHandler = {  handler: (nodeRequest: IncomingMessage, nodeResponse: ServerResponse) => void;  dispose: () => Promise<void>;}; const makeEffectServerHandler = (  requests: TestServer["requests"],): Effect.Effect<EffectServerHandler> =>  Effect.gen(function* () {    const handlersLayer = HttpApiBuilder.group(GeneratedApi, "repos", (handlers) =>      handlers        .handle("getRepo", ({ path, urlParams }) =>          Effect.sync(() => {            const include = urlParams.include;            requests.push({              method: "GET",              path: `/repos/${path.owner}/${path.repo}`,              query: include ? `?include=${encodeURIComponent(include)}` : "",              body: "",              headers: {},            });             return {              full_name: `${path.owner}/${path.repo}`,              include: include ?? null,            };          }),        )        .handle("createIssue", ({ path, payload }) =>          Effect.sync(() => {            const bodyText = JSON.stringify(payload);            requests.push({              method: "POST",              path: `/repos/${path.owner}/${path.repo}/issues`,              query: "",              body: bodyText,              headers: {},            });             return {              created: true,              owner: path.owner,              repo: path.repo,              body: payload,            };          }),        ),    );     const apiLayer = HttpApiBuilder.api(GeneratedApi).pipe(      Layer.provide(handlersLayer),    );     const web = HttpApiBuilder.toWebHandler(      Layer.mergeAll(apiLayer, NodeHttpServer.layerContext),    );     const handler: EffectServerHandler["handler"] = (nodeRequest, nodeResponse) => {      void (async () => {        const host = nodeRequest.headers.host ?? "127.0.0.1";        const url = `http://${host}${nodeRequest.url ?? "/"}`;         const headers = new Headers();        for (const [key, value] of Object.entries(nodeRequest.headers)) {          if (value === undefined) {            continue;          }           if (Array.isArray(value)) {            for (const item of value) {              headers.append(key, item);            }          } else {            headers.set(key, value);          }        }         const method = nodeRequest.method ?? "GET";        const hasBody = method !== "GET" && method !== "HEAD";        const requestInit: RequestInit & { duplex?: "half" } = {          method,          headers,        };         if (hasBody) {          (requestInit as { body?: unknown }).body = nodeRequest;          requestInit.duplex = "half";        }         const webRequest = new Request(url, requestInit);         const webResponse = await web.handler(webRequest);         nodeResponse.statusCode = webResponse.status;        webResponse.headers.forEach((value, key) => {          nodeResponse.setHeader(key, value);        });         if (!webResponse.body) {          nodeResponse.end();          return;        }         const reader = webResponse.body.getReader();        while (true) {          const { done, value } = await reader.read();          if (done) {            break;          }           nodeResponse.write(Buffer.from(value));        }         nodeResponse.end();      })().catch((error: unknown) => {        nodeResponse.statusCode = 500;        nodeResponse.end(          JSON.stringify({            error: error instanceof Error ? error.message : String(error),          }),        );      });    };     return {      handler,      dispose: web.dispose,    };  }); const makeTestServer = Effect.acquireRelease(  Effect.promise<TestServer>(    () =>      new Promise<TestServer>((resolve, reject) => {        const requests: TestServer["requests"] = [];         void Effect.runPromise(makeEffectServerHandler(requests))          .then(({ handler, dispose }) => {            const server = createServer(handler);             server.once("error", reject);            server.listen(0, "127.0.0.1", () => {              const address = server.address();              if (!address || typeof address === "string") {                reject(new Error("failed to resolve test server address"));                return;              }               resolve({                baseUrl: `http://127.0.0.1:${address.port}`,                requests,                close: async () => {                  await new Promise<void>((closeResolve, closeReject) => {                    server.close((error: Error | undefined) => {                      if (error) {                        closeReject(error);                        return;                      }                      closeResolve();                    });                  });                  await dispose();                },              });            });          })          .catch(reject);      }),  ),  (server: TestServer) =>    Effect.tryPromise({      try: () => server.close(),      catch: (error: unknown) =>        error instanceof Error ? error : new Error(String(error)),    }).pipe(Effect.orDie),  ); const ownerParam = HttpApiSchema.param("owner", Schema.String);const repoParam = HttpApiSchema.param("repo", Schema.String); const requestHeadersFromNode = (  request: IncomingMessage,): Record<string, string> =>  Object.fromEntries(    Object.entries(request.headers).flatMap(([key, value]) =>      value === undefined        ? []        : [[key, Array.isArray(value) ? value.join(",") : value]]),  ); class GeneratedReposApi extends HttpApiGroup.make("repos")  .add(    HttpApiEndpoint.get("getRepo")`/repos/${ownerParam}/${repoParam}`      .setUrlParams(        Schema.Struct({          include: Schema.optional(Schema.String),        }),      )      .addSuccess(Schema.Unknown),  )  .add(    HttpApiEndpoint.post("createIssue")`/repos/${ownerParam}/${repoParam}/issues`      .setPayload(        Schema.Struct({          title: Schema.String,        }),      )      .addSuccess(Schema.Unknown, { status: 201 }),  ) {} class GeneratedApi extends HttpApi.make("generated").add(GeneratedReposApi) {} const generatedOpenApiSpec = OpenApi.fromApi(GeneratedApi); const resolveToolDefinition = (value: ToolInput): ToolDefinition =>  typeof value === "object" && value !== null && "tool" in value    ? value    : { tool: value }; const resolveToolExecutor = (  tools: ToolMap,  path: string,): ((args: unknown) => Promise<unknown>) => {  const candidate = tools[path];  if (!candidate) {    throw new Error(`Missing tool: ${path}`);  }   const resolved = resolveToolDefinition(candidate);  if (!resolved.tool.execute) {    throw new Error(`Tool has no execute function: ${path}`);  }   return (args: unknown) => Promise.resolve(resolved.tool.execute?.(args));}; describe("openapi-tools", () => {  it.effect("extracts manifest and derives codemode tool paths", () =>    Effect.gen(function* () {      const manifest = yield* extractOpenApiManifest("demo", generatedOpenApiSpec);       const tools = createOpenApiToolsFromManifest({        manifest,        baseUrl: "https://example.com",        namespace: "source.demo",        sourceKey: "api.demo",      });       expect(Object.keys(tools).sort()).toEqual([        "source.demo.repos.createIssue",        "source.demo.repos.getRepo",      ]);       const resolvedGet = resolveToolDefinition(tools["source.demo.repos.getRepo"]!);      expect(resolvedGet.metadata?.sourceKey).toBe("api.demo");      expect(resolvedGet.metadata?.providerKind).toBe("openapi");      expect(resolvedGet.metadata?.providerData).toMatchObject({ group: "repos" });      expect(resolvedGet.metadata?.inputSchema).toBeDefined();    }),  );   it.effect("enriches extracted manifests with OpenAPI auth and response variants", () =>    Effect.gen(function* () {      const manifest = yield* extractOpenApiManifest("secure-demo", {        openapi: "3.1.0",        info: {          title: "Secure Demo",          version: "1.0.0",        },        security: [{          bearerToken: [],        }],        paths: {          "/projects/{id}": {            get: {              operationId: "projects.getProject",              security: [],              parameters: [{                name: "id",                in: "path",                required: true,                schema: {                  type: "string",                },              }],              responses: {                "200": {                  description: "Project",                  content: {                    "application/json": {                      schema: {                        $ref: "#/$defs/Project",                      },                    },                  },                },                "404": {                  description: "Missing",                  content: {                    "application/json": {                      schema: {                        $ref: "#/$defs/Missing",                      },                    },                  },                },                default: {                  description: "Unexpected",                  content: {                    "application/json": {                      schema: {                        type: "object",                        properties: {                          error: {                            type: "string",                          },                        },                      },                    },                  },                },              },            },          },          "/teams": {            get: {              operationId: "teams.listTeams",              security: [                { bearerToken: [] },                { apiKeyHeader: [] },              ],              responses: {                "200": {                  description: "Teams",                  content: {                    "application/json": {                      schema: {                        type: "object",                        properties: {                          teams: {                            type: "array",                            items: {                              type: "string",                            },                          },                        },                      },                    },                  },                },              },            },          },        },        components: {          securitySchemes: {            bearerToken: {              type: "http",              scheme: "bearer",              bearerFormat: "JWT",              description: "Bearer access token.",            },            apiKeyHeader: {              type: "apiKey",              in: "header",              name: "x-api-key",            },          },        },        $defs: {          Project: {            type: "object",            properties: {              id: {                type: "string",              },            },            required: ["id"],          },          Missing: {            type: "object",            properties: {              code: {                type: "string",              },            },            required: ["code"],          },        },      });       const projectTool = manifest.tools.find((tool) => tool.toolId === "projects.getProject");      const teamsTool = manifest.tools.find((tool) => tool.toolId === "teams.listTeams");       expect(projectTool?.authRequirement).toEqual({        kind: "none",      });      expect(projectTool?.responses?.map((response) => response.statusCode)).toEqual([        "200",        "default",        "404",      ]);      expect(projectTool?.responses?.find((response) => response.statusCode === "404")?.schema).toMatchObject({        $ref: "#/$defs/Missing",      });      expect(teamsTool?.authRequirement).toMatchObject({        kind: "anyOf",      });      expect(teamsTool?.securitySchemes).toMatchObject([        {          schemeName: "apiKeyHeader",          schemeType: "apiKey",          placementIn: "header",          placementName: "x-api-key",        },        {          schemeName: "bearerToken",          schemeType: "http",          scheme: "bearer",          bearerFormat: "JWT",        },      ]);      expect(Object.keys(manifest.refHintTable ?? {})).toEqual(        expect.arrayContaining([          "#/$defs/Project",          "#/$defs/Missing",        ]),      );    }),  );   it.effect("preserves OpenAPI servers, parameter serialization, and response headers", () =>    Effect.gen(function* () {      const manifest = yield* extractOpenApiManifest("serialized-demo", {        openapi: "3.1.0",        info: {          title: "Serialized Demo",          version: "1.0.0",        },        servers: [{          url: "https://api.example.test/{version}",          variables: {            version: {              default: "v1",            },          },        }],        paths: {          "/items/{itemId}": {            servers: [{              url: "https://regional.example.test/base",            }],            get: {              operationId: "items.getItem",              parameters: [                {                  name: "itemId",                  in: "path",                  required: true,                  style: "label",                  explode: true,                  schema: {                    type: "array",

Lines 1–500 of 1230.