Rhys Sullivanfmtc92c57d6 months agoHistory
577 lines · 20.0 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import { Effect, Fiber } from "effect"; import {  ElicitationResponse,  FormElicitation,  createExecutor,  definePlugin,  makeTestConfig,} from "@executor-js/sdk";import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs";import { createExecutionEngine } from "./engine";import { describeTool, makeExecutorToolInvoker, searchTools } from "./tool-invoker"; const codeExecutor = makeQuickJsExecutor(); const RepoInputSchema = {  type: "object",  properties: {    owner: { type: "string" },    repo: { type: "string" },  },  required: ["owner", "repo"],  additionalProperties: false,} as const; const ContactInputSchema = {  type: "object",  properties: {    email: { type: "string" },  },  required: ["email"],  additionalProperties: false,} as const; const EmptyInputSchema = {  type: "object",  properties: {},  additionalProperties: false,} as const; const acceptAll = () => Effect.succeed(new ElicitationResponse({ action: "accept" })); // ---------------------------------------------------------------------------// Test plugins — each one declares a namespace as a static source with N// tools. Handlers return static data; the suite only cares about discovery// + elicitation flow, not real invocation semantics.// --------------------------------------------------------------------------- const githubPlugin = definePlugin(() => ({  id: "github-test" as const,  storage: () => ({}),  staticSources: () => [    {      id: "github",      kind: "in-memory",      name: "GitHub",      tools: [        {          name: "listRepositoryIssues",          description: "List issues for a repository",          inputSchema: RepoInputSchema,          handler: () => Effect.succeed([]),        },        {          name: "getRepositoryDetails",          description: "Get repository details including the default branch",          inputSchema: RepoInputSchema,          handler: () => Effect.succeed({ defaultBranch: "main" }),        },        {          name: "searchDocs",          description: "Search GitHub API documentation",          inputSchema: EmptyInputSchema,          handler: () => Effect.succeed([]),        },      ],    },  ],})); const crmPlugin = definePlugin(() => ({  id: "crm-test" as const,  storage: () => ({}),  staticSources: () => [    {      id: "crm",      kind: "in-memory",      name: "CRM",      tools: [        {          name: "createContact",          description: "Create a CRM contact record",          inputSchema: ContactInputSchema,          handler: () => Effect.succeed({ id: "contact_1" }),        },        {          name: "listContacts",          description: "List CRM contacts",          inputSchema: EmptyInputSchema,          handler: () => Effect.succeed([]),        },      ],    },  ],})); const errorPlugin = definePlugin(() => ({  id: "error-test" as const,  storage: () => ({}),  staticSources: () => [    {      id: "records",      kind: "in-memory",      name: "Records",      tools: [        {          name: "queryRows",          description: "Query rows",          inputSchema: EmptyInputSchema,          handler: () =>            Effect.succeed({              data: null,              error: {                message: 'Field with name "DisplayName" does not exist',                code: "invalid_query",              },            }),        },      ],    },  ],})); const makeSearchExecutor = () =>  createExecutor(makeTestConfig({ plugins: [githubPlugin(), crmPlugin()] as const })); 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.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.getRepositoryDetails");       const crmMatches = yield* searchTools(executor, "crm create contact", 5);      expect(crmMatches.items[0]?.path).toBe("crm.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.listRepositoryIssues"]);       const crmOnly = yield* searchTools(executor, "list", 5, {        namespace: "crm",      });      expect(crmOnly.items.map((match) => match.path)).toEqual(["crm.listContacts"]);       const sandboxResult = yield* createExecutionEngine({ executor, codeExecutor }).execute(        'return await tools.search({ namespace: "crm", query: "create contact", limit: 5 });',        { onElicitation: acceptAll },      );      expect(sandboxResult.error).toBeUndefined();      expect(sandboxResult.result).toEqual(        expect.objectContaining({          items: [expect.objectContaining({ path: "crm.createContact" })],          total: 1,          hasMore: false,          nextOffset: null,        }),      );    }),  );   it.effect("supports executor-scoped source listing and tool search", () =>    Effect.gen(function* () {      const executor = yield* makeSearchExecutor();       const listed = yield* createExecutionEngine({ executor, codeExecutor }).execute(        "return await tools.executor.sources.list();",        { onElicitation: acceptAll },      );      expect(listed.error).toBeUndefined();      expect(listed.result).toEqual(        expect.objectContaining({          items: expect.arrayContaining([            expect.objectContaining({ id: "github", toolCount: 3 }),            expect.objectContaining({ id: "crm", toolCount: 2 }),          ]),          total: 2,          hasMore: false,          nextOffset: null,        }),      );       const searched = yield* createExecutionEngine({ executor, codeExecutor }).execute(        'return await tools.search({ query: "list contacts", namespace: "crm", limit: 5 });',        { onElicitation: acceptAll },      );      expect(searched.error).toBeUndefined();      expect(searched.result).toEqual(        expect.objectContaining({          items: [expect.objectContaining({ path: "crm.listContacts" })],        }),      );    }),  );   it.effect("paginates source listings via limit + offset", () =>    Effect.gen(function* () {      const executor = yield* makeSearchExecutor();      const engine = createExecutionEngine({ executor, codeExecutor });       // total = 2 (github, crm), sorted by name ("CRM" < "GitHub")      const firstPage = yield* engine.execute(        "return await tools.executor.sources.list({ limit: 1 });",        { onElicitation: acceptAll },      );      expect(firstPage.error).toBeUndefined();      expect(firstPage.result).toEqual(        expect.objectContaining({          items: [expect.objectContaining({ id: "crm" })],          total: 2,          hasMore: true,          nextOffset: 1,        }),      );       const secondPage = yield* engine.execute(        "return await tools.executor.sources.list({ limit: 1, offset: 1 });",        { onElicitation: acceptAll },      );      expect(secondPage.error).toBeUndefined();      expect(secondPage.result).toEqual(        expect.objectContaining({          items: [expect.objectContaining({ id: "github" })],          total: 2,          hasMore: false,          nextOffset: null,        }),      );    }),  );   it.effect("rejects negative offsets via the engine validator", () =>    Effect.gen(function* () {      const executor = yield* makeSearchExecutor();      const engine = createExecutionEngine({ executor, codeExecutor });       const badSearch = yield* engine.execute(        [          "try {",          '  await tools.search({ query: "list", offset: -1 });',          '  return "unexpected";',          "} catch (error) {",          "  return error instanceof Error ? error.message : String(error);",          "}",        ].join("\n"),        { onElicitation: acceptAll },      );      expect(badSearch.error).toBeUndefined();      expect(String(badSearch.result)).toContain(        "tools.search offset must be a non-negative number when provided",      );       const badList = yield* engine.execute(        [          "try {",          "  await tools.executor.sources.list({ offset: -5 });",          '  return "unexpected";',          "} catch (error) {",          "  return error instanceof Error ? error.message : String(error);",          "}",        ].join("\n"),        { onElicitation: acceptAll },      );      expect(badList.error).toBeUndefined();      expect(String(badList.result)).toContain(        "tools.executor.sources.list offset must be a non-negative number when provided",      );    }),  );   it.effect("describes tools with TypeScript previews", () =>    Effect.gen(function* () {      const executor = yield* makeSearchExecutor();       const described = yield* describeTool(executor, "github.listRepositoryIssues");      expect(described.path).toBe("github.listRepositoryIssues");      expect(described.name).toBe("listRepositoryIssues");      expect(described.description).toBe("List issues for a repository");      expect(described.inputTypeScript).toBe("{ owner: string; repo: string }");      expect(described.outputTypeScript).toBeUndefined();      expect(described.typeScriptDefinitions).toBeUndefined();    }),  );   it.effect("rejects malformed discover calls inside the sandbox", () =>    Effect.gen(function* () {      const executor = yield* makeSearchExecutor();      const engine = createExecutionEngine({ executor, codeExecutor });       const invalid = yield* engine.execute(        [          "try {",          '  await tools.search("github issues");',          '  return "unexpected";',          "} catch (error) {",          "  return error instanceof Error ? error.message : String(error);",          "}",        ].join("\n"),        { onElicitation: acceptAll },      );      expect(invalid.error).toBeUndefined();      expect(String(invalid.result)).toContain(        "tools.search expects an object: { query?: string; namespace?: string; limit?: number; offset?: number }",      );       const emptyQuery = yield* engine.execute(        'return await tools.search({ query: "", limit: 5 });',        { onElicitation: acceptAll },      );      expect(emptyQuery.error).toBeUndefined();      expect(emptyQuery.result).toEqual({        items: [],        total: 0,        hasMore: false,        nextOffset: null,      });       const invalidDescribe = yield* engine.execute(        [          "try {",          '  await tools.describe.tool({ path: "github.listRepositoryIssues", includeSchemas: true });',          '  return "unexpected";',          "} catch (error) {",          "  return error instanceof Error ? error.message : String(error);",          "}",        ].join("\n"),        { onElicitation: acceptAll },      );      expect(invalidDescribe.error).toBeUndefined();      expect(String(invalidDescribe.result)).toContain(        "tools.describe.tool no longer accepts includeSchemas",      );       const invalidSearch = yield* engine.execute(        'try { return await tools.search("crm"); } catch (error) { return error instanceof Error ? error.message : String(error); }',        { onElicitation: acceptAll },      );      expect(invalidSearch.error).toBeUndefined();      expect(String(invalidSearch.result)).toContain("tools.search expects an object");    }),  );   it.effect("converts message-bearing tool error results into execution errors", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(makeTestConfig({ plugins: [errorPlugin()] as const }));      const invoker = makeExecutorToolInvoker(executor, {        invokeOptions: { onElicitation: acceptAll },      });       const error = yield* Effect.flip(invoker.invoke({ path: "records.queryRows", args: {} }));       expect(error).toEqual(        expect.objectContaining({          message: 'Field with name "DisplayName" does not exist',        }),      );    }),  );}); // ---------------------------------------------------------------------------// pause/resume — multiple elicitations in a single execution// --------------------------------------------------------------------------- const apiPlugin = definePlugin(() => ({  id: "api-test" as const,  storage: () => ({}),  staticSources: () => [    {      id: "api",      kind: "in-memory",      name: "API",      tools: [        {          name: "multiApproval",          description: "A tool that elicits twice",          inputSchema: EmptyInputSchema,          handler: ({ elicit }) =>            Effect.gen(function* () {              const r1 = yield* elicit(                new FormElicitation({                  message: "First approval",                  requestedSchema: {},                }),              );              const r2 = yield* elicit(                new FormElicitation({                  message: "Second approval",                  requestedSchema: {},                }),              );              return { first: r1, second: r2 };            }),        },        {          name: "singleApproval",          description:            "A tool that elicits exactly once and then returns a value. Mirrors the shape of a typical `gmail.users.labels.create` style operation: one approval, one side effect, one success response.",          inputSchema: EmptyInputSchema,          handler: ({ elicit }) =>            Effect.gen(function* () {              const r = yield* elicit(                new FormElicitation({                  message: "Only approval",                  requestedSchema: {},                }),              );              return { ok: true, response: r };            }),        },      ],    },  ],})); describe("pause/resume with multiple elicitations", () => {  const makeElicitingExecutor = () =>    createExecutor(makeTestConfig({ plugins: [apiPlugin()] as const }));   it.effect(    "resume does not hang when execution hits a second elicitation",    () =>

Lines 1–500 of 577.