1607 lines · 50.7 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import { Effect, Predicate, Schema } from "effect";import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";import { FetchHttpClient, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"; import {  createExecutor,  definePlugin,  type FumaDb,  RemoveSecretInput,  Scope,  ScopeId,  SecretId,  SetSecretInput,  SetSourceCredentialBindingInput,  RemoveSourceCredentialBindingInput,  type InvokeOptions,  type SecretProvider,} from "@executor-js/sdk";import { makeTestConfig, memorySecretsPlugin } from "@executor-js/sdk/testing";import type { ConfigFileSink } from "@executor-js/config"; const TEST_SCOPE = "test-scope";import { openApiPlugin } from "./plugin";import { ConfiguredHeaderBinding, OAuth2SourceConfig } from "./types";import {  addOpenApiTestSource,  makeOpenApiHttpApiTestSourceConfig,  serveOpenApiHttpApiTestServer,  unwrapInvocation,} from "../testing"; const autoApprove: InvokeOptions = { onElicitation: "accept-all" }; type FumaQueryCall = {  readonly method: "findFirst" | "findMany";  readonly table: string;}; const recordFumaQueries = (db: FumaDb, calls: FumaQueryCall[]): FumaDb =>  new Proxy(db, {    get(target, prop, receiver) {      const value = Reflect.get(target, prop, receiver);      if (prop !== "findFirst" && prop !== "findMany") return value;      return (table: string, ...args: readonly unknown[]) => {        calls.push({ method: prop, table });        return (value as (tableName: string, ...innerArgs: readonly unknown[]) => unknown).call(          target,          table,          ...args,        );      };    },  }); // ---------------------------------------------------------------------------// Define a test API with Effect HttpApi// --------------------------------------------------------------------------- const Item = Schema.Struct({  id: Schema.Number,  name: Schema.String,});type Item = typeof Item.Type; const EchoHeaders = Schema.Struct({  authorization: Schema.optional(Schema.String),  "x-static": Schema.optional(Schema.String),});type EchoHeaders = typeof EchoHeaders.Type; 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); type TestApiSourceOptions = Omit<  Parameters<typeof makeOpenApiHttpApiTestSourceConfig>[1],  "scope"> & {  readonly scope?: string;}; const testApiSourceConfig = (options: TestApiSourceOptions = {}) =>  makeOpenApiHttpApiTestSourceConfig(TestApi, {    scope: TEST_SCOPE,    ...options,  }); const testApiSpec = () => {  const spec = testApiSourceConfig().spec;  return spec.kind === "blob" ? spec.value : spec.url;}; // ---------------------------------------------------------------------------// 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) => Item.make(item))))    .handle("getItem", (req) =>      Effect.succeed(        Item.make(          ITEMS.find((i) => i.id === req.params.itemId) ?? {            id: 0,            name: "Unknown",          },        ),      ),    )    .handle("echoHeaders", () =>      Effect.gen(function* () {        const req = yield* HttpServerRequest.HttpServerRequest;        return EchoHeaders.make({          authorization: req.headers["authorization"],          "x-static": req.headers["x-static"],        });      }),    )    .handle("queryRows", () =>      Effect.fail(        new QueryValidationError({          message: 'Field with name "DisplayName" does not exist',        }),      ),    ),); const servePluginTestApi = () =>  serveOpenApiHttpApiTestServer({    api: TestApi,    handlersLayer: ItemsGroupLive,  }); const serveSpecRequiringHeader = () => {  const state = { requests: 0, lastToken: null as string | null };  return serveOpenApiHttpApiTestServer({    api: TestApi,    handlersLayer: ItemsGroupLive,    transformSpec: (spec) => {      const { servers: _servers, ...rest } = spec;      return rest;    },    guardSpecRequest: (request) =>      Effect.sync(() => {        state.requests++;        state.lastToken = request.headers["x-spec-token"] ?? null;        if (state.requests === 1) {          return null;        }        if (state.lastToken !== "org-token") {          return HttpServerResponse.jsonUnsafe({ error: "missing token" }, { status: 401 });        }        return null;      }),  }).pipe(    Effect.map((server) => ({      specUrl: server.specUrl,      requestCount: () => state.requests,      lastToken: () => state.lastToken,    })),  );}; // ---------------------------------------------------------------------------// Tests// --------------------------------------------------------------------------- describe("OpenAPI Plugin", () => {  it.effect("previewSpec returns metadata and header presets", () =>    Effect.scoped(      Effect.gen(function* () {        const server = yield* servePluginTestApi();         const executor = yield* createExecutor(          makeTestConfig({            plugins: [              openApiPlugin({ httpClientLayer: server.httpClientLayer }),              memorySecretsPlugin(),            ] as const,          }),        );         const preview = yield* executor.openapi.previewSpec(server.specJson);         expect(preview.operationCount).toBeGreaterThanOrEqual(2);        expect(preview.servers).toBeDefined();      }),    ),  );   it.effect("registers static openapi executor tools", () =>    Effect.gen(function* () {      const clientLayer = FetchHttpClient.layer;       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("executor.openapi.previewSpec");      expect(ids).toContain("executor.openapi.getSource");      expect(ids).toContain("executor.openapi.addSource");      expect(ids).toContain("executor.openapi.configureSource");    }),  );   it.effect("lists executor as the static runtime source", () =>    Effect.gen(function* () {      const clientLayer = FetchHttpClient.layer;       const executor = yield* createExecutor(        makeTestConfig({          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       const sources = yield* executor.sources.list();      expect(sources.find((s) => s.id === "openapi")).toBeUndefined();      const control = sources.find((s) => s.id === "executor");      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 clientLayer = FetchHttpClient.layer;       const executor = yield* createExecutor(        makeTestConfig({          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       const preview = unwrapInvocation(        yield* executor.tools.invoke(          "executor.openapi.previewSpec",          { spec: testApiSpec() },          autoApprove,        ),      ).data as { operationCount: number; operations?: unknown };       expect(preview.operationCount).toBeGreaterThanOrEqual(2);      expect(preview.operations).toBeUndefined();    }),  );   it.effect("describes static previewSpec output from Standard Schema", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(        makeTestConfig({          plugins: [openApiPlugin(), memorySecretsPlugin()] as const,        }),      );       const schema = yield* executor.tools.schema("executor.openapi.previewSpec");       expect(schema).not.toBeNull();      expect(schema!.outputTypeScript).toContain("servers:");      expect(schema!.outputTypeScript).toContain("securitySchemes:");      expect(schema!.outputTypeScript).toContain("oauth2Presets:");      expect(schema!.outputTypeScript).toContain("title: string | null");      expect(schema!.outputTypeScript).not.toContain("operations:");      expect(schema!.outputTypeScript).not.toContain("_tag");    }),  );   it.effect("describes static addSource parameters from Standard Schema", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(        makeTestConfig({          plugins: [openApiPlugin(), memorySecretsPlugin()] as const,        }),      );       const schema = yield* executor.tools.schema("executor.openapi.addSource");       expect(schema).not.toBeNull();      expect(schema!.inputTypeScript).not.toContain("scope: string");      expect(schema!.inputTypeScript).toContain('kind: "url"');      expect(        (schema!.inputSchema as { properties?: Record<string, unknown> }).properties,      ).not.toHaveProperty("credentialTargetScope");      expect(schema!.inputTypeScript).not.toBe("Record<string, unknown>");    }),  );   it.effect("invokes static addSource through executor.tools.invoke", () =>    Effect.gen(function* () {      const clientLayer = FetchHttpClient.layer;      const userScope = ScopeId.make("static-user");      const orgScope = ScopeId.make("static-org");       const executor = yield* createExecutor(        makeTestConfig({          scopes: [            Scope.make({ id: userScope, name: "user", createdAt: new Date() }),            Scope.make({ id: orgScope, name: "org", createdAt: new Date() }),          ],          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       const result = unwrapInvocation(        yield* executor.tools.invoke(          "executor.openapi.addSource",          (({ scope: _scope, ...input }) => input)(            testApiSourceConfig({ scope: "org", namespace: "runtime" }),          ),          autoApprove,        ),      ).data as { sourceId: string; source: { id: string; scope: string }; toolCount: number };       expect(result).toEqual({        sourceId: "runtime",        source: { id: "runtime", scope: String(orgScope) },        toolCount: 4,      });      expect(yield* executor.openapi.getSource("runtime", String(userScope))).toBeNull();      expect((yield* executor.openapi.getSource("runtime", String(orgScope)))?.scope).toBe(        orgScope,      );      const inspected = unwrapInvocation(        yield* executor.tools.invoke(          "executor.openapi.getSource",          { namespace: "runtime", scope: "org" },          autoApprove,        ),      ).data as { source: { namespace: string; scope: string } | null };      expect(inspected.source).toMatchObject({ namespace: "runtime", scope: String(orgScope) });      expect((yield* executor.tools.list()).map((t) => t.id)).toContain("runtime.items.listItems");    }),  );   it.effect("static previewSpec returns actionable tool failures", () =>    Effect.gen(function* () {      const config = makeTestConfig({ plugins: [openApiPlugin()] as const });      const executor = yield* createExecutor(config);       const result = yield* executor.tools.invoke("executor.openapi.previewSpec", {        spec: "not openapi",      });       expect(result).toMatchObject({        ok: false,        error: {          code: "openapi_parse_failed",        },      });       yield* executor.close();      yield* Effect.promise(() => config.testDb.close());    }),  );   it.effect("requires approval before adding a source through the runtime tool", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(        makeTestConfig({ plugins: [openApiPlugin()] as const }),      );       const declined = yield* executor.tools        .invoke(          "executor.openapi.addSource",          (({ scope: _scope, ...input }) => input)(            testApiSourceConfig({ namespace: "runtime_declined" }),          ),          {            onElicitation: () => Effect.succeed({ action: "decline" as const }),          },        )        .pipe(Effect.flip);       expect(Predicate.isTagged(declined, "ElicitationDeclinedError")).toBe(true);      expect(yield* executor.openapi.getSource("runtime_declined", TEST_SCOPE)).toBeNull();      expect((yield* executor.tools.list()).map((t) => t.id)).not.toContain(        "runtime_declined.items.listItems",      );    }),  );   it.effect("adds an org source whose direct credentials are owned by the user scope", () =>    Effect.gen(function* () {      const clientLayer = FetchHttpClient.layer;      const userScope = ScopeId.make("openapi-user");      const orgScope = ScopeId.make("openapi-org");       const executor = yield* createExecutor(        makeTestConfig({          scopes: [            Scope.make({              id: userScope,              name: "user",              createdAt: new Date(),            }),            Scope.make({ id: orgScope, name: "org", createdAt: new Date() }),          ],          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       yield* executor.secrets.set(        SetSecretInput.make({          id: SecretId.make("user-query-token"),          scope: userScope,          name: "User query token",          value: "user-token",        }),      );       const input = testApiSourceConfig({        scope: String(orgScope),        namespace: "org_direct_user_credential",        queryParams: { token: { kind: "secret" } },      });       yield* executor.openapi.addSpec(input);       const bindings = yield* executor.sources.listBindings({        source: { id: "org_direct_user_credential", scope: orgScope },      });      expect(bindings).toEqual([]);    }),  );   it.effect("sources.configure removes bindings for credential slots no longer present", () =>    Effect.gen(function* () {      const clientLayer = FetchHttpClient.layer;       const executor = yield* createExecutor(        makeTestConfig({          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       yield* executor.secrets.set(        SetSecretInput.make({          id: SecretId.make("old-token"),          scope: ScopeId.make(TEST_SCOPE),          name: "Old token",          value: "old-secret",        }),      );       yield* executor.openapi.addSpec(

Lines 1–500 of 1607.