246 lines · 6.7 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import * as Effect from "effect/Effect";import * as Schema from "effect/Schema"; import {  allowAllToolInteractions,  makeToolInvokerFromTools,  toExecutorTool,} from "@executor/codemode-core";import { isDenoAvailable, makeDenoSubprocessExecutor } from "./index"; const numberPairInputSchema = Schema.standardSchemaV1(  Schema.Struct({    a: Schema.Number,    b: Schema.Number,  }),); const messageInputSchema = Schema.standardSchemaV1(  Schema.Struct({    message: Schema.String,  }),); const tools = {  "math.add": {    description: "Add two numbers",    inputSchema: numberPairInputSchema,    execute: ({ a, b }: { a: number; b: number }) => ({ sum: a + b }),  },  "notifications.send": toExecutorTool({    tool: {      description: "Send a message",      inputSchema: messageInputSchema,      execute: ({ message }: { message: string }) => ({        delivered: true,        message,      }),    },    metadata: {      interaction: "required",    },  }),}; it("reports unavailable Deno executables", () => {  expect(isDenoAvailable("__executor_missing_deno__")).toBe(false);}); it.effect("returns an actionable error when Deno is missing", () =>  Effect.gen(function* () {    const executor = makeDenoSubprocessExecutor({      denoExecutable: "__executor_missing_deno__",    });    const toolInvoker = makeToolInvokerFromTools({ tools });     const output = yield* executor.execute(      "return 1 + 2;",      toolInvoker,    );     expect(output.result).toBeNull();    expect(output.error).toContain("Install Deno or set DENO_BIN");  }),); const skipUnlessDeno = isDenoAvailable()  ? describe  : describe.skip; skipUnlessDeno("runtime-deno-subprocess", () => {  it.effect("executes simple code and returns result", () =>    Effect.gen(function* () {      const executor = makeDenoSubprocessExecutor();      const toolInvoker = makeToolInvokerFromTools({ tools });       const output = yield* executor.execute(        "return 1 + 2;",        toolInvoker,      );       expect(output.result).toBe(3);      expect(output.error).toBeUndefined();    }),  );   it.effect("executes code with tool calls", () =>    Effect.gen(function* () {      const executor = makeDenoSubprocessExecutor();      const toolInvoker = makeToolInvokerFromTools({        tools,        onToolInteraction: allowAllToolInteractions,      });       const output = yield* executor.execute(        [          "const math = await tools.math.add({ a: 19, b: 23 });",          "await tools.notifications.send({ message: `sum is ${math.sum}` });",          "return math;",        ].join("\n"),        toolInvoker,      );       expect(output.result).toEqual({ sum: 42 });      expect(output.error).toBeUndefined();    }),  );   it.effect("captures console.log output in logs", () =>    Effect.gen(function* () {      const executor = makeDenoSubprocessExecutor();      const toolInvoker = makeToolInvokerFromTools({ tools });       const output = yield* executor.execute(        [          'console.log("hello from sandbox");',          'console.warn("a warning");',          'console.error("an error");',          "return 42;",        ].join("\n"),        toolInvoker,      );       expect(output.result).toBe(42);      expect(output.logs).toContain("[log] hello from sandbox");      expect(output.logs).toContain("[warn] a warning");      expect(output.logs).toContain("[error] an error");    }),  );   it.effect("reports execution errors without crashing", () =>    Effect.gen(function* () {      const executor = makeDenoSubprocessExecutor();      const toolInvoker = makeToolInvokerFromTools({ tools });       const output = yield* executor.execute(        'throw new Error("boom");',        toolInvoker,      );       expect(output.result).toBeNull();      expect(output.error).toContain("boom");    }),  );   it.effect("handles tool call errors gracefully", () =>    Effect.gen(function* () {      const failingTools = {        "broken.thing": {          description: "Always fails",          inputSchema: Schema.standardSchemaV1(Schema.Struct({})),          execute: () => {            throw new Error("tool is broken");          },        },      };       const executor = makeDenoSubprocessExecutor();      const toolInvoker = makeToolInvokerFromTools({        tools: failingTools,      });       const output = yield* executor.execute(        "return await tools.broken.thing({});",        toolInvoker,      );       expect(output.result).toBeNull();      expect(output.error).toContain("tool is broken");    }),  );   it.effect("respects timeout", () =>    Effect.gen(function* () {      const executor = makeDenoSubprocessExecutor({        timeoutMs: 500,      });      const toolInvoker = makeToolInvokerFromTools({ tools });       const output = yield* executor.execute(        "await new Promise(() => {}); return 1;",        toolInvoker,      );       expect(output.result).toBeNull();      expect(output.error).toContain("timed out");    }),  );   it.effect("network access is denied by default", () =>    Effect.gen(function* () {      const executor = makeDenoSubprocessExecutor();      const toolInvoker = makeToolInvokerFromTools({ tools });       const output = yield* executor.execute(        'await fetch("https://example.com"); return 1;',        toolInvoker,      );       expect(output.result).toBeNull();      expect(output.error).toBeDefined();    }),  );   it.effect("network access can be allowed via permissions", () =>    Effect.gen(function* () {      const executor = makeDenoSubprocessExecutor({        permissions: {          allowNet: true,        },      });      const toolInvoker = makeToolInvokerFromTools({ tools });       const output = yield* executor.execute(        [          'const res = await fetch("https://example.com");',          "return res.status;",        ].join("\n"),        toolInvoker,      );       expect(output.result).toBe(200);      expect(output.error).toBeUndefined();    }),  );   it.effect("multiple sequential tool calls work correctly", () =>    Effect.gen(function* () {      const executor = makeDenoSubprocessExecutor();      const toolInvoker = makeToolInvokerFromTools({ tools });       const output = yield* executor.execute(        [          "const r1 = await tools.math.add({ a: 1, b: 2 });",          "const r2 = await tools.math.add({ a: r1.sum, b: 10 });",          "const r3 = await tools.math.add({ a: r2.sum, b: 100 });",          "return r3;",        ].join("\n"),        toolInvoker,      );       expect(output.result).toEqual({ sum: 113 });      expect(output.error).toBeUndefined();    }),  );});