Rhys Sullivanremove v3 workinged7a3488 months agoHistory
574 lines · 17.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;  }>;  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: "",            });             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,            });             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); 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?.providerDataJson).toContain('"group":"repos"');      expect(resolvedGet.metadata?.inputSchemaJson).toBeDefined();    }),  );   it.scoped("accepts OpenApi.fromApi generated specs", () =>    Effect.gen(function* () {      const server = yield* makeTestServer;       const extracted = yield* createOpenApiToolsFromSpec({        sourceName: "generated",        openApiSpec: generatedOpenApiSpec,        baseUrl: server.baseUrl,        namespace: "source.generated",      });       expect(Object.keys(extracted.tools).sort()).toEqual([        "source.generated.repos.createIssue",        "source.generated.repos.getRepo",      ]);       const getRepo = resolveToolExecutor(extracted.tools, "source.generated.repos.getRepo");      const result = yield* Effect.promise(() =>        getRepo({ owner: "octocat", repo: "hello-world" }),      );       expect(result).toEqual({        full_name: "octocat/hello-world",        include: null,      });      expect(server.requests).toHaveLength(1);      expect(server.requests[0]?.path).toBe("/repos/octocat/hello-world");    }),  );   it.scoped("executes extracted tools against HTTP endpoint", () =>    Effect.gen(function* () {      const server = yield* makeTestServer;       const extracted = yield* createOpenApiToolsFromSpec({        sourceName: "demo",        openApiSpec: generatedOpenApiSpec,        baseUrl: server.baseUrl,        namespace: "source.demo",      });       const getRepo = resolveToolExecutor(extracted.tools, "source.demo.repos.getRepo");      const createIssue = resolveToolExecutor(extracted.tools, "source.demo.repos.createIssue");       const getResult = yield* Effect.promise(() =>        getRepo({          path: { owner: "octocat", repo: "hello-world" },          query: { include: "all" },        }),      );       expect(getResult).toEqual({        full_name: "octocat/hello-world",        include: "all",      });       const postResult = yield* Effect.promise(() =>        createIssue({          owner: "octocat",          repo: "hello-world",          body: { title: "Bug report" },        }),      );       expect(postResult).toEqual({        created: true,        owner: "octocat",        repo: "hello-world",        body: { title: "Bug report" },      });       const missingBody = yield* Effect.either(        Effect.tryPromise({          try: () =>            createIssue({              owner: "octocat",              repo: "hello-world",            }),          catch: (error: unknown) =>            error instanceof Error ? error : new Error(String(error)),        }),      );       assertTrue(missingBody._tag === "Left");      if (missingBody._tag === "Left" && missingBody.left instanceof Error) {        assertInclude(missingBody.left.message, "Missing required request body");      }       expect(        server.requests.map(          (request: TestServer["requests"][number]) => request.path,        ),      ).toEqual([        "/repos/octocat/hello-world",        "/repos/octocat/hello-world/issues",      ]);      expect(server.requests[0]?.query).toContain("include=all");    }),  );   it.effect("throws on non-2xx OpenAPI responses", () =>    Effect.gen(function* () {      const manifest = yield* extractOpenApiManifest("generated", generatedOpenApiSpec);      const httpClientLayer = Layer.succeed(        HttpClient.HttpClient,        HttpClient.make((request) =>          Effect.sync(() =>            HttpClientResponse.fromWeb(              request,              new Response(                JSON.stringify({ error: "missing" }),                {                  status: 404,                  headers: {                    "content-type": "application/json",                  },                },              ),            ),          ),        ),      );      const tools = createOpenApiToolsFromManifest({        manifest,        baseUrl: "https://example.com",        namespace: "source.generated",        httpClientLayer,      });      const getRepo = resolveToolExecutor(tools, "source.generated.repos.getRepo");      const failure = yield* Effect.either(        Effect.tryPromise({          try: () => getRepo({ owner: "octocat", repo: "hello-world" }),          catch: (error: unknown) =>            error instanceof Error ? error : new Error(String(error)),        }),      );       assertTrue(failure._tag === "Left");      if (failure._tag === "Left" && failure.left instanceof Error) {        assertInclude(failure.left.message, "HTTP 404");      }    }),  );   it.effect("preserves base-path URLs when invoking tools", () =>    Effect.gen(function* () {      const manifest = yield* extractOpenApiManifest("generated", generatedOpenApiSpec);      let capturedUrl: string | null = null;      const httpClientLayer = Layer.succeed(        HttpClient.HttpClient,        HttpClient.make((request, url) =>          Effect.sync(() => {            capturedUrl = url.toString();             return HttpClientResponse.fromWeb(              request,              new Response(                JSON.stringify({                  full_name: "octocat/hello-world",                  include: null,                }),                {                  status: 200,                  headers: {                    "content-type": "application/json",                  },                },              ),            );          })        ),      );      const tools = createOpenApiToolsFromManifest({        manifest,        baseUrl: "https://example.com/api/v3",        namespace: "source.generated",        httpClientLayer,      });      const getRepo = resolveToolExecutor(tools, "source.generated.repos.getRepo");      const result = yield* Effect.promise(() =>        getRepo({ owner: "octocat", repo: "hello-world" }),      );       expect(result).toEqual({        full_name: "octocat/hello-world",        include: null,      });       expect(capturedUrl).toBe("https://example.com/api/v3/repos/octocat/hello-world");    }),  );   it.scoped("falls back to path-template args when extracted path parameters are missing", () =>    Effect.gen(function* () {      const server = yield* makeTestServer;       const extracted = yield* createOpenApiToolsFromSpec({        sourceName: "generated",        openApiSpec: generatedOpenApiSpec,        baseUrl: server.baseUrl,        namespace: "source.generated",      });       const manifestWithoutPathParameters = {        ...extracted.manifest,        tools: extracted.manifest.tools.map((tool) =>          tool.toolId === "repos/getRepo"            ? {                ...tool,                invocation: {                  ...tool.invocation,                  parameters: [],                },              }            : tool),

Lines 1–500 of 574.