1439 lines · 51.0 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import { Effect, Fiber, Schema } from "effect"; import {  AuthTemplateSlug,  ConnectionName,  ElicitationResponse,  FormElicitation,  IntegrationSlug,  OAuthClientSlug,  ProviderItemId,  ProviderKey,  ToolName,  ToolResult,  createExecutor,  definePlugin,  type AnyPlugin,  type CredentialProvider,  type Elicit,  type ToolDef,} from "@executor-js/sdk";import {  makeTestConfig,  memoryCredentialsPlugin,  serveOAuthTestServer,  typeCheckOutputTypeScript,} from "@executor-js/sdk/testing";import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs";import { createExecutionEngine } from "./engine";import {  describeTool,  makeExecutorToolInvoker,  searchTools,  type ToolDiscoveryProvider,} from "./tool-invoker"; // ---------------------------------------------------------------------------// v2 port. The v1 suite modelled namespaces as `staticSources` whose tools// surfaced in `tools.list()` at 2-segment ids (`github.getRepositoryDetails`).// In v2 tools are produced per-connection via `resolveTools` and addressed// `tools.<integration>.<owner>.<connection>.<tool>`. Each test plugin below// registers an integration + a memory credential provider, produces its tools// through `resolveTools`, and dispatches them in `invokeTool`. The harness// creates one `main` org connection per integration, so the sandbox-callable// path is `<integration>.org.main.<tool>`.// --------------------------------------------------------------------------- const codeExecutor = makeQuickJsExecutor(); // Standard-schema validators — used by `invokeTool` to validate args and emit// the `Missing key` issues that surface as `invalid_tool_arguments`.type Validator = ReturnType<typeof Schema.toStandardSchemaV1>; const RepoValidator: Validator = Schema.toStandardSchemaV1(  Schema.Struct({ owner: Schema.String, repo: Schema.String }),);const ContactValidator: Validator = Schema.toStandardSchemaV1(  Schema.Struct({ email: Schema.String }),);const EmptyValidator: Validator = Schema.toStandardSchemaV1(Schema.Struct({})); // Plain JSON Schema objects — stored on the produced ToolDef and rendered by// the describe TypeScript-preview path. (ToolDef schemas are opaque JSON to// core, exactly like the openapi plugin's spec-derived schemas.)const RepoInputJson = {  type: "object",  properties: { owner: { type: "string" }, repo: { type: "string" } },  required: ["owner", "repo"],} as const;const RepoDetailsOutputJson = {  type: "object",  properties: { defaultBranch: { type: "string" } },  required: ["defaultBranch"],} as const;const ContactInputJson = {  type: "object",  properties: { email: { type: "string" } },  required: ["email"],} as const;const EmptyInputJson = { type: "object", properties: {} } as const; const acceptAll = () => Effect.succeed(ElicitationResponse.make({ action: "accept" })); const TEMPLATE = AuthTemplateSlug.make("apiKey");const CONN = ConnectionName.make("main");const OAUTH_TEMPLATE = AuthTemplateSlug.make("oauth");const OAUTH_CLIENT = OAuthClientSlug.make("records-app"); type DescribedToolContract = {  readonly outputTypeScript: string;  readonly typeScriptDefinitions: Record<string, string>;}; const typeCheckDescribedInvocation = (  described: DescribedToolContract,  runtimeResult: unknown,  consumerSource: string,): readonly string[] =>  typeCheckOutputTypeScript(described, runtimeResult, {    consumerSource,    fileName: "described-tool-contract.ts",    typeName: "ToolOutput",    valueName: "invokedResult",  }); // ---------------------------------------------------------------------------// Test plugin builder — registers one integration, produces N tools via// resolveTools, and dispatches them in invokeTool. Handlers receive the args// already validated against the tool's standard input schema (so invalid args// surface as a ToolInvocationError → invalid_tool_arguments value).// --------------------------------------------------------------------------- type ToolHandlerInput = {  readonly args: unknown;  readonly elicit: Elicit;}; type TestToolSpec = {  readonly name: string;  readonly description: string;  /** Plain JSON Schema stored on the produced ToolDef. */  readonly inputJsonSchema?: unknown;  readonly outputJsonSchema?: unknown;  /** Standard-schema validator applied to args in `invokeTool`. */  readonly validator?: Validator;  readonly handler: (input: ToolHandlerInput) => Effect.Effect<unknown, unknown>;}; const memoryProvider = (key: string): CredentialProvider => {  const store = new Map<string, string>();  return {    key: ProviderKey.make(key),    writable: true,    get: (id) => Effect.sync(() => store.get(String(id)) ?? null),    set: (id, value) => Effect.sync(() => void store.set(String(id), value)),    has: (id) => Effect.sync(() => store.has(String(id))),    list: () =>      Effect.sync(() =>        Array.from(store.keys()).map((entryKey) => ({          id: ProviderItemId.make(entryKey),          name: entryKey,        })),      ),  };}; const validateArgs = (  validator: Validator | undefined,  args: unknown,): Effect.Effect<unknown, unknown> => {  if (validator == null) return Effect.succeed(args);  return Effect.promise(() => Promise.resolve(validator["~standard"].validate(args))).pipe(    Effect.flatMap((result) =>      "value" in result ? Effect.succeed(result.value) : Effect.fail(result),    ),  );}; const makeTestPlugin = (config: {  readonly pluginId: string;  readonly integration: string;  readonly tools: readonly TestToolSpec[];}) => {  const slug = IntegrationSlug.make(config.integration);  const byName = new Map(config.tools.map((spec) => [spec.name, spec] as const));  return definePlugin(() => ({    id: config.pluginId,    credentialProviders: [memoryProvider(`${config.pluginId}-memory`)],    storage: () => ({}),    resolveTools: () =>      Effect.succeed({        tools: config.tools.map(          (spec): ToolDef => ({            name: ToolName.make(spec.name),            description: spec.description,            inputSchema: spec.inputJsonSchema,            outputSchema: spec.outputJsonSchema,          }),        ),      }),    invokeTool: ({ toolRow, args, elicit }) => {      const spec = byName.get(toolRow.name);      if (!spec) return Effect.succeed(undefined);      return validateArgs(spec.validator, args).pipe(        Effect.flatMap((decoded) => spec.handler({ args: decoded, elicit })),      );    },    extension: (ctx) => ({      seed: () =>        ctx.core.integrations.register({          slug,          description: config.integration,          config: {},        }),    }),  }))();}; const githubPlugin = makeTestPlugin({  pluginId: "github-test",  integration: "github",  tools: [    {      name: "listRepositoryIssues",      description: "List issues for a repository",      inputJsonSchema: RepoInputJson,      validator: RepoValidator,      handler: () => Effect.succeed([]),    },    {      name: "getRepositoryDetails",      description: "Get repository details including the default branch",      inputJsonSchema: RepoInputJson,      validator: RepoValidator,      outputJsonSchema: RepoDetailsOutputJson,      handler: () => Effect.succeed({ defaultBranch: "main" }),    },    {      name: "searchDocs",      description: "Search GitHub API documentation",      inputJsonSchema: EmptyInputJson,      validator: EmptyValidator,      handler: () => Effect.succeed([]),    },  ],}); const crmPlugin = makeTestPlugin({  pluginId: "crm-test",  integration: "crm",  tools: [    {      name: "createContact",      description: "Create a CRM contact record",      inputJsonSchema: ContactInputJson,      validator: ContactValidator,      handler: () => Effect.succeed({ id: "contact_1" }),    },    {      name: "listContacts",      description: "List CRM contacts",      inputJsonSchema: EmptyInputJson,      validator: EmptyValidator,      handler: () => Effect.succeed([]),    },  ],}); const errorPlugin = makeTestPlugin({  pluginId: "error-test",  integration: "records",  tools: [    {      name: "queryRows",      description: "Query rows",      inputJsonSchema: EmptyInputJson,      validator: EmptyValidator,      handler: () =>        Effect.succeed(          ToolResult.fail({            code: "invalid_query",            message: 'Field with name "DisplayName" does not exist',          }),        ),    },  ],}); const oauthErrorPlugin = definePlugin(() => ({  id: "oauth-error-test" as const,  storage: () => ({}),  resolveTools: () =>    Effect.succeed({      tools: [        {          name: ToolName.make("queryRows"),          description: "Query rows",          inputSchema: EmptyInputJson,        },      ],    }),  invokeTool: ({ credential }) => Effect.succeed({ token: credential.value }),  extension: (ctx) => ({    seed: () =>      ctx.core.integrations.register({        slug: IntegrationSlug.make("oauth_records"),        description: "OAuth records",        config: {},      }),  }),}))(); const validatedInputPlugin = makeTestPlugin({  pluginId: "validated-input-test",  integration: "validated",  tools: [    {      name: "getRepositoryDetails",      description: "Get repository details including the default branch",      inputJsonSchema: RepoInputJson,      validator: RepoValidator,      outputJsonSchema: RepoDetailsOutputJson,      handler: () => Effect.succeed({ defaultBranch: "main" }),    },  ],}); const structuredFailurePlugin = makeTestPlugin({  pluginId: "structured-failure-test",  integration: "upstream",  tools: [    {      name: "nestedErrorBody",      description: "",      inputJsonSchema: EmptyInputJson,      validator: EmptyValidator,      handler: () =>        Effect.succeed(          ToolResult.fail({            code: "upstream_http_error",            status: 400,            message: 'The expression "foo" is not valid. Provide a valid expression.',            details: {              error: {                code: "invalidRequest",                message: 'The expression "foo" is not valid. Provide a valid expression.',              },            },          }),        ),    },    {      name: "flatErrorBody",      description: "",      inputJsonSchema: EmptyInputJson,      validator: EmptyValidator,      handler: () =>        Effect.succeed(          ToolResult.fail({            code: "upstream_http_error",            status: 400,            message: "Field 'XYZ' does not exist",            details: {              errorCode: 400,              errorMessage: "Field 'XYZ' does not exist",            },          }),        ),    },    {      name: "errorsArrayBody",      description: "",      inputJsonSchema: EmptyInputJson,      validator: EmptyValidator,      handler: () =>        Effect.succeed(          ToolResult.fail({            code: "upstream_http_error",            status: 403,            message: "Insufficient scope",            details: {              errors: [{ status: "403", title: "Forbidden", detail: "Insufficient scope" }],            },          }),        ),    },  ],}); // Provision: register each plugin's integration and create one org `main`// connection so per-connection tools exist and are addressable.const provision = (  executor: {    readonly connections: {      readonly create: (input: {        readonly owner: "org";        readonly name: typeof CONN;        readonly integration: ReturnType<typeof IntegrationSlug.make>;        readonly template: typeof TEMPLATE;        readonly value: string;      }) => Effect.Effect<unknown, unknown>;    };  } & Record<string, { readonly seed: () => Effect.Effect<unknown, unknown> }>,  specs: readonly { readonly pluginId: string; readonly integration: string }[],): Effect.Effect<void, unknown> =>  Effect.gen(function* () {    for (const spec of specs) {      yield* executor[spec.pluginId]!.seed();      yield* executor.connections.create({        owner: "org",        name: CONN,        integration: IntegrationSlug.make(spec.integration),        template: TEMPLATE,        value: "token",      });    }  }); const makeExecutorWith = <const TPlugins extends readonly AnyPlugin[]>(plugins: TPlugins) =>  createExecutor(makeTestConfig({ plugins })); const makeSearchExecutor = () =>  Effect.gen(function* () {    const executor = yield* makeExecutorWith([githubPlugin, crmPlugin] as const);    yield* provision(executor as never, [      { pluginId: "github-test", integration: "github" },      { pluginId: "crm-test", integration: "crm" },    ]);    return executor;  }); describe("tool discovery", () => {  it.effect("ranks matches using ids, namespaces, camelCase names, and descriptions", () =>    Effect.gen(function* () {      const executor = yield* makeSearchExecutor();       const githubMatches = yield* searchTools(executor, "github issues", 5);      expect(githubMatches.items.map((match) => match.path)).toEqual([        "github.org.main.listRepositoryIssues",      ]);      expect(githubMatches.items[0]?.score ?? 0).toBeGreaterThan(0);      expect(githubMatches.hasMore).toBe(false);      expect(githubMatches.nextOffset).toBeNull();       const repoMatches = yield* searchTools(executor, "repo details", 5);      expect(repoMatches.items[0]?.path).toBe("github.org.main.getRepositoryDetails");       const crmMatches = yield* searchTools(executor, "crm create contact", 5);      expect(crmMatches.items[0]?.path).toBe("crm.org.main.createContact");      expect(crmMatches.items[0]?.score ?? 0).toBeGreaterThan(crmMatches.items[1]?.score ?? 0);    }),  );   it.effect("returns no matches for empty queries instead of listing arbitrary tools", () =>    Effect.gen(function* () {      const executor = yield* makeSearchExecutor();      const matches = yield* searchTools(executor, "", 5);      expect(matches.items).toEqual([]);      expect(matches.total).toBe(0);      expect(matches.hasMore).toBe(false);      expect(matches.nextOffset).toBeNull();    }),  );   it.effect("paginates ranked matches via limit + offset with hasMore + nextOffset", () =>    Effect.gen(function* () {      const executor = yield* makeSearchExecutor();       // "list" matches `listRepositoryIssues`, `searchDocs` (description has      // "documentation" which tokenises adjacent), `listContacts`, etc.      // The exact match set isn't important — the pagination invariants are.      const all = yield* searchTools(executor, "list", 100);      expect(all.items.length).toBeGreaterThan(1);      expect(all.total).toBe(all.items.length);      expect(all.hasMore).toBe(false);      expect(all.nextOffset).toBeNull();       // First page (limit 1) — matches truncate, hasMore + nextOffset surface.      const firstPage = yield* searchTools(executor, "list", 1);      expect(firstPage.items).toEqual([all.items[0]]);      expect(firstPage.total).toBe(all.total);      expect(firstPage.hasMore).toBe(true);      expect(firstPage.nextOffset).toBe(1);       // Second page using nextOffset — order matches the un-paginated rank.      const secondPage = yield* searchTools(executor, "list", 1, {        offset: firstPage.nextOffset!,      });      expect(secondPage.items).toEqual([all.items[1]]);      expect(secondPage.total).toBe(all.total);      // Whether hasMore is true depends on total; at minimum it's consistent.      expect(secondPage.hasMore).toBe(all.total > 2);      expect(secondPage.nextOffset).toBe(secondPage.hasMore ? 2 : null);       // Offset past the end — empty page, no more.      const past = yield* searchTools(executor, "list", 5, { offset: all.total + 10 });      expect(past.items).toEqual([]);      expect(past.total).toBe(all.total);      expect(past.hasMore).toBe(false);      expect(past.nextOffset).toBeNull();    }),  );   it.effect("can narrow discovery to a namespace", () =>    Effect.gen(function* () {      const executor = yield* makeSearchExecutor();       const githubOnly = yield* searchTools(executor, "list", 5, {        namespace: "github",      });      expect(githubOnly.items.map((match) => match.path)).toEqual([        "github.org.main.listRepositoryIssues",      ]);       const crmOnly = yield* searchTools(executor, "list", 5, {        namespace: "crm",      });      expect(crmOnly.items.map((match) => match.path)).toEqual(["crm.org.main.listContacts"]);       const sandboxResult = yield* createExecutionEngine({ executor, codeExecutor }).execute(

Lines 1–500 of 1439.