920 lines · 31.2 KB · TypeScript
RawDownload
import { describe, it, expect } from "@effect/vitest";import { env } from "cloudflare:workers";import * as Cause from "effect/Cause";import * as Data from "effect/Data";import * as Effect from "effect/Effect";import type { SandboxToolInvoker } from "@executor-js/codemode-core";import { ExecutionToolError } from "@executor-js/execution";import {  classifySandboxFailure,  ToolDispatcher,  makeDynamicWorkerExecutor,  renderWorkerError,  serializeWorkerCause,} from "./executor"; class TestToolError extends Data.TaggedError("TestToolError")<{  readonly message: string;}> {} const makeInvoker = (  fn: (input: { path: string; args: unknown }) => unknown,): SandboxToolInvoker => ({  invoke: (input) =>    Effect.try({      try: () => fn(input),      catch: (error) => error,    }),}); const failingInvoker = (message: string): SandboxToolInvoker => ({  invoke: () => Effect.fail(new TestToolError({ message })),}); describe("ToolDispatcher", () => {  it("returns a success envelope on successful tool call", async () => {    const invoker = makeInvoker(({ args }) => args);    const dispatcher = new ToolDispatcher(invoker, Effect.runPromise);     const result = await dispatcher.call("test.tool", { key: "value" });    expect(result).toEqual({ ok: true, result: { key: "value" } });  });   it("serializes tagged failures into a structured error envelope", async () => {    const dispatcher = new ToolDispatcher(failingInvoker("tool broke"), Effect.runPromise);     const result = await dispatcher.call("broken.tool", {});    expect(result).toMatchObject({      ok: false,      error: {        kind: "fail",        message: "tool broke",        primary: { __type: "Error", name: "TestToolError", message: "tool broke" },        failures: [{ __type: "Error", name: "TestToolError", message: "tool broke" }],        defects: [],        interrupted: false,      },    });  });   it("serializes object-shaped tool errors without collapsing them", async () => {    const dispatcher = new ToolDispatcher(      {        invoke: () =>          Effect.fail({            code: "forbidden",            detail: "missing team access",          }),      },      Effect.runPromise,    );     const result = await dispatcher.call("broken.tool", {});    expect(result).toEqual({      ok: false,      error: {        kind: "fail",        message: '{"code":"forbidden","detail":"missing team access"}',        primary: {          code: "forbidden",          detail: "missing team access",        },        failures: [          {            code: "forbidden",            detail: "missing team access",          },        ],        defects: [],        interrupted: false,      },    });  });   it("handles undefined args", async () => {    const invoker = makeInvoker(({ args }) => args);    const dispatcher = new ToolDispatcher(invoker, Effect.runPromise);     const result = await dispatcher.call("test.tool", undefined);    expect(result).toEqual({ ok: true, result: undefined });  });   it("returns a failure envelope for circular RPC args", async () => {    const invoker = makeInvoker(({ args }) => args);    const dispatcher = new ToolDispatcher(invoker, Effect.runPromise);    const cyclic: Record<string, unknown> = {};    cyclic.self = cyclic;     const result = await dispatcher.call("test.tool", cyclic);     expect(result).toMatchObject({      ok: false,      error: {        message: "Tool RPC payload contains a circular reference",      },    });  });   it("allows shared object references in RPC args", async () => {    const invoker = makeInvoker(({ args }) => args);    const dispatcher = new ToolDispatcher(invoker, Effect.runPromise);    const shared = { value: 1 };     const result = await dispatcher.call("test.tool", { first: shared, second: shared });     expect(result).toEqual({      ok: true,      result: { first: { value: 1 }, second: { value: 1 } },    });  });   it("passes the tool path correctly", async () => {    let capturedPath = "";    const invoker = makeInvoker(({ path }) => {      capturedPath = path;      return "ok";    });    const dispatcher = new ToolDispatcher(invoker, Effect.runPromise);     await dispatcher.call("my.deep.tool.path", {});    expect(capturedPath).toBe("my.deep.tool.path");  });}); describe("serializeWorkerCause", () => {  it("captures defects", () => {    const serialized = serializeWorkerCause(Cause.die({ defect: true }));    expect(serialized.kind).toBe("die");    expect(serialized.defects).toEqual([{ defect: true }]);    expect(serialized.failures).toEqual([]);  });   it("captures interruptions", () => {    const serialized = serializeWorkerCause(Cause.interrupt());    expect(serialized.kind).toBe("interrupt");    expect(serialized.interrupted).toBe(true);    expect(renderWorkerError(serialized)).toBe("Interrupted");  });}); describe("classifySandboxFailure", () => {  // Each string here is a real `status.message` seen on the  // executor.runtime.* spans in production. They reject the worker loader  // or the evaluate RPC and were collapsed to an opaque "Internal tool  // error" before the model could act on them. CPU/memory/capacity limits  // can't be triggered deterministically inside a unit-test isolate, so we  // pin their classification here; the syntax and serialization paths also  // have live WorkerLoader coverage above.  it.each([    ["Failed to start Worker:\nUncaught SyntaxError: Unexpected token '='", "compilation"],    ["Unexpected token '{'", "compilation"],    ["Invalid or unexpected token", "compilation"],    ["Symbol(nope) could not be cloned.", "runtime"],    [      'Could not serialize object of type "Cloudflare". This type does not support serialization.',      "runtime",    ],    ["Worker exceeded CPU time limit.", "runtime"],    ["Worker exceeded memory limit.", "runtime"],    ["Too many concurrent dynamic workers", "runtime"],  ] as const)("classifies %j as %s", (message, expected) => {    expect(classifySandboxFailure({ __type: "Error", name: "Error", message }, message)).toBe(      expected,    );  });   it("classifies a SyntaxError by name even when the message is bare", () => {    expect(      classifySandboxFailure({ __type: "Error", name: "SyntaxError", message: "boom" }, "boom"),    ).toBe("compilation");  });   it("classifies a DataCloneError by name even when the message is bare", () => {    expect(      classifySandboxFailure({ __type: "Error", name: "DataCloneError", message: "boom" }, "boom"),    ).toBe("runtime");  });   it("leaves an unrecognized defect opaque (internal)", () => {    const message = "the RPC receiver does not implement the method";    expect(classifySandboxFailure({ __type: "Error", name: "TypeError", message }, message)).toBe(      "internal",    );  });}); describe("makeDynamicWorkerExecutor", () => {  const loader = (env as { LOADER: WorkerLoader }).LOADER;   it("executes simple code and returns result", async () => {    const executor = makeDynamicWorkerExecutor({ loader });    const invoker = makeInvoker(() => null);     const result = await Effect.runPromise(executor.execute("async () => 42", invoker));     expect(result.error).toBeUndefined();    expect(result.result).toBe(42);  });   it("recovers prose-wrapped fenced async arrow input", async () => {    const executor = makeDynamicWorkerExecutor({ loader });    const invoker = makeInvoker(() => null);     const result = await Effect.runPromise(      executor.execute(        ["Use this snippet.", "", "```ts", "async () => 42", "```"].join("\n"),        invoker,      ),    );     expect(result.error).toBeUndefined();    expect(result.result).toBe(42);  });   it("executes code that returns an object", async () => {    const executor = makeDynamicWorkerExecutor({ loader });    const invoker = makeInvoker(() => null);     const result = await Effect.runPromise(      executor.execute('async () => ({ hello: "world" })', invoker),    );     expect(result.error).toBeUndefined();    expect(result.result).toEqual({ hello: "world" });  });   it("captures console output in logs", async () => {    const executor = makeDynamicWorkerExecutor({ loader });    const invoker = makeInvoker(() => null);     const result = await Effect.runPromise(      executor.execute(        'async () => { console.log("hello"); console.warn("careful"); return 1; }',        invoker,      ),    );     expect(result.error).toBeUndefined();    expect(result.logs).toContain("hello");    expect(result.logs).toContain("[warn] careful");  });   it("returns error for throwing code", async () => {    const executor = makeDynamicWorkerExecutor({ loader });    const invoker = makeInvoker(() => null);     const result = await Effect.runPromise(      executor.execute('async () => { throw new Error("boom"); }', invoker),    );     expect(result.error).toBe("boom");    expect(result.result).toBeNull();  });   it("serializes thrown objects into the user-facing error text", async () => {    const executor = makeDynamicWorkerExecutor({ loader });    const invoker = makeInvoker(() => null);     const result = await Effect.runPromise(      executor.execute(        'async () => { throw { code: "bad_request", detail: "team missing" }; }',        invoker,      ),    );     expect(result.error).toBe('{"code":"bad_request","detail":"team missing"}');    expect(result.result).toBeNull();  });   it("invokes tools via the proxy and returns results", async () => {    const executor = makeDynamicWorkerExecutor({ loader });    const invoker = makeInvoker(({ path, args }) => {      if (path === "math.add") {        const { a, b } = args as { a: number; b: number };        return a + b;      }      return null;    });     const result = await Effect.runPromise(      executor.execute(        "async () => { const sum = await tools.math.add({ a: 3, b: 4 }); return sum; }",        invoker,      ),    );     expect(result.error).toBeUndefined();    expect(result.result).toBe(7);  });   it("surfaces infra defects through the worker bridge as an opaque generic", async () => {    const executor = makeDynamicWorkerExecutor({ loader });    const invoker = failingInvoker("not authorized");     const result = await Effect.runPromise(      executor.execute("async () => { return await tools.secret.read({}); }", invoker),    );     expect(result.error).toBe("Internal tool error");  });   it("surfaces a syntax error with the parser's descriptive message, not an opaque generic", async () => {    const executor = makeDynamicWorkerExecutor({ loader });    const invoker = makeInvoker(() => null);     // A genuine parse error: `const` with no binding name. Before the fix    // this threw in stripTypeScript, became a DynamicWorkerExecutionError    // on the failure channel, and the host collapsed it to the opaque    // "Internal tool error". The model needs the real reason to self-correct.    const result = await Effect.runPromise(      executor.execute("async () => { const = 5; return 1; }", invoker),    );     expect(result.result).toBeNull();    expect(result.error).toBeDefined();    expect(result.error).not.toBe("Internal tool error");    expect(result.error).not.toContain("Internal tool error");    expect(result.error?.toLowerCase()).toContain("unexpected");  });   it("surfaces smart-quote paste errors descriptively", async () => {    const executor = makeDynamicWorkerExecutor({ loader });    const invoker = makeInvoker(() => null);     // Curly quotes from a copy-paste are the most common real-world cause:    // the snippet looks fine to a human but is invalid JavaScript.    const result = await Effect.runPromise(      executor.execute("async () => { return “hello”; }", invoker),    );     expect(result.result).toBeNull();    expect(result.error).toBeDefined();    expect(result.error).not.toBe("Internal tool error");  });   it("surfaces a non-serializable return value descriptively, not opaquely", async () => {    const executor = makeDynamicWorkerExecutor({ loader });    const invoker = makeInvoker(() => null);     // Returning a value that can't cross the sandbox boundary (a Symbol, a    // host object) rejects the evaluate RPC with a DataCloneError. That is    // the user's own code, not a sandbox defect, so the model needs to be    // told what it returned can't be serialized rather than getting an    // opaque "Internal tool error". Production analog: "Could not serialize    // object of type Cloudflare".    const result = await Effect.runPromise(executor.execute("async () => Symbol('nope')", invoker));     expect(result.result).toBeNull();    expect(result.error).toBeDefined();    expect(result.error).not.toBe("Internal tool error");    expect(result.error).not.toContain("Internal tool error");    expect(result.error?.toLowerCase()).toContain("could not be cloned");  });   it("preserves public ExecutionToolError messages across the worker bridge", async () => {    const executor = makeDynamicWorkerExecutor({ loader });    const invoker = {      invoke: () =>        Effect.fail(          new ExecutionToolError({            message:              "tools.search expects an object: { query?: string; namespace?: string; limit?: number; offset?: number }",          }),        ),    } satisfies SandboxToolInvoker;     const result = await Effect.runPromise(      executor.execute("async () => await tools.search('github')", invoker),    );     expect(result.error).toBe(      "tools.search expects an object: { query?: string; namespace?: string; limit?: number; offset?: number }",    );  });   it("does not expose host error stack details to sandbox error handlers", async () => {    const executor = makeDynamicWorkerExecutor({ loader });    const invoker = {      invoke: () => {        const error = new Error("not authorized");        error.stack = "secret host stack";        return Effect.fail(error);      },    } satisfies SandboxToolInvoker;     const result = await Effect.runPromise(      executor.execute(        `async () => {          try {            await tools.secret.read({});          } catch (error) {            return {              message: error && error.message,              stack: error && error.stack,            };          }        }`,        invoker,      ),    );     expect(result.error).toBeUndefined();    expect(result.result).toMatchObject({ message: "Internal tool error" });    expect((result.result as { stack?: string }).stack).not.toContain("secret host stack");    expect((result.result as { message?: string }).message).not.toContain("not authorized");  });   it("collapses object-shaped tool defects to an opaque generic", async () => {    const executor = makeDynamicWorkerExecutor({ loader });    const invoker = {      invoke: () =>        Effect.fail({          code: "forbidden",          detail: "missing team access",        }),    } satisfies SandboxToolInvoker;     const result = await Effect.runPromise(      executor.execute("async () => { return await tools.secret.read({}); }", invoker),    );     expect(result.error).toBe("Internal tool error");    expect(result.result).toBeNull();  });   it("collapses message-bearing object tool defects to an opaque generic", async () => {    const executor = makeDynamicWorkerExecutor({ loader });    const invoker = {      invoke: () =>        Effect.fail({          code: "invalid_query",          message: 'Field with name "DisplayName" does not exist',        }),    } satisfies SandboxToolInvoker;     const result = await Effect.runPromise(      executor.execute("async () => { return await tools.records.query({}); }", invoker),    );     expect(result.error).toBe("Internal tool error");    expect(result.error).not.toContain("DisplayName");    expect(result.result).toBeNull();  });   it("handles multiple tool calls in sequence", async () => {    const executor = makeDynamicWorkerExecutor({ loader });    const invoker = makeInvoker(({ path }) => {      if (path === "data.first") return 10;      if (path === "data.second") return 20;      return 0;    });     const result = await Effect.runPromise(      executor.execute(        `async () => {          const a = await tools.data.first({});          const b = await tools.data.second({});          return a + b;        }`,        invoker,      ),    );     expect(result.error).toBeUndefined();    expect(result.result).toBe(30);  });   it("returns an execution error for circular tool args", async () => {    const executor = makeDynamicWorkerExecutor({ loader });    const invoker = makeInvoker(() => null);     const result = await Effect.runPromise(      executor.execute(        `async () => {          const payload = {};          payload.self = payload;          return await tools.cycles.send(payload);        }`,        invoker,      ),    );

Lines 1–500 of 920.