817 lines · 25.4 KB · TypeScript
RawDownload
import { expect, layer } from "@effect/vitest";import { Effect, Layer, Schema } from "effect";import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi";import { HttpClient, HttpRouter, HttpServerRequest } from "effect/unstable/http";import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; import {  createExecutor,  type DBAdapter,  definePlugin,  makeTestConfig,  Scope,  ScopeId,  SecretId,  SetSecretInput,  type InvokeOptions,  type SecretProvider,  type Where,} from "@executor-js/sdk"; const TEST_SCOPE = "test-scope";import { openApiPlugin } from "./plugin";import { OAuth2Auth } from "./types"; const autoApprove: InvokeOptions = { onElicitation: "accept-all" }; type FindManyCall = {  readonly model: string;  readonly where?: readonly Where[];}; const recordFindMany = (  adapter: DBAdapter,  calls: FindManyCall[],): DBAdapter => ({  ...adapter,  findMany: (data) => {    calls.push({ model: data.model, where: data.where });    return adapter.findMany(data);  },  transaction: (callback) =>    adapter.transaction((trx) =>      callback({        ...trx,        findMany: (data) => {          calls.push({ model: data.model, where: data.where });          return trx.findMany(data);        },      }),    ),}); // ---------------------------------------------------------------------------// In-memory secrets provider plugin — registered alongside openapi so// executor.secrets.set/get work in tests.// --------------------------------------------------------------------------- const memoryProvider: SecretProvider = (() => {  const store = new Map<string, string>();  return {    key: "memory",    writable: true,    get: (id, scope) =>      Effect.sync(() => store.get(`${scope}\u0000${id}`) ?? null),    set: (id, value, scope) =>      Effect.sync(() => {        store.set(`${scope}\u0000${id}`, value);      }),    delete: (id, scope) =>      Effect.sync(() => store.delete(`${scope}\u0000${id}`)),    list: () =>      Effect.sync(() =>        Array.from(store.keys()).map((k) => {          const name = k.split("\u0000", 2)[1] ?? k;          return { id: name, name };        }),      ),  };})(); const memorySecretsPlugin = definePlugin(() => ({  id: "memory-secrets" as const,  storage: () => ({}),  secretProviders: [memoryProvider],})); // ---------------------------------------------------------------------------// Define a test API with Effect HttpApi// --------------------------------------------------------------------------- class Item extends Schema.Class<Item>("Item")({  id: Schema.Number,  name: Schema.String,}) {} class EchoHeaders extends Schema.Class<EchoHeaders>("EchoHeaders")({  authorization: Schema.optional(Schema.String),  "x-static": Schema.optional(Schema.String),}) {} class QueryValidationError extends Schema.TaggedErrorClass<QueryValidationError>()(  "QueryValidationError",  {    message: Schema.String,  },) {} const ItemsGroup = HttpApiGroup.make("items")  .add(HttpApiEndpoint.get("listItems", "/items", { success: Schema.Array(Item) }))  .add(    HttpApiEndpoint.get("getItem", "/items/:itemId", {      params: Schema.Struct({ itemId: Schema.NumberFromString }),      success: Item,    }),  )  .add(HttpApiEndpoint.get("echoHeaders", "/echo-headers", { success: EchoHeaders }))  .add(    HttpApiEndpoint.get("queryRows", "/records/rows/:entryTypeId", {      params: Schema.Struct({ entryTypeId: Schema.String }),      success: Schema.Unknown,      error: QueryValidationError,    }),  ); const TestApi = HttpApi.make("testApi").add(ItemsGroup); const spec = OpenApi.fromApi(TestApi);const specJson = JSON.stringify(spec); // ---------------------------------------------------------------------------// Implement handlers// --------------------------------------------------------------------------- const ITEMS = [  { id: 1, name: "Widget" },  { id: 2, name: "Gadget" },  { id: 3, name: "Doohickey" },]; const ItemsGroupLive = HttpApiBuilder.group(TestApi, "items", (handlers) =>  handlers    .handle("listItems", () => Effect.succeed(ITEMS.map((item) => new Item(item))))    .handle("getItem", (req) =>      Effect.succeed(        new Item(          ITEMS.find((i) => i.id === req.params.itemId) ?? {            id: 0,            name: "Unknown",          },        ),      ),    )    .handle("echoHeaders", () =>      Effect.gen(function* () {        const req = yield* HttpServerRequest.HttpServerRequest;        return new EchoHeaders({          authorization: req.headers["authorization"],          "x-static": req.headers["x-static"],        });      }),    )    .handle("queryRows", () =>      Effect.fail(        new QueryValidationError({          message: 'Field with name "DisplayName" does not exist',        }),      ),    ),); // ---------------------------------------------------------------------------// Test layer: real server on port 0 + HttpClient pointing at it// --------------------------------------------------------------------------- const ApiLive = HttpApiBuilder.layer(TestApi).pipe(Layer.provide(ItemsGroupLive)); const TestLayer = HttpRouter.serve(ApiLive, { disableListenLog: true, disableLogger: true }).pipe(  Layer.provideMerge(NodeHttpServer.layerTest),); // ---------------------------------------------------------------------------// Tests// --------------------------------------------------------------------------- layer(TestLayer)("OpenAPI Plugin", (it) => {  it.effect("previewSpec returns metadata and header presets", () =>    Effect.gen(function* () {      const httpClient = yield* HttpClient.HttpClient;      const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient);       const executor = yield* createExecutor(        makeTestConfig({          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       const preview = yield* executor.openapi.previewSpec(specJson);       expect(preview.operationCount).toBeGreaterThanOrEqual(2);      expect(preview.servers).toBeDefined();    }),  );   it.effect("registers static openapi control tools", () =>    Effect.gen(function* () {      const httpClient = yield* HttpClient.HttpClient;      const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient);       const executor = yield* createExecutor(        makeTestConfig({          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       const tools = yield* executor.tools.list();      const ids = tools.map((t) => t.id);      expect(ids).toContain("openapi.previewSpec");      expect(ids).toContain("openapi.addSource");    }),  );   it.effect("lists openapi as a static runtime source", () =>    Effect.gen(function* () {      const httpClient = yield* HttpClient.HttpClient;      const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient);       const executor = yield* createExecutor(        makeTestConfig({          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       const sources = yield* executor.sources.list();      const control = sources.find((s) => s.id === "openapi");      expect(control).toBeDefined();      expect(control!.runtime).toBe(true);      expect(control!.canRemove).toBe(false);    }),  );   it.effect("invokes static previewSpec through executor.tools.invoke", () =>    Effect.gen(function* () {      const httpClient = yield* HttpClient.HttpClient;      const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient);       const executor = yield* createExecutor(        makeTestConfig({          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       const result = (yield* executor.tools.invoke(        "openapi.previewSpec",        { spec: specJson },        autoApprove,      )) as { operationCount: number };       expect(result.operationCount).toBeGreaterThanOrEqual(2);    }),  );   it.effect("invokes static addSource through executor.tools.invoke", () =>    Effect.gen(function* () {      const httpClient = yield* HttpClient.HttpClient;      const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient);       const executor = yield* createExecutor(        makeTestConfig({          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       const result = (yield* executor.tools.invoke(        "openapi.addSource",        { spec: specJson, namespace: "runtime" },        autoApprove,      )) as { sourceId: string; toolCount: number };       expect(result).toEqual({ sourceId: "runtime", toolCount: 4 });      expect((yield* executor.tools.list()).map((t) => t.id)).toContain(        "runtime.items.listItems",      );    }),  );   it.effect("resolves secret-backed headers at invocation time", () =>    Effect.gen(function* () {      const httpClient = yield* HttpClient.HttpClient;      const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient);       const executor = yield* createExecutor(        makeTestConfig({          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       yield* executor.secrets.set(        new SetSecretInput({          id: SecretId.make("test-api-token"),          scope: ScopeId.make(TEST_SCOPE),          name: "Test API Token",          value: "secret-value-123",        }),      );       yield* executor.openapi.addSpec({        spec: specJson,        scope: TEST_SCOPE,        namespace: "authed",        baseUrl: "",        headers: {          Authorization: { secretId: "test-api-token", prefix: "Bearer " },          "X-Static": "hello",        },      });       const result = (yield* executor.tools.invoke(        "authed.items.echoHeaders",        {},        autoApprove,      )) as { data: { authorization?: string; "x-static"?: string } | null; error: unknown };       expect(result.error).toBeNull();      const data = result.data!;      expect(data.authorization).toBe("Bearer secret-value-123");      expect(data["x-static"]).toBe("hello");    }),  );   it.effect("fails clearly when a secret is missing", () =>    Effect.gen(function* () {      const httpClient = yield* HttpClient.HttpClient;      const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient);       const executor = yield* createExecutor(        makeTestConfig({          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       yield* executor.openapi.addSpec({        spec: specJson,        scope: TEST_SCOPE,        namespace: "noauth",        baseUrl: "",        headers: {          Authorization: { secretId: "missing-token", prefix: "Bearer " },        },      });       const error = yield* Effect.flip(        executor.tools.invoke("noauth.items.listItems", {}, autoApprove),      );       expect((error as { _tag: string })._tag).toBe("ToolInvocationError");      expect((error as { message: string }).message).toContain("missing-token");    }),  );   it.effect("registers tools from an OpenAPI spec", () =>    Effect.gen(function* () {      const httpClient = yield* HttpClient.HttpClient;      const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient);      const executor = yield* createExecutor(        makeTestConfig({          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       const result = yield* executor.openapi.addSpec({        spec: specJson,        scope: TEST_SCOPE,        namespace: "test",        baseUrl: "",      });       expect(result.toolCount).toBeGreaterThanOrEqual(2);       const tools = yield* executor.tools.list();      const names = tools.map((t) => t.name);      expect(names).toContain("items.listItems");      expect(names).toContain("items.getItem");    }),  );   it.effect("invokes listItems", () =>    Effect.gen(function* () {      const httpClient = yield* HttpClient.HttpClient;      const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient);      const executor = yield* createExecutor(        makeTestConfig({          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       yield* executor.openapi.addSpec({        spec: specJson,        scope: TEST_SCOPE,        namespace: "test",        baseUrl: "",      });       const result = (yield* executor.tools.invoke(        "test.items.listItems",        {},        autoApprove,      )) as { data: unknown; error: unknown };      expect(result.error).toBeNull();      expect(result.data).toEqual(ITEMS);    }),  );   it.effect("invokes getItem with path parameter", () =>    Effect.gen(function* () {      const httpClient = yield* HttpClient.HttpClient;      const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient);      const executor = yield* createExecutor(        makeTestConfig({          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       yield* executor.openapi.addSpec({        spec: specJson,        scope: TEST_SCOPE,        namespace: "test",        baseUrl: "",      });       const result = (yield* executor.tools.invoke(        "test.items.getItem",        { itemId: "2" },        autoApprove,      )) as { data: unknown; error: unknown };      expect(result.error).toBeNull();      expect(result.data).toEqual({ id: 2, name: "Gadget" });    }),  );   it.effect("surfaces structured validation errors from OpenAPI tool calls", () =>    Effect.gen(function* () {      const httpClient = yield* HttpClient.HttpClient;      const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient);      const executor = yield* createExecutor(        makeTestConfig({          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       yield* executor.openapi.addSpec({        spec: specJson,        scope: TEST_SCOPE,        namespace: "records",        baseUrl: "",      });       const result = (yield* executor.tools.invoke(        "records.items.queryRows",        {          entryTypeId: "18538",          query: JSON.stringify([{ DisplayName: "Example" }]),          limit: 10,          skip: 0,        },        autoApprove,      )) as { data: unknown; error: unknown }; 

Lines 1–500 of 817.