1981 lines · 65.9 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import { Data, Effect, Exit, Predicate, Result, Schema } from "effect";import { FetchHttpClient } from "effect/unstable/http"; import { makeMemoryAdapter } from "@executor-js/storage-core/testing/memory";import type { DBAdapter, Where } from "@executor-js/storage-core"; import { makeInMemoryBlobStore } from "./blob";import { CreateConnectionInput, TokenMaterial } from "./connections";import { collectSchemas, createExecutor } from "./executor";import { ElicitationResponse, FormElicitation, UrlElicitation } from "./elicitation";import { defineSchema, definePlugin, tool } from "./plugin";import { RemoveSecretInput, SetSecretInput } from "./secrets";import { makeTestConfig } from "./testing";import type { SecretProvider } from "./secrets";import { ConnectionId, ScopeId, SecretId } from "./ids";import { Scope } from "./scope";import { SourceDetectionResult } from "./types"; type FindManyCall = {  readonly model: string;  readonly where?: readonly Where[];}; class TestPluginError extends Data.TaggedError("TestPluginError")<{  readonly message: string;}> {} 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);        },      }),    ),}); // ---------------------------------------------------------------------------// Tiny test plugin — declares a static source with two built-in tools, a// plugin schema for a per-row key/value table, and a dynamic invokeTool// handler. Exercises everything createExecutor has to wire up.// --------------------------------------------------------------------------- // Plugin-declared schema. `defineSchema` preserves literal types via// `const` inference — no `as const satisfies DBSchema` ceremony.const testSchema = defineSchema({  test_thing: {    fields: {      id: { type: "string", required: true },      value: { type: "string", required: true },    },  },}); let testAnnotationResolveCount = 0; const testPlugin = definePlugin(() => ({  id: "test" as const,  schema: testSchema,   // `adapter` is typed against testSchema automatically — no imports of  // DBAdapter, no typedAdapter wrapping. `model: "test_thing"` is  // narrowed to the schema's model names, and row data shape comes  // from the schema's field definitions.  storage: ({ adapter }) => ({    writeThing: (id: string, value: string) =>      adapter        .create({          model: "test_thing",          data: { id, value },          forceAllowId: true,        })        .pipe(Effect.asVoid),    readThing: (id: string) =>      adapter        .findOne({          model: "test_thing",          where: [{ field: "id", value: id }],        })        .pipe(Effect.map((row) => row?.value ?? null)),  }),  extension: (ctx) => ({    echo: (text: string) => Effect.succeed(`echo:${text}`),     addThing: (id: string, value: string) =>      ctx.transaction(        Effect.gen(function* () {          yield* ctx.storage.writeThing(id, value);          yield* ctx.core.sources.register({            id,            scope: ctx.scopes[0]!.id,            kind: "test",            name: id,            canRemove: true,            tools: [              { name: "read", description: "read the thing" },              { name: "write", description: "overwrite the thing" },            ],          });        }),      ),  }),  staticSources: (self) => [    {      id: "test.control",      kind: "control",      name: "Test Control",      tools: [        {          name: "echo",          description: "static echo tool",          handler: ({ args }) => self.echo((args as { text: string }).text),        },      ],    },  ],  invokeTool: ({ ctx, toolRow, args }) =>    Effect.gen(function* () {      // toolRow.source_id = the thing id (we registered the source with      // that id). toolRow.name = "read" | "write". No string splitting.      const thingId = toolRow.source_id;      if (toolRow.name === "read") {        return yield* ctx.storage.readThing(thingId);      }      if (toolRow.name === "write") {        const { value } = args as { value: string };        yield* ctx.storage.writeThing(thingId, value);        return { ok: true };      }      return yield* new TestPluginError({        message: `unknown tool ${toolRow.id}`,      });    }),   // Derived annotations: `write` gates on approval, `read` doesn't.  // Purely computed from the tool's name — no data persisted on the row.  resolveAnnotations: ({ toolRows }) =>    Effect.sync(() => {      testAnnotationResolveCount++;      const out: Record<string, { requiresApproval: boolean; approvalDescription?: string }> = {};      for (const row of toolRows) {        if (row.name === "write") {          out[row.id] = {            requiresApproval: true,            approvalDescription: `Overwrite ${row.source_id}`,          };        } else {          out[row.id] = { requiresApproval: false };        }      }      return out;    }),})); // ---------------------------------------------------------------------------// Test plugin that contributes an in-memory writable secret provider so// the secrets surface has something to talk to.// --------------------------------------------------------------------------- const memoryProvider: SecretProvider = (() => {  const store = new Map<string, string>();  const key = (scope: string, id: string) => `${scope}\u0000${id}`;  return {    key: "memory",    writable: true,    get: (id, scope) => Effect.sync(() => store.get(key(scope, id)) ?? null),    set: (id, value, scope) =>      Effect.sync(() => {        store.set(key(scope, id), value);      }),    delete: (id, scope) => Effect.sync(() => store.delete(key(scope, 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],})); const memoryConnectionPlugin = definePlugin(() => ({  id: "memory-connection" as const,  storage: () => ({}),  connectionProviders: [{ key: "memory-connection" }],})); const staleRouteProvider: SecretProvider = {  key: "stale-route",  writable: true,  get: () => Effect.succeed(null),  has: () => Effect.succeed(false),  set: () => Effect.void,  delete: () => Effect.succeed(false),  list: () => Effect.succeed([]),}; const staleRouteSecretsPlugin = definePlugin(() => ({  id: "stale-route-secrets" as const,  storage: () => ({}),  secretProviders: [staleRouteProvider],})); const opaqueRouteProvider: SecretProvider = {  key: "opaque-route",  writable: true,  get: () => Effect.succeed(null),  set: () => Effect.void,  delete: () => Effect.succeed(false),  list: () => Effect.succeed([]),}; const opaqueRouteSecretsPlugin = definePlugin(() => ({  id: "opaque-route-secrets" as const,  storage: () => ({}),  secretProviders: [opaqueRouteProvider],})); const providerOnlySecretPlugin = definePlugin(() => ({  id: "provider-only-secret" as const,  storage: () => ({}),  secretProviders: [    {      key: "provider-only",      writable: false,      get: (id: string) => Effect.succeed(id === "provider-token" ? "provider-value" : null),      list: () => Effect.succeed([{ id: "provider-token", name: "Provider token" }]),    } satisfies SecretProvider,  ],})); const fallbackOrderingPlugin = (laterCalls: { count: number }) =>  definePlugin(() => ({    id: "fallback-ordering" as const,    storage: () => ({}),    secretProviders: [      {        key: "first-fallback",        writable: false,        get: (id: string) => Effect.succeed(id === "token" ? "first-value" : null),        list: () => Effect.succeed([{ id: "token", name: "token" }]),      },      {        key: "later-fallback",        writable: false,        get: () =>          Effect.sync(() => {            laterCalls.count += 1;            return "later-value";          }),        list: () => Effect.succeed([{ id: "token", name: "token" }]),      },    ] satisfies readonly SecretProvider[],  })); const fallbackDisabledPlugin = definePlugin(() => ({  id: "fallback-disabled" as const,  storage: () => ({}),  secretProviders: [    {      key: "fallback-disabled-provider",      writable: false,      allowFallback: false,      get: (id: string) => Effect.succeed(id === "token" ? "disabled-value" : null),      list: () => Effect.succeed([{ id: "token", name: "token" }]),    },  ] satisfies readonly SecretProvider[],})); const duplicateToolsPlugin = definePlugin(() => ({  id: "duplicateTools" as const,  storage: () => ({}),  extension: (ctx) => ({    register: () =>      ctx.core.sources.register({        id: "dup-source",        scope: ctx.scopes[0]!.id,        kind: "test",        name: "Duplicate source",        tools: [          { name: "same", description: "first" },          { name: "same", description: "last" },        ],      }),  }),})); // ---------------------------------------------------------------------------// Tests// --------------------------------------------------------------------------- describe("createExecutor", () => {  it.effect("invokes a static tool via the in-memory pool", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(makeTestConfig({ plugins: [testPlugin()] as const }));      const result = yield* executor.tools.invoke(        "test.control.echo",        { text: "hi" },        { onElicitation: "accept-all" },      );      expect(result).toBe("echo:hi");    }),  );   it.effect("lists static tools alongside dynamic ones", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(makeTestConfig({ plugins: [testPlugin()] as const }));      yield* executor.test.addThing("thing1", "hello");       const tools = yield* executor.tools.list();      const ids = tools.map((t) => t.id);      expect(ids).toContain("test.control.echo");      expect(ids).toContain("thing1.read");      expect(ids).toContain("thing1.write");    }),  );   it.effect("filters tools by query", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(makeTestConfig({ plugins: [testPlugin()] as const }));      yield* executor.test.addThing("thing1", "hello");       const tools = yield* executor.tools.list({ query: "echo" });      expect(tools.map((t) => t.id)).toEqual(["test.control.echo"]);    }),  );   it.effect("pushes sourceId tool list filters into storage", () =>    Effect.gen(function* () {      const config = makeTestConfig({ plugins: [testPlugin()] as const });      const findManyCalls: FindManyCall[] = [];      const executor = yield* createExecutor({        ...config,        adapter: recordFindMany(config.adapter, findManyCalls),        onElicitation: "accept-all",      });      yield* executor.test.addThing("thing1", "hello");      yield* executor.test.addThing("thing2", "goodbye");       findManyCalls.length = 0;      const tools = yield* executor.tools.list({ sourceId: "thing1" });       expect(tools.map((t) => t.id).sort()).toEqual(["thing1.read", "thing1.write"]);      const toolRead = findManyCalls.find((call) => call.model === "tool");      expect(toolRead?.where).toEqual(        expect.arrayContaining([expect.objectContaining({ field: "source_id", value: "thing1" })]),      );    }),  );   it.effect("can list tools without resolving dynamic annotations", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(makeTestConfig({ plugins: [testPlugin()] as const }));      yield* executor.test.addThing("thing1", "hello");      testAnnotationResolveCount = 0;       const tools = yield* executor.tools.list({        sourceId: "thing1",        includeAnnotations: false,      });       expect(testAnnotationResolveCount).toBe(0);      expect(tools.map((t) => t.id).sort()).toEqual(["thing1.read", "thing1.write"]);      expect(tools.every((tool) => tool.annotations === undefined)).toBe(true);    }),  );   it.effect("bounds annotation resolution groups", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(makeTestConfig({ plugins: [testPlugin()] as const }));      for (let index = 0; index < 70; index++) {        yield* executor.test.addThing(`thing${index}`, "hello");      }      testAnnotationResolveCount = 0;       yield* executor.tools.list();       expect(testAnnotationResolveCount).toBe(64);    }),  );   it.effect("invokes a dynamic tool through plugin.invokeTool", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(makeTestConfig({ plugins: [testPlugin()] as const }));      yield* executor.test.addThing("thing1", "hello");       const result = yield* executor.tools.invoke(        "thing1.read",        {},        { onElicitation: "accept-all" },      );      expect(result).toBe("hello");    }),  );   it.effect("enforces tool annotations before invoking", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(makeTestConfig({ plugins: [testPlugin()] as const }));      yield* executor.test.addThing("thing1", "hello");      let approvalMessage = "";       // requiresApproval: true → declined → ElicitationDeclinedError      const declined = yield* executor.tools        .invoke(          "thing1.write",          { value: "updated" },          {            onElicitation: (ctx) =>              Effect.sync(() => {                approvalMessage = ctx.request.message;                return ElicitationResponse.make({ action: "decline" });              }),          },        )        .pipe(Effect.flip);      expect(Predicate.isTagged(declined, "ElicitationDeclinedError")).toBe(true);      expect(approvalMessage).toContain('"value": "updated"');       // auto-accept → succeeds      const accepted = yield* executor.tools.invoke(        "thing1.write",        { value: "updated" },        { onElicitation: "accept-all" },      );      expect(accepted).toEqual({ ok: true });    }),  );   it.effect("sources.list unions static runtime sources and dynamic ones", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(makeTestConfig({ plugins: [testPlugin()] as const }));      yield* executor.test.addThing("thing1", "hello");       const sources = yield* executor.sources.list();      const control = sources.find((s) => s.id === "test.control");      expect(control).toBeDefined();      expect(control!.runtime).toBe(true);      expect(control!.canRemove).toBe(false);       const dynamic = sources.find((s) => s.id === "thing1");      expect(dynamic).toBeDefined();      expect(dynamic!.runtime).toBe(false);      expect(dynamic!.canRemove).toBe(true);    }),  );   it.effect("orders source detection results by confidence", () =>    Effect.gen(function* () {      const lowConfidencePlugin = definePlugin(() => ({        id: "low-detect" as const,        storage: () => ({}),        detect: () =>          Effect.succeed(            SourceDetectionResult.make({              kind: "mcp",              confidence: "low",              endpoint: "https://example.com/source",              name: "Weak OAuth match",              namespace: "weak_oauth_match",            }),          ),      }));      const highConfidencePlugin = definePlugin(() => ({        id: "high-detect" as const,        storage: () => ({}),        detect: () =>          Effect.succeed(            SourceDetectionResult.make({              kind: "graphql",              confidence: "high",              endpoint: "https://example.com/source",              name: "GraphQL API",              namespace: "graphql_api",            }),          ),      }));       const executor = yield* createExecutor(        makeTestConfig({          plugins: [lowConfidencePlugin(), highConfidencePlugin()] as const,        }),      );       const results = yield* executor.sources.detect("https://example.com/source"); 

Lines 1–500 of 1981.